UNPKG

12.8 kBJavaScriptView Raw
1'use strict';
2
3const _ = require('lodash');
4const helpers = require('./helpers');
5
6// We've supplemented `Events` with a `triggerThen` method to allow for
7// asynchronous event handling via promises. We also mix this into the
8// prototypes of the main objects in the library.
9const Events = require('./base/events');
10
11// All core modules required for the bookshelf instance.
12const BookshelfModel = require('./model');
13const BookshelfCollection = require('./collection');
14const BookshelfRelation = require('./relation');
15const Errors = require('./errors');
16
17/**
18 * @class Bookshelf
19 * @classdesc
20 *
21 * The Bookshelf library is initialized by passing an initialized Knex client
22 * instance. The knex documentation provides a number of examples for different
23 * databases.
24 *
25 * @constructor
26 * @param {Knex} knex Knex instance.
27 */
28function Bookshelf(knex) {
29 if (!knex || knex.name !== 'knex') {
30 throw new Error('Invalid knex instance');
31 }
32 const bookshelf = {
33 VERSION: require('../package.json').version
34 };
35
36 const Model = (bookshelf.Model = BookshelfModel.extend(
37 {
38 _builder: builderFn,
39
40 // The `Model` constructor is referenced as a property on the `Bookshelf`
41 // instance, mixing in the correct `builder` method, as well as the
42 // `relation` method, passing in the correct `Model` & `Collection`
43 // constructors for later reference.
44 _relation(type, Target, options) {
45 if (type !== 'morphTo' && !_.isFunction(Target)) {
46 throw new Error(
47 'A valid target model must be defined for the ' + _.result(this, 'tableName') + ' ' + type + ' relation'
48 );
49 }
50 return new Relation(type, Target, options);
51 }
52 },
53 {
54 /**
55 * @method Model.forge
56 * @belongsTo Model
57 * @description
58 *
59 * A simple helper function to instantiate a new Model without needing `new`.
60 *
61 * @param {Object=} attributes Initial values for this model's attributes.
62 * @param {Object=} options Hash of options.
63 * @param {string=} options.tableName Initial value for {@linkcode Model#tableName tableName}.
64 * @param {Boolean=} [options.hasTimestamps=false]
65 *
66 * Initial value for {@linkcode Model#hasTimestamps hasTimestamps}.
67 *
68 * @param {Boolean} [options.parse=false]
69 *
70 * Convert attributes by {@linkcode Model#parse parse} before being
71 * {@linkcode Model#set set} on the `model`.
72 */
73 forge: function forge(attributes, options) {
74 return new this(attributes, options);
75 },
76
77 /**
78 * @method Model.collection
79 * @belongsTo Model
80 * @description
81 *
82 * A simple static helper to instantiate a new {@link Collection}, setting
83 * the current `model` as the collection's target.
84 *
85 * @example
86 *
87 * Customer.collection().fetch().then(function(collection) {
88 * // ...
89 * });
90 *
91 * @param {(Model[])=} models
92 * @param {Object=} options
93 * @returns {Collection}
94 */
95 collection(models, options) {
96 return new bookshelf.Collection(models || [], _.extend({}, options, {model: this}));
97 },
98
99 /**
100 * @method Model.count
101 * @belongsTo Model
102 * @since 0.8.2
103 * @description
104 *
105 * Gets the number of matching records in the database, respecting any
106 * previous calls to {@link Model#query query}. If a `column` is provided,
107 * records with a null value in that column will be excluded from the count.
108 *
109 * @param {string} [column='*']
110 * Specify a column to count - rows with null values in this column will be excluded.
111 * @param {Object=} options
112 * Hash of options.
113 * @returns {Promise<Number>}
114 * A promise resolving to the number of matching rows.
115 */
116 count(column, options) {
117 return this.forge().count(column, options);
118 },
119
120 /**
121 * @method Model.fetchAll
122 * @belongsTo Model
123 * @description
124 *
125 * Simple helper function for retrieving all instances of the given model.
126 *
127 * @see Model#fetchAll
128 * @returns {Promise<Collection>}
129 */
130 fetchAll(options) {
131 return this.forge().fetchAll(options);
132 }
133 }
134 ));
135
136 const Collection = (bookshelf.Collection = BookshelfCollection.extend(
137 {
138 _builder: builderFn
139 },
140 {
141 /**
142 * @method Collection.forge
143 * @belongsTo Collection
144 * @description
145 *
146 * A simple helper function to instantiate a new Collection without needing
147 * new.
148 *
149 * @param {(Object[]|Model[])=} [models]
150 * Set of models (or attribute hashes) with which to initialize the
151 * collection.
152 * @param {Object} options Hash of options.
153 *
154 * @example
155 *
156 * var Promise = require('bluebird');
157 * var Accounts = bookshelf.Collection.extend({
158 * model: Account
159 * });
160 *
161 * var accounts = Accounts.forge([
162 * {name: 'Person1'},
163 * {name: 'Person2'}
164 * ]);
165 *
166 * Promise.all(accounts.invokeMap('save')).then(function() {
167 * // collection models should now be saved...
168 * });
169 */
170 forge: function forge(models, options) {
171 return new this(models, options);
172 }
173 }
174 ));
175
176 // The collection also references the correct `Model`, specified above, for
177 // creating new `Model` instances in the collection.
178 Collection.prototype.model = Model;
179 Model.prototype.Collection = Collection;
180
181 const Relation = BookshelfRelation.extend({Model, Collection});
182
183 // A `Bookshelf` instance may be used as a top-level pub-sub bus, as it mixes
184 // in the `Events` object. It also contains the version number, and a
185 // `Transaction` method referencing the correct version of `knex` passed into
186 // the object.
187 _.extend(bookshelf, Events, Errors, {
188 /**
189 * @method Bookshelf#transaction
190 * @memberOf Bookshelf
191 * @description
192 *
193 * An alias to `{@link http://knexjs.org/#Transactions
194 * Knex#transaction}`, the `transaction` object must be passed along in the
195 * options of any relevant Bookshelf calls, to ensure all queries are on the
196 * same connection. The entire transaction block is a promise that will
197 * resolve when the transaction is committed, or fail if the transaction is
198 * rolled back.
199 *
200 * When fetching inside a transaction it's possible to specify a row-level
201 * lock by passing the wanted lock type in the `lock` option to
202 * {@linkcode Model#fetch fetch}. Available options are `forUpdate` and
203 * `forShare`.
204 *
205 * var Promise = require('bluebird');
206 *
207 * Bookshelf.transaction(function(t) {
208 * return new Library({name: 'Old Books'})
209 * .save(null, {transacting: t})
210 * .tap(function(model) {
211 * return Promise.map([
212 * {title: 'Canterbury Tales'},
213 * {title: 'Moby Dick'},
214 * {title: 'Hamlet'}
215 * ], function(info) {
216 * // Some validation could take place here.
217 * return new Book(info).save({'shelf_id': model.id}, {transacting: t});
218 * });
219 * });
220 * }).then(function(library) {
221 * console.log(library.related('books').pluck('title'));
222 * }).catch(function(err) {
223 * console.error(err);
224 * });
225 *
226 * @param {Bookshelf~transactionCallback} transactionCallback
227 * Callback containing transaction logic. The callback should return a
228 * promise.
229 *
230 * @returns {Promise<mixed>}
231 * A promise resolving to the value returned from {@link
232 * Bookshelf~transactionCallback transactionCallback}.
233 */
234 transaction() {
235 return this.knex.transaction.apply(this.knex, arguments);
236 },
237
238 /**
239 * @callback Bookshelf~transactionCallback
240 * @description
241 *
242 * A transaction block to be provided to {@link Bookshelf#transaction}.
243 *
244 * @see {@link http://knexjs.org/#Transactions Knex#transaction}
245 * @see Bookshelf#transaction
246 *
247 * @param {Transaction} transaction
248 * @returns {Promise<mixed>}
249 */
250
251 /**
252 * @method Bookshelf#plugin
253 * @memberOf Bookshelf
254 * @description
255 *
256 * This method provides a nice, tested, standardized way of adding plugins
257 * to a `Bookshelf` instance, injecting the current instance into the
258 * plugin, which should be a `module.exports`.
259 *
260 * You can add a plugin by specifying a string with the name of the plugin
261 * to load. In this case it will try to find a module. It will first check
262 * for a match within the `bookshelf/plugins` directory. If nothing is
263 * found it will pass the string to `require()`, so you can either require
264 * an npm dependency by name or one of your own modules by relative path:
265 *
266 * bookshelf.plugin('./bookshelf-plugins/my-favourite-plugin');
267 * bookshelf.plugin('plugin-from-npm');
268 *
269 * There are a few built-in plugins already, along with many independently
270 * developed ones. See [the list of available plugins](#plugins).
271 *
272 * You can also provide an array of strings or functions, which is the same
273 * as calling `bookshelf.plugin()` multiple times. In this case the same
274 * options object will be reused:
275 *
276 * bookshelf.plugin(['registry', './my-plugins/special-parse-format']);
277 *
278 * Example plugin:
279 *
280 * // Converts all string values to lower case when setting attributes on a model
281 * module.exports = function(bookshelf) {
282 * bookshelf.Model = bookshelf.Model.extend({
283 * set: function(key, value, options) {
284 * if (!key) return this;
285 * if (typeof value === 'string') value = value.toLowerCase();
286 * return bookshelf.Model.prototype.set.call(this, key, value, options);
287 * }
288 * });
289 * }
290 *
291 * @param {string|array|Function} plugin
292 * The plugin or plugins to add. If you provide a string it can
293 * represent a built-in plugin, an npm package or a file somewhere on
294 * your project. You can also pass a function as argument to add it as a
295 * plugin. Finally, it's also possible to pass an array of strings or
296 * functions to add them all at once.
297 * @param {mixed} options
298 * This can be anything you want and it will be passed directly to the
299 * plugin as the second argument when loading it.
300 */
301 plugin(plugin, options) {
302 if (_.isString(plugin)) {
303 try {
304 require('./plugins/' + plugin)(this, options);
305 } catch (e) {
306 if (e.code !== 'MODULE_NOT_FOUND') {
307 throw e;
308 }
309 if (!process.browser) {
310 require(plugin)(this, options);
311 }
312 }
313 } else if (Array.isArray(plugin)) {
314 plugin.forEach((p) => this.plugin(p, options));
315 } else {
316 plugin(this, options);
317 }
318
319 return this;
320 }
321 });
322
323 /**
324 * @member Bookshelf#knex
325 * @memberOf Bookshelf
326 * @type {Knex}
327 * @description
328 * A reference to the {@link http://knexjs.org Knex.js} instance being used by Bookshelf.
329 */
330 bookshelf.knex = knex;
331
332 function builderFn(tableNameOrBuilder) {
333 let builder = null;
334
335 if (_.isString(tableNameOrBuilder)) {
336 builder = bookshelf.knex(tableNameOrBuilder);
337 } else if (tableNameOrBuilder == null) {
338 builder = bookshelf.knex.queryBuilder();
339 } else {
340 // Assuming here that `tableNameOrBuilder` is a QueryBuilder instance. Not
341 // aware of a way to check that this is the case (ie. using
342 // `Knex.isQueryBuilder` or equivalent).
343 builder = tableNameOrBuilder;
344 }
345
346 return builder.on('query', (data) => this.trigger('query', data));
347 }
348
349 // Attach `where`, `query`, and `fetchAll` as static methods.
350 ['where', 'query'].forEach((method) => {
351 Model[method] = Collection[method] = function() {
352 const model = this.forge();
353 return model[method].apply(model, arguments);
354 };
355 });
356
357 return bookshelf;
358}
359
360// Constructor for a new `Bookshelf` object, it accepts an active `knex`
361// instance and initializes the appropriate `Model` and `Collection`
362// constructors for use in the current instance.
363Bookshelf.initialize = function(knex) {
364 helpers.warn("Bookshelf.initialize is deprecated, pass knex directly: require('bookshelf')(knex)");
365 return new Bookshelf(knex);
366};
367
368module.exports = Bookshelf;