@depup/bookshelf 1.2.0-depup.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +18 -0
- package/.nycrc.yml +3 -0
- package/.prettierrc +8 -0
- package/CHANGELOG.md +764 -0
- package/LICENSE +22 -0
- package/README.md +32 -0
- package/bookshelf.js +8 -0
- package/changes.json +14 -0
- package/lib/base/collection.js +802 -0
- package/lib/base/eager.js +111 -0
- package/lib/base/events.js +129 -0
- package/lib/base/model.js +995 -0
- package/lib/base/relation.js +69 -0
- package/lib/bookshelf.js +558 -0
- package/lib/collection.js +545 -0
- package/lib/constants.js +2 -0
- package/lib/eager.js +122 -0
- package/lib/errors.js +36 -0
- package/lib/extend.js +41 -0
- package/lib/helpers.js +212 -0
- package/lib/model.js +1568 -0
- package/lib/relation.js +947 -0
- package/lib/sync.js +244 -0
- package/package.json +111 -0
package/lib/relation.js
ADDED
|
@@ -0,0 +1,947 @@
|
|
|
1
|
+
const _ = require('lodash');
|
|
2
|
+
const inflection = require('inflection');
|
|
3
|
+
|
|
4
|
+
const Helpers = require('./helpers');
|
|
5
|
+
const ModelBase = require('./base/model');
|
|
6
|
+
const RelationBase = require('./base/relation');
|
|
7
|
+
const Promise = require('bluebird');
|
|
8
|
+
const constants = require('./constants');
|
|
9
|
+
const push = Array.prototype.push;
|
|
10
|
+
const removePivotPrefix = (key) => key.slice(constants.PIVOT_PREFIX.length);
|
|
11
|
+
const hasPivotPrefix = (key) => _.startsWith(key, constants.PIVOT_PREFIX);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @classdesc
|
|
15
|
+
* Used internally, the `Relation` class helps in simplifying the relationship building,
|
|
16
|
+
* centralizing all logic dealing with type and option handling.
|
|
17
|
+
*
|
|
18
|
+
* @extends RelationBase
|
|
19
|
+
* @class
|
|
20
|
+
*/
|
|
21
|
+
const Relation = RelationBase.extend(
|
|
22
|
+
/** @lends Relation.prototype */ {
|
|
23
|
+
/**
|
|
24
|
+
* Assembles the new model or collection we're creating an instance of, gathering any relevant
|
|
25
|
+
* primitives from the parent object without keeping any hard references.
|
|
26
|
+
*
|
|
27
|
+
* @param {Model} parent The parent to which this relation belongs to.
|
|
28
|
+
* @return {Model|Collection|Object} The new model or collection instance.
|
|
29
|
+
*/
|
|
30
|
+
init(parent) {
|
|
31
|
+
this.parentId = parent.id;
|
|
32
|
+
this.parentTableName = _.result(parent, 'tableName');
|
|
33
|
+
this.parentIdAttribute = this.attribute('parentIdAttribute', parent);
|
|
34
|
+
|
|
35
|
+
// Use formatted attributes so that morphKey and foreignKey will match attribute keys.
|
|
36
|
+
this.parentAttributes = parent.format(_.clone(parent.attributes));
|
|
37
|
+
|
|
38
|
+
if (this.type === 'morphTo' && !parent._isEager) {
|
|
39
|
+
// If the parent object is eager loading, and it's a polymorphic `morphTo` relation, we
|
|
40
|
+
// can't know what the target will be until the models are sorted and matched.
|
|
41
|
+
this.target = Helpers.morphCandidate(this.candidates, this.parentAttributes[this.key('morphKey')]);
|
|
42
|
+
this.targetTableName = _.result(this.target.prototype, 'tableName');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this.targetIdAttribute = this.attribute('targetIdAttribute', parent);
|
|
46
|
+
this.parentFk = this.attribute('parentFk');
|
|
47
|
+
|
|
48
|
+
const target = this.target ? this.relatedInstance() : {};
|
|
49
|
+
target.relatedData = this;
|
|
50
|
+
|
|
51
|
+
if (this.type === 'belongsToMany') {
|
|
52
|
+
_.extend(target, PivotHelpers);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return target;
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Initializes a `through` relation, setting the `Target` model and `options`, which includes
|
|
60
|
+
* any additional keys for the relation.
|
|
61
|
+
*
|
|
62
|
+
* @param {Model|Collection} source
|
|
63
|
+
* @param {Model} Target The pivot model the related models or collections run through.
|
|
64
|
+
* @param {object} options Additional properties to set on the relation object.
|
|
65
|
+
*/
|
|
66
|
+
through(source, Target, options) {
|
|
67
|
+
const type = this.type;
|
|
68
|
+
if (type !== 'hasOne' && type !== 'hasMany' && type !== 'belongsToMany' && type !== 'belongsTo') {
|
|
69
|
+
throw new Error('`through` is only chainable from `hasOne`, `belongsTo`, `hasMany`, or `belongsToMany`');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this.throughTarget = Target;
|
|
73
|
+
this.throughTableName = _.result(Target.prototype, 'tableName');
|
|
74
|
+
|
|
75
|
+
_.extend(this, options);
|
|
76
|
+
_.extend(source, PivotHelpers);
|
|
77
|
+
|
|
78
|
+
this.parentIdAttribute = this.attribute('parentIdAttribute');
|
|
79
|
+
this.targetIdAttribute = this.attribute('targetIdAttribute');
|
|
80
|
+
this.throughIdAttribute = this.attribute('throughIdAttribute', Target);
|
|
81
|
+
this.parentFk = this.attribute('parentFk');
|
|
82
|
+
|
|
83
|
+
// Set the appropriate foreign key if we're doing a belongsToMany, for convenience.
|
|
84
|
+
if (this.type === 'belongsToMany') {
|
|
85
|
+
this.foreignKey = this.throughForeignKey;
|
|
86
|
+
} else if (this.otherKey) {
|
|
87
|
+
this.foreignKey = this.otherKey;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return source;
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Generates and returns a specified key.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} keyName
|
|
97
|
+
* Can be one of `foreignKey`, `morphKey`, `morphValue`, `otherKey` or `throughForeignKey`.
|
|
98
|
+
* @return {string|undefined}
|
|
99
|
+
*/
|
|
100
|
+
key(keyName) {
|
|
101
|
+
if (this[keyName]) return this[keyName];
|
|
102
|
+
switch (keyName) {
|
|
103
|
+
case 'otherKey':
|
|
104
|
+
this[keyName] = singularMemo(this.targetTableName) + '_' + this.targetIdAttribute;
|
|
105
|
+
break;
|
|
106
|
+
case 'throughForeignKey':
|
|
107
|
+
this[keyName] = singularMemo(this.joinTable()) + '_' + this.throughIdAttribute;
|
|
108
|
+
break;
|
|
109
|
+
case 'foreignKey':
|
|
110
|
+
switch (this.type) {
|
|
111
|
+
case 'morphTo': {
|
|
112
|
+
const idKeyName = this.columnNames && this.columnNames[1] ? this.columnNames[1] : this.morphName + '_id';
|
|
113
|
+
this[keyName] = idKeyName;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case 'belongsTo':
|
|
117
|
+
this[keyName] = singularMemo(this.targetTableName) + '_' + this.targetIdAttribute;
|
|
118
|
+
break;
|
|
119
|
+
default:
|
|
120
|
+
if (this.isMorph()) {
|
|
121
|
+
this[keyName] = this.columnNames && this.columnNames[1] ? this.columnNames[1] : this.morphName + '_id';
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
this[keyName] = singularMemo(this.parentTableName) + '_' + this.parentIdAttribute;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
break;
|
|
128
|
+
case 'morphKey':
|
|
129
|
+
this[keyName] = this.columnNames && this.columnNames[0] ? this.columnNames[0] : this.morphName + '_type';
|
|
130
|
+
break;
|
|
131
|
+
case 'morphValue':
|
|
132
|
+
this[keyName] = this.morphValue || this.parentTableName || this.targetTableName;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
return this[keyName];
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get the correct name for the following attributes:
|
|
140
|
+
* - parentIdAttribute
|
|
141
|
+
* - targetIdAttribute
|
|
142
|
+
* - throughIdAttribute
|
|
143
|
+
* - parentFk
|
|
144
|
+
*
|
|
145
|
+
* @param {string} attribute The attribute name being requested.
|
|
146
|
+
* @param {Model} [parent] The parent model.
|
|
147
|
+
* @return {string}
|
|
148
|
+
*/
|
|
149
|
+
attribute(attribute, parent) {
|
|
150
|
+
switch (attribute) {
|
|
151
|
+
case 'parentIdAttribute':
|
|
152
|
+
if (this.isThrough()) {
|
|
153
|
+
if (this.type === 'belongsTo' && this.throughForeignKey) {
|
|
154
|
+
return this.throughForeignKey;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (this.type === 'belongsToMany' && this.isThroughForeignKeyTargeted()) {
|
|
158
|
+
return this.throughForeignKeyTarget;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (this.isOtherKeyTargeted()) {
|
|
162
|
+
return this.otherKeyTarget;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return this.parentIdAttribute; // Return attribute calculated on `init` by default.
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (this.type === 'belongsTo' && this.foreignKey) {
|
|
169
|
+
return this.foreignKey;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (this.type !== 'belongsTo' && this.isForeignKeyTargeted()) {
|
|
173
|
+
return this.foreignKeyTarget;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return _.result(parent, 'idAttribute');
|
|
177
|
+
|
|
178
|
+
case 'targetIdAttribute':
|
|
179
|
+
if (this.isThrough()) {
|
|
180
|
+
if ((this.type === 'belongsToMany' || this.type === 'belongsTo') && this.isOtherKeyTargeted()) {
|
|
181
|
+
return this.otherKeyTarget;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return this.targetIdAttribute; // Return attribute calculated on `init` by default.
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (this.type === 'morphTo' && !parent._isEager) {
|
|
188
|
+
return _.result(this.target.prototype, 'idAttribute');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (this.type === 'belongsTo' && this.isForeignKeyTargeted()) {
|
|
192
|
+
return this.foreignKeyTarget;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (this.type === 'belongsToMany' && this.isOtherKeyTargeted()) {
|
|
196
|
+
return this.otherKeyTarget;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return this.targetIdAttribute;
|
|
200
|
+
|
|
201
|
+
case 'throughIdAttribute':
|
|
202
|
+
if (this.type !== 'belongsToMany' && this.isThroughForeignKeyTargeted()) {
|
|
203
|
+
return this.throughForeignKeyTarget;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (this.type === 'belongsToMany' && this.throughForeignKey) {
|
|
207
|
+
return this.throughForeignKey;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return _.result(parent.prototype, 'idAttribute');
|
|
211
|
+
|
|
212
|
+
case 'parentFk':
|
|
213
|
+
if (!this.hasParentAttributes()) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (this.isThrough()) {
|
|
218
|
+
if (this.type === 'belongsToMany' && this.isThroughForeignKeyTargeted()) {
|
|
219
|
+
return this.parentAttributes[this.throughForeignKeyTarget];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (this.type === 'belongsTo') {
|
|
223
|
+
return this.throughForeignKey ? this.parentAttributes[this.parentIdAttribute] : this.parentId;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (this.isOtherKeyTargeted()) {
|
|
227
|
+
return this.parentAttributes[this.otherKeyTarget];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return this.parentFk; // Return attribute calculated on `init` by default.
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return this.parentAttributes[this.isInverse() ? this.key('foreignKey') : this.parentIdAttribute];
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Injects the necessary `select` constraints into a `knex` query builder.
|
|
239
|
+
*
|
|
240
|
+
* @param {Knex} knex Knex instance.
|
|
241
|
+
* @param {object} options
|
|
242
|
+
* @return {undefined}
|
|
243
|
+
*/
|
|
244
|
+
selectConstraints(knex, options) {
|
|
245
|
+
const 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 (Array.isArray(options.columns)) {
|
|
255
|
+
knex.columns(options.columns);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const currentColumns = _.find(knex._statements, {grouping: 'columns'});
|
|
259
|
+
|
|
260
|
+
if (!currentColumns || currentColumns.length === 0) {
|
|
261
|
+
knex.distinct(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 limit the query to a single item.
|
|
267
|
+
if (this.isSingle() && !resp) knex.limit(1);
|
|
268
|
+
|
|
269
|
+
// Finally, add (and validate) the WHERE conditions necessary for constraining the relation.
|
|
270
|
+
this.whereClauses(knex, resp);
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Injects and validates necessary `through` constraints for the current model.
|
|
275
|
+
*
|
|
276
|
+
* @param {Knex} knex Knex instance.
|
|
277
|
+
* @return {undefined}
|
|
278
|
+
*/
|
|
279
|
+
joinColumns(knex) {
|
|
280
|
+
const columns = [];
|
|
281
|
+
const joinTable = this.joinTable();
|
|
282
|
+
if (this.isThrough()) columns.push(this.throughIdAttribute);
|
|
283
|
+
columns.push(this.key('foreignKey'));
|
|
284
|
+
if (this.type === 'belongsToMany') columns.push(this.key('otherKey'));
|
|
285
|
+
push.apply(columns, this.pivotColumns);
|
|
286
|
+
knex.columns(
|
|
287
|
+
_.map(columns, function(col) {
|
|
288
|
+
return joinTable + '.' + col + ' as _pivot_' + col;
|
|
289
|
+
})
|
|
290
|
+
);
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Generates the join clauses necessary for the current relation.
|
|
295
|
+
*
|
|
296
|
+
* @param {Knex} knex Knex instance.
|
|
297
|
+
* @return {undefined}
|
|
298
|
+
*/
|
|
299
|
+
joinClauses(knex) {
|
|
300
|
+
const joinTable = this.joinTable();
|
|
301
|
+
|
|
302
|
+
if (this.type === 'belongsTo' || this.type === 'belongsToMany') {
|
|
303
|
+
const targetKey = this.type === 'belongsTo' ? this.key('foreignKey') : this.key('otherKey');
|
|
304
|
+
|
|
305
|
+
knex.join(joinTable, joinTable + '.' + targetKey, '=', this.targetTableName + '.' + this.targetIdAttribute);
|
|
306
|
+
|
|
307
|
+
// A `belongsTo` -> `through` is currently the only relation with two joins.
|
|
308
|
+
if (this.type === 'belongsTo') {
|
|
309
|
+
knex.join(
|
|
310
|
+
this.parentTableName,
|
|
311
|
+
joinTable + '.' + this.throughIdAttribute,
|
|
312
|
+
'=',
|
|
313
|
+
this.parentTableName + '.' + this.key('throughForeignKey')
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
knex.join(
|
|
318
|
+
joinTable,
|
|
319
|
+
joinTable + '.' + this.throughIdAttribute,
|
|
320
|
+
'=',
|
|
321
|
+
this.targetTableName + '.' + this.key('throughForeignKey')
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Check that there isn't an incorrect foreign key set, versus the one passed in when the
|
|
328
|
+
* relation was formed.
|
|
329
|
+
*
|
|
330
|
+
* @param {Knex} knex Knex instance.
|
|
331
|
+
* @param {object} response
|
|
332
|
+
* @return {undefined}
|
|
333
|
+
*/
|
|
334
|
+
whereClauses(knex, response) {
|
|
335
|
+
let key;
|
|
336
|
+
|
|
337
|
+
if (this.isJoined()) {
|
|
338
|
+
const isBelongsTo = this.type === 'belongsTo';
|
|
339
|
+
const targetTable = isBelongsTo ? this.parentTableName : this.joinTable();
|
|
340
|
+
|
|
341
|
+
const column = isBelongsTo ? this.parentIdAttribute : this.key('foreignKey');
|
|
342
|
+
|
|
343
|
+
key = `${targetTable}.${column}`;
|
|
344
|
+
} else {
|
|
345
|
+
const column = this.isInverse() ? this.targetIdAttribute : this.key('foreignKey');
|
|
346
|
+
|
|
347
|
+
key = `${this.targetTableName}.${column}`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const method = response ? 'whereIn' : 'where';
|
|
351
|
+
const ids = response ? this.eagerKeys(response) : this.parentFk;
|
|
352
|
+
knex[method](key, ids);
|
|
353
|
+
|
|
354
|
+
if (this.isMorph()) {
|
|
355
|
+
const table = this.targetTableName;
|
|
356
|
+
const key = this.key('morphKey');
|
|
357
|
+
const value = this.key('morphValue');
|
|
358
|
+
knex.where(`${table}.${key}`, value);
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Fetches all eagerly loaded foreign keys from the current relation.
|
|
364
|
+
*
|
|
365
|
+
* @param {object} response
|
|
366
|
+
* @return {array} The requested eager keys.
|
|
367
|
+
*/
|
|
368
|
+
eagerKeys(response) {
|
|
369
|
+
const key = this.isInverse() && !this.isThrough() ? this.key('foreignKey') : this.parentIdAttribute;
|
|
370
|
+
return _.reject(
|
|
371
|
+
_(response)
|
|
372
|
+
.map(key)
|
|
373
|
+
.uniq()
|
|
374
|
+
.value(),
|
|
375
|
+
_.isNil
|
|
376
|
+
);
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Generates the appropriate default join table name for a
|
|
381
|
+
* {@link Model#belongsToMany belongsToMany} or {@link Model#through through} relation.
|
|
382
|
+
* The default name is composed of the two table names ordered alphabetically and joined by an
|
|
383
|
+
* underscore.
|
|
384
|
+
*
|
|
385
|
+
* @return {string} The table name.
|
|
386
|
+
*/
|
|
387
|
+
joinTable() {
|
|
388
|
+
if (this.isThrough()) return this.throughTableName;
|
|
389
|
+
return this.joinTableName || [this.parentTableName, this.targetTableName].sort().join('_');
|
|
390
|
+
},
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Creates a new model or collection instance, depending on the `relatedData` settings and the
|
|
394
|
+
* models passed in.
|
|
395
|
+
*
|
|
396
|
+
* @param {Model[]} [models]
|
|
397
|
+
* @param {object} [options]
|
|
398
|
+
* @return {Model|Collection} The new instance.
|
|
399
|
+
*/
|
|
400
|
+
relatedInstance(models, options) {
|
|
401
|
+
models = models || [];
|
|
402
|
+
options = options || {};
|
|
403
|
+
const Target = this.target;
|
|
404
|
+
|
|
405
|
+
// If it's a single model, check whether there's already a model we can pick from, otherwise
|
|
406
|
+
// create a new instance.
|
|
407
|
+
if (this.isSingle()) {
|
|
408
|
+
if (!(Target.prototype instanceof ModelBase)) {
|
|
409
|
+
throw new Error(`The ${this.type} related object must be a Bookshelf.Model`);
|
|
410
|
+
}
|
|
411
|
+
return models[0] || new Target();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Allows us to just use a model, but create a temporary collection for a "*-many" relation.
|
|
415
|
+
if (Target.prototype instanceof ModelBase) {
|
|
416
|
+
return Target.collection(models, {
|
|
417
|
+
parse: true,
|
|
418
|
+
merge: options.merge,
|
|
419
|
+
remove: options.remove
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
return new Target(models, {parse: true});
|
|
423
|
+
},
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Groups the eagerly loaded relations according to the type of relationship we're handling for
|
|
427
|
+
* easy attachment to the parent models.
|
|
428
|
+
*
|
|
429
|
+
* @param {string} relationName The relation name being paired to its parent models.
|
|
430
|
+
* @param {Model[]} related The related models obtained from the eager load fetch call.
|
|
431
|
+
* @param {Model[]} parentModels The parent models of the eager fetched relation.
|
|
432
|
+
* @param {options} options Eager fetch query options.
|
|
433
|
+
* @return {Model[]} The eager fetch models.
|
|
434
|
+
*/
|
|
435
|
+
eagerPair(relationName, related, parentModels, options) {
|
|
436
|
+
// If this is a morphTo, we only want to pair on the morphValue for the current relation.
|
|
437
|
+
if (this.type === 'morphTo') {
|
|
438
|
+
parentModels = _.filter(parentModels, (m) => {
|
|
439
|
+
return m.get(this.key('morphKey')) === this.key('morphValue');
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// If this is a `through` or `belongsToMany` relation, we need to cleanup and setup the
|
|
444
|
+
// `interim` model.
|
|
445
|
+
if (this.isJoined()) related = this.parsePivot(related);
|
|
446
|
+
|
|
447
|
+
// Group all of the related models for easier association with their parent models.
|
|
448
|
+
const idKey = (key) => (_.isBuffer(key) ? key.toString('hex') : key);
|
|
449
|
+
const grouped = _.groupBy(related, (m) => {
|
|
450
|
+
let key;
|
|
451
|
+
if (m.pivot) {
|
|
452
|
+
if (this.isInverse() && this.isThrough()) {
|
|
453
|
+
key = this.isThroughForeignKeyTargeted() ? m.pivot.get(this.throughForeignKeyTarget) : m.pivot.id;
|
|
454
|
+
} else {
|
|
455
|
+
key = m.pivot.get(this.key('foreignKey'));
|
|
456
|
+
}
|
|
457
|
+
} else if (this.isInverse()) {
|
|
458
|
+
key = this.isForeignKeyTargeted() ? m.get(this.foreignKeyTarget) : m.id;
|
|
459
|
+
} else {
|
|
460
|
+
key = m.get(this.key('foreignKey'));
|
|
461
|
+
}
|
|
462
|
+
return idKey(key);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// Loop over the `parentModels` and attach the grouped sub-models, keeping the `relatedData`
|
|
466
|
+
// on the new related instance.
|
|
467
|
+
_.each(parentModels, (model) => {
|
|
468
|
+
let groupedKey;
|
|
469
|
+
if (!this.isInverse()) {
|
|
470
|
+
const parsedKey = Object.keys(model.parse({[this.parentIdAttribute]: null}))[0];
|
|
471
|
+
groupedKey = idKey(model.get(parsedKey));
|
|
472
|
+
} else {
|
|
473
|
+
const keyColumn = this.key(this.isThrough() ? 'throughForeignKey' : 'foreignKey');
|
|
474
|
+
const formatted = model.format(_.clone(model.attributes));
|
|
475
|
+
groupedKey = idKey(formatted[keyColumn]);
|
|
476
|
+
}
|
|
477
|
+
if (groupedKey != null) {
|
|
478
|
+
const relation = (model.relations[relationName] = this.relatedInstance(grouped[groupedKey], options));
|
|
479
|
+
if (this.type === 'belongsToMany') {
|
|
480
|
+
// If type is `belongsToMany` then the relatedData needs to be recreated through the
|
|
481
|
+
// parent model
|
|
482
|
+
relation.relatedData = model[relationName]().relatedData;
|
|
483
|
+
} else {
|
|
484
|
+
relation.relatedData = this;
|
|
485
|
+
}
|
|
486
|
+
if (this.isJoined()) _.extend(relation, PivotHelpers);
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
// Now that related models have been successfully paired, update each with its parsed
|
|
491
|
+
// attributes
|
|
492
|
+
related.map((model) => {
|
|
493
|
+
model.attributes = model.parse(model.attributes);
|
|
494
|
+
model.formatTimestamps()._previousAttributes = _.cloneDeep(model.attributes);
|
|
495
|
+
model._reset();
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
return related;
|
|
499
|
+
},
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Creates new pivot models in case any of the models being processed have pivot attributes.
|
|
503
|
+
* This is only true for models belonging to {@link Model#belongsToMany belongsToMany} and
|
|
504
|
+
* {@link Model#through through} relations. All other models will discard any existing pivot
|
|
505
|
+
* attributes if present.
|
|
506
|
+
*
|
|
507
|
+
* @param {Model[]} models List of models being processed.
|
|
508
|
+
* @return {Model[]} Parsed model list possibly containing additional pivot models.
|
|
509
|
+
*/
|
|
510
|
+
parsePivot(models) {
|
|
511
|
+
return _.map(models, (model) => {
|
|
512
|
+
// Separate pivot attributes.
|
|
513
|
+
const grouped = _.reduce(
|
|
514
|
+
model.attributes,
|
|
515
|
+
(acc, value, key) => {
|
|
516
|
+
if (hasPivotPrefix(key)) {
|
|
517
|
+
acc.pivot[removePivotPrefix(key)] = value;
|
|
518
|
+
} else {
|
|
519
|
+
acc.model[key] = value;
|
|
520
|
+
}
|
|
521
|
+
return acc;
|
|
522
|
+
},
|
|
523
|
+
{model: {}, pivot: {}}
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
// Assign non-pivot attributes to model.
|
|
527
|
+
model.attributes = grouped.model;
|
|
528
|
+
|
|
529
|
+
// If there are any pivot attributes create a new pivot model with these attributes.
|
|
530
|
+
if (!_.isEmpty(grouped.pivot)) {
|
|
531
|
+
const Through = this.throughTarget;
|
|
532
|
+
const tableName = this.joinTable();
|
|
533
|
+
model.pivot = Through != null ? new Through(grouped.pivot) : new this.Model(grouped.pivot, {tableName});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return model;
|
|
537
|
+
});
|
|
538
|
+
},
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Sets the pivot column names to be retrieved along with the current model. This allows for
|
|
542
|
+
* additional fields to be pulled from the joining table.
|
|
543
|
+
*
|
|
544
|
+
* @param {string|string[]} columns Extra column names to fetch.
|
|
545
|
+
* @return {undefined}
|
|
546
|
+
*/
|
|
547
|
+
withPivot(columns) {
|
|
548
|
+
if (!Array.isArray(columns)) columns = [columns];
|
|
549
|
+
this.pivotColumns = this.pivotColumns || [];
|
|
550
|
+
push.apply(this.pivotColumns, columns);
|
|
551
|
+
},
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Checks whether or not a relation is of the {@link Relation#through through} type.
|
|
555
|
+
*
|
|
556
|
+
* @return {boolean}
|
|
557
|
+
*/
|
|
558
|
+
isThrough() {
|
|
559
|
+
return this.throughTarget != null;
|
|
560
|
+
},
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Checks whether or not a relation has joins. Only {@link Model#belongsToMany belongsToMany}
|
|
564
|
+
* and {@link Model#through through} relations make use of joins currently.
|
|
565
|
+
*
|
|
566
|
+
* @return {boolean}
|
|
567
|
+
*/
|
|
568
|
+
isJoined() {
|
|
569
|
+
return this.type === 'belongsToMany' || this.isThrough();
|
|
570
|
+
},
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Checks whether or not a relation is of the {@link Model#morphOne morphOne} or
|
|
574
|
+
* {@link Model#morphMany morphMany} type.
|
|
575
|
+
*
|
|
576
|
+
* @return {boolean}
|
|
577
|
+
*/
|
|
578
|
+
isMorph() {
|
|
579
|
+
return this.type === 'morphOne' || this.type === 'morphMany';
|
|
580
|
+
},
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Checks whether or not a relation is of the single type (one to one).
|
|
584
|
+
*
|
|
585
|
+
* @return {boolean}
|
|
586
|
+
*/
|
|
587
|
+
isSingle() {
|
|
588
|
+
const type = this.type;
|
|
589
|
+
return type === 'hasOne' || type === 'belongsTo' || type === 'morphOne' || type === 'morphTo';
|
|
590
|
+
},
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Checks whether or not the relation is the inverse of a {@link Model#morphOne morphOne},
|
|
594
|
+
* {@link Model#morphMany morphMany}, {@link Model#hasOne hasOne} or
|
|
595
|
+
* {@link Model#hasMany hasMany} relation.
|
|
596
|
+
*
|
|
597
|
+
* @return {boolean}
|
|
598
|
+
*/
|
|
599
|
+
isInverse() {
|
|
600
|
+
return this.type === 'belongsTo' || this.type === 'morphTo';
|
|
601
|
+
},
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Checks whether or not the relation has a foreign key target set.
|
|
605
|
+
*
|
|
606
|
+
* @return {boolean}
|
|
607
|
+
*/
|
|
608
|
+
isForeignKeyTargeted() {
|
|
609
|
+
return this.foreignKeyTarget != null;
|
|
610
|
+
},
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Checks whether or not the {@link Model#through through} relation has a foreign key target
|
|
614
|
+
* set.
|
|
615
|
+
*
|
|
616
|
+
* @return {boolean}
|
|
617
|
+
*/
|
|
618
|
+
isThroughForeignKeyTargeted() {
|
|
619
|
+
return this.throughForeignKeyTarget != null;
|
|
620
|
+
},
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Checks whether or not the relation has a the `other` foreign key target set.
|
|
624
|
+
*
|
|
625
|
+
* @return {boolean}
|
|
626
|
+
*/
|
|
627
|
+
isOtherKeyTargeted() {
|
|
628
|
+
return this.otherKeyTarget != null;
|
|
629
|
+
},
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Checks whether or not the relation has the parent attributes set.
|
|
633
|
+
*
|
|
634
|
+
* @return {boolean}
|
|
635
|
+
*/
|
|
636
|
+
hasParentAttributes() {
|
|
637
|
+
return this.parentAttributes != null;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
// Simple memoization of the singularize call.
|
|
643
|
+
const singularMemo = (function() {
|
|
644
|
+
const cache = Object.create(null);
|
|
645
|
+
return function(arg) {
|
|
646
|
+
if (!(arg in cache)) {
|
|
647
|
+
cache[arg] = inflection.singularize(arg);
|
|
648
|
+
}
|
|
649
|
+
return cache[arg];
|
|
650
|
+
};
|
|
651
|
+
})();
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Specific to many-to-many relationships, these methods are mixed into the
|
|
655
|
+
* {@link Model#belongsToMany belongsToMany} relationships when they are created, providing helpers
|
|
656
|
+
* for attaching and detaching related models.
|
|
657
|
+
*
|
|
658
|
+
* @mixin
|
|
659
|
+
*/
|
|
660
|
+
const PivotHelpers = {
|
|
661
|
+
/**
|
|
662
|
+
* Attaches one or more `ids` or models from a foreign table to the current
|
|
663
|
+
* table, on a {@linkplain many-to-many} relation. Creates and saves a new
|
|
664
|
+
* model and attaches the model with the related model.
|
|
665
|
+
*
|
|
666
|
+
* var admin1 = new Admin({username: 'user1', password: 'test'});
|
|
667
|
+
* var admin2 = new Admin({username: 'user2', password: 'test'});
|
|
668
|
+
*
|
|
669
|
+
* Promise.all([admin1.save(), admin2.save()])
|
|
670
|
+
* .then(function() {
|
|
671
|
+
* return Promise.all([
|
|
672
|
+
* new Site({id: 1}).admins().attach([admin1, admin2]),
|
|
673
|
+
* new Site({id: 2}).admins().attach(admin2)
|
|
674
|
+
* ]);
|
|
675
|
+
* })
|
|
676
|
+
*
|
|
677
|
+
* This method (along with {@link Collection#detach} and {@link
|
|
678
|
+
* Collection#updatePivot}) are mixed in to a {@link Collection} when
|
|
679
|
+
* returned by a {@link Model#belongsToMany belongsToMany} relation.
|
|
680
|
+
*
|
|
681
|
+
* @method Collection#attach
|
|
682
|
+
* @param {mixed|mixed[]} ids
|
|
683
|
+
* One or more ID values or models to be attached to the relation.
|
|
684
|
+
* @param {Object} options
|
|
685
|
+
* A hash of options.
|
|
686
|
+
* @param {Transaction} options.transacting
|
|
687
|
+
* Optionally run the query in a transaction.
|
|
688
|
+
* @returns {Promise<Collection>}
|
|
689
|
+
* A promise resolving to the updated Collection where this method was called.
|
|
690
|
+
*/
|
|
691
|
+
attach(ids, options) {
|
|
692
|
+
return Promise.try(() => this.triggerThen('attaching', this, ids, options))
|
|
693
|
+
.then(() => this._handler('insert', ids, options))
|
|
694
|
+
.then((response) => this.triggerThen('attached', this, response, options))
|
|
695
|
+
.return(this);
|
|
696
|
+
},
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Detach one or more related objects from their pivot tables. If a model or
|
|
700
|
+
* id is passed, it attempts to remove from the pivot table based on that
|
|
701
|
+
* foreign key. If no parameters are specified, we assume we will detach all
|
|
702
|
+
* related associations.
|
|
703
|
+
*
|
|
704
|
+
* This method (along with {@link Collection#attach} and {@link
|
|
705
|
+
* Collection#updatePivot}) are mixed in to a {@link Collection} when returned
|
|
706
|
+
* by a {@link Model#belongsToMany belongsToMany} relation.
|
|
707
|
+
*
|
|
708
|
+
* @method Collection#detach
|
|
709
|
+
* @param {mixed|mixed[]} [ids]
|
|
710
|
+
* One or more ID values or models to be detached from the relation.
|
|
711
|
+
* @param {Object} options
|
|
712
|
+
* A hash of options.
|
|
713
|
+
* @param {Transaction} options.transacting
|
|
714
|
+
* Optionally run the query in a transaction.
|
|
715
|
+
* @returns {Promise<undefined>}
|
|
716
|
+
* A promise resolving to the updated Collection where this method was called.
|
|
717
|
+
*/
|
|
718
|
+
detach(ids, options) {
|
|
719
|
+
return Promise.try(() => this.triggerThen('detaching', this, ids, options))
|
|
720
|
+
.then(() => this._handler('delete', ids, options))
|
|
721
|
+
.then((response) => this.triggerThen('detached', this, response, options))
|
|
722
|
+
.return(this);
|
|
723
|
+
},
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* The `updatePivot` method is used exclusively on {@link Model#belongsToMany
|
|
727
|
+
* belongsToMany} relations, and allows for updating pivot rows on the joining
|
|
728
|
+
* table.
|
|
729
|
+
*
|
|
730
|
+
* This method (along with {@link Collection#attach} and {@link
|
|
731
|
+
* Collection#detach}) are mixed in to a {@link Collection} when returned
|
|
732
|
+
* by a {@link Model#belongsToMany belongsToMany} relation.
|
|
733
|
+
*
|
|
734
|
+
* @method Collection#updatePivot
|
|
735
|
+
* @param {Object} attributes
|
|
736
|
+
* Values to be set in the `update` query.
|
|
737
|
+
* @param {Object} [options]
|
|
738
|
+
* A hash of options.
|
|
739
|
+
* @param {function|Object} [options.query]
|
|
740
|
+
* Constrain the update query. Similar to the `method` argument to {@link
|
|
741
|
+
* Model#query}.
|
|
742
|
+
* @param {Boolean} [options.require=false]
|
|
743
|
+
* Causes promise to be rejected with an Error if no rows were updated.
|
|
744
|
+
* @param {Transaction} [options.transacting]
|
|
745
|
+
* Optionally run the query in a transaction.
|
|
746
|
+
* @returns {Promise<Number>}
|
|
747
|
+
* A promise resolving to number of rows updated.
|
|
748
|
+
*/
|
|
749
|
+
updatePivot: function(attributes, options) {
|
|
750
|
+
return this._handler('update', attributes, options);
|
|
751
|
+
},
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* The `withPivot` method is used exclusively on {@link Model#belongsToMany
|
|
755
|
+
* belongsToMany} relations, and allows for additional fields to be pulled
|
|
756
|
+
* from the joining table.
|
|
757
|
+
*
|
|
758
|
+
* var Tag = bookshelf.model('Tag', {
|
|
759
|
+
* comments: function() {
|
|
760
|
+
* return this.belongsToMany(Comment).withPivot(['created_at', 'order']);
|
|
761
|
+
* }
|
|
762
|
+
* });
|
|
763
|
+
*
|
|
764
|
+
* @method Collection#withPivot
|
|
765
|
+
* @param {string[]} columns
|
|
766
|
+
* Names of columns to be included when retrieving pivot table rows.
|
|
767
|
+
* @returns {Collection}
|
|
768
|
+
* Self, this method is chainable.
|
|
769
|
+
*/
|
|
770
|
+
withPivot: function(columns) {
|
|
771
|
+
this.relatedData.withPivot(columns);
|
|
772
|
+
return this;
|
|
773
|
+
},
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Helper for handling either the {@link Collection#attach attach} or
|
|
777
|
+
* {@link Collection#detach detach} call on the {@link Model#belongsToMany belongsToMany} or
|
|
778
|
+
* ({@link Model#hasOne hasOne}/{@link Model#hasMany hasMany}).{@link Model#through through}
|
|
779
|
+
* relationship.
|
|
780
|
+
*
|
|
781
|
+
* @private
|
|
782
|
+
* @param {string} method
|
|
783
|
+
* Type of query being handled. This will be `insert` for {@link Collection#attach attach}
|
|
784
|
+
* calls, `delete` for {@link Collection#detach detach} calls and `update` for
|
|
785
|
+
* {@link Collection#updatePivot updatePivot} calls.
|
|
786
|
+
* @param {mixed|null} The ids of the models to attach, detach or update.
|
|
787
|
+
* @param {object} [options] Query options.
|
|
788
|
+
* @return {Promise}
|
|
789
|
+
*/
|
|
790
|
+
_handler: Promise.method(function(method, ids, options) {
|
|
791
|
+
const pending = [];
|
|
792
|
+
if (ids == null) {
|
|
793
|
+
if (method === 'insert') return Promise.resolve(this);
|
|
794
|
+
if (method === 'delete') pending.push(this._processPivot(method, null, options));
|
|
795
|
+
}
|
|
796
|
+
if (!Array.isArray(ids)) ids = ids ? [ids] : [];
|
|
797
|
+
_.each(ids, (id) => pending.push(this._processPivot(method, id, options)));
|
|
798
|
+
return Promise.all(pending).return(this);
|
|
799
|
+
}),
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Handles preparing the appropriate constraints and then delegates the database interaction to
|
|
803
|
+
* `_processPlainPivot` for non-{@link Model#through through} pivot definitions, or
|
|
804
|
+
* `_processModelPivot` for {@link Model#through through} models.
|
|
805
|
+
*
|
|
806
|
+
* @private
|
|
807
|
+
* @param {string} method
|
|
808
|
+
* Type of query being handled. This will be `insert` for {@link Collection#attach attach}
|
|
809
|
+
* calls, `delete` for {@link Collection#detach detach} calls and `update` for
|
|
810
|
+
* {@link Collection#updatePivot updatePivot} calls.
|
|
811
|
+
* @param {Model|object|mixed} item
|
|
812
|
+
* The item can be an object, in which case it's either a model that we're looking to attach to
|
|
813
|
+
* this model, or a hash of attributes to set in the relation. Otherwise it's a foreign key.
|
|
814
|
+
* @return {Promise}
|
|
815
|
+
*/
|
|
816
|
+
_processPivot: Promise.method(function(method, item) {
|
|
817
|
+
const relatedData = this.relatedData,
|
|
818
|
+
args = Array.prototype.slice.call(arguments),
|
|
819
|
+
fks = {},
|
|
820
|
+
data = {};
|
|
821
|
+
|
|
822
|
+
fks[relatedData.key('foreignKey')] = relatedData.parentFk;
|
|
823
|
+
|
|
824
|
+
if (_.isObject(item)) {
|
|
825
|
+
if (item instanceof ModelBase) {
|
|
826
|
+
fks[relatedData.key('otherKey')] = item.id;
|
|
827
|
+
} else if (method !== 'update') {
|
|
828
|
+
_.extend(data, item);
|
|
829
|
+
}
|
|
830
|
+
} else if (item) {
|
|
831
|
+
fks[relatedData.key('otherKey')] = item;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
args.push(_.extend(data, fks), fks);
|
|
835
|
+
|
|
836
|
+
if (this.relatedData.throughTarget) {
|
|
837
|
+
return this._processModelPivot.apply(this, args);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
return this._processPlainPivot.apply(this, args);
|
|
841
|
+
}),
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Applies constraints to the knex builder and handles shelling out to either the `insert` or
|
|
845
|
+
* `delete` call for the current model.
|
|
846
|
+
*
|
|
847
|
+
* @private
|
|
848
|
+
* @param {string} method
|
|
849
|
+
* Type of query being handled. This will be `insert` for {@link Collection#attach attach}
|
|
850
|
+
* calls, `delete` for {@link Collection#detach detach} calls and `update` for
|
|
851
|
+
* {@link Collection#updatePivot updatePivot} calls.
|
|
852
|
+
* @param {Model|object|mixed} item
|
|
853
|
+
* The item can be an object, in which case it's either a model that we're looking to attach to
|
|
854
|
+
* this model, or a hash of attributes to set in the relation. Otherwise it's a foreign key.
|
|
855
|
+
* @param {object} [options] Query options.
|
|
856
|
+
* @param {object} [data] The model data to constrain the query or attach to the relation.
|
|
857
|
+
* @return {Promise}
|
|
858
|
+
*/
|
|
859
|
+
_processPlainPivot: Promise.method(function(method, item, options, data) {
|
|
860
|
+
const relatedData = this.relatedData;
|
|
861
|
+
|
|
862
|
+
// Grab the `knex` query builder for the current model, and
|
|
863
|
+
// check if we have any additional constraints for the query.
|
|
864
|
+
const builder = this._builder(relatedData.joinTable());
|
|
865
|
+
if (options && options.query) {
|
|
866
|
+
Helpers.query.call(null, {_knex: builder}, [options.query]);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
if (options) {
|
|
870
|
+
if (options.transacting) builder.transacting(options.transacting);
|
|
871
|
+
if (options.debug) builder.debug();
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const collection = this;
|
|
875
|
+
if (method === 'delete') {
|
|
876
|
+
return builder
|
|
877
|
+
.where(data)
|
|
878
|
+
.del()
|
|
879
|
+
.then(function() {
|
|
880
|
+
if (!item) return collection.reset();
|
|
881
|
+
const model = collection.get(data[relatedData.key('otherKey')]);
|
|
882
|
+
if (model) {
|
|
883
|
+
collection.remove(model);
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
if (method === 'update') {
|
|
888
|
+
return builder
|
|
889
|
+
.where(data)
|
|
890
|
+
.update(item)
|
|
891
|
+
.then(function(numUpdated) {
|
|
892
|
+
if (options && options.require === true && numUpdated === 0) {
|
|
893
|
+
throw new Error('No rows were updated');
|
|
894
|
+
}
|
|
895
|
+
return numUpdated;
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
return this.triggerThen('creating', this, data, options).then(function() {
|
|
900
|
+
return builder.insert(data).then(function() {
|
|
901
|
+
collection.add(item);
|
|
902
|
+
});
|
|
903
|
+
});
|
|
904
|
+
}),
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Loads or prepares a pivot model based on the constraints and deals with pivot model changes by
|
|
908
|
+
* calling the appropriate Bookshelf Model API methods.
|
|
909
|
+
*
|
|
910
|
+
* @private
|
|
911
|
+
* @param {string} method
|
|
912
|
+
* Type of query being handled. This will be `insert` for {@link Collection#attach attach}
|
|
913
|
+
* calls, `delete` for {@link Collection#detach detach} calls and `update` for
|
|
914
|
+
* {@link Collection#updatePivot updatePivot} calls.
|
|
915
|
+
* @param {Model|object|mixed} item
|
|
916
|
+
* The item can be an object, in which case it's either a model that we're looking to attach to
|
|
917
|
+
* this model, or a hash of attributes to set in the relation. Otherwise it's a foreign key.
|
|
918
|
+
* @param {object} options Query options.
|
|
919
|
+
* @param {object} data The model data to constrain the query or attach to the relation.
|
|
920
|
+
* @param {object} fks
|
|
921
|
+
* @return {Promise}
|
|
922
|
+
*/
|
|
923
|
+
_processModelPivot: Promise.method(function(method, item, options, data, fks) {
|
|
924
|
+
const relatedData = this.relatedData,
|
|
925
|
+
JoinModel = relatedData.throughTarget,
|
|
926
|
+
joinModel = new JoinModel();
|
|
927
|
+
|
|
928
|
+
fks = joinModel.parse(fks);
|
|
929
|
+
data = joinModel.parse(data);
|
|
930
|
+
|
|
931
|
+
if (method === 'insert') {
|
|
932
|
+
return joinModel.set(data).save(null, options);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
return joinModel
|
|
936
|
+
.set(fks)
|
|
937
|
+
.fetch()
|
|
938
|
+
.then(function(instance) {
|
|
939
|
+
if (method === 'delete') {
|
|
940
|
+
return instance.destroy(options);
|
|
941
|
+
}
|
|
942
|
+
return instance.save(item, options);
|
|
943
|
+
});
|
|
944
|
+
})
|
|
945
|
+
};
|
|
946
|
+
|
|
947
|
+
module.exports = Relation;
|