UNPKG

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