UNPKG

bookshelf

Version:

A lightweight ORM for PostgreSQL, MySQL, and SQLite3

546 lines (523 loc) 20.9 kB
const _ = require('lodash'); const Sync = require('./sync'); const Helpers = require('./helpers'); const EagerRelation = require('./eager'); const Errors = require('./errors'); const CollectionBase = require('./base/collection'); const Promise = require('bluebird'); const createError = require('create-error'); /** * When creating a {@link Collection}, you may choose to pass in the initial array of * {@link Model models}. The collection's {@link Collection#comparator comparator} may be included * as an option. Passing `false` as the comparator option will prevent sorting. If you define an * {@link Collection#initialize initialize} function, it will be invoked when the collection is * created. * * If you would like to customize the Collection used by your models when calling * {@link Model#fetchAll} or {@link Model#fetchPage} you can use the following process: * * const Test = bookshelf.model('Test', { * tableName: 'test' * }, { * collection(...args) { * return new Tests(...args) * } * }) * const Tests = bookshelf.collection('Tests', { * get model() { * return Test * }, * initialize () { * this.constructor.__super__.initialize.apply(this, arguments) * // Collection will emit fetching event as expected even on eager queries. * this.on('fetching', () => {}) * }, * doStuff() { * // This method will be available in the results collection returned * // by Test.fetchAll() and Test.fetchPage() * } * }) * * @example * const TabSet = bookshelf.collection('TabSet', { * model: Tab * }) * const tabs = new TabSet([tab1, tab2, tab3]) * * @class Collection * @extends CollectionBase * @classdesc * Collections are ordered sets of models returned from the database, from a * {@link Model#fetchAll fetchAll} call. * @param {(Model[])=} models Initial array of models. * @param {Object=} options * @param {Boolean} [options.comparator=false] * {@link Collection#comparator Comparator} for collection, or `false` to disable sorting. */ const BookshelfCollection = (module.exports = CollectionBase.extend( /** @lends Collection.prototype */ { /** * Used to define relationships where a {@link Model#hasMany hasMany} or * {@link Model#belongsToMany belongsToMany} relation passes "through" an `Interim` model. This * is exactly like the equivalent {@link Model#through model method} except that it applies to * the collections that the above mentioned relation methods return instead of individual * models. * * A good example of where this would be useful is if a book {@link Model#hasMany hasMany} * paragraphs *through* chapters. See the example above for how this can be used. * * @example * const Chapter = bookshelf.model('Chapter', { * tableName: 'chapters', * paragraphs() { * return this.hasMany(Paragraph) * } * }) * * const Paragraph = bookshelf.model('Paragraph', { * tableName: 'paragraphs', * chapter() { * return this.belongsTo(Chapter) * } * }) * * const Book = bookshelf.model('Book', { * tableName: 'books', * // Find all paragraphs associated with this book, by * // passing through the "Chapter" model. * paragraphs() { * return this.hasMany(Paragraph).through(Chapter) * } * }) * * @param {Model} Interim Pivot model. * @param {string} [throughForeignKey] * Foreign key in this collection's model. This is the model that the `hasMany` or * `belongsToMany` relations return. By default, the `foreignKey` is assumed to be the * singular form of the `Target` model's tableName, followed by `_id` / * `_{{{@link Model#idAttribute idAttribute}}}`. * @param {string} [otherKey] * Foreign key in the `Interim` model. By default, the `otherKey` is assumed to be the * singular form of this model's tableName, followed by `_id` / * `_{{{@link Model#idAttribute idAttribute}}}`. * @param {string} [throughForeignKeyTarget] * Column in this collection's model which `throughForeignKey` references, if other than the * default of the model's `id` / `{@link Model#idAttribute idAttribute}`. * @param {string} [otherKeyTarget] * Column in the `Interim` model which `otherKey` references, if other than `id` / * `{@link Model#idAttribute idAttribute}`. * @returns {Collection} The related but empty collection. */ through: function(Interim, throughForeignKey, otherKey, throughForeignKeyTarget, otherKeyTarget) { return this.relatedData.through(this, Interim, { throughForeignKey, otherKey, throughForeignKeyTarget, otherKeyTarget }); }, /** * Fetch the default set of models for this collection from the database, * resetting the collection when they arrive. If you wish to trigger an * error if the fetched collection is empty, pass `{require: true}` as one * of the options to the {@link Collection#fetch fetch} call. A {@link * Collection#fetched "fetched"} event will be fired when records are * successfully retrieved. If you need to constrain the query performed by * `fetch`, you can call the {@link Collection#query query} method before * calling `fetch`. * * If you'd like to only fetch specific columns, you may specify a `columns` * property in the options for the `fetch` call. * * The `withRelated` option may be specified to fetch the models of the * collection, eager loading any specified {@link Relation relations} named on * the model. A single property, or an array of properties can be specified as * a value for the `withRelated` property. The results of these relation * queries will be loaded into a relations property on the respective models, * may be retrieved with the {@link Model#related related} method. * * @fires Collection#fetched * @throws {Collection.EmptyError} Thrown if no records are found. * @param {Object=} options * @param {Boolean} [options.require=false] * Whether or not to throw a {@link Collection.EmptyError} if no records are found. * You can pass the `require: true` option to override this behavior. * @param {string|string[]} [options.withRelated=[]] * A relation, or list of relations, to be eager loaded as part of the `fetch` operation. * @param {boolean} [options.debug=false] * Whether to enable debugging mode or not. When enabled will show information about the * queries being run. * @returns {Promise<Collection>} */ fetch: Promise.method(function(options) { options = options ? _.clone(options) : {}; return ( this.sync(options) .select() .bind(this) .tap(function(response) { if (!response || response.length === 0) { throw new this.constructor.EmptyError('EmptyResponse'); } }) // Now, load all of the data onto the collection as necessary. .tap(function(response) { return this._handleResponse(response, options); }) // If the "withRelated" is specified, we also need to eager load all of the // data on the collection, as a side-effect, before we ultimately jump into the // next step of the collection. Since the `columns` are only relevant to the current // level, ensure those are omitted from the options. .tap(function(response) { if (options.withRelated) { return this._handleEager(response, _.omit(options, 'columns')); } }) .tap(function(response) { /** * @event Collection#fetched * @tutorial events * * @description * Fired after a `fetch` operation. A promise may be returned from the * event handler for async behaviour. * * @param {Collection} collection The collection performing the {@link Collection#fetch}. * @param {Object} response Knex query response. * @param {Object} options Options object passed to {@link Collection#fetch fetch}. * @returns {Promise} */ return this.triggerThen('fetched', this, response, options); }) .catch(this.constructor.EmptyError, function(err) { if (options.require) throw err; this.reset([], {silent: true}); }) .return(this) ); }), fetchPage(options) { if (!options) options = {}; return Helpers.fetchPage.call(this, options); }, /** * Get the number of records in the collection's table. * * @example * // select count(*) from shareholders where company_id = 1 and share &gt; 0.1; * new Company({id: 1}) * .shareholders() * .where('share', '>', '0.1') * .count() * .then((count) => { * assert(count === 3) * }) * * @since 0.8.2 * @see Model#count * @param {string} [column='*'] * Specify a column to count. Rows with `null` values in this column will be excluded. * @param {Object} [options] Hash of options. * @returns {Promise<number|string>} */ count: Promise.method(function(column, options) { if (!_.isString(column)) { options = column; column = undefined; } if (options) options = _.clone(options); return this.sync(options).count(column); }), /** * Fetch and return a single {@link Model model} from the collection, * maintaining any {@link Relation relation} data from the collection, and * any {@link Collection#query query} parameters that have already been passed * to the collection. Especially helpful on relations, where you would only * like to return a single model from the associated collection. * * @example * // select * from authors where site_id = 1 and id = 2 limit 1; * new Site({id:1}) * .authors() * .query({where: {id: 2}}) * .fetchOne() * .then(function(model) { * // ... * }); * * @param {Object=} options * @param {Boolean} [options.require=true] * Whether or not to reject the returned Promise with a {@link Model.NotFoundError} if no * records can be fetched from the database. * @param {(string|string[])} [options.columns='*'] * Limit the number of columns fetched. * @param {Transaction} [options.transacting] Optionally run the query in a transaction. * @param {string} [options.lock] * Type of row-level lock to use. Valid options are `forShare` and * `forUpdate`. This only works in conjunction with the `transacting` * option, and requires a database that supports it. * @param {boolean} [options.debug=false] * Whether to enable debugging mode or not. When enabled will show information about the * queries being run. * @throws {Model.NotFoundError} * @returns {Promise<Model|null>} * A promise resolving to the fetched {@link Model} or `null` if none exists and the * `require: false` option is passed or {@link Model#requireFetch requireFetch} is set to * `false`. */ fetchOne: Promise.method(function(options) { const model = new this.model(); model._knex = this.query().clone(); this.resetQuery(); if (this.relatedData) model.relatedData = this.relatedData; return model.fetch(options); }), /** * This method is used to eager load relations onto a Collection, in a similar way that the * `withRelated` property works on {@link Collection#fetch fetch}. Nested eager loads can be * specified by separating the nested relations with `.`. * * @param {string|string[]} relations The relation, or relations, to be loaded. * @param {Object} [options] Hash of options. * @param {Transaction} [options.transacting] * @param {string} [options.lock] * Type of row-level lock to use. Valid options are `forShare` and `forUpdate`. This only * works in conjunction with the `transacting` option, and requires a database that supports * it. * @param {boolean} [options.debug=false] * Whether to enable debugging mode or not. When enabled will show information about the * queries being run. * @returns {Promise<Collection>} A promise resolving to this {@link Collection collection}. */ load: Promise.method(function(relations, options) { if (!Array.isArray(relations)) relations = [relations]; options = _.assignIn({}, options, { shallow: true, withRelated: relations }); return new EagerRelation(this.models, this.toJSON(options), new this.model()).fetch(options).return(this); }), /** * Convenience method to create a new {@link Model model} instance within a * collection. Equivalent to instantiating a model with a hash of {@link * Model#attributes attributes}, {@link Model#save saving} the model to the * database then adding the model to the collection. * * When used on a relation, `create` will automatically set foreign key * attributes before persisting the `Model`. * * @example * const { courses, ...attributes } = req.body; * * Student.forge(attributes).save().tap(student => * Promise.map(courses, course => student.related('courses').create(course)) * ).then(student => * res.status(200).send(student) * ).catch(error => * res.status(500).send(error.message) * ); * * @param {Object} model A set of attributes to be set on the new model. * @param {Object} [options] * @param {Transaction} [options.transacting] * @param {boolean} [options.debug=false] * Whether to enable debugging mode or not. When enabled will show information about the * queries being run. * @returns {Promise<Model>} A promise resolving with the new {@link Model model}. */ create: Promise.method(function(model, options) { options = options != null ? _.clone(options) : {}; const relatedData = this.relatedData; model = this._prepareModel(model, options); // If we've already added things on the query chain, these are likely intended for the model. if (this._knex) { model._knex = this._knex; this.resetQuery(); } return Helpers.saveConstraints(model, relatedData) .save(null, options) .bind(this) .then(function() { if (relatedData && relatedData.type === 'belongsToMany') { return this.attach(model, _.omit(options, 'query')); } }) .then(function() { this.add(model, options); }) .return(model); }), /** * Used to reset the internal state of the current query builder instance. This method is called * internally each time a database action is completed by {@link Sync}. * * @private * @returns {Collection} Self, this method is chainable. */ resetQuery: function() { this._knex = null; return this; }, /** * This method is used to tap into the underlying Knex query builder instance for the current * collection. * * If called with no arguments, it will return the query builder directly, otherwise it will * call the specified `method` on the query builder, applying any additional arguments from the * `collection.query` call. * * If the `method` argument is a function, it will be called with the Knex query builder as the * context and the first argument. * * @see {@link http://knexjs.org/#Builder Knex `QueryBuilder`} * @example * let qb = collection.query(); * qb.where({id: 1}).select().then(function(resp) { * // ... * }); * * collection.query(function(qb) { * qb.where('id', '>', 5).andWhere('first_name', '=', 'Test'); * }).fetch() * .then(function(collection) { * // ... * }); * * collection * .query('where', 'other_id', '=', '5') * .fetch() * .then(function(collection) { * // ... * }); * * @param {function|Object|...string=} arguments The query method. * @returns {Collection|QueryBuilder} * This collection or, if called with no arguments, the underlying query builder. */ query: function() { return Helpers.query(this, Array.from(arguments)); }, /** * This is used as convenience for the most common {@link Collection#query query} method: * adding a `WHERE` clause to the builder. Any additional knex methods may be accessed using * {@link Collection#query query}. * * Accepts either `key, value` syntax, or a hash of attributes to constrain the results. * * @example * collection * .where('favorite_color', '<>', 'green') * .fetch() * .then(results => { * // ... * }) * * // or * * collection * .where('favorite_color', 'red') * .fetch() * .then(results => { * // ... * }) * * collection * .where({favorite_color: 'red', shoe_size: 12}) * .fetch() * .then(results => { * // ... * }) * * @see Collection#query * @param {Object|...string} conditions * Either `key, [operator], value` syntax, or a hash of attributes to match. Note that these * must be formatted as they are in the database, not how they are stored after * {@link Model#parse}. * @returns {Collection} Self, this method is chainable. */ where() { return this.query.apply(this, ['where'].concat(Array.from(arguments))); }, /** * Specifies the column to sort on and sort order. * * The order parameter is optional, and defaults to 'ASC'. You may * also specify 'DESC' order by prepending a hyphen to the sort column * name. `orderBy("date", 'DESC')` is the same as `orderBy("-date")`. * * Unless specified using dot notation (i.e., "table.column"), the default * table will be the table name of the model `orderBy` was called on. * * @since 0.9.3 * @example * Cars.forge().orderBy('color', 'ASC').fetch() * .then(function (rows) { // ... * * @param {string} column Column to sort on. * @param {string} order Ascending (`'ASC'`) or descending (`'DESC'`) order. */ orderBy() { return Helpers.orderBy.apply(null, [this].concat(Array.from(arguments))); }, /** * Creates and returns a new `Bookshelf.Sync` instance. * * @private */ sync: function(options) { return new Sync(this, options); }, /* Ensure that QueryBuilder is copied on clone. */ clone() { const cloned = BookshelfCollection.__super__.clone.apply(this, arguments); if (this._knex != null) { cloned._knex = cloned._builder(this._knex.clone()); } return cloned; }, /** * Handles the response data for the collection, returning from the collection's `fetch` call. * * @private */ _handleResponse: function(response, options) { const relatedData = this.relatedData; this.set(response, { merge: options.merge, remove: options.remove, silent: true, parse: true }).invokeMap(function() { this.formatTimestamps(); this._reset(); this._previousAttributes = _.cloneDeep(this.attributes); }); if (relatedData && relatedData.isJoined()) { relatedData.parsePivot(this.models); } }, /** * Handle the related data loading on the collection. * * @private */ _handleEager: function(response, options) { return new EagerRelation(this.models, response, new this.model()).fetch(options); } }, /** @lends Collection */ { extended: function(child) { /** * @class Collection.EmptyError * @description * Thrown by default when no records are found by {@link Collection#fetch fetch} or * {@link Collection#fetchOne}. This behavior can be overrided with the * {@link Model#requireFetch} option. */ child.EmptyError = createError(this.EmptyError); } } )); BookshelfCollection.EmptyError = Errors.EmptyError;