'use strict';
const _ = require('lodash');
// We've supplemented `Events` with a `triggerThen` method to allow for
// asynchronous event handling via promises. We also mix this into the
// prototypes of the main objects in the library.
const Events = require('./base/events');
// All core modules required for the bookshelf instance.
const BookshelfModel = require('./model');
const BookshelfCollection = require('./collection');
const BookshelfRelation = require('./relation');
const errors = require('./errors');
function preventOverwrite(store, name) {
if (store[name]) throw new Error(`${name} is already defined in the registry`);
}
/**
* @class
* @classdesc
*
* The Bookshelf library is initialized by passing an initialized Knex client
* instance. The knex documentation provides a number of examples for different
* databases.
*
* @constructor
* @param {Knex} knex Knex instance.
*/
function Bookshelf(knex) {
if (!knex || knex.name !== 'knex') {
throw new Error('Invalid knex instance');
}
function resolveModel(input) {
if (typeof input !== 'string') return input;
return (
bookshelf.collection(input) ||
bookshelf.model(input) ||
(function() {
throw new errors.ModelNotResolvedError(`The model ${input} could not be resolved from the registry.`);
})()
);
}
/** @lends Bookshelf.prototype */
const bookshelf = {
registry: {
collections: {},
models: {}
},
VERSION: require('../package.json').version,
collection(name, Collection, staticProperties) {
if (Collection) {
preventOverwrite(this.registry.collections, name);
if (_.isPlainObject(Collection)) {
Collection = this.Collection.extend(Collection, staticProperties);
}
this.registry.collections[name] = Collection;
}
return this.registry.collections[name] || bookshelf.resolve(name);
},
/**
* Registers a model. Omit the second argument `Model` to return a previously registered model that matches the
* provided name.
*
* Note that when registering a model with this method it will also be available to all relation methods, allowing
* you to use a string name in that case. See the calls to `hasMany()` in the examples above.
*
* @example
* // Defining and registering a model
* module.exports = bookshelf.model('Customer', {
* tableName: 'customers',
* orders() {
* return this.hasMany('Order')
* }
* })
*
* // Retrieving a previously registered model
* const Customer = bookshelf.model('Customer')
*
* // Registering already defined models
* // file: customer.js
* const Customer = bookshelf.Model.extend({
* tableName: 'customers',
* orders() {
* return this.hasMany('Order')
* }
* })
* module.exports = bookshelf.model('Customer', Customer)
*
* // file: order.js
* const Order = bookshelf.Model.extend({
* tableName: 'orders',
* customer() {
* return this.belongsTo('Customer')
* }
* })
* module.exports = bookshelf.model('Order', Order)
*
* @param {string} name
* The name to save the model as, or the name of the model to retrieve if no further arguments are passed to this
* method.
* @param {Model|Object} [Model]
* The model to register. If a plain object is passed it will be converted to a {@link Model}. See example above.
* @param {Object} [staticProperties]
* If a plain object is passed as second argument, this can be used to specify additional static properties and
* methods for the new model that is created.
* @return {Model} The registered model.
*/
model(name, Model, staticProperties) {
if (Model) {
preventOverwrite(this.registry.models, name);
if (_.isPlainObject(Model)) Model = this.Model.extend(Model, staticProperties);
this.registry.models[name] = Model;
}
return this.registry.models[name] || bookshelf.resolve(name);
},
/**
* Override this in your bookshelf instance to define a custom function that will resolve the location of a model or
* collection when using the {@link Bookshelf#model} method or when passing a string with a model name in any of the
* collection methods (e.g. {@link Model#hasOne}, {@link Model#hasMany}, etc.).
*
* This will only be used if the specified name cannot be found in the registry. Note that this function
* can return anything you'd like, so it's not restricted in functionality.
*
* @example
* const Customer = bookshelf.model('Customer', {
* tableName: 'customers'
* })
*
* bookshelf.resolve = (name) => {
* if (name === 'SpecialCustomer') return Customer;
* }
*
* @param {string} name The model name to resolve.
* @return {*} The return value will depend on what your re-implementation of this function does.
*/
resolve(name) {}
};
const Model = (bookshelf.Model = BookshelfModel.extend(
{
_builder: builderFn,
// The `Model` constructor is referenced as a property on the `Bookshelf` instance, mixing in the correct
// `builder` method, as well as the `relation` method, passing in the correct `Model` & `Collection`
// constructors for later reference.
_relation(type, Target, options) {
Target = resolveModel(Target);
if (type !== 'morphTo' && !_.isFunction(Target)) {
throw new Error(
'A valid target model must be defined for the ' + _.result(this, 'tableName') + ' ' + type + ' relation'
);
}
return new Relation(type, Target, options);
},
morphTo(relationName, ...args) {
let candidates = args;
let columnNames = null;
if (Array.isArray(args[0]) || args[0] === null || args[0] === undefined) {
candidates = args.slice(1);
columnNames = args[0];
}
if (Array.isArray(columnNames)) {
// Try to use the columnNames as target instead
try {
columnNames[0] = resolveModel(columnNames[0]);
} catch (error) {
// If it did not work, they were real columnNames
if (error instanceof errors.ModelNotResolvedError) throw error;
}
}
const models = candidates.map((candidate) => {
if (!Array.isArray(candidate)) return resolveModel(candidate);
const model = candidate[0];
const morphValue = candidate[1];
return [resolveModel(model), morphValue];
});
return BookshelfModel.prototype.morphTo.apply(this, [relationName, columnNames].concat(models));
},
through(Source, ...rest) {
return BookshelfModel.prototype.through.apply(this, [resolveModel(Source), ...rest]);
}
},
{
/**
* @method Model.forge
* @description
*
* A simple helper function to instantiate a new Model without needing `new`.
*
* @param {Object=} attributes Initial values for this model's attributes.
* @param {Object=} options Hash of options.
* @param {string=} options.tableName Initial value for {@linkcode Model#tableName tableName}.
* @param {Boolean=} [options.hasTimestamps=false]
*
* Initial value for {@linkcode Model#hasTimestamps hasTimestamps}.
*
* @param {Boolean} [options.parse=false]
*
* Convert attributes by {@linkcode Model#parse parse} before being
* {@linkcode Model#set set} on the `model`.
*/
forge: function forge(attributes, options) {
return new this(attributes, options);
},
/**
* A simple static helper to instantiate a new {@link Collection}, setting the model it's
* called on as the collection's target model.
*
* @example
* Customer.collection().fetch().then((customers) => {
* // ...
* })
*
* @method Model.collection
* @param {Model[]} [models] Any models to be added to the collection.
* @param {Object} [options] Additional options to pass to the {@link Collection} constructor.
* @param {string|function} [options.comparator]
* If specified this is used to sort the collection. It can be a string representing the
* model attribute to sort by, or a custom function. Check the documentation for {@link
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
* Array.prototype.sort} for more info on how to use a custom comparator function. If this
* options is not specified the collection sort order depends on what the database returns.
* @returns {Collection}
* The newly created collection. It will be empty unless any models were passed as the first
* argument.
*/
collection(models, options) {
return new bookshelf.Collection(models || [], _.extend({}, options, {model: this}));
},
/**
* Shortcut to a model's `count` method so you don't need to instantiate a new model to count
* the number of records.
*
* @example
* Duck.count().then((count) => {
* console.log('number of ducks', count)
* })
*
* @method Model.count
* @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.
* @param {boolean} [options.debug=false]
* Whether to enable debugging mode or not. When enabled will show information about the
* queries being run.
* @returns {Promise<number|string>}
*/
count(column, options) {
return this.forge().count(column, options);
},
/**
* @method Model.fetchAll
* @description
*
* Simple helper function for retrieving all instances of the given model.
*
* @see Model#fetchAll
* @returns {Promise<Collection>}
*/
fetchAll(options) {
return this.forge().fetchAll(options);
}
}
));
const Collection = (bookshelf.Collection = BookshelfCollection.extend(
{
_builder: builderFn,
through(Source, ...args) {
return BookshelfCollection.prototype.through.apply(this, [resolveModel(Source), ...args]);
}
},
{
/**
* @method Collection.forge
* @description
*
* A simple helper function to instantiate a new Collection without needing
* new.
*
* @param {(Object[]|Model[])=} [models]
* Set of models (or attribute hashes) with which to initialize the
* collection.
* @param {Object} options Hash of options.
*
* @example
*
* var Promise = require('bluebird');
* var Accounts = bookshelf.Collection.extend({
* model: Account
* });
*
* var accounts = Accounts.forge([
* {name: 'Person1'},
* {name: 'Person2'}
* ]);
*
* Promise.all(accounts.invokeMap('save')).then(function() {
* // collection models should now be saved...
* });
*/
forge: function forge(models, options) {
return new this(models, options);
}
}
));
// The collection also references the correct `Model`, specified above, for
// creating new `Model` instances in the collection.
Collection.prototype.model = Model;
Model.prototype.Collection = Collection;
const Relation = BookshelfRelation.extend({Model, Collection});
// A `Bookshelf` instance may be used as a top-level pub-sub bus, as it mixes
// in the `Events` object. It also contains the version number, and a
// `Transaction` method referencing the correct version of `knex` passed into
// the object.
_.extend(bookshelf, Events, errors, {
/**
* An alias to `{@link http://knexjs.org/#Transactions Knex#transaction}`. The `transaction`
* object must be passed along in the options of any relevant Bookshelf calls, to ensure all
* queries are on the same connection. The entire transaction block is wrapped around a Promise
* that will commit the transaction if it resolves successfully, or roll it back if the Promise
* is rejected.
*
* Note that there is no need to explicitly call `transaction.commit()` or
* `transaction.rollback()` since the entire transaction will be committed if there are no
* errors inside the transaction block.
*
* When fetching inside a transaction it's possible to specify a row-level lock by passing the
* wanted lock type in the `lock` option to {@linkcode Model#fetch fetch}. Available options are
* `lock: 'forUpdate'` and `lock: 'forShare'`.
*
* @example
* var Promise = require('bluebird')
*
* Bookshelf.transaction((t) => {
* return new Library({name: 'Old Books'})
* .save(null, {transacting: t})
* .tap(function(model) {
* return Promise.map([
* {title: 'Canterbury Tales'},
* {title: 'Moby Dick'},
* {title: 'Hamlet'}
* ], (info) => {
* return new Book(info).save({'shelf_id': model.id}, {transacting: t})
* })
* })
* }).then((library) => {
* console.log(library.related('books').pluck('title'))
* }).catch((err) => {
* console.error(err)
* })
*
* @method Bookshelf#transaction
* @param {Bookshelf~transactionCallback} transactionCallback
* Callback containing transaction logic. The callback should return a Promise.
* @returns {Promise}
* A promise resolving to the value returned from
* {@link Bookshelf~transactionCallback transactionCallback}.
*/
transaction() {
return this.knex.transaction.apply(this.knex, arguments);
},
/**
* This is a transaction block to be provided to {@link Bookshelf#transaction}. All of the
* database operations inside it can be part of the same transaction by passing the
* `transacting: transaction` option to {@link Model#fetch fetch}, {@link Model#save save} or
* {@link Model#destroy destroy}.
*
* Note that unless you explicitly pass the `transaction` object along to any relevant model
* operations, those operations will not be part of the transaction, even though they may be
* inside the transaction callback.
*
* @callback Bookshelf~transactionCallback
* @see {@link http://knexjs.org/#Transactions Knex#transaction}
* @see Bookshelf#transaction
*
* @param {Transaction} transaction
* @returns {Promise}
* The Promise will resolve to the return value of the callback, or be rejected with an error
* thrown inside it. If it resolves, the entire transaction is committed, otherwise it is
* rolled back.
*/
/**
* @method Bookshelf#plugin
* @memberOf Bookshelf
* @description
*
* This method provides a nice, tested, standardized way of adding plugins
* to a `Bookshelf` instance, injecting the current instance into the
* plugin, which should be a `module.exports`.
*
* You can add a plugin by specifying a string with the name of the plugin
* to load. In this case it will try to find a module. It will pass the
* string to `require()`, so you can either require a third-party dependency
* by name or one of your own modules by relative path:
*
* bookshelf.plugin('./bookshelf-plugins/my-favourite-plugin');
* bookshelf.plugin('plugin-from-npm');
*
* There are a few official plugins published in `npm`, along with many
* independently developed ones. See
* [the list of available plugins](index.html#official-plugins).
*
* You can also provide an array of strings or functions, which is the same
* as calling `bookshelf.plugin()` multiple times. In this case the same
* options object will be reused:
*
* bookshelf.plugin(['cool-plugin', './my-plugins/even-cooler-plugin']);
*
* Example plugin:
*
* // Converts all string values to lower case when setting attributes on a model
* module.exports = function(bookshelf) {
* bookshelf.Model = bookshelf.Model.extend({
* set(key, value, options) {
* if (!key) return this
* if (typeof value === 'string') value = value.toLowerCase()
* return bookshelf.Model.prototype.set.call(this, key, value, options)
* }
* })
* }
*
* @param {string|array|Function} plugin
* The plugin or plugins to load. If you provide a string it can
* represent an npm package or a file somewhere on your project. You can
* also pass a function as argument to add it as a plugin. Finally, it's
* also possible to pass an array of strings or functions to add them all
* at once.
* @param {mixed} options
* This can be anything you want and it will be passed directly to the
* plugin as the second argument when loading it.
* @return {Bookshelf} The bookshelf instance for chaining.
*/
plugin(plugin, options) {
if (_.isString(plugin)) {
if (plugin === 'pagination') {
const message =
'Pagination plugin was moved into core Bookshelf. You can now use `fetchPage()` without having to ' +
"call `.plugin('pagination')`. Remove any `.plugin('pagination')` calls to clear this message.";
return console.warn(message); // eslint-disable-line no-console
}
if (plugin === 'visibility') {
const message =
'Visibility plugin was moved into core Bookshelf. You can now set the `hidden` and `visible` properties ' +
"without having to call `.plugin('visibility')`. Remove any `.plugin('visibility')` calls to clear this " +
'message.';
return console.warn(message); // eslint-disable-line no-console
}
if (plugin === 'registry') {
const message =
'Registry plugin was moved into core Bookshelf. You can now register models using `bookshelf.model()` ' +
"and collections using `bookshelf.collection()` without having to call `.plugin('registry')`. Remove " +
"any `.plugin('registry')` calls to clear this message.";
return console.warn(message); // eslint-disable-line no-console
}
if (plugin === 'processor') {
const message =
'Processor plugin was removed from core Bookshelf. To migrate to the new standalone package follow the ' +
'instructions in https://github.com/bookshelf/bookshelf/wiki/Migrating-from-0.15.1-to-1.0.0#processor-plugin';
return console.warn(message); // eslint-disable-line no-console
}
if (plugin === 'case-converter') {
const message =
'Case converter plugin was removed from core Bookshelf. To migrate to the new standalone package follow ' +
'the instructions in https://github.com/bookshelf/bookshelf/wiki/Migrating-from-0.15.1-to-1.0.0#case-converter-plugin';
return console.warn(message); // eslint-disable-line no-console
}
if (plugin === 'virtuals') {
const message =
'Virtuals plugin was removed from core Bookshelf. To migrate to the new standalone package follow ' +
'the instructions in https://github.com/bookshelf/bookshelf/wiki/Migrating-from-0.15.1-to-1.0.0#virtuals-plugin';
return console.warn(message); // eslint-disable-line no-console
}
require(plugin)(this, options);
} else if (Array.isArray(plugin)) {
plugin.forEach((p) => this.plugin(p, options));
} else {
plugin(this, options);
}
return this;
}
});
/**
* @member Bookshelf#knex
* @type {Knex}
* @description
* A reference to the {@link http://knexjs.org Knex.js} instance being used by Bookshelf.
*/
bookshelf.knex = knex;
function builderFn(tableNameOrBuilder) {
let builder = null;
if (_.isString(tableNameOrBuilder)) {
builder = bookshelf.knex(tableNameOrBuilder);
} else if (tableNameOrBuilder == null) {
builder = bookshelf.knex.queryBuilder();
} else {
// Assuming here that `tableNameOrBuilder` is a QueryBuilder instance. Not
// aware of a way to check that this is the case (ie. using
// `Knex.isQueryBuilder` or equivalent).
builder = tableNameOrBuilder;
}
return builder.on('query', (data) => this.trigger('query', data));
}
// Attach `where`, `query`, and `fetchAll` as static methods.
['where', 'query'].forEach((method) => {
Model[method] = Collection[method] = function() {
const model = this.forge();
return model[method].apply(model, arguments);
};
});
return bookshelf;
}
module.exports = Bookshelf;