'use strict'; const _ = require('lodash'); const createError = require('create-error'); const Sync = require('./sync'); const Helpers = require('./helpers'); const EagerRelation = require('./eager'); const Errors = require('./errors'); const ModelBase = require('./base/model'); const Promise = require('bluebird'); /** * @class Model * @extends ModelBase * @inheritdoc * @classdesc * Models are simple objects representing individual database rows, specifying * the tableName and any relations to other models. They can be extended with * any domain-specific methods, which can handle components such as validations, * computed properties, and access control. * * @constructor * @description * When defining a model you should use the {@link Bookshelf#model bookshelf.model} method, since it will allow you to * avoid circular dependency problems. However, it's still possible to create models using the regular constructor. * * When creating an instance of a model, you can pass in the initial values of * the attributes, which will be {@link Model#set set} on the * model. If you define an {@link initialize} function, it will be invoked * when the model is created. * * new Book({ * title: "One Thousand and One Nights", * author: "Scheherazade" * }); * * In rare cases, if you're looking to get fancy, you may want to override * {@link Model#constructor constructor}, which allows you to replace the * actual constructor function for your model. * * let Book = bookshelf.model('Book', { * tableName: 'documents', * constructor: function() { * bookshelf.Model.apply(this, arguments); * this.on('saving', function(model, attrs, options) { * options.query.where('type', '=', 'book'); * }); * } * }); * * @param {Object} attributes Initial values for this model's attributes. * @param {Object=} options Hash of options. * @param {string=} options.tableName Initial value for {@link Model#tableName tableName}. * @param {Boolean=} [options.hasTimestamps=false] * * Initial value for {@link Model#hasTimestamps hasTimestamps}. * * @param {Boolean} [options.parse=false] * * Convert attributes by {@link Model#parse parse} before being {@link * Model#set set} on the model. * */ const BookshelfModel = ModelBase.extend( { /** * This relation specifies that this table has exactly one of another type of object, specified by a foreign key in * the other table. * * @example * const Record = bookshelf.model('Record', { * tableName: 'health_records' * }) * * const Patient = bookshelf.model('Patient', { * tableName: 'patients', * record() { * return this.hasOne('Record') * } * }) * * // select * from `health_records` where `patient_id` = 1 * new Patient({id: 1}).related('record').fetch().then(function(model) { * // ... * }) * * // Alternatively, if you don't need the relation loaded on the patient's relations hash: * new Patient({id: 1}).record().fetch().then(function(model) { * // ... * }) * * @method Model#hasOne * @param {Model|string} Target * Constructor of {@link Model} targeted by join. Can be a string specifying a previously registered model with * {@link Bookshelf#model}. * @param {string} [foreignKey] * Foreign key in the `Target` model. By default the foreign key is assumed to be the singular form of this * model's {@link Model#tableName tableName} followed by `_id` / `_{{{@link Model#idAttribute idAttribute}}}`. * @param {string} [foreignKeyTarget] * Column in this model's table which `foreignKey` references, if other than this model's `id` / * `{@link Model#idAttribute idAttribute}`. * @returns {Model} * The return value will always be a model, even if the relation doesn't exist, but in that case the relation will * be `null` when {@link Model#serialize serializing} the model. */ hasOne(Target, foreignKey, foreignKeyTarget) { return this._relation('hasOne', Target, { foreignKey, foreignKeyTarget }).init(this); }, /** * This relation specifies that this model has one or more rows in another table which match on this model's primary * key. * * @example * const Author = bookshelf.model('Author', { * tableName: 'authors', * books() { * return this.hasMany('Book') * } * }) * * // select * from `authors` where id = 1 * // select * from `books` where author_id = 1 * Author.where({id: 1}).fetch({withRelated: ['books']}).then(function(author) { * console.log(JSON.stringify(author.related('books'))) * }) * * @method Model#hasMany * @param {Model|string} Target * Constructor of {@link Model} targeted by join. Can be a string specifying a previously registered model with * {@link Bookshelf#model}. * @param {string} [foreignKey] * ForeignKey in the `Target` model. By default, the foreign key is assumed to be the singular form of this * model's tableName, followed by `_id` / `_{{{@link Model#idAttribute idAttribute}}}`. * @param {string} [foreignKeyTarget] * Column in this model's table which `foreignKey` references, if other than this model's `id` / * `{@link Model#idAttribute idAttribute}`. * @returns {Collection} A new empty Collection. */ hasMany(Target, foreignKey, foreignKeyTarget) { return this._relation('hasMany', Target, { foreignKey, foreignKeyTarget }).init(this); }, /** * This relationship is used when a model is a member of another `Target` model. * * It can be used in {@tutorial one-to-one} associations as the inverse of a * {@link Model#hasOne hasOne}. It can also used in {@tutorial one-to-many} associations as the * inverse of {@link Model#hasMany hasMany}, and is the "one" side of that association. In both * cases, the belongsTo relationship is used for a model that is a member of another Target * model, referenced by the `foreignKey` attribute in the current model. * * @example * const Book = bookshelf.model('Book', { * tableName: 'books', * author() { * return this.belongsTo('Author') * } * }) * * // select * from `books` where id = 1 * // select * from `authors` where id = book.author_id * Book.where({id: 1}).fetch({withRelated: ['author']}).then((book) => { * console.log(JSON.stringify(book.related('author'))) * }) * * @method Model#belongsTo * @param {Model|string} Target * Constructor of {@link Model} targeted by the join. Can be a string specifying a previously registered model * with {@link Bookshelf#model}. * @param {string} [foreignKey] * Foreign key in this model. By default, the `foreignKey` is assumed to be the singular form * of the `Target` model's tableName, followed by `_id`, or * `_{{{@link Model#idAttribute idAttribute}}}` if the `idAttribute` property is set. * @param {string} [foreignKeyTarget] * Column in the `Target` model's table which `foreignKey` references. This is only needed in * case it's other than `Target` model's `id` / `{@link Model#idAttribute idAttribute}`. * @returns {Model} * The return value will always be a model, even if the relation doesn't exist, but in that * case the relation will be `null` when {@link Model#serialize serializing} the model. */ belongsTo(Target, foreignKey, foreignKeyTarget) { return this._relation('belongsTo', Target, { foreignKey, foreignKeyTarget }).init(this); }, /** * Defines a many-to-many relation, where the current model is joined to one or more of a * `Target` model through another table. The default name for the joining table is the two * models' table names joined by an underscore, and ordered alphabetically. For example, a * `users` table and an `accounts` table would have a joining table named `accounts_users`. * * The default key names in the joining table are the singular versions of the model table * names, followed by `_id` / `_{{{@link Model#idAttribute idAttribute}}}`. So in the above * example the columns in the joining table would be `user_id`, `account_id`, and `access`, * which is used as an example of how dynamic relations can be formed using different contexts. * * To customize the keys or the {@link Model#tableName tableName} used for the join table, you * may specify them in the arguments to the function call: * * this.belongsToMany(Account, 'users_accounts', 'userId', 'accountId') * * If you wish to create a belongsToMany association where the joining table has a primary key * and extra attributes in the model, you may create a `belongsToMany` * {@link Relation#through through} relation: * * const Doctor = bookshelf.model('Doctor', { * patients() { * return this.belongsToMany('Patient').through('Appointment') * } * }) * * const Appointment = bookshelf.model('Appointment', { * patient() { * return this.belongsTo('Patient') * }, * doctor() { * return this.belongsTo('Doctor') * } * }) * * const Patient = bookshelf.model('Patient', { * doctors() { * return this.belongsToMany('Doctor').through('Appointment') * } * }) * * Collections returned by a `belongsToMany` relation are decorated with several pivot helper * methods. If you need more information about these methods see * {@link Collection#attach attach}, {@link Collection#detach detach}, * {@link Collection#updatePivot updatePivot} and {@link Collection#withPivot withPivot}. * * @example * const Account = bookshelf.model('Account', { * tableName: 'accounts' * }) * * const User = bookshelf.model('User', { * tableName: 'users', * allAccounts() { * return this.belongsToMany('Account') * }, * adminAccounts() { * return this.belongsToMany('Account').query({where: {access: 'admin'}}) * }, * viewAccounts() { * return this.belongsToMany('Account').query({where: {access: 'readonly'}}) * } * }) * * @method Model#belongsToMany * @param {Model|string} Target * Constructor of {@link Model} targeted by join. Can be a string specifying a previously registered model with * {@link Bookshelf#model}. * @param {string} [joinTableName] * Name of the joining table. Defaults to the two table names ordered alphabetically and * joined by an underscore. * @param {string} [foreignKey] * Foreign key in this model. By default, the `foreignKey` is assumed to be the singular form * of this model's tableName, followed by `_id` / `_{{{@link Model#idAttribute idAttribute}}}`. * @param {string} [otherKey] * Foreign key in the `Target` model. By default, this is assumed to be the singular form of * the `Target` model's tableName, followed by `_id` / * `_{{{@link Model#idAttribute idAttribute}}}`. * @param {string} [foreignKeyTarget] * Column in this model's table which `foreignKey` references. This is only needed if it's not * the default `id` / `{@link Model#idAttribute idAttribute}`. * @param {string} [otherKeyTarget] * Column in the `Target` model's table which `otherKey` references. This is only needed, if * it's not the expected default of the `Target` model's `id` / * `{@link Model#idAttribute idAttribute}`. * @returns {Collection} * A new empty collection that is decorated with extra pivot helper methods. See the * description below for more info. */ belongsToMany(Target, joinTableName, foreignKey, otherKey, foreignKeyTarget, otherKeyTarget) { return this._relation('belongsToMany', Target, { joinTableName, foreignKey, otherKey, foreignKeyTarget, otherKeyTarget }).init(this); }, /** * The {@link Model#morphOne morphOne} is used to signify a {@link oneToOne * one-to-one} {@link polymorphicRelation polymorphic relation} with * another `Target` model, where the `name` of the model is used to determine * which database table keys are used. The naming convention requires the * `name` prefix an `_id` and `_type` field in the database. So for the case * below the table names would be `imageable_type` and `imageable_id`. The * `morphValue` may be optionally set to store/retrieve a different value in * the `_type` column than the {@link Model#tableName}. * * let Site = bookshelf.model('Site', { * tableName: 'sites', * photo: function() { * return this.morphOne('Photo', 'imageable'); * } * }); * * And with custom `columnNames`: * * let Site = bookshelf.model('Site', { * tableName: 'sites', * photo: function() { * return this.morphOne('Photo', 'imageable', ['ImageableType', 'ImageableId']); * } * }); * * Note that both `columnNames` and `morphValue` are optional arguments. How * your argument is treated when only one is specified, depends on the type. * If your argument is an array, it will be assumed to contain custom * `columnNames`. If it's not, it will be assumed to indicate a `morphValue`. * * @method Model#morphOne * @param {Model|string} Target * Constructor of {@link Model} targeted by join. Can be a string specifying a previously registered model with * {@link Bookshelf#model}. * @param {string} [name] Prefix for `_id` and `_type` columns. * @param {(string[])} [columnNames] * Array containing two column names, the first is the `_type` while the second is the `_id`. * @param {string} [morphValue=Target#{@link Model#tableName tableName}] * The string value associated with this relationship. Stored in the `_type` column of the polymorphic table. * Defaults to `Target#{@link Model#tableName tableName}`. * @returns {Model} The related model. */ morphOne(Target, name, columnNames, morphValue) { return this._morphOneOrMany(Target, name, columnNames, morphValue, 'morphOne'); }, /** * {@link Model#morphMany morphMany} is essentially the same as a {@link * Model#morphOne morphOne}, but creating a {@link Collection collection} * rather than a {@link Model model} (similar to a {@link Model#hasOne * hasOne} vs. {@link Model#hasMany hasMany} relation). * * {@link Model#morphMany morphMany} is used to signify a {@link oneToMany * one-to-many} or {@link manyToMany many-to-many} {@link polymorphicRelation * polymorphic relation} with another `Target` model, where the `name` of the * model is used to determine which database table keys are used. The naming * convention requires the `name` prefix an `_id` and `_type` field in the * database. So for the case below the table names would be `imageable_type` * and `imageable_id`. The `morphValue` may be optionally set to * store/retrieve a different value in the `_type` column than the `Target`'s * {@link Model#tableName tableName}. * * let Post = bookshelf.model('Post', { * tableName: 'posts', * photos: function() { * return this.morphMany('Photo', 'imageable'); * } * }); * * And with custom columnNames: * * let Post = bookshelf.model('Post'{ * tableName: 'posts', * photos: function() { * return this.morphMany('Photo', 'imageable', ['ImageableType', 'ImageableId']); * } * }); * * @method Model#morphMany * @param {Model|string} Target * Constructor of {@link Model} targeted by join. Can be a string specifying a previously registered model with * {@link Bookshelf#model}. * @param {string} [name] Prefix for `_id` and `_type` columns. * @param {(string[])} [columnNames] * Array containing two column names, the first is the `_type` while the second is the `_id`. * @param {string} [morphValue=Target#{@link Model#tableName tablename}] * The string value associated with this relationship. Stored in the `_type` column of the polymorphic table. * Defaults to `Target`#{@link Model#tableName tablename}. * @returns {Collection} A collection of related models. */ morphMany(Target, name, columnNames, morphValue) { return this._morphOneOrMany(Target, name, columnNames, morphValue, 'morphMany'); }, /** * This relation is used to specify the inverse of the {@link Model#morphOne morphOne} or * {@link Model#morphMany morphMany} relations, where the `targets` must be passed to signify which * {@link Model models} are the potential opposite end of the {@link polymorphicRelation polymorphic relation}: * * const Photo = bookshelf.model('Photo', { * tableName: 'photos', * imageable() { * return this.morphTo('imageable', 'Site', 'Post') * } * }) * * And with custom column names: * * const Photo = bookshelf.model('Photo', { * tableName: 'photos', * imageable() { * return this.morphTo('imageable', ['ImageableType', 'ImageableId'], 'Site', 'Post') * } * }) * * And with custom morphValues, the inverse of the `morphValue` of {@link Model#morphOne morphOne} and * {@link Model#morphMany morphMany}, where the `morphValues` may be optionally set to check against a different * value in the `_type` column other than the {@link Model#tableName}, for example, a more descriptive name, or a * name that betters adheres to whatever standard you are using for models: * * const Photo = bookshelf.model('Photo', { * tableName: 'photos', * imageable() { * return this.morphTo('imageable', ['Site', 'favicon'], ['Post', 'cover_photo']) * } * }) * * @method Model#morphTo * @param {string} name Prefix for `_id` and `_type` columns. * @param {string[]} [columnNames] * Array containing two column names, where the first is the `_type` and the second is the `_id`. * @param {Model|string} [Target] * Constructor of {@link Model} targeted by join. Can be a string specifying a previously registered model with * {@link Bookshelf#model}. * @returns {Model} The related but empty model. */ morphTo(morphName) { if (!_.isString(morphName)) throw new Error('The `morphTo` name must be specified.'); let columnNames, candidates; if (arguments[1] == null || (Array.isArray(arguments[1]) && _.isString(arguments[1][0]))) { columnNames = arguments[1] || null; // may be `null` or `undefined` candidates = _.drop(arguments, 2); } else { columnNames = null; candidates = _.drop(arguments, 1); } candidates = _.map(candidates, (target) => { if (Array.isArray(target)) return target; // Set up the morphValue by default as the tableName return [target, _.result(target.prototype, 'tableName')]; }); return this._relation('morphTo', null, {morphName, columnNames, candidates}).init(this); }, /** * Helps to create dynamic relations between {@link Model models} where a {@link Model#hasOne hasOne} or * {@link Model#belongsTo belongsTo} relation may run through another `Interim` model. This is exactly like the * equivalent {@link Collection#through collection method} except that it applies to the models that the above * mentioned relation methods return instead of collections. * * This method creates a pivot model, which it assigns to {@link Model#pivot model.pivot} after it is created. When * serializing the model with {@link Model#toJSON toJSON}, the pivot model is flattened to values prefixed with * `_pivot_`. * * A good example of where this would be useful is if a paragraph {@link Model#hasMany belongTo} a book *through* a * chapter. See the example above on how this can be expressed. * * @method Model#through * @example * const Chapter = bookshelf.model('Chapter', { * tableName: 'chapters', * paragraphs() { * return this.hasMany('Paragraph') * } * }) * const Book = bookshelf.model('Book', { * tableName: 'books', * chapters() { * return this.hasMany('Chapter') * } * }) * const Paragraph = bookshelf.model('Paragraph', { * tableName: 'paragraphs', * chapter() { * return this.belongsTo('Chapter') * }, * * // Find the book where this paragraph is included, by passing through * // the "Chapter" model. * book() { * return this.belongsTo('Book').through('Chapter') * } * }) * * @param {Model|string} Interim * Pivot model. Can be a string specifying a previously registered model with {@link Bookshelf#model}. * @param {string} [throughForeignKey] * Foreign key in this model. By default, the foreign key is assumed to be the singular form of the `Target` * model's tableName, followed by `_id` or `_{{{@link Model#idAttribute idAttribute}}}`. * @param {string} [otherKey] * Foreign key in the `Interim` model. By default, the other key is assumed to be the singular form of this * model's tableName, followed by `_id` / `_{{{@link Model#idAttribute idAttribute}}}`. * @param {string} [throughForeignKeyTarget] * Column in the `Target` model which `throughForeignKey` references, if other than `Target` model's `id` / * `{@link Model#idAttribute idAttribute}`. * @param {string} [otherKeyTarget] * Column in this model which `otherKey` references, if other than `id` / `{@link Model#idAttribute idAttribute}`. * @returns {Model} The related but empty Model. */ through(Interim, throughForeignKey, otherKey, throughForeignKeyTarget, otherKeyTarget) { return this.relatedData.through(this, Interim, { throughForeignKey, otherKey, throughForeignKeyTarget, otherKeyTarget }); }, /** * @method Model#refresh * @since 0.8.2 * @description * * Update the attributes of a model, fetching it by its primary key. If no * attribute matches its {@link Model#idAttribute idAttribute}, then fetch by * all available fields. * * @param {Object} options * A hash of options. See {@link Model#fetch} for details. * @returns {Promise} * A promise resolving to this model. */ refresh(options = {}) { let attributes = {}; // If this is new, we use all its attributes. Otherwise we just grab the primary key. if (this.isNew()) { attributes = this.attributes; } else { attributes[this.idAttribute] = this.attributes[this.idAttribute] || this.attributes[this.parsedIdAttribute()]; } return this._doFetch(attributes, options).tap(() => { if (!options.silent) this._previousAttributes = _.cloneDeep(this.attributes); }); }, /** * This method is similar to {@link Model#fetchAll}, but fetches a single page of results as * specified by the limit (page size) and offset (page number). * * Any options that may be passed to {@link Model#fetchAll} may also be passed in the options * to this method. Additionally, to perform pagination, you may include **either** an `offset` * and `limit`, **or** a `page` and `pageSize`. * * By default, with no parameters or some missing parameters, `fetchPage` will use default * values of `{page: 1, pageSize: 10}`. * * @example * new Car() * .fetchPage({ * pageSize: 15, // Defaults to 10 if not specified * page: 3, // Defaults to 1 if not specified * withRelated: ['engine'] // Passed to Model#fetchAll * }) * .then(function(results) { * console.log(results) // Paginated results object with metadata example below * }) * * // Pagination results: * { * models: [ * // Regular bookshelf Collection * ], * // other standard Collection attributes * // ... * pagination: { * rowCount: 53, // Total number of rows found for the query before pagination * pageCount: 4, // Total number of pages of results * page: 3, // The requested page number * pageSize: 15 // The requested number of rows per page * } * } * * @method Model#fetchPage * @param {Object} [options] * Besides the basic options that can be passed to {@link Model#fetchAll}, there are some additional pagination * options that can be specified. * @param {number} [options.pageSize] * How many models to include in each page, defaulting to 10 if not specified. Used only together with the `page` * option. * @param {number} [options.page] * Page number to retrieve. If greater than the available rows it will return an empty Collection. The first page * is number `1`. Used only with the `pageSize` option. * @param {number} [options.limit] * How many models to include in each page, defaulting to 10 if not specified. Used only together with the * `offset` option. * @param {number} [options.offset] * Index to begin fetching results from. The default and initial value is `0`. Used only with the `limit` option. * @param {boolean} [options.disableCount=false] * Whether to disable the query for counting how many records are in the full result. * @param {boolean} [options.debug=false] * Whether to enable debugging mode or not. When enabled will show information about the * queries being run. * @returns {Promise} * Returns a Promise that will resolve to the paginated collection of models. */ fetchPage(options = {}) { return Helpers.fetchPage.call(this, options); }, /** * Fetches a {@link Model model} from the database, using any {@link * Model#attributes attributes} currently set on the model to constrain the * results. * * A {@link Model#event:fetching "fetching"} event will be fired just before the * record is fetched; a good place to hook into for validation. {@link * Model#event:fetched "fetched"} event will be fired when a record is * successfully retrieved. * * If you need to constrain the query performed by fetch, you can call * {@link Model#query query} or {@link Model#where where} before calling * fetch. * * // select * from `books` where `ISBN-13` = '9780440180296' * new Book({'ISBN-13': '9780440180296'}) * .fetch() * .then(function(model) { * // outputs 'Slaughterhouse Five' * console.log(model.get('title')); * }); * * If you'd like to only fetch specific columns, you may specify a `columns` * property in the `options` for the fetch call, or use * {@link Model#query query}, tapping into the * {@link https://knexjs.org/#Builder-column|Knex column} method to specify * which columns will be fetched. * * A single property, or an array of properties can be specified as a value for * the `withRelated` property. You can also execute callbacks on relations * queries (eg. for sorting a relation). The results of these relation queries * will be loaded into a {@link Model#relations relations} property on the * model, may be retrieved with the {@link Model#related related} method, and * will be serialized as properties on a {@link Model#toJSON toJSON} call * unless `{shallow: true}` is passed. * * let Book = bookshelf.model('Book', { * tableName: 'books', * editions: function() { * return this.hasMany('Edition'); * }, * chapters: function() { * return this.hasMany('Chapter'); * }, * genre: function() { * return this.belongsTo('Genre'); * } * }) * * new Book({'ISBN-13': '9780440180296'}).fetch({ * withRelated: [ * 'genre', 'editions', * { chapters: function(query) { query.orderBy('chapter_number'); }} * ] * }).then(function(book) { * console.log(book.related('genre').toJSON()); * console.log(book.related('editions').toJSON()); * console.log(book.toJSON()); * }); * * @method Model#fetch * @param {Object=} options Hash of options. * @param {Boolean=} [options.require=true] * Whether or not to reject the returned response with a * {@link Model.NotFoundError NotFoundError} if there are no results when * fetching. If set to `false` it will resolve with `null` instead. * @param {string|string[]} [options.columns='*'] * Specify columns to be retrieved. * @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 {string|Object|mixed[]} [options.withRelated] * Relations to be retrieved with `Model` instance. Either one or more * relation names or objects mapping relation names to query callbacks. * @param {boolean} [options.debug=false] * Whether to enable debugging mode or not. When enabled will show information about the * queries being run. * @fires Model#fetching * @fires Model#fetched * @throws {Model.NotFoundError} * @returns {Promise} * A promise resolving to the fetched {@link Model model} or `null` if * none exists and the `require: false` option is passed. * */ fetch(options) { return this._doFetch(this.attributes, options).tap(() => { this._previousAttributes = _.cloneDeep(this.attributes); }); }, _doFetch: Promise.method(function(attributes, options) { options = options ? _.clone(options) : {}; // Run the `first` call on the `sync` object to fetch a single model. return ( this.sync(options) .first(attributes) .bind(this) // Jump the rest of the chain if the response doesn't exist... .tap(function(response) { if (!response || response.length === 0) { throw new this.constructor.NotFoundError('EmptyResponse'); } }) // Now, load all of the data into the model as necessary. .tap(this._handleResponse) // If the "withRelated" is specified, we also need to eager load all of the // data on the model, as a side-effect, before we ultimately jump into the // next step of the model. 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) { /** * Fired after a `fetch` operation. A promise may be returned from the * event handler for async behaviour. * * @event Model#fetched * @tutorial events * @param {Model} model * The model firing the event. * @param {Object} response * Knex query response. * @param {Object} options * Options object passed to {@link Model#fetch fetch}. * @returns {Promise} * If the handler returns a promise, `fetch` will wait for it to * be resolved. */ if (!options.silent) return this.triggerThen('fetched', this, response, options); }) .return(this) .catch(this.constructor.NotFoundError, function(err) { if ((this.requireFetch && options.require !== false) || options.require) throw err; return null; }) ); }), // Private for now. all() { const collection = this.constructor.collection(); collection._knex = this.query().clone(); this.resetQuery(); if (this.relatedData) collection.relatedData = this.relatedData; return collection; }, /** * Gets the number of matching records in the database, respecting any previous calls to * {@link Model#query}. If the `column` argument is provided, records with a `null` value in * that column will be excluded from the count. * * **Note** that in PostgreSQL the result is a string by default. To read more about the * reasons for this see the [pull request](https://github.com/brianc/node-postgres/pull/353) * that implemented it in the `node-postgres` database driver. If you're sure that the * results will always be less than 253 (9007199254740991) you can override * the default string parser like this: * * require('pg').defaults.parseInt8 = true * * Put this snippet before the call to `require('knex')` wherever you are initalizing * `knex`. * * @example * new Duck().where('color', 'blue').count('name').then((count) => { * console.log('number of blue ducks', count) * }) * * @method Model#count * @since 0.8.2 * @fires Model#counting * @param {string} [column='*'] * Specify a column to count. Rows with `null` values in this column will be excluded. * @param {Object} [options] Hash of options. * @param {boolean} [options.debug=false] * Whether to enable debugging mode or not. When enabled will show information about the * queries being run. * @returns {Promise} * A promise resolving to the number of matching rows. By default this will be a number, * except with PostgreSQL where it will be a string. Check the description to see how to * return a number instead in this case. */ count(column, options) { return this.all().count(column, options); }, /** * Fetches a collection of {@link Model models} from the database, using any * query parameters currently set on the model to constrain the results. * * Returns a Promise that will resolve with the fetched collection. If there * are no results it will resolve with an empty collection. If instead you * wish the Promise to be rejected with a {@link Collection.EmptyError}, * pass the `require: true` option. * * If you need to constrain the results, you can call the {@link Model#query query} * or {@link Model#where where} methods before calling this method. * * @method Model#fetchAll * @param {Object} [options] Set of options to modify the request. * @param {boolean} [options.require=false] * Whether or not to reject the returned Promise with a {@link Collection.EmptyError} if no records can be * fetched from the database. * @param {Transaction} [options.transacting] Optionally run the query in a transaction. * @param {boolean} [options.debug=false] * Whether to enable debugging mode or not. When enabled will show information about the * queries being run. * @fires Model#fetching:collection * @fires Model#fetched:collection * @throws {Collection.EmptyError} * This error is used to reject the Promise in the event of an empty response from the * database in case the `require: true` fetch option is used. * @returns {Promise} A Promise resolving to the fetched {@link Collection collection}. */ fetchAll(options) { const collection = this.all(); return collection .once('fetching', (__, columns, opts) => { /** * Fired before a {@link Model#fetchAll fetchAll} operation. A promise * may be returned from the event handler for async behaviour. * * @event Model#fetching:collection * @tutorial events * @param {Collection} collection * The collection that is going to be fetched. At this point it's still empty since the * fetch hasn't happened yet. * @param {string[]} columns * The columns to be retrieved by the query as provided by the underlying query builder. * If the `columns` option is not specified the value of this will usually be an array * with a single string `'tableName.*'`. * @param {Object} options Options object passed to {@link Model#fetchAll fetchAll}. * @returns {Promise} */ return this.triggerThen('fetching:collection', collection, columns, opts); }) .once('fetched', (__, response, opts) => { /** * Fired after a {@link Model#fetchAll fetchAll} operation. A promise * may be returned from the event handler for async behaviour. * * @event Model#fetched:collection * @tutorial events * @param {Collection} collection The collection that has been fetched. * @param {Object} response * The raw response from the underlying query builder. This will be an array with objects * representing each row, similar to the output of a * {@link Model#serialize serialized Model}. * @param {Object} options Options object passed to {@link Model#fetchAll fetchAll}. * @returns {Promise} */ return this.triggerThen('fetched:collection', collection, response, opts); }) .fetch(options); }, /** * The load method takes an array of relations to eager load attributes onto a {@link Model}, in a similar way that * the `withRelated` option works on {@link Model#fetch fetch}. Dot separated attributes may be used to specify deep * eager loading. * * It is possible to pass an object with query callbacks to filter the relations to eager load. An example is * presented above. * * @example * // Using an array of strings with relation names * new Posts().fetch().then(function(collection) { * return collection.at(0).load(['author', 'content', 'comments.tags']) * }).then(function(model) { * JSON.stringify(model) * * // { * // title: 'post title', * // author: {...}, * // content: {...}, * // comments: [ * // {tags: [...]}, {tags: [...]} * // ] * // } * }) * * // Using an object with query callbacks to filter the relations * new Posts().fetch().then(function(collection) { * return collection.at(0).load({comments: function(qb) { * qb.where('comments.is_approved', '=', true) * }}) * }).then(function(model) { * JSON.stringify(model) * // the model now includes all approved comments * }) * * @method Model#load * @param {string|Object|mixed[]} relations The relation, or relations, to be loaded. * @param {Object} [options] Hash of options. * @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. * @returns {Promise} A promise resolving to this {@link Model model}. */ load: Promise.method(function(relations, options) { const columns = this.format(_.assignIn({}, this.attributes)); const withRelated = Array.isArray(relations) ? relations : [relations]; return this._handleEager([columns], _.assignIn({}, options, {shallow: true, withRelated})).return(this); }), /** * @method Model#save * @description * * This method is used to perform either an insert or update query using the * model's set {@link Model#attributes attributes}. * * If the model {@link Model#isNew isNew}, any {@link Model#defaults defaults} * will be set and an `insert` query will be performed. Otherwise it will * `update` the record with a corresponding ID. It is also possible to * set default attributes on an `update` by passing the `{defaults: true}` * option in the second argument to the `save` call. This will also use the * same {@link Model#defaults defaults} as the `insert` operation. * * The type of operation to perform (either `insert` or `update`) can be * overriden with the `method` option: * * // This forces an insert with the specified id instead of the expected update * new Post({name: 'New Article', id: 34}) * .save(null, {method: 'insert'}) * .then((model) => { * // ... * }) * * If you only wish to update with the params passed to the save, you may pass * a `{patch: true}` option in the second argument to `save`: * * // UPDATE authors SET "bio" = 'Short user bio' WHERE "id" = 1 * new Author({id: 1, first_name: 'User'}) * .save({bio: 'Short user bio'}, {patch: true}) * .then((model) => { * // ... * }) * * Several events fire on the model when starting the save process: * - {@link Model#event:creating "creating"} if the model is being inserted. * - {@link Model#event:updating "updating"} event if the model is being updated. * - {@link Model#event:saving "saving"} event in either case. * * To prevent saving the model (for example, with validation), throwing an error * inside one of these event listeners will stop the save process and reject the * Promise. * * If you wish to modify the query when the {@link Model#event:saving "saving"} * event is fired, the `knex` query object is available in `options.query`. * * After the save is complete the following events will fire: * - {@link Model#event:created "created"} if a new model was inserted in the * database * - {@link Model#event:updated "updated"} if an existing model was updated. * - {@link Model#event:saved "saved"} event either way. * * See the {@tutorial events} guide for further details. * * @example * // Save with no arguments * Model.forge({id: 5, firstName: 'John', lastName: 'Smith'}).save().then((model) => { * //... * }) * * // Or add attributes during save * Model.forge({id: 5}).save({firstName: 'John', lastName: 'Smith'}).then((model) => { * //... * }) * * // Or, if you prefer, for a single attribute * Model.forge({id: 5}).save('name', 'John Smith').then((model) => { * //... * }) * * @param {Object} [attrs] * Object containing the key: value pairs that you wish to save. If used with the `patch` * option only these values will be saved and any values already set on the model will be * ignored. * * Instead of specifying this argument you can provide both a `key` and `value` * arguments to save a single value. This is demonstrated in the example. * @param {Object} [options] * @param {Transaction} [options.transacting] Optionally run the query in a transaction. * @param {string} [options.method] * Explicitly select a save method, either `"update"` or `"insert"`. * @param {Boolean} [options.defaults=false] * Whether to assign or not {@link Model#defaults default} attribute values * on a model when performing an update or create operation. * @param {Boolean} [options.patch=false] * Only save attributes supplied as arguments to the `save` call, ignoring any * attributes that may be already set on the model. * @param {Boolean} [options.require=true] * Whether or not to throw a {@link Model.NoRowsUpdatedError} if no records * are affected by save. * @param {boolean} [options.debug=false] * Whether to enable debugging mode or not. When enabled will show information about the * queries being run. * @param {boolean} [options.autoRefresh=true] * Weather to enable auto refresh such that after a model is saved it will be populated with all * the attributes that are present in the database, so you don't need to manually call * {@link Model#refresh refresh} to update it. This will use two queries unless * the database supports the `RETURNING` statement, in which case the model will * be saved and its data fetched with a single query. * @fires Model#saving * @fires Model#creating * @fires Model#updating * @fires Model#created * @fires Model#updated * @fires Model#saved * @throws {Model.NoRowsUpdatedError} * @returns {Promise} A promise resolving to the saved and updated model. */ save: Promise.method(function(key, val, options) { let attrs; // Handle both `"key", value` and `{key: value}` -style arguments. if (key == null || typeof key === 'object') { attrs = key || {}; options = _.clone(val) || {}; } else { attrs = { [key]: val }; options = options ? _.clone(options) : {}; } return Promise.bind(this) .then(function() { return this.saveMethod(options); }) .then(function(method) { // Determine which kind of save we will do: update or insert. options.method = method; // If the object is being created, we merge any defaults here rather than // during object creation. if (method === 'insert' || options.defaults) { const defaults = _.result(this, 'defaults'); if (defaults) { attrs = _.defaultsDeep({}, attrs, this.attributes, defaults); } } // Set the attributes on the model. Note that we do this before adding // timestamps, as `timestamp` calls `set` internally. this.set(attrs, {silent: true}); // Now set timestamps if appropriate. Extend `attrs` so that the // timestamps will be provided for a patch operation. if (this.hasTimestamps) { Object.assign(attrs, this.timestamp(options)); } // If there are any save constraints, set them on the model. if (this.relatedData && this.relatedData.type !== 'morphTo') { Helpers.saveConstraints(this, this.relatedData); } const getAttributesToSave = function(method, options, model) { return method === 'update' && options.patch ? attrs : model.attributes; }; // Gives access to the `query` object in the `options`, in case we need it // in any event handlers. const sync = this.sync(options); options.query = sync.query; /** * Saving event. * * Fired before an `insert` or `update` query. A Promise may be returned from the event * handler for async behaviour. Throwing an exception from the handler will cancel the * save process. * * @event Model#saving * @tutorial events * @param {Model} model * The model firing the event. Its attributes are already changed but not commited to * the database yet. * @param {Object} attrs Attributes that will be inserted or updated. * @param {Object} options Options object passed to {@link Model#save save}. * @param {QueryBuilder} options.query * Query builder to be used for saving. This can be used to modify or add to the query * before it is executed. * @returns {Promise} */ /** * Creating event. * * Fired before an `insert` query. A Promise may be returned from the event handler for * async behaviour. Throwing an exception from the handler will cancel the save process. * * @event Model#creating * @tutorial events * @param {Model} model The model firing the event. * @param {Object} attrs Attributes that will be inserted. * @param {Object} options Options object passed to {@link Model#save save}. * @param {QueryBuilder} options.query * Query builder to be used for saving. This can be used to modify or add to the query * before it is executed. * @returns {Promise} */ /** * Updating event. * * Fired before an `update` query. A Promise may be returned from the event handler for * async behaviour. Throwing an exception from the handler will cancel the save process. * * @event Model#updating * @tutorial events * @param {Model} model * The model firing the event. Its attributes are already changed but not commited to * the database yet. * @param {Object} attrs Attributes that will be updated. * @param {Object} options Options object passed to {@link Model#save save}. * @param {QueryBuilder} options.query * Query builder to be used for saving. This can be used to modify or add to the query * before it is executed. * @returns {Promise} */ return this.triggerThen( method === 'insert' ? 'saving creating' : 'saving updating', this, getAttributesToSave(method, options, this), options ) .bind(this) .then(function() { return sync[options.method](getAttributesToSave(method, options, this)); }) .then(function(resp) { // Only valid for databases that support RETURNING const isObjectResponse = resp && typeof resp[0] === 'object'; // After a successful database save, the id is updated if the model was created if (method === 'insert' && this.id == null) { let updatedAttrs; if (!isObjectResponse) { const updatedCols = {}; updatedCols[this.idAttribute] = this.id = resp[0]; updatedAttrs = this.parse(updatedCols); } else { updatedAttrs = this.parse(resp[0]); this.id = updatedAttrs[this.parsedIdAttribute()]; } Object.assign(this.attributes, updatedAttrs); } else if (method === 'update' && (resp === 0 || resp.length === 0)) { if (options.require !== false) { throw new this.constructor.NoRowsUpdatedError('No Rows Updated'); } } else if (isObjectResponse) { Object.assign(this.attributes, this.parse(resp[0])); } if (resp === 0 || resp.length === 0) return resp; if (isObjectResponse) return this; if (options.autoRefresh === false) return this; return this.refresh({silent: true, transacting: options.transacting}); }) .then(function() { const eventsToTrigger = method === 'insert' ? 'created saved' : 'updated saved'; this._reset(); /** * Saved event. * * Fired after an `insert` or `update` query. * * @event Model#saved * @tutorial events * @param {Model} model * The model firing the event with its attributes matching what's in the database. * @param {Object} options Options object passed to {@link Model#save save}. * @returns {Promise} */ /** * Created event. * * Fired after an `insert` query. * * @event Model#created * @tutorial events * @param {Model} model * The model firing the event with its attributes matching what's in the database. * @param {Object} options Options object passed to {@link Model#save save}. * @returns {Promise} */ /** * Updated event. * * Fired after an `update` query. * * @event Model#updated * @tutorial events * @param {Model} model * The model firing the event with its attributes matching what's in the database. * @param {Object} options Options object passed to {@link Model#save save}. * @returns {Promise} */ return this.triggerThen(eventsToTrigger, this, options); }); }) .return(this); }), /** * `destroy` performs a `delete` on the model, using the model's {@link * Model#idAttribute idAttribute} to constrain the query. * * A {@link Model#event:destroying "destroying"} event is triggered on the model * before being destroyed. To prevent destroying the model, throwing an error * inside one of the event listeners will stop destroying the model and reject the * promise. * * A {@link Model#event:destroyed "destroyed"} event is fired after the model's * removal is completed. * * @method Model#destroy * * @param {Object} [options] Hash of options. * @param {Transaction} [options.transacting] Optionally run the query in a transaction. * @param {Boolean} [options.require=true] * Throw a {@link Model.NoRowsDeletedError} if no records are affected by destroy. This is * the default behavior as of version 0.13.0. * @param {boolean} [options.debug=false] * Whether to enable debugging mode or not. When enabled will show information about the * queries being run. * * @example * * new User({id: 1}) * .destroy() * .then(function(model) { * // ... * }); * * @fires Model#destroying * @fires Model#destroyed * * @throws {Model.NoRowsDeletedError} * * @returns {Promise} A promise resolving to the destroyed and thus * empty model, i.e. all attributes are `undefined`. */ destroy: Promise.method(function(options) { options = options ? _.clone(options) : {}; const sync = this.sync(options); options.query = sync.query; return Promise.bind(this) .then(function() { /** * Destroying event. * * Fired before a `delete` query. A promise may be returned from the event * handler for async behaviour. Throwing an exception from the handler * will reject the promise and cancel the deletion. * * @event Model#destroying * @tutorial events * @param {Model} model The model firing the event. * @param {Object} options Options object passed to {@link Model#destroy destroy}. * @returns {Promise} */ return this.triggerThen('destroying', this, options); }) .then(function() { return sync.del(); }) .then(function(affectedRows) { if (options.require !== false && affectedRows === 0) { throw new this.constructor.NoRowsDeletedError('No Rows Deleted'); } this._previousAttributes = _.clone(this.attributes); this.clear(); /** * Destroyed event. * * Fired after a `delete` query. A promise may be returned from the event * handler for async behaviour. * * @event Model#destroyed * @tutorial events * @param {Model} model The model firing the event. * @param {Object} options Options object passed to {@link Model#destroy destroy}. * @returns {Promise} */ return this.triggerThen('destroyed', this, options); }) .then(this._reset); }), /** * 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} * * @method Model#resetQuery * @returns {Model} Self, this method is chainable. */ resetQuery() { this._knex = null; return this; }, /** * The `query` method is used to tap into the underlying Knex query builder * instance for the current model. 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 * `model.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, * returning the current model. * * @example * * model * .query('where', 'other_id', '=', '5') * .fetch() * .then(function(model) { * // ... * }); * * model * .query({where: {other_id: '5'}, orWhere: {key: 'value'}}) * .fetch() * .then(function(model) { * // ... * }); * * model.query(function(qb) { * qb.where('other_person', 'LIKE', '%Demo').orWhere('other_id', '>', 10); * }).fetch() * .then(function(model) { * // ... * }); * * let qb = model.query(); * qb.where({id: 1}).select().then(function(resp) { * // ... * }); * * @method Model#query * @param {function|Object|...string=} arguments The query method. * @returns {Model|QueryBuilder} * Will return this model or, if called with no arguments, the underlying query builder. * * @see {@link http://knexjs.org/#Builder Knex `QueryBuilder`} */ query() { return Helpers.query(this, Array.from(arguments)); }, /** * The where method is used as convenience for the most common {@link * Model#query query} method, adding a where clause to the builder. Any * additional knex methods may be accessed using {@link Model#query query}. * * Accepts either key, value syntax, or a hash of attributes. * * @example * * model.where('favorite_color', '<>', 'green').fetch().then(function() { //... * // or * model.where('favorite_color', 'red').fetch().then(function() { //... * // or * model.where({favorite_color: 'red', shoe_size: 12}).fetch().then(function() { //... * * @method Model#where * @param {Object|...string} method * * 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 {Model} Self, this method is chainable. * * @see Model#query */ where() { return this.query.apply(this, ['where'].concat(Array.from(arguments))); }, /** * @method Model#orderBy * @since 0.9.3 * @description * * 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. * * @example * * Car.forge().orderBy('color', 'ASC').fetchAll() * .then(function (rows) { // ... * * @param sort {string} * Column to sort on * @param order {string} * Ascending ('ASC') or descending ('DESC') order */ orderBy() { return Helpers.orderBy.apply(null, [this].concat(Array.from(arguments))); }, /* Ensure that QueryBuilder is copied on clone. */ clone() { // This needs to use the direct apply method because the spread operator // incorrectly converts to `clone.apply(ModelBase.prototype, arguments)` // instead of `apply(this, arguments)` const cloned = BookshelfModel.__super__.clone.apply(this, arguments); if (this._knex != null) { cloned._knex = cloned._builder(this._knex.clone()); } return cloned; }, /** * Creates and returns a new Bookshelf.Sync instance. * * @method Model#sync * @private * @returns Sync */ sync(options) { return new Sync(this, options); }, /** * Helper for setting up the `morphOne` or `morphMany` relations. * * @method Model#_morphOneOrMany * @private */ _morphOneOrMany(Target, morphName, columnNames, morphValue, type) { if (!Array.isArray(columnNames)) { // Shift by one place morphValue = columnNames; columnNames = null; } if (!morphName || !Target) throw new Error('The polymorphic `name` and `Target` are required.'); return this._relation(type, Target, { morphName: morphName, morphValue: morphValue, columnNames: columnNames }).init(this); }, /** * @name Model#_handleResponse * @private * @description * * Handles the response data for the model, returning from the model's fetch call. * * @param {Object} Response from Knex query. * * @todo: need to check on Backbone's status there, ticket #2636 * @todo: {silent: true, parse: true}, for parity with collection#set */ _handleResponse(response) { const relatedData = this.relatedData; this.set(this.parse(response[0]), {silent: true}) .formatTimestamps() ._reset(); if (relatedData && relatedData.isJoined()) { relatedData.parsePivot([this]); } }, /** * @name Model#_handleEager * @private * @description * * Handles the related data loading on the model. * * @param {Object} Response from Knex query. */ _handleEager(response, options) { return new EagerRelation([this], response, this).fetch(options); } }, { extended(child) { /** * Thrown when no records are found by {@link Model#fetch fetch} or * {@link Model#refresh refresh} unless called with the `{require: false}` * option. * * @class Model.NotFoundError */ child.NotFoundError = createError(this.NotFoundError); /** * Thrown when no records are saved by {@link Model#save save} * unless called with the `{require: false}` option. * * @class Model.NoRowsUpdatedError */ child.NoRowsUpdatedError = createError(this.NoRowsUpdatedError); /** * Thrown when no record is deleted by {@link Model#destroy destroy} * unless called with the `{require: false}` option. * * @class Model.NoRowsDeletedError */ child.NoRowsDeletedError = createError(this.NoRowsDeletedError); }, fetchPage() { const model = this.forge(); return model.fetchPage.apply(model, arguments); } } ); BookshelfModel.NotFoundError = Errors.NotFoundError; BookshelfModel.NoRowsUpdatedError = Errors.NoRowsUpdatedError; BookshelfModel.NoRowsDeletedError = Errors.NoRowsDeletedError; module.exports = BookshelfModel;