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 > 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;