UNPKG

21.4 kBJavaScriptView Raw
1'use strict';
2
3const _ = require('lodash');
4
5// We've supplemented `Events` with a `triggerThen` method to allow for
6// asynchronous event handling via promises. We also mix this into the
7// prototypes of the main objects in the library.
8const Events = require('./base/events');
9
10// All core modules required for the bookshelf instance.
11const BookshelfModel = require('./model');
12const BookshelfCollection = require('./collection');
13const BookshelfRelation = require('./relation');
14const errors = require('./errors');
15
16function preventOverwrite(store, name) {
17 if (store[name]) throw new Error(`${name} is already defined in the registry`);
18}
19
20/**
21 * @class
22 * @classdesc
23 *
24 * The Bookshelf library is initialized by passing an initialized Knex client
25 * instance. The knex documentation provides a number of examples for different
26 * databases.
27 *
28 * @constructor
29 * @param {Knex} knex Knex instance.
30 */
31function Bookshelf(knex) {
32 if (!knex || knex.name !== 'knex') {
33 throw new Error('Invalid knex instance');
34 }
35
36 function resolveModel(input) {
37 if (typeof input !== 'string') return input;
38
39 return (
40 bookshelf.collection(input) ||
41 bookshelf.model(input) ||
42 (function() {
43 throw new errors.ModelNotResolvedError(`The model ${input} could not be resolved from the registry.`);
44 })()
45 );
46 }
47
48 /** @lends Bookshelf.prototype */
49 const bookshelf = {
50 registry: {
51 collections: {},
52 models: {}
53 },
54 VERSION: require('../package.json').version,
55
56 collection(name, Collection, staticProperties) {
57 if (Collection) {
58 preventOverwrite(this.registry.collections, name);
59
60 if (_.isPlainObject(Collection)) {
61 Collection = this.Collection.extend(Collection, staticProperties);
62 }
63
64 this.registry.collections[name] = Collection;
65 }
66
67 return this.registry.collections[name] || bookshelf.resolve(name);
68 },
69
70 /**
71 * Registers a model. Omit the second argument `Model` to return a previously registered model that matches the
72 * provided name.
73 *
74 * Note that when registering a model with this method it will also be available to all relation methods, allowing
75 * you to use a string name in that case. See the calls to `hasMany()` in the examples above.
76 *
77 * @example
78 * // Defining and registering a model
79 * module.exports = bookshelf.model('Customer', {
80 * tableName: 'customers',
81 * orders() {
82 * return this.hasMany('Order')
83 * }
84 * })
85 *
86 * // Retrieving a previously registered model
87 * const Customer = bookshelf.model('Customer')
88 *
89 * // Registering already defined models
90 * // file: customer.js
91 * const Customer = bookshelf.Model.extend({
92 * tableName: 'customers',
93 * orders() {
94 * return this.hasMany('Order')
95 * }
96 * })
97 * module.exports = bookshelf.model('Customer', Customer)
98 *
99 * // file: order.js
100 * const Order = bookshelf.Model.extend({
101 * tableName: 'orders',
102 * customer() {
103 * return this.belongsTo('Customer')
104 * }
105 * })
106 * module.exports = bookshelf.model('Order', Order)
107 *
108 * @param {string} name
109 * The name to save the model as, or the name of the model to retrieve if no further arguments are passed to this
110 * method.
111 * @param {Model|Object} [Model]
112 * The model to register. If a plain object is passed it will be converted to a {@link Model}. See example above.
113 * @param {Object} [staticProperties]
114 * If a plain object is passed as second argument, this can be used to specify additional static properties and
115 * methods for the new model that is created.
116 * @return {Model} The registered model.
117 */
118 model(name, Model, staticProperties) {
119 if (Model) {
120 preventOverwrite(this.registry.models, name);
121 if (_.isPlainObject(Model)) Model = this.Model.extend(Model, staticProperties);
122 this.registry.models[name] = Model;
123 }
124
125 return this.registry.models[name] || bookshelf.resolve(name);
126 },
127
128 /**
129 * Override this in your bookshelf instance to define a custom function that will resolve the location of a model or
130 * collection when using the {@link Bookshelf#model} method or when passing a string with a model name in any of the
131 * collection methods (e.g. {@link Model#hasOne}, {@link Model#hasMany}, etc.).
132 *
133 * This will only be used if the specified name cannot be found in the registry. Note that this function
134 * can return anything you'd like, so it's not restricted in functionality.
135 *
136 * @example
137 * const Customer = bookshelf.model('Customer', {
138 * tableName: 'customers'
139 * })
140 *
141 * bookshelf.resolve = (name) => {
142 * if (name === 'SpecialCustomer') return Customer;
143 * }
144 *
145 * @param {string} name The model name to resolve.
146 * @return {*} The return value will depend on what your re-implementation of this function does.
147 */
148 resolve(name) {}
149 };
150
151 const Model = (bookshelf.Model = BookshelfModel.extend(
152 {
153 _builder: builderFn,
154
155 // The `Model` constructor is referenced as a property on the `Bookshelf` instance, mixing in the correct
156 // `builder` method, as well as the `relation` method, passing in the correct `Model` & `Collection`
157 // constructors for later reference.
158 _relation(type, Target, options) {
159 Target = resolveModel(Target);
160
161 if (type !== 'morphTo' && !_.isFunction(Target)) {
162 throw new Error(
163 'A valid target model must be defined for the ' + _.result(this, 'tableName') + ' ' + type + ' relation'
164 );
165 }
166 return new Relation(type, Target, options);
167 },
168
169 morphTo(relationName, ...args) {
170 let candidates = args;
171 let columnNames = null;
172
173 if (Array.isArray(args[0]) || args[0] === null || args[0] === undefined) {
174 candidates = args.slice(1);
175 columnNames = args[0];
176 }
177
178 if (Array.isArray(columnNames)) {
179 // Try to use the columnNames as target instead
180 try {
181 columnNames[0] = resolveModel(columnNames[0]);
182 } catch (error) {
183 // If it did not work, they were real columnNames
184 if (error instanceof errors.ModelNotResolvedError) throw error;
185 }
186 }
187
188 const models = candidates.map((candidate) => {
189 if (!Array.isArray(candidate)) return resolveModel(candidate);
190
191 const model = candidate[0];
192 const morphValue = candidate[1];
193
194 return [resolveModel(model), morphValue];
195 });
196
197 return BookshelfModel.prototype.morphTo.apply(this, [relationName, columnNames].concat(models));
198 },
199
200 through(Source, ...rest) {
201 return BookshelfModel.prototype.through.apply(this, [resolveModel(Source), ...rest]);
202 }
203 },
204 {
205 /**
206 * @method Model.forge
207 * @description
208 *
209 * A simple helper function to instantiate a new Model without needing `new`.
210 *
211 * @param {Object=} attributes Initial values for this model's attributes.
212 * @param {Object=} options Hash of options.
213 * @param {string=} options.tableName Initial value for {@linkcode Model#tableName tableName}.
214 * @param {Boolean=} [options.hasTimestamps=false]
215 *
216 * Initial value for {@linkcode Model#hasTimestamps hasTimestamps}.
217 *
218 * @param {Boolean} [options.parse=false]
219 *
220 * Convert attributes by {@linkcode Model#parse parse} before being
221 * {@linkcode Model#set set} on the `model`.
222 */
223 forge: function forge(attributes, options) {
224 return new this(attributes, options);
225 },
226
227 /**
228 * A simple static helper to instantiate a new {@link Collection}, setting the model it's
229 * called on as the collection's target model.
230 *
231 * @example
232 * Customer.collection().fetch().then((customers) => {
233 * // ...
234 * })
235 *
236 * @method Model.collection
237 * @param {Model[]} [models] Any models to be added to the collection.
238 * @param {Object} [options] Additional options to pass to the {@link Collection} constructor.
239 * @param {string|function} [options.comparator]
240 * If specified this is used to sort the collection. It can be a string representing the
241 * model attribute to sort by, or a custom function. Check the documentation for {@link
242 * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
243 * Array.prototype.sort} for more info on how to use a custom comparator function. If this
244 * options is not specified the collection sort order depends on what the database returns.
245 * @returns {Collection}
246 * The newly created collection. It will be empty unless any models were passed as the first
247 * argument.
248 */
249 collection(models, options) {
250 return new bookshelf.Collection(models || [], _.extend({}, options, {model: this}));
251 },
252
253 /**
254 * Shortcut to a model's `count` method so you don't need to instantiate a new model to count
255 * the number of records.
256 *
257 * @example
258 * Duck.count().then((count) => {
259 * console.log('number of ducks', count)
260 * })
261 *
262 * @method Model.count
263 * @since 0.8.2
264 * @see Model#count
265 * @param {string} [column='*']
266 * Specify a column to count. Rows with `null` values in this column will be excluded.
267 * @param {Object} [options] Hash of options.
268 * @param {boolean} [options.debug=false]
269 * Whether to enable debugging mode or not. When enabled will show information about the
270 * queries being run.
271 * @returns {Promise<number|string>}
272 */
273 count(column, options) {
274 return this.forge().count(column, options);
275 },
276
277 /**
278 * @method Model.fetchAll
279 * @description
280 *
281 * Simple helper function for retrieving all instances of the given model.
282 *
283 * @see Model#fetchAll
284 * @returns {Promise<Collection>}
285 */
286 fetchAll(options) {
287 return this.forge().fetchAll(options);
288 }
289 }
290 ));
291
292 const Collection = (bookshelf.Collection = BookshelfCollection.extend(
293 {
294 _builder: builderFn,
295 through(Source, ...args) {
296 return BookshelfCollection.prototype.through.apply(this, [resolveModel(Source), ...args]);
297 }
298 },
299 {
300 /**
301 * @method Collection.forge
302 * @description
303 *
304 * A simple helper function to instantiate a new Collection without needing
305 * new.
306 *
307 * @param {(Object[]|Model[])=} [models]
308 * Set of models (or attribute hashes) with which to initialize the
309 * collection.
310 * @param {Object} options Hash of options.
311 *
312 * @example
313 *
314 * var Promise = require('bluebird');
315 * var Accounts = bookshelf.Collection.extend({
316 * model: Account
317 * });
318 *
319 * var accounts = Accounts.forge([
320 * {name: 'Person1'},
321 * {name: 'Person2'}
322 * ]);
323 *
324 * Promise.all(accounts.invokeMap('save')).then(function() {
325 * // collection models should now be saved...
326 * });
327 */
328 forge: function forge(models, options) {
329 return new this(models, options);
330 }
331 }
332 ));
333
334 // The collection also references the correct `Model`, specified above, for
335 // creating new `Model` instances in the collection.
336 Collection.prototype.model = Model;
337 Model.prototype.Collection = Collection;
338
339 const Relation = BookshelfRelation.extend({Model, Collection});
340
341 // A `Bookshelf` instance may be used as a top-level pub-sub bus, as it mixes
342 // in the `Events` object. It also contains the version number, and a
343 // `Transaction` method referencing the correct version of `knex` passed into
344 // the object.
345 _.extend(bookshelf, Events, errors, {
346 /**
347 * An alias to `{@link http://knexjs.org/#Transactions Knex#transaction}`. The `transaction`
348 * object must be passed along in the options of any relevant Bookshelf calls, to ensure all
349 * queries are on the same connection. The entire transaction block is wrapped around a Promise
350 * that will commit the transaction if it resolves successfully, or roll it back if the Promise
351 * is rejected.
352 *
353 * Note that there is no need to explicitly call `transaction.commit()` or
354 * `transaction.rollback()` since the entire transaction will be committed if there are no
355 * errors inside the transaction block.
356 *
357 * When fetching inside a transaction it's possible to specify a row-level lock by passing the
358 * wanted lock type in the `lock` option to {@linkcode Model#fetch fetch}. Available options are
359 * `lock: 'forUpdate'` and `lock: 'forShare'`.
360 *
361 * @example
362 * var Promise = require('bluebird')
363 *
364 * Bookshelf.transaction((t) => {
365 * return new Library({name: 'Old Books'})
366 * .save(null, {transacting: t})
367 * .tap(function(model) {
368 * return Promise.map([
369 * {title: 'Canterbury Tales'},
370 * {title: 'Moby Dick'},
371 * {title: 'Hamlet'}
372 * ], (info) => {
373 * return new Book(info).save({'shelf_id': model.id}, {transacting: t})
374 * })
375 * })
376 * }).then((library) => {
377 * console.log(library.related('books').pluck('title'))
378 * }).catch((err) => {
379 * console.error(err)
380 * })
381 *
382 * @method Bookshelf#transaction
383 * @param {Bookshelf~transactionCallback} transactionCallback
384 * Callback containing transaction logic. The callback should return a Promise.
385 * @returns {Promise}
386 * A promise resolving to the value returned from
387 * {@link Bookshelf~transactionCallback transactionCallback}.
388 */
389 transaction() {
390 return this.knex.transaction.apply(this.knex, arguments);
391 },
392
393 /**
394 * This is a transaction block to be provided to {@link Bookshelf#transaction}. All of the
395 * database operations inside it can be part of the same transaction by passing the
396 * `transacting: transaction` option to {@link Model#fetch fetch}, {@link Model#save save} or
397 * {@link Model#destroy destroy}.
398 *
399 * Note that unless you explicitly pass the `transaction` object along to any relevant model
400 * operations, those operations will not be part of the transaction, even though they may be
401 * inside the transaction callback.
402 *
403 * @callback Bookshelf~transactionCallback
404 * @see {@link http://knexjs.org/#Transactions Knex#transaction}
405 * @see Bookshelf#transaction
406 *
407 * @param {Transaction} transaction
408 * @returns {Promise}
409 * The Promise will resolve to the return value of the callback, or be rejected with an error
410 * thrown inside it. If it resolves, the entire transaction is committed, otherwise it is
411 * rolled back.
412 */
413
414 /**
415 * @method Bookshelf#plugin
416 * @memberOf Bookshelf
417 * @description
418 *
419 * This method provides a nice, tested, standardized way of adding plugins
420 * to a `Bookshelf` instance, injecting the current instance into the
421 * plugin, which should be a `module.exports`.
422 *
423 * You can add a plugin by specifying a string with the name of the plugin
424 * to load. In this case it will try to find a module. It will pass the
425 * string to `require()`, so you can either require a third-party dependency
426 * by name or one of your own modules by relative path:
427 *
428 * bookshelf.plugin('./bookshelf-plugins/my-favourite-plugin');
429 * bookshelf.plugin('plugin-from-npm');
430 *
431 * There are a few official plugins published in `npm`, along with many
432 * independently developed ones. See
433 * [the list of available plugins](index.html#official-plugins).
434 *
435 * You can also provide an array of strings or functions, which is the same
436 * as calling `bookshelf.plugin()` multiple times. In this case the same
437 * options object will be reused:
438 *
439 * bookshelf.plugin(['cool-plugin', './my-plugins/even-cooler-plugin']);
440 *
441 * Example plugin:
442 *
443 * // Converts all string values to lower case when setting attributes on a model
444 * module.exports = function(bookshelf) {
445 * bookshelf.Model = bookshelf.Model.extend({
446 * set(key, value, options) {
447 * if (!key) return this
448 * if (typeof value === 'string') value = value.toLowerCase()
449 * return bookshelf.Model.prototype.set.call(this, key, value, options)
450 * }
451 * })
452 * }
453 *
454 * @param {string|array|Function} plugin
455 * The plugin or plugins to load. If you provide a string it can
456 * represent an npm package or a file somewhere on your project. You can
457 * also pass a function as argument to add it as a plugin. Finally, it's
458 * also possible to pass an array of strings or functions to add them all
459 * at once.
460 * @param {mixed} options
461 * This can be anything you want and it will be passed directly to the
462 * plugin as the second argument when loading it.
463 * @return {Bookshelf} The bookshelf instance for chaining.
464 */
465 plugin(plugin, options) {
466 if (_.isString(plugin)) {
467 if (plugin === 'pagination') {
468 const message =
469 'Pagination plugin was moved into core Bookshelf. You can now use `fetchPage()` without having to ' +
470 "call `.plugin('pagination')`. Remove any `.plugin('pagination')` calls to clear this message.";
471 return console.warn(message); // eslint-disable-line no-console
472 }
473
474 if (plugin === 'visibility') {
475 const message =
476 'Visibility plugin was moved into core Bookshelf. You can now set the `hidden` and `visible` properties ' +
477 "without having to call `.plugin('visibility')`. Remove any `.plugin('visibility')` calls to clear this " +
478 'message.';
479 return console.warn(message); // eslint-disable-line no-console
480 }
481
482 if (plugin === 'registry') {
483 const message =
484 'Registry plugin was moved into core Bookshelf. You can now register models using `bookshelf.model()` ' +
485 "and collections using `bookshelf.collection()` without having to call `.plugin('registry')`. Remove " +
486 "any `.plugin('registry')` calls to clear this message.";
487 return console.warn(message); // eslint-disable-line no-console
488 }
489
490 if (plugin === 'processor') {
491 const message =
492 'Processor plugin was removed from core Bookshelf. To migrate to the new standalone package follow the ' +
493 'instructions in https://github.com/bookshelf/bookshelf/wiki/Migrating-from-0.15.1-to-1.0.0#processor-plugin';
494 return console.warn(message); // eslint-disable-line no-console
495 }
496
497 if (plugin === 'case-converter') {
498 const message =
499 'Case converter plugin was removed from core Bookshelf. To migrate to the new standalone package follow ' +
500 'the instructions in https://github.com/bookshelf/bookshelf/wiki/Migrating-from-0.15.1-to-1.0.0#case-converter-plugin';
501 return console.warn(message); // eslint-disable-line no-console
502 }
503
504 if (plugin === 'virtuals') {
505 const message =
506 'Virtuals plugin was removed from core Bookshelf. To migrate to the new standalone package follow ' +
507 'the instructions in https://github.com/bookshelf/bookshelf/wiki/Migrating-from-0.15.1-to-1.0.0#virtuals-plugin';
508 return console.warn(message); // eslint-disable-line no-console
509 }
510
511 require(plugin)(this, options);
512 } else if (Array.isArray(plugin)) {
513 plugin.forEach((p) => this.plugin(p, options));
514 } else {
515 plugin(this, options);
516 }
517
518 return this;
519 }
520 });
521
522 /**
523 * @member Bookshelf#knex
524 * @type {Knex}
525 * @description
526 * A reference to the {@link http://knexjs.org Knex.js} instance being used by Bookshelf.
527 */
528 bookshelf.knex = knex;
529
530 function builderFn(tableNameOrBuilder) {
531 let builder = null;
532
533 if (_.isString(tableNameOrBuilder)) {
534 builder = bookshelf.knex(tableNameOrBuilder);
535 } else if (tableNameOrBuilder == null) {
536 builder = bookshelf.knex.queryBuilder();
537 } else {
538 // Assuming here that `tableNameOrBuilder` is a QueryBuilder instance. Not
539 // aware of a way to check that this is the case (ie. using
540 // `Knex.isQueryBuilder` or equivalent).
541 builder = tableNameOrBuilder;
542 }
543
544 return builder.on('query', (data) => this.trigger('query', data));
545 }
546
547 // Attach `where`, `query`, and `fetchAll` as static methods.
548 ['where', 'query'].forEach((method) => {
549 Model[method] = Collection[method] = function() {
550 const model = this.forge();
551 return model[method].apply(model, arguments);
552 };
553 });
554
555 return bookshelf;
556}
557
558module.exports = Bookshelf;