1 | ;
|
2 |
|
3 | const _ = require('lodash');
|
4 | const createError = require('create-error');
|
5 |
|
6 | const Sync = require('./sync');
|
7 | const Helpers = require('./helpers');
|
8 | const EagerRelation = require('./eager');
|
9 | const Errors = require('./errors');
|
10 |
|
11 | const ModelBase = require('./base/model');
|
12 | const Promise = require('bluebird');
|
13 |
|
14 | /**
|
15 | * @class Model
|
16 | * @extends ModelBase
|
17 | * @inheritdoc
|
18 | * @classdesc
|
19 | * Models are simple objects representing individual database rows, specifying
|
20 | * the tableName and any relations to other models. They can be extended with
|
21 | * any domain-specific methods, which can handle components such as validations,
|
22 | * computed properties, and access control.
|
23 | *
|
24 | * @constructor
|
25 | * @description
|
26 | * When defining a model you should use the {@link Bookshelf#model bookshelf.model} method, since it will allow you to
|
27 | * avoid circular dependency problems. However, it's still possible to create models using the regular constructor.
|
28 | *
|
29 | * When creating an instance of a model, you can pass in the initial values of
|
30 | * the attributes, which will be {@link Model#set set} on the
|
31 | * model. If you define an {@link initialize} function, it will be invoked
|
32 | * when the model is created.
|
33 | *
|
34 | * new Book({
|
35 | * title: "One Thousand and One Nights",
|
36 | * author: "Scheherazade"
|
37 | * });
|
38 | *
|
39 | * In rare cases, if you're looking to get fancy, you may want to override
|
40 | * {@link Model#constructor constructor}, which allows you to replace the
|
41 | * actual constructor function for your model.
|
42 | *
|
43 | * let Book = bookshelf.model('Book', {
|
44 | * tableName: 'documents',
|
45 | * constructor: function() {
|
46 | * bookshelf.Model.apply(this, arguments);
|
47 | * this.on('saving', function(model, attrs, options) {
|
48 | * options.query.where('type', '=', 'book');
|
49 | * });
|
50 | * }
|
51 | * });
|
52 | *
|
53 | * @param {Object} attributes Initial values for this model's attributes.
|
54 | * @param {Object=} options Hash of options.
|
55 | * @param {string=} options.tableName Initial value for {@link Model#tableName tableName}.
|
56 | * @param {Boolean=} [options.hasTimestamps=false]
|
57 | *
|
58 | * Initial value for {@link Model#hasTimestamps hasTimestamps}.
|
59 | *
|
60 | * @param {Boolean} [options.parse=false]
|
61 | *
|
62 | * Convert attributes by {@link Model#parse parse} before being {@link
|
63 | * Model#set set} on the model.
|
64 | *
|
65 | */
|
66 | const BookshelfModel = ModelBase.extend(
|
67 | {
|
68 | /**
|
69 | * This relation specifies that this table has exactly one of another type of object, specified by a foreign key in
|
70 | * the other table.
|
71 | *
|
72 | * @example
|
73 | * const Record = bookshelf.model('Record', {
|
74 | * tableName: 'health_records'
|
75 | * })
|
76 | *
|
77 | * const Patient = bookshelf.model('Patient', {
|
78 | * tableName: 'patients',
|
79 | * record() {
|
80 | * return this.hasOne('Record')
|
81 | * }
|
82 | * })
|
83 | *
|
84 | * // select * from `health_records` where `patient_id` = 1
|
85 | * new Patient({id: 1}).related('record').fetch().then(function(model) {
|
86 | * // ...
|
87 | * })
|
88 | *
|
89 | * // Alternatively, if you don't need the relation loaded on the patient's relations hash:
|
90 | * new Patient({id: 1}).record().fetch().then(function(model) {
|
91 | * // ...
|
92 | * })
|
93 | *
|
94 | * @method Model#hasOne
|
95 | * @param {Model|string} Target
|
96 | * Constructor of {@link Model} targeted by join. Can be a string specifying a previously registered model with
|
97 | * {@link Bookshelf#model}.
|
98 | * @param {string} [foreignKey]
|
99 | * Foreign key in the `Target` model. By default the foreign key is assumed to be the singular form of this
|
100 | * model's {@link Model#tableName tableName} followed by `_id` / `_{{{@link Model#idAttribute idAttribute}}}`.
|
101 | * @param {string} [foreignKeyTarget]
|
102 | * Column in this model's table which `foreignKey` references, if other than this model's `id` /
|
103 | * `{@link Model#idAttribute idAttribute}`.
|
104 | * @returns {Model}
|
105 | * The return value will always be a model, even if the relation doesn't exist, but in that case the relation will
|
106 | * be `null` when {@link Model#serialize serializing} the model.
|
107 | */
|
108 | hasOne(Target, foreignKey, foreignKeyTarget) {
|
109 | return this._relation('hasOne', Target, {
|
110 | foreignKey,
|
111 | foreignKeyTarget
|
112 | }).init(this);
|
113 | },
|
114 |
|
115 | /**
|
116 | * This relation specifies that this model has one or more rows in another table which match on this model's primary
|
117 | * key.
|
118 | *
|
119 | * @example
|
120 | * const Author = bookshelf.model('Author', {
|
121 | * tableName: 'authors',
|
122 | * books() {
|
123 | * return this.hasMany('Book')
|
124 | * }
|
125 | * })
|
126 | *
|
127 | * // select * from `authors` where id = 1
|
128 | * // select * from `books` where author_id = 1
|
129 | * Author.where({id: 1}).fetch({withRelated: ['books']}).then(function(author) {
|
130 | * console.log(JSON.stringify(author.related('books')))
|
131 | * })
|
132 | *
|
133 | * @method Model#hasMany
|
134 | * @param {Model|string} Target
|
135 | * Constructor of {@link Model} targeted by join. Can be a string specifying a previously registered model with
|
136 | * {@link Bookshelf#model}.
|
137 | * @param {string} [foreignKey]
|
138 | * ForeignKey in the `Target` model. By default, the foreign key is assumed to be the singular form of this
|
139 | * model's tableName, followed by `_id` / `_{{{@link Model#idAttribute idAttribute}}}`.
|
140 | * @param {string} [foreignKeyTarget]
|
141 | * Column in this model's table which `foreignKey` references, if other than this model's `id` /
|
142 | * `{@link Model#idAttribute idAttribute}`.
|
143 | * @returns {Collection} A new empty Collection.
|
144 | */
|
145 | hasMany(Target, foreignKey, foreignKeyTarget) {
|
146 | return this._relation('hasMany', Target, {
|
147 | foreignKey,
|
148 | foreignKeyTarget
|
149 | }).init(this);
|
150 | },
|
151 |
|
152 | /**
|
153 | * This relationship is used when a model is a member of another `Target` model.
|
154 | *
|
155 | * It can be used in {@tutorial one-to-one} associations as the inverse of a
|
156 | * {@link Model#hasOne hasOne}. It can also used in {@tutorial one-to-many} associations as the
|
157 | * inverse of {@link Model#hasMany hasMany}, and is the "one" side of that association. In both
|
158 | * cases, the belongsTo relationship is used for a model that is a member of another Target
|
159 | * model, referenced by the `foreignKey` attribute in the current model.
|
160 | *
|
161 | * @example
|
162 | * const Book = bookshelf.model('Book', {
|
163 | * tableName: 'books',
|
164 | * author() {
|
165 | * return this.belongsTo('Author')
|
166 | * }
|
167 | * })
|
168 | *
|
169 | * // select * from `books` where id = 1
|
170 | * // select * from `authors` where id = book.author_id
|
171 | * Book.where({id: 1}).fetch({withRelated: ['author']}).then((book) => {
|
172 | * console.log(JSON.stringify(book.related('author')))
|
173 | * })
|
174 | *
|
175 | * @method Model#belongsTo
|
176 | * @param {Model|string} Target
|
177 | * Constructor of {@link Model} targeted by the join. Can be a string specifying a previously registered model
|
178 | * with {@link Bookshelf#model}.
|
179 | * @param {string} [foreignKey]
|
180 | * Foreign key in this model. By default, the `foreignKey` is assumed to be the singular form
|
181 | * of the `Target` model's tableName, followed by `_id`, or
|
182 | * `_{{{@link Model#idAttribute idAttribute}}}` if the `idAttribute` property is set.
|
183 | * @param {string} [foreignKeyTarget]
|
184 | * Column in the `Target` model's table which `foreignKey` references. This is only needed in
|
185 | * case it's other than `Target` model's `id` / `{@link Model#idAttribute idAttribute}`.
|
186 | * @returns {Model}
|
187 | * The return value will always be a model, even if the relation doesn't exist, but in that
|
188 | * case the relation will be `null` when {@link Model#serialize serializing} the model.
|
189 | */
|
190 | belongsTo(Target, foreignKey, foreignKeyTarget) {
|
191 | return this._relation('belongsTo', Target, {
|
192 | foreignKey,
|
193 | foreignKeyTarget
|
194 | }).init(this);
|
195 | },
|
196 |
|
197 | /**
|
198 | * Defines a many-to-many relation, where the current model is joined to one or more of a
|
199 | * `Target` model through another table. The default name for the joining table is the two
|
200 | * models' table names joined by an underscore, and ordered alphabetically. For example, a
|
201 | * `users` table and an `accounts` table would have a joining table named `accounts_users`.
|
202 | *
|
203 | * The default key names in the joining table are the singular versions of the model table
|
204 | * names, followed by `_id` / `_{{{@link Model#idAttribute idAttribute}}}`. So in the above
|
205 | * example the columns in the joining table would be `user_id`, `account_id`, and `access`,
|
206 | * which is used as an example of how dynamic relations can be formed using different contexts.
|
207 | *
|
208 | * To customize the keys or the {@link Model#tableName tableName} used for the join table, you
|
209 | * may specify them in the arguments to the function call:
|
210 | *
|
211 | * this.belongsToMany(Account, 'users_accounts', 'userId', 'accountId')
|
212 | *
|
213 | * If you wish to create a belongsToMany association where the joining table has a primary key
|
214 | * and extra attributes in the model, you may create a `belongsToMany`
|
215 | * {@link Relation#through through} relation:
|
216 | *
|
217 | * const Doctor = bookshelf.model('Doctor', {
|
218 | * patients() {
|
219 | * return this.belongsToMany('Patient').through('Appointment')
|
220 | * }
|
221 | * })
|
222 | *
|
223 | * const Appointment = bookshelf.model('Appointment', {
|
224 | * patient() {
|
225 | * return this.belongsTo('Patient')
|
226 | * },
|
227 | * doctor() {
|
228 | * return this.belongsTo('Doctor')
|
229 | * }
|
230 | * })
|
231 | *
|
232 | * const Patient = bookshelf.model('Patient', {
|
233 | * doctors() {
|
234 | * return this.belongsToMany('Doctor').through('Appointment')
|
235 | * }
|
236 | * })
|
237 | *
|
238 | * Collections returned by a `belongsToMany` relation are decorated with several pivot helper
|
239 | * methods. If you need more information about these methods see
|
240 | * {@link Collection#attach attach}, {@link Collection#detach detach},
|
241 | * {@link Collection#updatePivot updatePivot} and {@link Collection#withPivot withPivot}.
|
242 | *
|
243 | * @example
|
244 | * const Account = bookshelf.model('Account', {
|
245 | * tableName: 'accounts'
|
246 | * })
|
247 | *
|
248 | * const User = bookshelf.model('User', {
|
249 | * tableName: 'users',
|
250 | * allAccounts() {
|
251 | * return this.belongsToMany('Account')
|
252 | * },
|
253 | * adminAccounts() {
|
254 | * return this.belongsToMany('Account').query({where: {access: 'admin'}})
|
255 | * },
|
256 | * viewAccounts() {
|
257 | * return this.belongsToMany('Account').query({where: {access: 'readonly'}})
|
258 | * }
|
259 | * })
|
260 | *
|
261 | * @method Model#belongsToMany
|
262 | * @param {Model|string} Target
|
263 | * Constructor of {@link Model} targeted by join. Can be a string specifying a previously registered model with
|
264 | * {@link Bookshelf#model}.
|
265 | * @param {string} [joinTableName]
|
266 | * Name of the joining table. Defaults to the two table names ordered alphabetically and
|
267 | * joined by an underscore.
|
268 | * @param {string} [foreignKey]
|
269 | * Foreign key in this model. By default, the `foreignKey` is assumed to be the singular form
|
270 | * of this model's tableName, followed by `_id` / `_{{{@link Model#idAttribute idAttribute}}}`.
|
271 | * @param {string} [otherKey]
|
272 | * Foreign key in the `Target` model. By default, this is assumed to be the singular form of
|
273 | * the `Target` model's tableName, followed by `_id` /
|
274 | * `_{{{@link Model#idAttribute idAttribute}}}`.
|
275 | * @param {string} [foreignKeyTarget]
|
276 | * Column in this model's table which `foreignKey` references. This is only needed if it's not
|
277 | * the default `id` / `{@link Model#idAttribute idAttribute}`.
|
278 | * @param {string} [otherKeyTarget]
|
279 | * Column in the `Target` model's table which `otherKey` references. This is only needed, if
|
280 | * it's not the expected default of the `Target` model's `id` /
|
281 | * `{@link Model#idAttribute idAttribute}`.
|
282 | * @returns {Collection}
|
283 | * A new empty collection that is decorated with extra pivot helper methods. See the
|
284 | * description below for more info.
|
285 | */
|
286 | belongsToMany(Target, joinTableName, foreignKey, otherKey, foreignKeyTarget, otherKeyTarget) {
|
287 | return this._relation('belongsToMany', Target, {
|
288 | joinTableName,
|
289 | foreignKey,
|
290 | otherKey,
|
291 | foreignKeyTarget,
|
292 | otherKeyTarget
|
293 | }).init(this);
|
294 | },
|
295 |
|
296 | /**
|
297 | * The {@link Model#morphOne morphOne} is used to signify a {@link oneToOne
|
298 | * one-to-one} {@link polymorphicRelation polymorphic relation} with
|
299 | * another `Target` model, where the `name` of the model is used to determine
|
300 | * which database table keys are used. The naming convention requires the
|
301 | * `name` prefix an `_id` and `_type` field in the database. So for the case
|
302 | * below the table names would be `imageable_type` and `imageable_id`. The
|
303 | * `morphValue` may be optionally set to store/retrieve a different value in
|
304 | * the `_type` column than the {@link Model#tableName}.
|
305 | *
|
306 | * let Site = bookshelf.model('Site', {
|
307 | * tableName: 'sites',
|
308 | * photo: function() {
|
309 | * return this.morphOne('Photo', 'imageable');
|
310 | * }
|
311 | * });
|
312 | *
|
313 | * And with custom `columnNames`:
|
314 | *
|
315 | * let Site = bookshelf.model('Site', {
|
316 | * tableName: 'sites',
|
317 | * photo: function() {
|
318 | * return this.morphOne('Photo', 'imageable', ['ImageableType', 'ImageableId']);
|
319 | * }
|
320 | * });
|
321 | *
|
322 | * Note that both `columnNames` and `morphValue` are optional arguments. How
|
323 | * your argument is treated when only one is specified, depends on the type.
|
324 | * If your argument is an array, it will be assumed to contain custom
|
325 | * `columnNames`. If it's not, it will be assumed to indicate a `morphValue`.
|
326 | *
|
327 | * @method Model#morphOne
|
328 | * @param {Model|string} Target
|
329 | * Constructor of {@link Model} targeted by join. Can be a string specifying a previously registered model with
|
330 | * {@link Bookshelf#model}.
|
331 | * @param {string} [name] Prefix for `_id` and `_type` columns.
|
332 | * @param {(string[])} [columnNames]
|
333 | * Array containing two column names, the first is the `_type` while the second is the `_id`.
|
334 | * @param {string} [morphValue=Target#{@link Model#tableName tableName}]
|
335 | * The string value associated with this relationship. Stored in the `_type` column of the polymorphic table.
|
336 | * Defaults to `Target#{@link Model#tableName tableName}`.
|
337 | * @returns {Model} The related model.
|
338 | */
|
339 | morphOne(Target, name, columnNames, morphValue) {
|
340 | return this._morphOneOrMany(Target, name, columnNames, morphValue, 'morphOne');
|
341 | },
|
342 |
|
343 | /**
|
344 | * {@link Model#morphMany morphMany} is essentially the same as a {@link
|
345 | * Model#morphOne morphOne}, but creating a {@link Collection collection}
|
346 | * rather than a {@link Model model} (similar to a {@link Model#hasOne
|
347 | * hasOne} vs. {@link Model#hasMany hasMany} relation).
|
348 | *
|
349 | * {@link Model#morphMany morphMany} is used to signify a {@link oneToMany
|
350 | * one-to-many} or {@link manyToMany many-to-many} {@link polymorphicRelation
|
351 | * polymorphic relation} with another `Target` model, where the `name` of the
|
352 | * model is used to determine which database table keys are used. The naming
|
353 | * convention requires the `name` prefix an `_id` and `_type` field in the
|
354 | * database. So for the case below the table names would be `imageable_type`
|
355 | * and `imageable_id`. The `morphValue` may be optionally set to
|
356 | * store/retrieve a different value in the `_type` column than the `Target`'s
|
357 | * {@link Model#tableName tableName}.
|
358 | *
|
359 | * let Post = bookshelf.model('Post', {
|
360 | * tableName: 'posts',
|
361 | * photos: function() {
|
362 | * return this.morphMany('Photo', 'imageable');
|
363 | * }
|
364 | * });
|
365 | *
|
366 | * And with custom columnNames:
|
367 | *
|
368 | * let Post = bookshelf.model('Post'{
|
369 | * tableName: 'posts',
|
370 | * photos: function() {
|
371 | * return this.morphMany('Photo', 'imageable', ['ImageableType', 'ImageableId']);
|
372 | * }
|
373 | * });
|
374 | *
|
375 | * @method Model#morphMany
|
376 | * @param {Model|string} Target
|
377 | * Constructor of {@link Model} targeted by join. Can be a string specifying a previously registered model with
|
378 | * {@link Bookshelf#model}.
|
379 | * @param {string} [name] Prefix for `_id` and `_type` columns.
|
380 | * @param {(string[])} [columnNames]
|
381 | * Array containing two column names, the first is the `_type` while the second is the `_id`.
|
382 | * @param {string} [morphValue=Target#{@link Model#tableName tablename}]
|
383 | * The string value associated with this relationship. Stored in the `_type` column of the polymorphic table.
|
384 | * Defaults to `Target`#{@link Model#tableName tablename}.
|
385 | * @returns {Collection} A collection of related models.
|
386 | */
|
387 | morphMany(Target, name, columnNames, morphValue) {
|
388 | return this._morphOneOrMany(Target, name, columnNames, morphValue, 'morphMany');
|
389 | },
|
390 |
|
391 | /**
|
392 | * This relation is used to specify the inverse of the {@link Model#morphOne morphOne} or
|
393 | * {@link Model#morphMany morphMany} relations, where the `targets` must be passed to signify which
|
394 | * {@link Model models} are the potential opposite end of the {@link polymorphicRelation polymorphic relation}:
|
395 | *
|
396 | * const Photo = bookshelf.model('Photo', {
|
397 | * tableName: 'photos',
|
398 | * imageable() {
|
399 | * return this.morphTo('imageable', 'Site', 'Post')
|
400 | * }
|
401 | * })
|
402 | *
|
403 | * And with custom column names:
|
404 | *
|
405 | * const Photo = bookshelf.model('Photo', {
|
406 | * tableName: 'photos',
|
407 | * imageable() {
|
408 | * return this.morphTo('imageable', ['ImageableType', 'ImageableId'], 'Site', 'Post')
|
409 | * }
|
410 | * })
|
411 | *
|
412 | * And with custom morphValues, the inverse of the `morphValue` of {@link Model#morphOne morphOne} and
|
413 | * {@link Model#morphMany morphMany}, where the `morphValues` may be optionally set to check against a different
|
414 | * value in the `_type` column other than the {@link Model#tableName}, for example, a more descriptive name, or a
|
415 | * name that betters adheres to whatever standard you are using for models:
|
416 | *
|
417 | * const Photo = bookshelf.model('Photo', {
|
418 | * tableName: 'photos',
|
419 | * imageable() {
|
420 | * return this.morphTo('imageable', ['Site', 'favicon'], ['Post', 'cover_photo'])
|
421 | * }
|
422 | * })
|
423 | *
|
424 | * @method Model#morphTo
|
425 | * @param {string} name Prefix for `_id` and `_type` columns.
|
426 | * @param {string[]} [columnNames]
|
427 | * Array containing two column names, where the first is the `_type` and the second is the `_id`.
|
428 | * @param {Model|string} [Target]
|
429 | * Constructor of {@link Model} targeted by join. Can be a string specifying a previously registered model with
|
430 | * {@link Bookshelf#model}.
|
431 | * @returns {Model} The related but empty model.
|
432 | */
|
433 | morphTo(morphName) {
|
434 | if (!_.isString(morphName)) throw new Error('The `morphTo` name must be specified.');
|
435 | let columnNames, candidates;
|
436 | if (arguments[1] == null || (Array.isArray(arguments[1]) && _.isString(arguments[1][0]))) {
|
437 | columnNames = arguments[1] || null; // may be `null` or `undefined`
|
438 | candidates = _.drop(arguments, 2);
|
439 | } else {
|
440 | columnNames = null;
|
441 | candidates = _.drop(arguments, 1);
|
442 | }
|
443 |
|
444 | candidates = _.map(candidates, (target) => {
|
445 | if (Array.isArray(target)) return target;
|
446 |
|
447 | // Set up the morphValue by default as the tableName
|
448 | return [target, _.result(target.prototype, 'tableName')];
|
449 | });
|
450 |
|
451 | return this._relation('morphTo', null, {morphName, columnNames, candidates}).init(this);
|
452 | },
|
453 |
|
454 | /**
|
455 | * Helps to create dynamic relations between {@link Model models} where a {@link Model#hasOne hasOne} or
|
456 | * {@link Model#belongsTo belongsTo} relation may run through another `Interim` model. This is exactly like the
|
457 | * equivalent {@link Collection#through collection method} except that it applies to the models that the above
|
458 | * mentioned relation methods return instead of collections.
|
459 | *
|
460 | * This method creates a pivot model, which it assigns to {@link Model#pivot model.pivot} after it is created. When
|
461 | * serializing the model with {@link Model#toJSON toJSON}, the pivot model is flattened to values prefixed with
|
462 | * `_pivot_`.
|
463 | *
|
464 | * A good example of where this would be useful is if a paragraph {@link Model#hasMany belongTo} a book *through* a
|
465 | * chapter. See the example above on how this can be expressed.
|
466 | *
|
467 | * @method Model#through
|
468 | * @example
|
469 | * const Chapter = bookshelf.model('Chapter', {
|
470 | * tableName: 'chapters',
|
471 | * paragraphs() {
|
472 | * return this.hasMany('Paragraph')
|
473 | * }
|
474 | * })
|
475 |
|
476 | * const Book = bookshelf.model('Book', {
|
477 | * tableName: 'books',
|
478 | * chapters() {
|
479 | * return this.hasMany('Chapter')
|
480 | * }
|
481 | * })
|
482 | *
|
483 | const Paragraph = bookshelf.model('Paragraph', {
|
484 | * tableName: 'paragraphs',
|
485 | * chapter() {
|
486 | * return this.belongsTo('Chapter')
|
487 | * },
|
488 | *
|
489 | * // Find the book where this paragraph is included, by passing through
|
490 | * // the "Chapter" model.
|
491 | * book() {
|
492 | * return this.belongsTo('Book').through('Chapter')
|
493 | * }
|
494 | * })
|
495 | *
|
496 | * @param {Model|string} Interim
|
497 | * Pivot model. Can be a string specifying a previously registered model with {@link Bookshelf#model}.
|
498 | * @param {string} [throughForeignKey]
|
499 | * Foreign key in this model. By default, the foreign key is assumed to be the singular form of the `Target`
|
500 | * model's tableName, followed by `_id` or `_{{{@link Model#idAttribute idAttribute}}}`.
|
501 | * @param {string} [otherKey]
|
502 | * Foreign key in the `Interim` model. By default, the other key is assumed to be the singular form of this
|
503 | * model's tableName, followed by `_id` / `_{{{@link Model#idAttribute idAttribute}}}`.
|
504 | * @param {string} [throughForeignKeyTarget]
|
505 | * Column in the `Target` model which `throughForeignKey` references, if other than `Target` model's `id` /
|
506 | * `{@link Model#idAttribute idAttribute}`.
|
507 | * @param {string} [otherKeyTarget]
|
508 | * Column in this model which `otherKey` references, if other than `id` / `{@link Model#idAttribute idAttribute}`.
|
509 | * @returns {Model} The related but empty Model.
|
510 | */
|
511 | through(Interim, throughForeignKey, otherKey, throughForeignKeyTarget, otherKeyTarget) {
|
512 | return this.relatedData.through(this, Interim, {
|
513 | throughForeignKey,
|
514 | otherKey,
|
515 | throughForeignKeyTarget,
|
516 | otherKeyTarget
|
517 | });
|
518 | },
|
519 |
|
520 | /**
|
521 | * @method Model#refresh
|
522 | * @since 0.8.2
|
523 | * @description
|
524 | *
|
525 | * Update the attributes of a model, fetching it by its primary key. If no
|
526 | * attribute matches its {@link Model#idAttribute idAttribute}, then fetch by
|
527 | * all available fields.
|
528 | *
|
529 | * @param {Object} options
|
530 | * A hash of options. See {@link Model#fetch} for details.
|
531 | * @returns {Promise<Model>}
|
532 | * A promise resolving to this model.
|
533 | */
|
534 | refresh(options = {}) {
|
535 | let attributes = {};
|
536 |
|
537 | // If this is new, we use all its attributes. Otherwise we just grab the primary key.
|
538 | if (this.isNew()) {
|
539 | attributes = this.attributes;
|
540 | } else {
|
541 | attributes[this.idAttribute] = this.attributes[this.idAttribute] || this.attributes[this.parsedIdAttribute()];
|
542 | }
|
543 |
|
544 | return this._doFetch(attributes, options).tap(() => {
|
545 | if (!options.silent) this._previousAttributes = _.cloneDeep(this.attributes);
|
546 | });
|
547 | },
|
548 |
|
549 | /**
|
550 | * This method is similar to {@link Model#fetchAll}, but fetches a single page of results as
|
551 | * specified by the limit (page size) and offset (page number).
|
552 | *
|
553 | * Any options that may be passed to {@link Model#fetchAll} may also be passed in the options
|
554 | * to this method. Additionally, to perform pagination, you may include **either** an `offset`
|
555 | * and `limit`, **or** a `page` and `pageSize`.
|
556 | *
|
557 | * By default, with no parameters or some missing parameters, `fetchPage` will use default
|
558 | * values of `{page: 1, pageSize: 10}`.
|
559 | *
|
560 | * @example
|
561 | * new Car()
|
562 | * .fetchPage({
|
563 | * pageSize: 15, // Defaults to 10 if not specified
|
564 | * page: 3, // Defaults to 1 if not specified
|
565 | * withRelated: ['engine'] // Passed to Model#fetchAll
|
566 | * })
|
567 | * .then(function(results) {
|
568 | * console.log(results) // Paginated results object with metadata example below
|
569 | * })
|
570 | *
|
571 | * // Pagination results:
|
572 | * {
|
573 | * models: [
|
574 | * // Regular bookshelf Collection
|
575 | * ],
|
576 | * // other standard Collection attributes
|
577 | * // ...
|
578 | * pagination: {
|
579 | * rowCount: 53, // Total number of rows found for the query before pagination
|
580 | * pageCount: 4, // Total number of pages of results
|
581 | * page: 3, // The requested page number
|
582 | * pageSize: 15 // The requested number of rows per page
|
583 | * }
|
584 | * }
|
585 | *
|
586 | * @method Model#fetchPage
|
587 | * @param {Object} [options]
|
588 | * Besides the basic options that can be passed to {@link Model#fetchAll}, there are some additional pagination
|
589 | * options that can be specified.
|
590 | * @param {number} [options.pageSize]
|
591 | * How many models to include in each page, defaulting to 10 if not specified. Used only together with the `page`
|
592 | * option.
|
593 | * @param {number} [options.page]
|
594 | * Page number to retrieve. If greater than the available rows it will return an empty Collection. The first page
|
595 | * is number `1`. Used only with the `pageSize` option.
|
596 | * @param {number} [options.limit]
|
597 | * How many models to include in each page, defaulting to 10 if not specified. Used only together with the
|
598 | * `offset` option.
|
599 | * @param {number} [options.offset]
|
600 | * Index to begin fetching results from. The default and initial value is `0`. Used only with the `limit` option.
|
601 | * @param {boolean} [options.debug=false]
|
602 | * Whether to enable debugging mode or not. When enabled will show information about the
|
603 | * queries being run.
|
604 | * @returns {Promise<Collection>}
|
605 | * Returns a Promise that will resolve to the paginated collection of models.
|
606 | */
|
607 | fetchPage(options = {}) {
|
608 | return Helpers.fetchPage.call(this, options);
|
609 | },
|
610 |
|
611 | /**
|
612 | * Fetches a {@link Model model} from the database, using any {@link
|
613 | * Model#attributes attributes} currently set on the model to constrain the
|
614 | * results.
|
615 | *
|
616 | * A {@link Model#event:fetching "fetching"} event will be fired just before the
|
617 | * record is fetched; a good place to hook into for validation. {@link
|
618 | * Model#event:fetched "fetched"} event will be fired when a record is
|
619 | * successfully retrieved.
|
620 | *
|
621 | * If you need to constrain the query performed by fetch, you can call
|
622 | * {@link Model#query query} or {@link Model#where where} before calling
|
623 | * fetch.
|
624 | *
|
625 | * // select * from `books` where `ISBN-13` = '9780440180296'
|
626 | * new Book({'ISBN-13': '9780440180296'})
|
627 | * .fetch()
|
628 | * .then(function(model) {
|
629 | * // outputs 'Slaughterhouse Five'
|
630 | * console.log(model.get('title'));
|
631 | * });
|
632 | *
|
633 | * If you'd like to only fetch specific columns, you may specify a `columns`
|
634 | * property in the `options` for the fetch call, or use
|
635 | * {@link Model#query query}, tapping into the
|
636 | * {@link https://knexjs.org/#Builder-column|Knex column} method to specify
|
637 | * which columns will be fetched.
|
638 | *
|
639 | * A single property, or an array of properties can be specified as a value for
|
640 | * the `withRelated` property. You can also execute callbacks on relations
|
641 | * queries (eg. for sorting a relation). The results of these relation queries
|
642 | * will be loaded into a {@link Model#relations relations} property on the
|
643 | * model, may be retrieved with the {@link Model#related related} method, and
|
644 | * will be serialized as properties on a {@link Model#toJSON toJSON} call
|
645 | * unless `{shallow: true}` is passed.
|
646 | *
|
647 | * let Book = bookshelf.model('Book', {
|
648 | * tableName: 'books',
|
649 | * editions: function() {
|
650 | * return this.hasMany('Edition');
|
651 | * },
|
652 | * chapters: function() {
|
653 | * return this.hasMany('Chapter');
|
654 | * },
|
655 | * genre: function() {
|
656 | * return this.belongsTo('Genre');
|
657 | * }
|
658 | * })
|
659 | *
|
660 | * new Book({'ISBN-13': '9780440180296'}).fetch({
|
661 | * withRelated: [
|
662 | * 'genre', 'editions',
|
663 | * { chapters: function(query) { query.orderBy('chapter_number'); }}
|
664 | * ]
|
665 | * }).then(function(book) {
|
666 | * console.log(book.related('genre').toJSON());
|
667 | * console.log(book.related('editions').toJSON());
|
668 | * console.log(book.toJSON());
|
669 | * });
|
670 | *
|
671 | * @method Model#fetch
|
672 | * @param {Object=} options Hash of options.
|
673 | * @param {Boolean=} [options.require=true]
|
674 | * Whether or not to reject the returned response with a
|
675 | * {@link Model.NotFoundError NotFoundError} if there are no results when
|
676 | * fetching. If set to `false` it will resolve with `null` instead.
|
677 | * @param {string|string[]} [options.columns='*']
|
678 | * Specify columns to be retrieved.
|
679 | * @param {Transaction} [options.transacting]
|
680 | * Optionally run the query in a transaction.
|
681 | * @param {string} [options.lock]
|
682 | * Type of row-level lock to use. Valid options are `forShare` and
|
683 | * `forUpdate`. This only works in conjunction with the `transacting`
|
684 | * option, and requires a database that supports it.
|
685 | * @param {string|Object|mixed[]} [options.withRelated]
|
686 | * Relations to be retrieved with `Model` instance. Either one or more
|
687 | * relation names or objects mapping relation names to query callbacks.
|
688 | * @param {boolean} [options.debug=false]
|
689 | * Whether to enable debugging mode or not. When enabled will show information about the
|
690 | * queries being run.
|
691 | * @fires Model#fetching
|
692 | * @fires Model#fetched
|
693 | * @throws {Model.NotFoundError}
|
694 | * @returns {Promise<Model|null>}
|
695 | * A promise resolving to the fetched {@link Model model} or `null` if
|
696 | * none exists and the `require: false` option is passed.
|
697 | *
|
698 | */
|
699 | fetch(options) {
|
700 | return this._doFetch(this.attributes, options).tap(() => {
|
701 | this._previousAttributes = _.cloneDeep(this.attributes);
|
702 | });
|
703 | },
|
704 |
|
705 | _doFetch: Promise.method(function(attributes, options) {
|
706 | options = options ? _.clone(options) : {};
|
707 |
|
708 | // Run the `first` call on the `sync` object to fetch a single model.
|
709 | return (
|
710 | this.sync(options)
|
711 | .first(attributes)
|
712 | .bind(this)
|
713 |
|
714 | // Jump the rest of the chain if the response doesn't exist...
|
715 | .tap(function(response) {
|
716 | if (!response || response.length === 0) {
|
717 | throw new this.constructor.NotFoundError('EmptyResponse');
|
718 | }
|
719 | })
|
720 |
|
721 | // Now, load all of the data into the model as necessary.
|
722 | .tap(this._handleResponse)
|
723 |
|
724 | // If the "withRelated" is specified, we also need to eager load all of the
|
725 | // data on the model, as a side-effect, before we ultimately jump into the
|
726 | // next step of the model. Since the `columns` are only relevant to the
|
727 | // current level, ensure those are omitted from the options.
|
728 | .tap(function(response) {
|
729 | if (options.withRelated) {
|
730 | return this._handleEager(response, _.omit(options, 'columns'));
|
731 | }
|
732 | })
|
733 |
|
734 | .tap(function(response) {
|
735 | /**
|
736 | * Fired after a `fetch` operation. A promise may be returned from the
|
737 | * event handler for async behaviour.
|
738 | *
|
739 | * @event Model#fetched
|
740 | * @tutorial events
|
741 | * @param {Model} model
|
742 | * The model firing the event.
|
743 | * @param {Object} response
|
744 | * Knex query response.
|
745 | * @param {Object} options
|
746 | * Options object passed to {@link Model#fetch fetch}.
|
747 | * @returns {Promise}
|
748 | * If the handler returns a promise, `fetch` will wait for it to
|
749 | * be resolved.
|
750 | */
|
751 | if (!options.silent) return this.triggerThen('fetched', this, response, options);
|
752 | })
|
753 | .return(this)
|
754 | .catch(this.constructor.NotFoundError, function(err) {
|
755 | if ((this.requireFetch && options.require !== false) || options.require) throw err;
|
756 | return null;
|
757 | })
|
758 | );
|
759 | }),
|
760 |
|
761 | // Private for now.
|
762 | all() {
|
763 | const collection = this.constructor.collection();
|
764 | collection._knex = this.query().clone();
|
765 | this.resetQuery();
|
766 | if (this.relatedData) collection.relatedData = this.relatedData;
|
767 | return collection;
|
768 | },
|
769 |
|
770 | /**
|
771 | * Gets the number of matching records in the database, respecting any previous calls to
|
772 | * {@link Model#query}. If the `column` argument is provided, records with a `null` value in
|
773 | * that column will be excluded from the count.
|
774 | *
|
775 | * **Note** that in PostgreSQL the result is a string by default. To read more about the
|
776 | * reasons for this see the [pull request](https://github.com/brianc/node-postgres/pull/353)
|
777 | * that implemented it in the `node-postgres` database driver. If you're sure that the
|
778 | * results will always be less than 2<sup>53</sup> (9007199254740991) you can override
|
779 | * the default string parser like this:
|
780 | *
|
781 | * require('pg').defaults.parseInt8 = true
|
782 | *
|
783 | * Put this snippet before the call to `require('knex')` wherever you are initalizing
|
784 | * `knex`.
|
785 | *
|
786 | * @example
|
787 | * new Duck().where('color', 'blue').count('name').then((count) => {
|
788 | * console.log('number of blue ducks', count)
|
789 | * })
|
790 | *
|
791 | * @method Model#count
|
792 | * @since 0.8.2
|
793 | * @fires Model#counting
|
794 | * @param {string} [column='*']
|
795 | * Specify a column to count. Rows with `null` values in this column will be excluded.
|
796 | * @param {Object} [options] Hash of options.
|
797 | * @param {boolean} [options.debug=false]
|
798 | * Whether to enable debugging mode or not. When enabled will show information about the
|
799 | * queries being run.
|
800 | * @returns {Promise<number|string>}
|
801 | * A promise resolving to the number of matching rows. By default this will be a number,
|
802 | * except with PostgreSQL where it will be a string. Check the description to see how to
|
803 | * return a number instead in this case.
|
804 | */
|
805 | count(column, options) {
|
806 | return this.all().count(column, options);
|
807 | },
|
808 |
|
809 | /**
|
810 | * Fetches a collection of {@link Model models} from the database, using any
|
811 | * query parameters currently set on the model to constrain the results.
|
812 | *
|
813 | * Returns a Promise that will resolve with the fetched collection. If there
|
814 | * are no results it will resolve with an empty collection. If instead you
|
815 | * wish the Promise to be rejected with a {@link Collection.EmptyError},
|
816 | * pass the `require: true` option.
|
817 | *
|
818 | * If you need to constrain the results, you can call the {@link Model#query query}
|
819 | * or {@link Model#where where} methods before calling this method.
|
820 | *
|
821 | * @method Model#fetchAll
|
822 | * @param {Object} [options] Set of options to modify the request.
|
823 | * @param {boolean} [options.require=false]
|
824 | * Whether or not to reject the returned Promise with a {@link Collection.EmptyError} if no records can be
|
825 | * fetched from the database.
|
826 | * @param {Transaction} [options.transacting] Optionally run the query in a transaction.
|
827 | * @param {boolean} [options.debug=false]
|
828 | * Whether to enable debugging mode or not. When enabled will show information about the
|
829 | * queries being run.
|
830 | * @fires Model#fetching:collection
|
831 | * @fires Model#fetched:collection
|
832 | * @throws {Collection.EmptyError}
|
833 | * This error is used to reject the Promise in the event of an empty response from the
|
834 | * database in case the `require: true` fetch option is used.
|
835 | * @returns {Promise} A Promise resolving to the fetched {@link Collection collection}.
|
836 | */
|
837 | fetchAll(options) {
|
838 | const collection = this.all();
|
839 | return collection
|
840 | .once('fetching', (__, columns, opts) => {
|
841 | /**
|
842 | * Fired before a {@link Model#fetchAll fetchAll} operation. A promise
|
843 | * may be returned from the event handler for async behaviour.
|
844 | *
|
845 | * @event Model#fetching:collection
|
846 | * @tutorial events
|
847 | * @param {Collection} collection
|
848 | * The collection that is going to be fetched. At this point it's still empty since the
|
849 | * fetch hasn't happened yet.
|
850 | * @param {string[]} columns
|
851 | * The columns to be retrieved by the query as provided by the underlying query builder.
|
852 | * If the `columns` option is not specified the value of this will usually be an array
|
853 | * with a single string `'tableName.*'`.
|
854 | * @param {Object} options Options object passed to {@link Model#fetchAll fetchAll}.
|
855 | * @returns {Promise}
|
856 | */
|
857 | return this.triggerThen('fetching:collection', collection, columns, opts);
|
858 | })
|
859 | .once('fetched', (__, response, opts) => {
|
860 | /**
|
861 | * Fired after a {@link Model#fetchAll fetchAll} operation. A promise
|
862 | * may be returned from the event handler for async behaviour.
|
863 | *
|
864 | * @event Model#fetched:collection
|
865 | * @tutorial events
|
866 | * @param {Collection} collection The collection that has been fetched.
|
867 | * @param {Object} response
|
868 | * The raw response from the underlying query builder. This will be an array with objects
|
869 | * representing each row, similar to the output of a
|
870 | * {@link Model#serialize serialized Model}.
|
871 | * @param {Object} options Options object passed to {@link Model#fetchAll fetchAll}.
|
872 | * @returns {Promise}
|
873 | */
|
874 | return this.triggerThen('fetched:collection', collection, response, opts);
|
875 | })
|
876 | .fetch(options);
|
877 | },
|
878 |
|
879 | /**
|
880 | * The load method takes an array of relations to eager load attributes onto a {@link Model}, in a similar way that
|
881 | * the `withRelated` option works on {@link Model#fetch fetch}. Dot separated attributes may be used to specify deep
|
882 | * eager loading.
|
883 | *
|
884 | * It is possible to pass an object with query callbacks to filter the relations to eager load. An example is
|
885 | * presented above.
|
886 | *
|
887 | * @example
|
888 | * // Using an array of strings with relation names
|
889 | * new Posts().fetch().then(function(collection) {
|
890 | * return collection.at(0).load(['author', 'content', 'comments.tags'])
|
891 | * }).then(function(model) {
|
892 | * JSON.stringify(model)
|
893 | *
|
894 | * // {
|
895 | * // title: 'post title',
|
896 | * // author: {...},
|
897 | * // content: {...},
|
898 | * // comments: [
|
899 | * // {tags: [...]}, {tags: [...]}
|
900 | * // ]
|
901 | * // }
|
902 | * })
|
903 | *
|
904 | * // Using an object with query callbacks to filter the relations
|
905 | * new Posts().fetch().then(function(collection) {
|
906 | * return collection.at(0).load({comments: function(qb) {
|
907 | * qb.where('comments.is_approved', '=', true)
|
908 | * }})
|
909 | * }).then(function(model) {
|
910 | * JSON.stringify(model)
|
911 | * // the model now includes all approved comments
|
912 | * })
|
913 | *
|
914 | * @method Model#load
|
915 | * @param {string|Object|mixed[]} relations The relation, or relations, to be loaded.
|
916 | * @param {Object} [options] Hash of options.
|
917 | * @param {Transaction} [options.transacting] Optionally run the query in a transaction.
|
918 | * @param {string} [options.lock]
|
919 | * Type of row-level lock to use. Valid options are `forShare` and `forUpdate`. This only works in conjunction
|
920 | * with the `transacting` option, and requires a database that supports it.
|
921 | * @param {boolean} [options.debug=false]
|
922 | * Whether to enable debugging mode or not. When enabled will show information about the
|
923 | * queries being run.
|
924 | * @returns {Promise<Model>} A promise resolving to this {@link Model model}.
|
925 | */
|
926 | load: Promise.method(function(relations, options) {
|
927 | const columns = this.format(_.assignIn({}, this.attributes));
|
928 | const withRelated = Array.isArray(relations) ? relations : [relations];
|
929 | return this._handleEager([columns], _.assignIn({}, options, {shallow: true, withRelated})).return(this);
|
930 | }),
|
931 |
|
932 | /**
|
933 | * @method Model#save
|
934 | * @description
|
935 | *
|
936 | * This method is used to perform either an insert or update query using the
|
937 | * model's set {@link Model#attributes attributes}.
|
938 | *
|
939 | * If the model {@link Model#isNew isNew}, any {@link Model#defaults defaults}
|
940 | * will be set and an `insert` query will be performed. Otherwise it will
|
941 | * `update` the record with a corresponding ID. It is also possible to
|
942 | * set default attributes on an `update` by passing the `{defaults: true}`
|
943 | * option in the second argument to the `save` call. This will also use the
|
944 | * same {@link Model#defaults defaults} as the `insert` operation.
|
945 | *
|
946 | * The type of operation to perform (either `insert` or `update`) can be
|
947 | * overriden with the `method` option:
|
948 | *
|
949 | * // This forces an insert with the specified id instead of the expected update
|
950 | * new Post({name: 'New Article', id: 34})
|
951 | * .save(null, {method: 'insert'})
|
952 | * .then((model) => {
|
953 | * // ...
|
954 | * })
|
955 | *
|
956 | * If you only wish to update with the params passed to the save, you may pass
|
957 | * a `{patch: true}` option in the second argument to `save`:
|
958 | *
|
959 | * // UPDATE authors SET "bio" = 'Short user bio' WHERE "id" = 1
|
960 | * new Author({id: 1, first_name: 'User'})
|
961 | * .save({bio: 'Short user bio'}, {patch: true})
|
962 | * .then((model) => {
|
963 | * // ...
|
964 | * })
|
965 | *
|
966 | * After a model is saved it will be populated with all the attributes that are
|
967 | * present in the database, so you don't need to manually call
|
968 | * {@link Model#refresh refresh} to update it. This will use two queries unless
|
969 | * the database supports the `RETURNING` statement, in which case the model will
|
970 | * be saved and its data fetched with a single query.
|
971 | *
|
972 | * Several events fire on the model when starting the save process:
|
973 | * - {@link Model#event:creating "creating"} if the model is being inserted.
|
974 | * - {@link Model#event:updating "updating"} event if the model is being updated.
|
975 | * - {@link Model#event:saving "saving"} event in either case.
|
976 | *
|
977 | * To prevent saving the model (for example, with validation), throwing an error
|
978 | * inside one of these event listeners will stop the save process and reject the
|
979 | * Promise.
|
980 | *
|
981 | * If you wish to modify the query when the {@link Model#event:saving "saving"}
|
982 | * event is fired, the `knex` query object is available in `options.query`.
|
983 | *
|
984 | * After the save is complete the following events will fire:
|
985 | * - {@link Model#event:created "created"} if a new model was inserted in the
|
986 | * database
|
987 | * - {@link Model#event:updated "updated"} if an existing model was updated.
|
988 | * - {@link Model#event:saved "saved"} event either way.
|
989 | *
|
990 | * See the {@tutorial events} guide for further details.
|
991 | *
|
992 | * @example
|
993 | * // Save with no arguments
|
994 | * Model.forge({id: 5, firstName: 'John', lastName: 'Smith'}).save().then((model) => {
|
995 | * //...
|
996 | * })
|
997 | *
|
998 | * // Or add attributes during save
|
999 | * Model.forge({id: 5}).save({firstName: 'John', lastName: 'Smith'}).then((model) => {
|
1000 | * //...
|
1001 | * })
|
1002 | *
|
1003 | * // Or, if you prefer, for a single attribute
|
1004 | * Model.forge({id: 5}).save('name', 'John Smith').then((model) => {
|
1005 | * //...
|
1006 | * })
|
1007 | *
|
1008 | * @param {string} [key] Attribute name.
|
1009 | * @param {*} [val] Attribute value.
|
1010 | * @param {Object} [attrs]
|
1011 | * You can use a single object argument with all the values you wish to save
|
1012 | * instead of specifying both the `key` and `value` arguments.
|
1013 | * @param {Object} [options]
|
1014 | * @param {Transaction} [options.transacting] Optionally run the query in a transaction.
|
1015 | * @param {string} [options.method]
|
1016 | * Explicitly select a save method, either `"update"` or `"insert"`.
|
1017 | * @param {Boolean} [options.defaults=false]
|
1018 | * Whether to assign or not {@link Model#defaults default} attribute values
|
1019 | * on a model when performing an update or create operation.
|
1020 | * @param {Boolean} [options.patch=false]
|
1021 | * Only save attributes supplied as arguments to the `save` call, ignoring any
|
1022 | * attributes that may be already set on the model.
|
1023 | * @param {Boolean} [options.require=true]
|
1024 | * Whether or not to throw a {@link Model.NoRowsUpdatedError} if no records
|
1025 | * are affected by save.
|
1026 | * @param {boolean} [options.debug=false]
|
1027 | * Whether to enable debugging mode or not. When enabled will show information about the
|
1028 | * queries being run.
|
1029 | * @fires Model#saving
|
1030 | * @fires Model#creating
|
1031 | * @fires Model#updating
|
1032 | * @fires Model#created
|
1033 | * @fires Model#updated
|
1034 | * @fires Model#saved
|
1035 | * @throws {Model.NoRowsUpdatedError}
|
1036 | * @returns {Promise<Model>} A promise resolving to the saved and updated model.
|
1037 | */
|
1038 | save: Promise.method(function(key, val, options) {
|
1039 | let attrs;
|
1040 |
|
1041 | // Handle both `"key", value` and `{key: value}` -style arguments.
|
1042 | if (key == null || typeof key === 'object') {
|
1043 | attrs = key || {};
|
1044 | options = _.clone(val) || {};
|
1045 | } else {
|
1046 | attrs = {
|
1047 | [key]: val
|
1048 | };
|
1049 | options = options ? _.clone(options) : {};
|
1050 | }
|
1051 |
|
1052 | return Promise.bind(this)
|
1053 | .then(function() {
|
1054 | return this.saveMethod(options);
|
1055 | })
|
1056 | .then(function(method) {
|
1057 | // Determine which kind of save we will do: update or insert.
|
1058 | options.method = method;
|
1059 |
|
1060 | // If the object is being created, we merge any defaults here rather than
|
1061 | // during object creation.
|
1062 | if (method === 'insert' || options.defaults) {
|
1063 | const defaults = _.result(this, 'defaults');
|
1064 | if (defaults) {
|
1065 | attrs = _.defaultsDeep({}, attrs, this.attributes, defaults);
|
1066 | }
|
1067 | }
|
1068 |
|
1069 | // Set the attributes on the model. Note that we do this before adding
|
1070 | // timestamps, as `timestamp` calls `set` internally.
|
1071 | this.set(attrs, {silent: true});
|
1072 |
|
1073 | // Now set timestamps if appropriate. Extend `attrs` so that the
|
1074 | // timestamps will be provided for a patch operation.
|
1075 | if (this.hasTimestamps) {
|
1076 | Object.assign(attrs, this.timestamp(options));
|
1077 | }
|
1078 |
|
1079 | // If there are any save constraints, set them on the model.
|
1080 | if (this.relatedData && this.relatedData.type !== 'morphTo') {
|
1081 | Helpers.saveConstraints(this, this.relatedData);
|
1082 | }
|
1083 |
|
1084 | const attributesToSave = method === 'update' && options.patch ? attrs : this.attributes;
|
1085 |
|
1086 | // Gives access to the `query` object in the `options`, in case we need it
|
1087 | // in any event handlers.
|
1088 | const sync = this.sync(options);
|
1089 | options.query = sync.query;
|
1090 |
|
1091 | /**
|
1092 | * Saving event.
|
1093 | *
|
1094 | * Fired before an `insert` or `update` query. A Promise may be returned from the event
|
1095 | * handler for async behaviour. Throwing an exception from the handler will cancel the
|
1096 | * save process.
|
1097 | *
|
1098 | * @event Model#saving
|
1099 | * @tutorial events
|
1100 | * @param {Model} model
|
1101 | * The model firing the event. Its attributes are already changed but not commited to
|
1102 | * the database yet.
|
1103 | * @param {Object} attrs Attributes that will be inserted or updated.
|
1104 | * @param {Object} options Options object passed to {@link Model#save save}.
|
1105 | * @param {QueryBuilder} options.query
|
1106 | * Query builder to be used for saving. This can be used to modify or add to the query
|
1107 | * before it is executed.
|
1108 | * @returns {Promise}
|
1109 | */
|
1110 |
|
1111 | /**
|
1112 | * Creating event.
|
1113 | *
|
1114 | * Fired before an `insert` query. A Promise may be returned from the event handler for
|
1115 | * async behaviour. Throwing an exception from the handler will cancel the save process.
|
1116 | *
|
1117 | * @event Model#creating
|
1118 | * @tutorial events
|
1119 | * @param {Model} model The model firing the event.
|
1120 | * @param {Object} attrs Attributes that will be inserted.
|
1121 | * @param {Object} options Options object passed to {@link Model#save save}.
|
1122 | * @param {QueryBuilder} options.query
|
1123 | * Query builder to be used for saving. This can be used to modify or add to the query
|
1124 | * before it is executed.
|
1125 | * @returns {Promise}
|
1126 | */
|
1127 |
|
1128 | /**
|
1129 | * Updating event.
|
1130 | *
|
1131 | * Fired before an `update` query. A Promise may be returned from the event handler for
|
1132 | * async behaviour. Throwing an exception from the handler will cancel the save process.
|
1133 | *
|
1134 | * @event Model#updating
|
1135 | * @tutorial events
|
1136 | * @param {Model} model
|
1137 | * The model firing the event. Its attributes are already changed but not commited to
|
1138 | * the database yet.
|
1139 | * @param {Object} attrs Attributes that will be updated.
|
1140 | * @param {Object} options Options object passed to {@link Model#save save}.
|
1141 | * @param {QueryBuilder} options.query
|
1142 | * Query builder to be used for saving. This can be used to modify or add to the query
|
1143 | * before it is executed.
|
1144 | * @returns {Promise}
|
1145 | */
|
1146 | return this.triggerThen(
|
1147 | method === 'insert' ? 'saving creating' : 'saving updating',
|
1148 | this,
|
1149 | attributesToSave,
|
1150 | options
|
1151 | )
|
1152 | .bind(this)
|
1153 | .then(function() {
|
1154 | return sync[options.method](attributesToSave);
|
1155 | })
|
1156 | .then(function(resp) {
|
1157 | // Only valid for databases that support RETURNING
|
1158 | const isObjectResponse = resp && typeof resp[0] === 'object';
|
1159 |
|
1160 | // After a successful database save, the id is updated if the model was created
|
1161 | if (method === 'insert' && this.id == null) {
|
1162 | let updatedAttrs;
|
1163 |
|
1164 | if (!isObjectResponse) {
|
1165 | const updatedCols = {};
|
1166 | updatedCols[this.idAttribute] = this.id = resp[0];
|
1167 | updatedAttrs = this.parse(updatedCols);
|
1168 | } else {
|
1169 | updatedAttrs = this.parse(resp[0]);
|
1170 | this.id = updatedAttrs[this.parsedIdAttribute()];
|
1171 | }
|
1172 |
|
1173 | Object.assign(this.attributes, updatedAttrs);
|
1174 | } else if (method === 'update' && (resp === 0 || resp.length === 0)) {
|
1175 | if (options.require !== false) {
|
1176 | throw new this.constructor.NoRowsUpdatedError('No Rows Updated');
|
1177 | }
|
1178 | } else {
|
1179 | Object.assign(this.attributes, this.parse(resp[0]));
|
1180 | }
|
1181 |
|
1182 | if (resp === 0 || resp.length === 0) return resp;
|
1183 | if (isObjectResponse) return this;
|
1184 |
|
1185 | return this.refresh({silent: true, transacting: options.transacting});
|
1186 | })
|
1187 | .then(function() {
|
1188 | const eventsToTrigger = method === 'insert' ? 'created saved' : 'updated saved';
|
1189 | this._reset();
|
1190 |
|
1191 | /**
|
1192 | * Saved event.
|
1193 | *
|
1194 | * Fired after an `insert` or `update` query.
|
1195 | *
|
1196 | * @event Model#saved
|
1197 | * @tutorial events
|
1198 | * @param {Model} model
|
1199 | * The model firing the event with its attributes matching what's in the database.
|
1200 | * @param {Object} options Options object passed to {@link Model#save save}.
|
1201 | * @returns {Promise}
|
1202 | */
|
1203 |
|
1204 | /**
|
1205 | * Created event.
|
1206 | *
|
1207 | * Fired after an `insert` query.
|
1208 | *
|
1209 | * @event Model#created
|
1210 | * @tutorial events
|
1211 | * @param {Model} model
|
1212 | * The model firing the event with its attributes matching what's in the database.
|
1213 | * @param {Object} options Options object passed to {@link Model#save save}.
|
1214 | * @returns {Promise}
|
1215 | */
|
1216 |
|
1217 | /**
|
1218 | * Updated event.
|
1219 | *
|
1220 | * Fired after an `update` query.
|
1221 | *
|
1222 | * @event Model#updated
|
1223 | * @tutorial events
|
1224 | * @param {Model} model
|
1225 | * The model firing the event with its attributes matching what's in the database.
|
1226 | * @param {Object} options Options object passed to {@link Model#save save}.
|
1227 | * @returns {Promise}
|
1228 | */
|
1229 | return this.triggerThen(eventsToTrigger, this, options);
|
1230 | });
|
1231 | })
|
1232 | .return(this);
|
1233 | }),
|
1234 |
|
1235 | /**
|
1236 | * `destroy` performs a `delete` on the model, using the model's {@link
|
1237 | * Model#idAttribute idAttribute} to constrain the query.
|
1238 | *
|
1239 | * A {@link Model#event:destroying "destroying"} event is triggered on the model
|
1240 | * before being destroyed. To prevent destroying the model, throwing an error
|
1241 | * inside one of the event listeners will stop destroying the model and reject the
|
1242 | * promise.
|
1243 | *
|
1244 | * A {@link Model#event:destroyed "destroyed"} event is fired after the model's
|
1245 | * removal is completed.
|
1246 | *
|
1247 | * @method Model#destroy
|
1248 | *
|
1249 | * @param {Object} [options] Hash of options.
|
1250 | * @param {Transaction} [options.transacting] Optionally run the query in a transaction.
|
1251 | * @param {Boolean} [options.require=true]
|
1252 | * Throw a {@link Model.NoRowsDeletedError} if no records are affected by destroy. This is
|
1253 | * the default behavior as of version 0.13.0.
|
1254 | * @param {boolean} [options.debug=false]
|
1255 | * Whether to enable debugging mode or not. When enabled will show information about the
|
1256 | * queries being run.
|
1257 | *
|
1258 | * @example
|
1259 | *
|
1260 | * new User({id: 1})
|
1261 | * .destroy()
|
1262 | * .then(function(model) {
|
1263 | * // ...
|
1264 | * });
|
1265 | *
|
1266 | * @fires Model#destroying
|
1267 | * @fires Model#destroyed
|
1268 | *
|
1269 | * @throws {Model.NoRowsDeletedError}
|
1270 | *
|
1271 | * @returns {Promise<Model>} A promise resolving to the destroyed and thus
|
1272 | * empty model, i.e. all attributes are `undefined`.
|
1273 | */
|
1274 | destroy: Promise.method(function(options) {
|
1275 | options = options ? _.clone(options) : {};
|
1276 | const sync = this.sync(options);
|
1277 | options.query = sync.query;
|
1278 | return Promise.bind(this)
|
1279 | .then(function() {
|
1280 | /**
|
1281 | * Destroying event.
|
1282 | *
|
1283 | * Fired before a `delete` query. A promise may be returned from the event
|
1284 | * handler for async behaviour. Throwing an exception from the handler
|
1285 | * will reject the promise and cancel the deletion.
|
1286 | *
|
1287 | * @event Model#destroying
|
1288 | * @tutorial events
|
1289 | * @param {Model} model The model firing the event.
|
1290 | * @param {Object} options Options object passed to {@link Model#destroy destroy}.
|
1291 | * @returns {Promise}
|
1292 | */
|
1293 | return this.triggerThen('destroying', this, options);
|
1294 | })
|
1295 | .then(function() {
|
1296 | return sync.del();
|
1297 | })
|
1298 | .then(function(affectedRows) {
|
1299 | if (options.require !== false && affectedRows === 0) {
|
1300 | throw new this.constructor.NoRowsDeletedError('No Rows Deleted');
|
1301 | }
|
1302 |
|
1303 | this._previousAttributes = _.clone(this.attributes);
|
1304 | this.clear();
|
1305 |
|
1306 | /**
|
1307 | * Destroyed event.
|
1308 | *
|
1309 | * Fired after a `delete` query. A promise may be returned from the event
|
1310 | * handler for async behaviour.
|
1311 | *
|
1312 | * @event Model#destroyed
|
1313 | * @tutorial events
|
1314 | * @param {Model} model The model firing the event.
|
1315 | * @param {Object} options Options object passed to {@link Model#destroy destroy}.
|
1316 | * @returns {Promise}
|
1317 | */
|
1318 | return this.triggerThen('destroyed', this, options);
|
1319 | })
|
1320 | .then(this._reset);
|
1321 | }),
|
1322 |
|
1323 | /**
|
1324 | * Used to reset the internal state of the current query builder instance.
|
1325 | * This method is called internally each time a database action is completed
|
1326 | * by {@link Sync}
|
1327 | *
|
1328 | * @method Model#resetQuery
|
1329 | * @returns {Model} Self, this method is chainable.
|
1330 | */
|
1331 | resetQuery() {
|
1332 | this._knex = null;
|
1333 | return this;
|
1334 | },
|
1335 |
|
1336 | /**
|
1337 | * The `query` method is used to tap into the underlying Knex query builder
|
1338 | * instance for the current model. If called with no arguments, it will
|
1339 | * return the query builder directly. Otherwise, it will call the specified
|
1340 | * method on the query builder, applying any additional arguments from the
|
1341 | * `model.query` call. If the method argument is a function, it will be
|
1342 | * called with the Knex query builder as the context and the first argument,
|
1343 | * returning the current model.
|
1344 | *
|
1345 | * @example
|
1346 | *
|
1347 | * model
|
1348 | * .query('where', 'other_id', '=', '5')
|
1349 | * .fetch()
|
1350 | * .then(function(model) {
|
1351 | * // ...
|
1352 | * });
|
1353 | *
|
1354 | * model
|
1355 | * .query({where: {other_id: '5'}, orWhere: {key: 'value'}})
|
1356 | * .fetch()
|
1357 | * .then(function(model) {
|
1358 | * // ...
|
1359 | * });
|
1360 | *
|
1361 | * model.query(function(qb) {
|
1362 | * qb.where('other_person', 'LIKE', '%Demo').orWhere('other_id', '>', 10);
|
1363 | * }).fetch()
|
1364 | * .then(function(model) {
|
1365 | * // ...
|
1366 | * });
|
1367 | *
|
1368 | * let qb = model.query();
|
1369 | * qb.where({id: 1}).select().then(function(resp) {
|
1370 | * // ...
|
1371 | * });
|
1372 | *
|
1373 | * @method Model#query
|
1374 | * @param {function|Object|...string=} arguments The query method.
|
1375 | * @returns {Model|QueryBuilder}
|
1376 | * Will return this model or, if called with no arguments, the underlying query builder.
|
1377 | *
|
1378 | * @see {@link http://knexjs.org/#Builder Knex `QueryBuilder`}
|
1379 | */
|
1380 | query() {
|
1381 | return Helpers.query(this, Array.from(arguments));
|
1382 | },
|
1383 |
|
1384 | /**
|
1385 | * The where method is used as convenience for the most common {@link
|
1386 | * Model#query query} method, adding a where clause to the builder. Any
|
1387 | * additional knex methods may be accessed using {@link Model#query query}.
|
1388 | *
|
1389 | * Accepts either key, value syntax, or a hash of attributes.
|
1390 | *
|
1391 | * @example
|
1392 | *
|
1393 | * model.where('favorite_color', '<>', 'green').fetch().then(function() { //...
|
1394 | * // or
|
1395 | * model.where('favorite_color', 'red').fetch().then(function() { //...
|
1396 | * // or
|
1397 | * model.where({favorite_color: 'red', shoe_size: 12}).fetch().then(function() { //...
|
1398 | *
|
1399 | * @method Model#where
|
1400 | * @param {Object|...string} method
|
1401 | *
|
1402 | * Either `key, [operator], value` syntax, or a hash of attributes to
|
1403 | * match. Note that these must be formatted as they are in the database,
|
1404 | * not how they are stored after {@link Model#parse}.
|
1405 | *
|
1406 | * @returns {Model} Self, this method is chainable.
|
1407 | *
|
1408 | * @see Model#query
|
1409 | */
|
1410 | where() {
|
1411 | return this.query.apply(this, ['where'].concat(Array.from(arguments)));
|
1412 | },
|
1413 |
|
1414 | /**
|
1415 | * @method Model#orderBy
|
1416 | * @since 0.9.3
|
1417 | * @description
|
1418 | *
|
1419 | * Specifies the column to sort on and sort order.
|
1420 | *
|
1421 | * The order parameter is optional, and defaults to 'ASC'. You may
|
1422 | * also specify 'DESC' order by prepending a hyphen to the sort column
|
1423 | * name. `orderBy("date", 'DESC')` is the same as `orderBy("-date")`.
|
1424 | *
|
1425 | * Unless specified using dot notation (i.e., "table.column"), the default
|
1426 | * table will be the table name of the model `orderBy` was called on.
|
1427 | *
|
1428 | * @example
|
1429 | *
|
1430 | * Car.forge().orderBy('color', 'ASC').fetchAll()
|
1431 | * .then(function (rows) { // ...
|
1432 | *
|
1433 | * @param sort {string}
|
1434 | * Column to sort on
|
1435 | * @param order {string}
|
1436 | * Ascending ('ASC') or descending ('DESC') order
|
1437 | */
|
1438 | orderBy() {
|
1439 | return Helpers.orderBy.apply(null, [this].concat(Array.from(arguments)));
|
1440 | },
|
1441 |
|
1442 | /* Ensure that QueryBuilder is copied on clone. */
|
1443 | clone() {
|
1444 | // This needs to use the direct apply method because the spread operator
|
1445 | // incorrectly converts to `clone.apply(ModelBase.prototype, arguments)`
|
1446 | // instead of `apply(this, arguments)`
|
1447 | const cloned = BookshelfModel.__super__.clone.apply(this, arguments);
|
1448 | if (this._knex != null) {
|
1449 | cloned._knex = cloned._builder(this._knex.clone());
|
1450 | }
|
1451 | return cloned;
|
1452 | },
|
1453 |
|
1454 | /**
|
1455 | * Creates and returns a new Bookshelf.Sync instance.
|
1456 | *
|
1457 | * @method Model#sync
|
1458 | * @private
|
1459 | * @returns Sync
|
1460 | */
|
1461 | sync(options) {
|
1462 | return new Sync(this, options);
|
1463 | },
|
1464 |
|
1465 | /**
|
1466 | * Helper for setting up the `morphOne` or `morphMany` relations.
|
1467 | *
|
1468 | * @method Model#_morphOneOrMany
|
1469 | * @private
|
1470 | */
|
1471 | _morphOneOrMany(Target, morphName, columnNames, morphValue, type) {
|
1472 | if (!Array.isArray(columnNames)) {
|
1473 | // Shift by one place
|
1474 | morphValue = columnNames;
|
1475 | columnNames = null;
|
1476 | }
|
1477 | if (!morphName || !Target) throw new Error('The polymorphic `name` and `Target` are required.');
|
1478 |
|
1479 | return this._relation(type, Target, {
|
1480 | morphName: morphName,
|
1481 | morphValue: morphValue,
|
1482 | columnNames: columnNames
|
1483 | }).init(this);
|
1484 | },
|
1485 |
|
1486 | /**
|
1487 | * @name Model#_handleResponse
|
1488 | * @private
|
1489 | * @description
|
1490 | *
|
1491 | * Handles the response data for the model, returning from the model's fetch call.
|
1492 | *
|
1493 | * @param {Object} Response from Knex query.
|
1494 | *
|
1495 | * @todo: need to check on Backbone's status there, ticket #2636
|
1496 | * @todo: {silent: true, parse: true}, for parity with collection#set
|
1497 | */
|
1498 | _handleResponse(response) {
|
1499 | const relatedData = this.relatedData;
|
1500 |
|
1501 | this.set(this.parse(response[0]), {silent: true})
|
1502 | .formatTimestamps()
|
1503 | ._reset();
|
1504 |
|
1505 | if (relatedData && relatedData.isJoined()) {
|
1506 | relatedData.parsePivot([this]);
|
1507 | }
|
1508 | },
|
1509 |
|
1510 | /**
|
1511 | * @name Model#_handleEager
|
1512 | * @private
|
1513 | * @description
|
1514 | *
|
1515 | * Handles the related data loading on the model.
|
1516 | *
|
1517 | * @param {Object} Response from Knex query.
|
1518 | */
|
1519 | _handleEager(response, options) {
|
1520 | return new EagerRelation([this], response, this).fetch(options);
|
1521 | }
|
1522 | },
|
1523 | {
|
1524 | extended(child) {
|
1525 | /**
|
1526 | * Thrown when no records are found by {@link Model#fetch fetch} or
|
1527 | * {@link Model#refresh refresh} unless called with the `{require: false}`
|
1528 | * option.
|
1529 | *
|
1530 | * @class Model.NotFoundError
|
1531 | */
|
1532 | child.NotFoundError = createError(this.NotFoundError);
|
1533 |
|
1534 | /**
|
1535 | * Thrown when no records are saved by {@link Model#save save}
|
1536 | * unless called with the `{require: false}` option.
|
1537 | *
|
1538 | * @class Model.NoRowsUpdatedError
|
1539 | */
|
1540 | child.NoRowsUpdatedError = createError(this.NoRowsUpdatedError);
|
1541 |
|
1542 | /**
|
1543 | * Thrown when no record is deleted by {@link Model#destroy destroy}
|
1544 | * unless called with the `{require: false}` option.
|
1545 | *
|
1546 | * @class Model.NoRowsDeletedError
|
1547 | */
|
1548 | child.NoRowsDeletedError = createError(this.NoRowsDeletedError);
|
1549 | },
|
1550 |
|
1551 | fetchPage() {
|
1552 | const model = this.forge();
|
1553 | return model.fetchPage.apply(model, arguments);
|
1554 | }
|
1555 | }
|
1556 | );
|
1557 |
|
1558 | BookshelfModel.NotFoundError = Errors.NotFoundError;
|
1559 | BookshelfModel.NoRowsUpdatedError = Errors.NoRowsUpdatedError;
|
1560 | BookshelfModel.NoRowsDeletedError = Errors.NoRowsDeletedError;
|
1561 |
|
1562 | module.exports = BookshelfModel;
|