1 | ;
|
2 |
|
3 | const _ = require('lodash');
|
4 | const 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.
|
9 | const Events = require('./base/events');
|
10 |
|
11 | // All core modules required for the bookshelf instance.
|
12 | const BookshelfModel = require('./model');
|
13 | const BookshelfCollection = require('./collection');
|
14 | const BookshelfRelation = require('./relation');
|
15 | const 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 | */
|
28 | function 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.
|
363 | Bookshelf.initialize = function(knex) {
|
364 | helpers.warn("Bookshelf.initialize is deprecated, pass knex directly: require('bookshelf')(knex)");
|
365 | return new Bookshelf(knex);
|
366 | };
|
367 |
|
368 | module.exports = Bookshelf;
|