UNPKG

26.3 kBJavaScriptView Raw
1// Relation
2// ---------------
3'use strict';
4
5const _ = require('lodash');
6const inflection = require('inflection');
7
8const Helpers = require('./helpers');
9const ModelBase = require('./base/model');
10const RelationBase = require('./base/relation');
11const Promise = require('./base/promise');
12const constants = require('./constants');
13const push = Array.prototype.push;
14const removePivotPrefix = (key) => key.slice(constants.PIVOT_PREFIX.length);
15const hasPivotPrefix = (key) => _.startsWith(key, constants.PIVOT_PREFIX);
16
17const Relation = RelationBase.extend({
18 // Assembles the new model or collection we're creating an instance of,
19 // gathering any relevant primitives from the parent object,
20 // without keeping any hard references.
21 init(parent) {
22 this.parentId = parent.id;
23 this.parentTableName = _.result(parent, 'tableName');
24 this.parentIdAttribute = this.attribute('parentIdAttribute', parent);
25
26 // Use formatted attributes so that morphKey and foreignKey will match
27 // attribute keys.
28 this.parentAttributes = parent.format(_.clone(parent.attributes));
29
30 if (this.type === 'morphTo' && !parent._isEager) {
31 // If the parent object is eager loading, and it's a polymorphic `morphTo` relation,
32 // we can't know what the target will be until the models are sorted and matched.
33 this.target = Helpers.morphCandidate(this.candidates, this.parentAttributes[this.key('morphKey')]);
34 this.targetTableName = _.result(this.target.prototype, 'tableName');
35 }
36
37 this.targetIdAttribute = this.attribute('targetIdAttribute', parent);
38 this.parentFk = this.attribute('parentFk');
39
40 const target = this.target ? this.relatedInstance() : {};
41 target.relatedData = this;
42
43 if (this.type === 'belongsToMany') {
44 _.extend(target, pivotHelpers);
45 }
46
47 return target;
48 },
49
50 // Initializes a `through` relation, setting the `Target` model and `options`,
51 // which includes any additional keys for the relation.
52 through(source, Target, options) {
53 const type = this.type;
54 if (type !== 'hasOne' && type !== 'hasMany' && type !== 'belongsToMany' && type !== 'belongsTo') {
55 throw new Error('`through` is only chainable from `hasOne`, `belongsTo`, `hasMany`, or `belongsToMany`');
56 }
57
58 this.throughTarget = Target;
59 this.throughTableName = _.result(Target.prototype, 'tableName');
60
61 _.extend(this, options);
62 _.extend(source, pivotHelpers);
63
64 this.parentIdAttribute = this.attribute('parentIdAttribute');
65 this.targetIdAttribute = this.attribute('targetIdAttribute');
66 this.throughIdAttribute = this.attribute('throughIdAttribute', Target);
67 this.parentFk = this.attribute('parentFk');
68
69 // Set the appropriate foreign key if we're doing a belongsToMany, for convenience.
70 if (this.type === 'belongsToMany') {
71 this.foreignKey = this.throughForeignKey;
72 } else if (this.otherKey) {
73 this.foreignKey = this.otherKey;
74 }
75
76 return source;
77 },
78
79 // Generates and returns a specified key, for convenience... one of
80 // `foreignKey`, `otherKey`, `throughForeignKey`.
81 key(keyName) {
82 if (this[keyName]) return this[keyName];
83 switch (keyName) {
84 case 'otherKey':
85 this[keyName] = singularMemo(this.targetTableName) + '_' + this.targetIdAttribute;
86 break;
87 case 'throughForeignKey':
88 this[keyName] = singularMemo(this.joinTable()) + '_' + this.throughIdAttribute;
89 break;
90 case 'foreignKey':
91 switch (this.type) {
92 case 'morphTo': {
93 const idKeyName = this.columnNames && this.columnNames[1] ? this.columnNames[1] : this.morphName + '_id';
94 this[keyName] = idKeyName;
95 break;
96 }
97 case 'belongsTo':
98 this[keyName] = singularMemo(this.targetTableName) + '_' + this.targetIdAttribute;
99 break;
100 default:
101 if (this.isMorph()) {
102 this[keyName] = this.columnNames && this.columnNames[1] ? this.columnNames[1] : this.morphName + '_id';
103 break;
104 }
105 this[keyName] = singularMemo(this.parentTableName) + '_' + this.parentIdAttribute;
106 break;
107 }
108 break;
109 case 'morphKey':
110 this[keyName] = this.columnNames && this.columnNames[0] ? this.columnNames[0] : this.morphName + '_type';
111 break;
112 case 'morphValue':
113 this[keyName] = this.morphValue || this.parentTableName || this.targetTableName;
114 break;
115 }
116 return this[keyName];
117 },
118
119 // Get the correct value for the following attributes:
120 // parentIdAttribute, targetIdAttribute, throughIdAttribute and parentFk.
121 attribute(attribute, parent) {
122 switch (attribute) {
123 case 'parentIdAttribute':
124 if (this.isThrough()) {
125 if (this.type === 'belongsTo' && this.throughForeignKey) {
126 return this.throughForeignKey;
127 }
128
129 if (this.type === 'belongsToMany' && this.isThroughForeignKeyTargeted()) {
130 return this.throughForeignKeyTarget;
131 }
132
133 if (this.isOtherKeyTargeted()) {
134 return this.otherKeyTarget;
135 }
136
137 // Return attribute calculated on `init` by default.
138 return this.parentIdAttribute;
139 }
140
141 if (this.type === 'belongsTo' && this.foreignKey) {
142 return this.foreignKey;
143 }
144
145 if (this.type !== 'belongsTo' && this.isForeignKeyTargeted()) {
146 return this.foreignKeyTarget;
147 }
148
149 return _.result(parent, 'idAttribute');
150
151 case 'targetIdAttribute':
152 if (this.isThrough()) {
153 if ((this.type === 'belongsToMany' || this.type === 'belongsTo') && this.isOtherKeyTargeted()) {
154 return this.otherKeyTarget;
155 }
156
157 // Return attribute calculated on `init` by default.
158 return this.targetIdAttribute;
159 }
160
161 if (this.type === 'morphTo' && !parent._isEager) {
162 return _.result(this.target.prototype, 'idAttribute');
163 }
164
165 if (this.type === 'belongsTo' && this.isForeignKeyTargeted()) {
166 return this.foreignKeyTarget;
167 }
168
169 if (this.type === 'belongsToMany' && this.isOtherKeyTargeted()) {
170 return this.otherKeyTarget;
171 }
172
173 return this.targetIdAttribute;
174
175 case 'throughIdAttribute':
176 if (this.type !== 'belongsToMany' && this.isThroughForeignKeyTargeted()) {
177 return this.throughForeignKeyTarget;
178 }
179
180 if (this.type === 'belongsToMany' && this.throughForeignKey) {
181 return this.throughForeignKey;
182 }
183
184 return _.result(parent.prototype, 'idAttribute');
185
186 case 'parentFk':
187 if (!this.hasParentAttributes()) {
188 return;
189 }
190
191 if (this.isThrough()) {
192 if (this.type === 'belongsToMany' && this.isThroughForeignKeyTargeted()) {
193 return this.parentAttributes[this.throughForeignKeyTarget];
194 }
195
196 if (this.type === 'belongsTo') {
197 return this.throughForeignKey ? this.parentAttributes[this.parentIdAttribute] : this.parentId;
198 }
199
200 if (this.isOtherKeyTargeted()) {
201 return this.parentAttributes[this.otherKeyTarget];
202 }
203
204 // Return attribute calculated on `init` by default.
205 return this.parentFk;
206 }
207
208 return this.parentAttributes[this.isInverse() ? this.key('foreignKey') : this.parentIdAttribute];
209 }
210 },
211
212 // Injects the necessary `select` constraints into a `knex` query builder.
213 selectConstraints(knex, options) {
214 const resp = options.parentResponse;
215
216 // The `belongsToMany` and `through` relations have joins & pivot columns.
217 if (this.isJoined()) this.joinClauses(knex);
218
219 // Call the function, if one exists, to constrain the eager loaded query.
220 if (options._beforeFn) options._beforeFn.call(knex, knex);
221
222 // The base select column
223 if (Array.isArray(options.columns)) {
224 knex.columns(options.columns);
225 }
226
227 const currentColumns = _.find(knex._statements, {grouping: 'columns'});
228
229 if (!currentColumns || currentColumns.length === 0) {
230 knex.distinct(this.targetTableName + '.*');
231 }
232
233 if (this.isJoined()) this.joinColumns(knex);
234
235 // If this is a single relation and we're not eager loading,
236 // limit the query to a single item.
237 if (this.isSingle() && !resp) knex.limit(1);
238
239 // Finally, add (and validate) the where conditions, necessary for constraining the relation.
240 this.whereClauses(knex, resp);
241 },
242
243 // Inject & validates necessary `through` constraints for the current model.
244 joinColumns(knex) {
245 const columns = [];
246 const joinTable = this.joinTable();
247 if (this.isThrough()) columns.push(this.throughIdAttribute);
248 columns.push(this.key('foreignKey'));
249 if (this.type === 'belongsToMany') columns.push(this.key('otherKey'));
250 push.apply(columns, this.pivotColumns);
251 knex.columns(
252 _.map(columns, function(col) {
253 return joinTable + '.' + col + ' as _pivot_' + col;
254 })
255 );
256 },
257
258 // Generates the join clauses necessary for the current relation.
259 joinClauses(knex) {
260 const joinTable = this.joinTable();
261
262 if (this.type === 'belongsTo' || this.type === 'belongsToMany') {
263 const targetKey = this.type === 'belongsTo' ? this.key('foreignKey') : this.key('otherKey');
264
265 knex.join(joinTable, joinTable + '.' + targetKey, '=', this.targetTableName + '.' + this.targetIdAttribute);
266
267 // A `belongsTo` -> `through` is currently the only relation with two joins.
268 if (this.type === 'belongsTo') {
269 knex.join(
270 this.parentTableName,
271 joinTable + '.' + this.throughIdAttribute,
272 '=',
273 this.parentTableName + '.' + this.key('throughForeignKey')
274 );
275 }
276 } else {
277 knex.join(
278 joinTable,
279 joinTable + '.' + this.throughIdAttribute,
280 '=',
281 this.targetTableName + '.' + this.key('throughForeignKey')
282 );
283 }
284 },
285
286 // Check that there isn't an incorrect foreign key set, vs. the one
287 // passed in when the relation was formed.
288 whereClauses(knex, response) {
289 let key;
290
291 if (this.isJoined()) {
292 const isBelongsTo = this.type === 'belongsTo';
293 const targetTable = isBelongsTo ? this.parentTableName : this.joinTable();
294
295 const column = isBelongsTo ? this.parentIdAttribute : this.key('foreignKey');
296
297 key = `${targetTable}.${column}`;
298 } else {
299 const column = this.isInverse() ? this.targetIdAttribute : this.key('foreignKey');
300
301 key = `${this.targetTableName}.${column}`;
302 }
303
304 const method = response ? 'whereIn' : 'where';
305 const ids = response ? this.eagerKeys(response) : this.parentFk;
306 knex[method](key, ids);
307
308 if (this.isMorph()) {
309 const table = this.targetTableName;
310 const key = this.key('morphKey');
311 const value = this.key('morphValue');
312 knex.where(`${table}.${key}`, value);
313 }
314 },
315
316 // Fetches all `eagerKeys` from the current relation.
317 eagerKeys(response) {
318 const key = this.isInverse() && !this.isThrough() ? this.key('foreignKey') : this.parentIdAttribute;
319 return _.reject(
320 _(response)
321 .map(key)
322 .uniq()
323 .value(),
324 _.isNil
325 );
326 },
327
328 // Generates the appropriate standard join table.
329 joinTable() {
330 if (this.isThrough()) return this.throughTableName;
331 return this.joinTableName || [this.parentTableName, this.targetTableName].sort().join('_');
332 },
333
334 // Creates a new model or collection instance, depending on
335 // the `relatedData` settings and the models passed in.
336 relatedInstance(models, options) {
337 models = models || [];
338 options = options || {};
339
340 const Target = this.target;
341
342 // If it's a single model, check whether there's already a model
343 // we can pick from... otherwise create a new instance.
344 if (this.isSingle()) {
345 if (!(Target.prototype instanceof ModelBase)) {
346 throw new Error(`The ${this.type} related object must be a Bookshelf.Model`);
347 }
348 return models[0] || new Target();
349 }
350
351 // Allows us to just use a model, but create a temporary
352 // collection for a "*-many" relation.
353 if (Target.prototype instanceof ModelBase) {
354 return Target.collection(models, {
355 parse: true,
356 merge: options.merge,
357 remove: options.remove
358 });
359 }
360 return new Target(models, {parse: true});
361 },
362
363 // Groups the related response according to the type of relationship
364 // we're handling, for easy attachment to the parent models.
365 eagerPair(relationName, related, parentModels, options) {
366 // If this is a morphTo, we only want to pair on the morphValue for the current relation.
367 if (this.type === 'morphTo') {
368 parentModels = _.filter(parentModels, (m) => {
369 return m.get(this.key('morphKey')) === this.key('morphValue');
370 });
371 }
372
373 // If this is a `through` or `belongsToMany` relation, we need to cleanup & setup the `interim` model.
374 if (this.isJoined()) related = this.parsePivot(related);
375
376 // Group all of the related models for easier association with their parent models.
377 const idKey = (key) => (_.isBuffer(key) ? key.toString('hex') : key);
378 const grouped = _.groupBy(related, (m) => {
379 let key;
380 if (m.pivot) {
381 if (this.isInverse() && this.isThrough()) {
382 key = this.isThroughForeignKeyTargeted() ? m.pivot.get(this.throughForeignKeyTarget) : m.pivot.id;
383 } else {
384 key = m.pivot.get(this.key('foreignKey'));
385 }
386 } else if (this.isInverse()) {
387 key = this.isForeignKeyTargeted() ? m.get(this.foreignKeyTarget) : m.id;
388 } else {
389 key = m.get(this.key('foreignKey'));
390 }
391 return idKey(key);
392 });
393
394 // Loop over the `parentModels` and attach the grouped sub-models,
395 // keeping the `relatedData` on the new related instance.
396 _.each(parentModels, (model) => {
397 let groupedKey;
398 if (!this.isInverse()) {
399 const parsedKey = Object.keys(model.parse({[this.parentIdAttribute]: null}))[0];
400 groupedKey = idKey(model.get(parsedKey));
401 } else {
402 const keyColumn = this.key(this.isThrough() ? 'throughForeignKey' : 'foreignKey');
403 const formatted = model.format(_.clone(model.attributes));
404 groupedKey = idKey(formatted[keyColumn]);
405 }
406 if (groupedKey != null) {
407 const relation = (model.relations[relationName] = this.relatedInstance(grouped[groupedKey], options));
408 if (this.type === 'belongsToMany') {
409 // If type is of "belongsToMany" then the relatedData need to be recreated through the parent model
410 relation.relatedData = model[relationName]().relatedData;
411 } else {
412 relation.relatedData = this;
413 }
414 if (this.isJoined()) _.extend(relation, pivotHelpers);
415 }
416 });
417
418 // Now that related models have been successfully paired, update each with
419 // its parsed attributes
420 related.map((model) => {
421 model.attributes = model.parse(model.attributes);
422 model.formatTimestamps()._previousAttributes = _.cloneDeep(model.attributes);
423 model._reset();
424 });
425
426 return related;
427 },
428
429 parsePivot(models) {
430 return _.map(models, (model) => {
431 // Separate pivot attributes.
432 const grouped = _.reduce(
433 model.attributes,
434 (acc, value, key) => {
435 if (hasPivotPrefix(key)) {
436 acc.pivot[removePivotPrefix(key)] = value;
437 } else {
438 acc.model[key] = value;
439 }
440 return acc;
441 },
442 {model: {}, pivot: {}}
443 );
444
445 // Assign non-pivot attributes to model.
446 model.attributes = grouped.model;
447
448 // If there are any pivot attributes, create a new pivot model with these
449 // attributes.
450 if (!_.isEmpty(grouped.pivot)) {
451 const Through = this.throughTarget;
452 const tableName = this.joinTable();
453 model.pivot = Through != null ? new Through(grouped.pivot) : new this.Model(grouped.pivot, {tableName});
454 }
455
456 return model;
457 });
458 },
459
460 // A few predicates to help clarify some of the logic above.
461 isThrough() {
462 return this.throughTarget != null;
463 },
464 isJoined() {
465 return this.type === 'belongsToMany' || this.isThrough();
466 },
467 isMorph() {
468 return this.type === 'morphOne' || this.type === 'morphMany';
469 },
470 isSingle() {
471 const type = this.type;
472 return type === 'hasOne' || type === 'belongsTo' || type === 'morphOne' || type === 'morphTo';
473 },
474 isInverse() {
475 return this.type === 'belongsTo' || this.type === 'morphTo';
476 },
477 isForeignKeyTargeted() {
478 return this.foreignKeyTarget != null;
479 },
480 isThroughForeignKeyTargeted() {
481 return this.throughForeignKeyTarget != null;
482 },
483 isOtherKeyTargeted() {
484 return this.otherKeyTarget != null;
485 },
486 hasParentAttributes() {
487 return this.parentAttributes != null;
488 },
489
490 // Sets the `pivotColumns` to be retrieved along with the current model.
491 withPivot(columns) {
492 if (!Array.isArray(columns)) columns = [columns];
493 this.pivotColumns = this.pivotColumns || [];
494 push.apply(this.pivotColumns, columns);
495 }
496});
497
498// Simple memoization of the singularize call.
499const singularMemo = (function() {
500 const cache = Object.create(null);
501 return function(arg) {
502 if (!(arg in cache)) {
503 cache[arg] = inflection.singularize(arg);
504 }
505 return cache[arg];
506 };
507})();
508
509// Specific to many-to-many relationships, these methods are mixed
510// into the `belongsToMany` relationships when they are created,
511// providing helpers for attaching and detaching related models.
512const pivotHelpers = {
513 /**
514 * Attaches one or more `ids` or models from a foreign table to the current
515 * table, on a {@linkplain many-to-many} relation. Creates and saves a new
516 * model and attaches the model with the related model.
517 *
518 * var admin1 = new Admin({username: 'user1', password: 'test'});
519 * var admin2 = new Admin({username: 'user2', password: 'test'});
520 *
521 * Promise.all([admin1.save(), admin2.save()])
522 * .then(function() {
523 * return Promise.all([
524 * new Site({id: 1}).admins().attach([admin1, admin2]),
525 * new Site({id: 2}).admins().attach(admin2)
526 * ]);
527 * })
528 *
529 * This method (along with {@link Collection#detach} and {@link
530 * Collection#updatePivot}) are mixed in to a {@link Collection} when
531 * returned by a {@link Model#belongsToMany belongsToMany} relation.
532 *
533 * @method Collection#attach
534 * @param {mixed|mixed[]} ids
535 * One or more ID values or models to be attached to the relation.
536 * @param {Object} options
537 * A hash of options.
538 * @param {Transaction} options.transacting
539 * Optionally run the query in a transaction.
540 * @returns {Promise<Collection>}
541 * A promise resolving to the updated Collection where this method was called.
542 */
543 attach(ids, options) {
544 return Promise.try(() => this.triggerThen('attaching', this, ids, options))
545 .then(() => this._handler('insert', ids, options))
546 .then((response) => this.triggerThen('attached', this, response, options))
547 .return(this);
548 },
549
550 /**
551 * Detach one or more related objects from their pivot tables. If a model or
552 * id is passed, it attempts to remove from the pivot table based on that
553 * foreign key. If no parameters are specified, we assume we will detach all
554 * related associations.
555 *
556 * This method (along with {@link Collection#attach} and {@link
557 * Collection#updatePivot}) are mixed in to a {@link Collection} when returned
558 * by a {@link Model#belongsToMany belongsToMany} relation.
559 *
560 * @method Collection#detach
561 * @param {mixed|mixed[]} [ids]
562 * One or more ID values or models to be detached from the relation.
563 * @param {Object} options
564 * A hash of options.
565 * @param {Transaction} options.transacting
566 * Optionally run the query in a transaction.
567 * @returns {Promise<undefined>}
568 * A promise resolving to the updated Collection where this method was called.
569 */
570 detach(ids, options) {
571 return Promise.try(() => this.triggerThen('detaching', this, ids, options))
572 .then(() => this._handler('delete', ids, options))
573 .then((response) => this.triggerThen('detached', this, response, options))
574 .return(this);
575 },
576
577 /**
578 * The `updatePivot` method is used exclusively on {@link Model#belongsToMany
579 * belongsToMany} relations, and allows for updating pivot rows on the joining
580 * table.
581 *
582 * This method (along with {@link Collection#attach} and {@link
583 * Collection#detach}) are mixed in to a {@link Collection} when returned
584 * by a {@link Model#belongsToMany belongsToMany} relation.
585 *
586 * @method Collection#updatePivot
587 * @param {Object} attributes
588 * Values to be set in the `update` query.
589 * @param {Object} [options]
590 * A hash of options.
591 * @param {function|Object} [options.query]
592 * Constrain the update query. Similar to the `method` argument to {@link
593 * Model#query}.
594 * @param {Boolean} [options.require=false]
595 * Causes promise to be rejected with an Error if no rows were updated.
596 * @param {Transaction} [options.transacting]
597 * Optionally run the query in a transaction.
598 * @returns {Promise<Number>}
599 * A promise resolving to number of rows updated.
600 */
601 updatePivot: function(attributes, options) {
602 return this._handler('update', attributes, options);
603 },
604
605 /**
606 * The `withPivot` method is used exclusively on {@link Model#belongsToMany
607 * belongsToMany} relations, and allows for additional fields to be pulled
608 * from the joining table.
609 *
610 * var Tag = bookshelf.Model.extend({
611 * comments: function() {
612 * return this.belongsToMany(Comment).withPivot(['created_at', 'order']);
613 * }
614 * });
615 *
616 * @method Collection#withPivot
617 * @param {string[]} columns
618 * Names of columns to be included when retrieving pivot table rows.
619 * @returns {Collection}
620 * Self, this method is chainable.
621 */
622 withPivot: function(columns) {
623 this.relatedData.withPivot(columns);
624 return this;
625 },
626
627 // Helper for handling either the `attach` or `detach` call on
628 // the `belongsToMany` or `hasOne` / `hasMany` :through relationship.
629 _handler: Promise.method(function(method, ids, options) {
630 const pending = [];
631 if (ids == null) {
632 if (method === 'insert') return Promise.resolve(this);
633 if (method === 'delete') pending.push(this._processPivot(method, null, options));
634 }
635 if (!Array.isArray(ids)) ids = ids ? [ids] : [];
636 _.each(ids, (id) => pending.push(this._processPivot(method, id, options)));
637 return Promise.all(pending).return(this);
638 }),
639
640 // Handles preparing the appropriate constraints and then delegates
641 // the database interaction to _processPlainPivot for non-.through()
642 // pivot definitions, or _processModelPivot for .through() models.
643 // Returns a promise.
644 _processPivot: Promise.method(function(method, item) {
645 const relatedData = this.relatedData,
646 args = Array.prototype.slice.call(arguments),
647 fks = {},
648 data = {};
649
650 fks[relatedData.key('foreignKey')] = relatedData.parentFk;
651
652 // If the item is an object, it's either a model
653 // that we're looking to attach to this model, or
654 // a hash of attributes to set in the relation.
655 if (_.isObject(item)) {
656 if (item instanceof ModelBase) {
657 fks[relatedData.key('otherKey')] = item.id;
658 } else if (method !== 'update') {
659 _.extend(data, item);
660 }
661 } else if (item) {
662 fks[relatedData.key('otherKey')] = item;
663 }
664
665 args.push(_.extend(data, fks), fks);
666
667 if (this.relatedData.throughTarget) {
668 return this._processModelPivot.apply(this, args);
669 }
670
671 return this._processPlainPivot.apply(this, args);
672 }),
673
674 // Applies constraints to the knex builder and handles shelling out
675 // to either the `insert` or `delete` call for the current model,
676 // returning a promise.
677 _processPlainPivot: Promise.method(function(method, item, options, data) {
678 const relatedData = this.relatedData;
679
680 // Grab the `knex` query builder for the current model, and
681 // check if we have any additional constraints for the query.
682 const builder = this._builder(relatedData.joinTable());
683 if (options && options.query) {
684 Helpers.query.call(null, {_knex: builder}, [options.query]);
685 }
686
687 if (options) {
688 if (options.transacting) builder.transacting(options.transacting);
689 if (options.debug) builder.debug();
690 }
691
692 const collection = this;
693 if (method === 'delete') {
694 return builder
695 .where(data)
696 .del()
697 .then(function() {
698 if (!item) return collection.reset();
699 const model = collection.get(data[relatedData.key('otherKey')]);
700 if (model) {
701 collection.remove(model);
702 }
703 });
704 }
705 if (method === 'update') {
706 return builder
707 .where(data)
708 .update(item)
709 .then(function(numUpdated) {
710 if (options && options.require === true && numUpdated === 0) {
711 throw new Error('No rows were updated');
712 }
713 return numUpdated;
714 });
715 }
716
717 return this.triggerThen('creating', this, data, options).then(function() {
718 return builder.insert(data).then(function() {
719 collection.add(item);
720 });
721 });
722 }),
723
724 // Loads or prepares a pivot model based on the constraints and deals with
725 // pivot model changes by calling the appropriate Bookshelf Model API
726 // methods. Returns a promise.
727 _processModelPivot: Promise.method(function(method, item, options, data, fks) {
728 const relatedData = this.relatedData,
729 JoinModel = relatedData.throughTarget,
730 joinModel = new JoinModel();
731
732 fks = joinModel.parse(fks);
733 data = joinModel.parse(data);
734
735 if (method === 'insert') {
736 return joinModel.set(data).save(null, options);
737 }
738
739 return joinModel
740 .set(fks)
741 .fetch({
742 require: true
743 })
744 .then(function(instance) {
745 if (method === 'delete') {
746 return instance.destroy(options);
747 }
748 return instance.save(item, options);
749 });
750 })
751};
752
753module.exports = Relation;