559 lines
21 KiB
JavaScript
559 lines
21 KiB
JavaScript
'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;
|