lib/collection.js

  1. const _ = require('lodash');
  2. const Sync = require('./sync');
  3. const Helpers = require('./helpers');
  4. const EagerRelation = require('./eager');
  5. const Errors = require('./errors');
  6. const CollectionBase = require('./base/collection');
  7. const Promise = require('bluebird');
  8. const createError = require('create-error');
  9. /**
  10. * When creating a {@link Collection}, you may choose to pass in the initial array of
  11. * {@link Model models}. The collection's {@link Collection#comparator comparator} may be included
  12. * as an option. Passing `false` as the comparator option will prevent sorting. If you define an
  13. * {@link Collection#initialize initialize} function, it will be invoked when the collection is
  14. * created.
  15. *
  16. * If you would like to customize the Collection used by your models when calling
  17. * {@link Model#fetchAll} or {@link Model#fetchPage} you can use the following process:
  18. *
  19. * const Test = bookshelf.model('Test', {
  20. * tableName: 'test'
  21. * }, {
  22. * collection(...args) {
  23. * return new Tests(...args)
  24. * }
  25. * })
  26. * const Tests = bookshelf.collection('Tests', {
  27. * get model() {
  28. * return Test
  29. * },
  30. * initialize () {
  31. * this.constructor.__super__.initialize.apply(this, arguments)
  32. * // Collection will emit fetching event as expected even on eager queries.
  33. * this.on('fetching', () => {})
  34. * },
  35. * doStuff() {
  36. * // This method will be available in the results collection returned
  37. * // by Test.fetchAll() and Test.fetchPage()
  38. * }
  39. * })
  40. *
  41. * @example
  42. * const TabSet = bookshelf.collection('TabSet', {
  43. * model: Tab
  44. * })
  45. * const tabs = new TabSet([tab1, tab2, tab3])
  46. *
  47. * @class Collection
  48. * @extends CollectionBase
  49. * @classdesc
  50. * Collections are ordered sets of models returned from the database, from a
  51. * {@link Model#fetchAll fetchAll} call.
  52. * @param {(Model[])=} models Initial array of models.
  53. * @param {Object=} options
  54. * @param {Boolean} [options.comparator=false]
  55. * {@link Collection#comparator Comparator} for collection, or `false` to disable sorting.
  56. */
  57. const BookshelfCollection = (module.exports = CollectionBase.extend(
  58. /** @lends Collection.prototype */
  59. {
  60. /**
  61. * Used to define relationships where a {@link Model#hasMany hasMany} or
  62. * {@link Model#belongsToMany belongsToMany} relation passes "through" an `Interim` model. This
  63. * is exactly like the equivalent {@link Model#through model method} except that it applies to
  64. * the collections that the above mentioned relation methods return instead of individual
  65. * models.
  66. *
  67. * A good example of where this would be useful is if a book {@link Model#hasMany hasMany}
  68. * paragraphs *through* chapters. See the example above for how this can be used.
  69. *
  70. * @example
  71. * const Chapter = bookshelf.model('Chapter', {
  72. * tableName: 'chapters',
  73. * paragraphs() {
  74. * return this.hasMany(Paragraph)
  75. * }
  76. * })
  77. *
  78. * const Paragraph = bookshelf.model('Paragraph', {
  79. * tableName: 'paragraphs',
  80. * chapter() {
  81. * return this.belongsTo(Chapter)
  82. * }
  83. * })
  84. *
  85. * const Book = bookshelf.model('Book', {
  86. * tableName: 'books',
  87. * // Find all paragraphs associated with this book, by
  88. * // passing through the "Chapter" model.
  89. * paragraphs() {
  90. * return this.hasMany(Paragraph).through(Chapter)
  91. * }
  92. * })
  93. *
  94. * @param {Model} Interim Pivot model.
  95. * @param {string} [throughForeignKey]
  96. * Foreign key in this collection's model. This is the model that the `hasMany` or
  97. * `belongsToMany` relations return. By default, the `foreignKey` is assumed to be the
  98. * singular form of the `Target` model's tableName, followed by `_id` /
  99. * `_{{{@link Model#idAttribute idAttribute}}}`.
  100. * @param {string} [otherKey]
  101. * Foreign key in the `Interim` model. By default, the `otherKey` is assumed to be the
  102. * singular form of this model's tableName, followed by `_id` /
  103. * `_{{{@link Model#idAttribute idAttribute}}}`.
  104. * @param {string} [throughForeignKeyTarget]
  105. * Column in this collection's model which `throughForeignKey` references, if other than the
  106. * default of the model's `id` / `{@link Model#idAttribute idAttribute}`.
  107. * @param {string} [otherKeyTarget]
  108. * Column in the `Interim` model which `otherKey` references, if other than `id` /
  109. * `{@link Model#idAttribute idAttribute}`.
  110. * @returns {Collection} The related but empty collection.
  111. */
  112. through: function(Interim, throughForeignKey, otherKey, throughForeignKeyTarget, otherKeyTarget) {
  113. return this.relatedData.through(this, Interim, {
  114. throughForeignKey,
  115. otherKey,
  116. throughForeignKeyTarget,
  117. otherKeyTarget
  118. });
  119. },
  120. /**
  121. * Fetch the default set of models for this collection from the database,
  122. * resetting the collection when they arrive. If you wish to trigger an
  123. * error if the fetched collection is empty, pass `{require: true}` as one
  124. * of the options to the {@link Collection#fetch fetch} call. A {@link
  125. * Collection#fetched "fetched"} event will be fired when records are
  126. * successfully retrieved. If you need to constrain the query performed by
  127. * `fetch`, you can call the {@link Collection#query query} method before
  128. * calling `fetch`.
  129. *
  130. * If you'd like to only fetch specific columns, you may specify a `columns`
  131. * property in the options for the `fetch` call.
  132. *
  133. * The `withRelated` option may be specified to fetch the models of the
  134. * collection, eager loading any specified {@link Relation relations} named on
  135. * the model. A single property, or an array of properties can be specified as
  136. * a value for the `withRelated` property. The results of these relation
  137. * queries will be loaded into a relations property on the respective models,
  138. * may be retrieved with the {@link Model#related related} method.
  139. *
  140. * @fires Collection#fetched
  141. * @throws {Collection.EmptyError} Thrown if no records are found.
  142. * @param {Object=} options
  143. * @param {Boolean} [options.require=false]
  144. * Whether or not to throw a {@link Collection.EmptyError} if no records are found.
  145. * You can pass the `require: true` option to override this behavior.
  146. * @param {string|string[]} [options.withRelated=[]]
  147. * A relation, or list of relations, to be eager loaded as part of the `fetch` operation.
  148. * @param {boolean} [options.debug=false]
  149. * Whether to enable debugging mode or not. When enabled will show information about the
  150. * queries being run.
  151. * @returns {Promise<Collection>}
  152. */
  153. fetch: Promise.method(function(options) {
  154. options = options ? _.clone(options) : {};
  155. return (
  156. this.sync(options)
  157. .select()
  158. .bind(this)
  159. .tap(function(response) {
  160. if (!response || response.length === 0) {
  161. throw new this.constructor.EmptyError('EmptyResponse');
  162. }
  163. })
  164. // Now, load all of the data onto the collection as necessary.
  165. .tap(function(response) {
  166. return this._handleResponse(response, options);
  167. })
  168. // If the "withRelated" is specified, we also need to eager load all of the
  169. // data on the collection, as a side-effect, before we ultimately jump into the
  170. // next step of the collection. Since the `columns` are only relevant to the current
  171. // level, ensure those are omitted from the options.
  172. .tap(function(response) {
  173. if (options.withRelated) {
  174. return this._handleEager(response, _.omit(options, 'columns'));
  175. }
  176. })
  177. .tap(function(response) {
  178. /**
  179. * @event Collection#fetched
  180. * @tutorial events
  181. *
  182. * @description
  183. * Fired after a `fetch` operation. A promise may be returned from the
  184. * event handler for async behaviour.
  185. *
  186. * @param {Collection} collection The collection performing the {@link Collection#fetch}.
  187. * @param {Object} response Knex query response.
  188. * @param {Object} options Options object passed to {@link Collection#fetch fetch}.
  189. * @returns {Promise}
  190. */
  191. return this.triggerThen('fetched', this, response, options);
  192. })
  193. .catch(this.constructor.EmptyError, function(err) {
  194. if (options.require) throw err;
  195. this.reset([], {silent: true});
  196. })
  197. .return(this)
  198. );
  199. }),
  200. fetchPage(options) {
  201. if (!options) options = {};
  202. return Helpers.fetchPage.call(this, options);
  203. },
  204. /**
  205. * Get the number of records in the collection's table.
  206. *
  207. * @example
  208. * // select count(*) from shareholders where company_id = 1 and share &gt; 0.1;
  209. * new Company({id: 1})
  210. * .shareholders()
  211. * .where('share', '>', '0.1')
  212. * .count()
  213. * .then((count) => {
  214. * assert(count === 3)
  215. * })
  216. *
  217. * @since 0.8.2
  218. * @see Model#count
  219. * @param {string} [column='*']
  220. * Specify a column to count. Rows with `null` values in this column will be excluded.
  221. * @param {Object} [options] Hash of options.
  222. * @returns {Promise<number|string>}
  223. */
  224. count: Promise.method(function(column, options) {
  225. if (!_.isString(column)) {
  226. options = column;
  227. column = undefined;
  228. }
  229. if (options) options = _.clone(options);
  230. return this.sync(options).count(column);
  231. }),
  232. /**
  233. * Fetch and return a single {@link Model model} from the collection,
  234. * maintaining any {@link Relation relation} data from the collection, and
  235. * any {@link Collection#query query} parameters that have already been passed
  236. * to the collection. Especially helpful on relations, where you would only
  237. * like to return a single model from the associated collection.
  238. *
  239. * @example
  240. * // select * from authors where site_id = 1 and id = 2 limit 1;
  241. * new Site({id:1})
  242. * .authors()
  243. * .query({where: {id: 2}})
  244. * .fetchOne()
  245. * .then(function(model) {
  246. * // ...
  247. * });
  248. *
  249. * @param {Object=} options
  250. * @param {Boolean} [options.require=true]
  251. * Whether or not to reject the returned Promise with a {@link Model.NotFoundError} if no
  252. * records can be fetched from the database.
  253. * @param {(string|string[])} [options.columns='*']
  254. * Limit the number of columns fetched.
  255. * @param {Transaction} [options.transacting] Optionally run the query in a transaction.
  256. * @param {string} [options.lock]
  257. * Type of row-level lock to use. Valid options are `forShare` and
  258. * `forUpdate`. This only works in conjunction with the `transacting`
  259. * option, and requires a database that supports it.
  260. * @param {boolean} [options.debug=false]
  261. * Whether to enable debugging mode or not. When enabled will show information about the
  262. * queries being run.
  263. * @throws {Model.NotFoundError}
  264. * @returns {Promise<Model|null>}
  265. * A promise resolving to the fetched {@link Model} or `null` if none exists and the
  266. * `require: false` option is passed or {@link Model#requireFetch requireFetch} is set to
  267. * `false`.
  268. */
  269. fetchOne: Promise.method(function(options) {
  270. const model = new this.model();
  271. model._knex = this.query().clone();
  272. this.resetQuery();
  273. if (this.relatedData) model.relatedData = this.relatedData;
  274. return model.fetch(options);
  275. }),
  276. /**
  277. * This method is used to eager load relations onto a Collection, in a similar way that the
  278. * `withRelated` property works on {@link Collection#fetch fetch}. Nested eager loads can be
  279. * specified by separating the nested relations with `.`.
  280. *
  281. * @param {string|string[]} relations The relation, or relations, to be loaded.
  282. * @param {Object} [options] Hash of options.
  283. * @param {Transaction} [options.transacting]
  284. * @param {string} [options.lock]
  285. * Type of row-level lock to use. Valid options are `forShare` and `forUpdate`. This only
  286. * works in conjunction with the `transacting` option, and requires a database that supports
  287. * it.
  288. * @param {boolean} [options.debug=false]
  289. * Whether to enable debugging mode or not. When enabled will show information about the
  290. * queries being run.
  291. * @returns {Promise<Collection>} A promise resolving to this {@link Collection collection}.
  292. */
  293. load: Promise.method(function(relations, options) {
  294. if (!Array.isArray(relations)) relations = [relations];
  295. options = _.assignIn({}, options, {
  296. shallow: true,
  297. withRelated: relations
  298. });
  299. return new EagerRelation(this.models, this.toJSON(options), new this.model()).fetch(options).return(this);
  300. }),
  301. /**
  302. * Convenience method to create a new {@link Model model} instance within a
  303. * collection. Equivalent to instantiating a model with a hash of {@link
  304. * Model#attributes attributes}, {@link Model#save saving} the model to the
  305. * database then adding the model to the collection.
  306. *
  307. * When used on a relation, `create` will automatically set foreign key
  308. * attributes before persisting the `Model`.
  309. *
  310. * @example
  311. * const { courses, ...attributes } = req.body;
  312. *
  313. * Student.forge(attributes).save().tap(student =>
  314. * Promise.map(courses, course => student.related('courses').create(course))
  315. * ).then(student =>
  316. * res.status(200).send(student)
  317. * ).catch(error =>
  318. * res.status(500).send(error.message)
  319. * );
  320. *
  321. * @param {Object} model A set of attributes to be set on the new model.
  322. * @param {Object} [options]
  323. * @param {Transaction} [options.transacting]
  324. * @param {boolean} [options.debug=false]
  325. * Whether to enable debugging mode or not. When enabled will show information about the
  326. * queries being run.
  327. * @returns {Promise<Model>} A promise resolving with the new {@link Model model}.
  328. */
  329. create: Promise.method(function(model, options) {
  330. options = options != null ? _.clone(options) : {};
  331. const relatedData = this.relatedData;
  332. model = this._prepareModel(model, options);
  333. // If we've already added things on the query chain, these are likely intended for the model.
  334. if (this._knex) {
  335. model._knex = this._knex;
  336. this.resetQuery();
  337. }
  338. return Helpers.saveConstraints(model, relatedData)
  339. .save(null, options)
  340. .bind(this)
  341. .then(function() {
  342. if (relatedData && relatedData.type === 'belongsToMany') {
  343. return this.attach(model, _.omit(options, 'query'));
  344. }
  345. })
  346. .then(function() {
  347. this.add(model, options);
  348. })
  349. .return(model);
  350. }),
  351. /**
  352. * Used to reset the internal state of the current query builder instance. This method is called
  353. * internally each time a database action is completed by {@link Sync}.
  354. *
  355. * @private
  356. * @returns {Collection} Self, this method is chainable.
  357. */
  358. resetQuery: function() {
  359. this._knex = null;
  360. return this;
  361. },
  362. /**
  363. * This method is used to tap into the underlying Knex query builder instance for the current
  364. * collection.
  365. *
  366. * If called with no arguments, it will return the query builder directly, otherwise it will
  367. * call the specified `method` on the query builder, applying any additional arguments from the
  368. * `collection.query` call.
  369. *
  370. * If the `method` argument is a function, it will be called with the Knex query builder as the
  371. * context and the first argument.
  372. *
  373. * @see {@link http://knexjs.org/#Builder Knex `QueryBuilder`}
  374. * @example
  375. * let qb = collection.query();
  376. * qb.where({id: 1}).select().then(function(resp) {
  377. * // ...
  378. * });
  379. *
  380. * collection.query(function(qb) {
  381. * qb.where('id', '>', 5).andWhere('first_name', '=', 'Test');
  382. * }).fetch()
  383. * .then(function(collection) {
  384. * // ...
  385. * });
  386. *
  387. * collection
  388. * .query('where', 'other_id', '=', '5')
  389. * .fetch()
  390. * .then(function(collection) {
  391. * // ...
  392. * });
  393. *
  394. * @param {function|Object|...string=} arguments The query method.
  395. * @returns {Collection|QueryBuilder}
  396. * This collection or, if called with no arguments, the underlying query builder.
  397. */
  398. query: function() {
  399. return Helpers.query(this, Array.from(arguments));
  400. },
  401. /**
  402. * This is used as convenience for the most common {@link Collection#query query} method:
  403. * adding a `WHERE` clause to the builder. Any additional knex methods may be accessed using
  404. * {@link Collection#query query}.
  405. *
  406. * Accepts either `key, value` syntax, or a hash of attributes to constrain the results.
  407. *
  408. * @example
  409. * collection
  410. * .where('favorite_color', '<>', 'green')
  411. * .fetch()
  412. * .then(results => {
  413. * // ...
  414. * })
  415. *
  416. * // or
  417. *
  418. * collection
  419. * .where('favorite_color', 'red')
  420. * .fetch()
  421. * .then(results => {
  422. * // ...
  423. * })
  424. *
  425. * collection
  426. * .where({favorite_color: 'red', shoe_size: 12})
  427. * .fetch()
  428. * .then(results => {
  429. * // ...
  430. * })
  431. *
  432. * @see Collection#query
  433. * @param {Object|...string} conditions
  434. * Either `key, [operator], value` syntax, or a hash of attributes to match. Note that these
  435. * must be formatted as they are in the database, not how they are stored after
  436. * {@link Model#parse}.
  437. * @returns {Collection} Self, this method is chainable.
  438. */
  439. where() {
  440. return this.query.apply(this, ['where'].concat(Array.from(arguments)));
  441. },
  442. /**
  443. * Specifies the column to sort on and sort order.
  444. *
  445. * The order parameter is optional, and defaults to 'ASC'. You may
  446. * also specify 'DESC' order by prepending a hyphen to the sort column
  447. * name. `orderBy("date", 'DESC')` is the same as `orderBy("-date")`.
  448. *
  449. * Unless specified using dot notation (i.e., "table.column"), the default
  450. * table will be the table name of the model `orderBy` was called on.
  451. *
  452. * @since 0.9.3
  453. * @example
  454. * Cars.forge().orderBy('color', 'ASC').fetch()
  455. * .then(function (rows) { // ...
  456. *
  457. * @param {string} column Column to sort on.
  458. * @param {string} order Ascending (`'ASC'`) or descending (`'DESC'`) order.
  459. */
  460. orderBy() {
  461. return Helpers.orderBy.apply(null, [this].concat(Array.from(arguments)));
  462. },
  463. /**
  464. * Creates and returns a new `Bookshelf.Sync` instance.
  465. *
  466. * @private
  467. */
  468. sync: function(options) {
  469. return new Sync(this, options);
  470. },
  471. /* Ensure that QueryBuilder is copied on clone. */
  472. clone() {
  473. const cloned = BookshelfCollection.__super__.clone.apply(this, arguments);
  474. if (this._knex != null) {
  475. cloned._knex = cloned._builder(this._knex.clone());
  476. }
  477. return cloned;
  478. },
  479. /**
  480. * Handles the response data for the collection, returning from the collection's `fetch` call.
  481. *
  482. * @private
  483. */
  484. _handleResponse: function(response, options) {
  485. const relatedData = this.relatedData;
  486. this.set(response, {
  487. merge: options.merge,
  488. remove: options.remove,
  489. silent: true,
  490. parse: true
  491. }).invokeMap(function() {
  492. this.formatTimestamps();
  493. this._reset();
  494. this._previousAttributes = _.cloneDeep(this.attributes);
  495. });
  496. if (relatedData && relatedData.isJoined()) {
  497. relatedData.parsePivot(this.models);
  498. }
  499. },
  500. /**
  501. * Handle the related data loading on the collection.
  502. *
  503. * @private
  504. */
  505. _handleEager: function(response, options) {
  506. return new EagerRelation(this.models, response, new this.model()).fetch(options);
  507. }
  508. },
  509. /** @lends Collection */
  510. {
  511. extended: function(child) {
  512. /**
  513. * @class Collection.EmptyError
  514. * @description
  515. * Thrown by default when no records are found by {@link Collection#fetch fetch} or
  516. * {@link Collection#fetchOne}. This behavior can be overrided with the
  517. * {@link Model#requireFetch} option.
  518. */
  519. child.EmptyError = createError(this.EmptyError);
  520. }
  521. }
  522. ));
  523. BookshelfCollection.EmptyError = Errors.EmptyError;