1 | ;
|
2 |
|
3 | const _ = 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.
|
8 | const Events = require('./base/events');
|
9 |
|
10 | // All core modules required for the bookshelf instance.
|
11 | const BookshelfModel = require('./model');
|
12 | const BookshelfCollection = require('./collection');
|
13 | const BookshelfRelation = require('./relation');
|
14 | const errors = require('./errors');
|
15 |
|
16 | function 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 | */
|
31 | function 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 |
|
558 | module.exports = Bookshelf;
|