UNPKG

9.47 kBJavaScriptView Raw
1// Sync
2// ---------------
3'use strict';
4
5const _ = require('lodash');
6const Promise = require('bluebird');
7const validLocks = ['forShare', 'forUpdate'];
8
9function supportsReturning(client = {}) {
10 if (!client.config || !client.config.client) return false;
11 return ['postgresql', 'postgres', 'pg', 'oracle', 'mssql'].includes(client.config.client);
12}
13
14// Sync is the dispatcher for any database queries,
15// taking the "syncing" `model` or `collection` being queried, along with
16// a hash of options that are used in the various query methods.
17// If the `transacting` option is set, the query is assumed to be
18// part of a transaction, and this information is passed along to `Knex`.
19const Sync = function(syncing, options) {
20 options = options || {};
21 this.query = syncing.query();
22 this.syncing = syncing.resetQuery();
23 this.options = options;
24 if (options.debug) this.query.debug();
25 if (options.transacting) {
26 this.query.transacting(options.transacting);
27 if (validLocks.indexOf(options.lock) > -1) this.query[options.lock]();
28 }
29 if (options.withSchema) this.query.withSchema(options.withSchema);
30};
31
32_.extend(Sync.prototype, {
33 // Prefix all keys of the passed in object with the
34 // current table name
35 prefixFields: function(fields) {
36 const tableName = this.syncing.tableName;
37 const prefixed = {};
38 for (const key in fields) {
39 prefixed[tableName + '.' + key] = fields[key];
40 }
41 return prefixed;
42 },
43
44 // Select the first item from the database - only used by models.
45 first: Promise.method(function(attributes) {
46 const model = this.syncing;
47 const query = this.query;
48
49 // We'll never use an JSON object for a search, because even
50 // PostgreSQL, which has JSON type columns, does not support the `=`
51 // operator.
52 //
53 // NOTE: `_.omit` returns an empty object, even if attributes are null.
54 const whereAttributes = _.omitBy(attributes, (attribute, name) => {
55 return _.isPlainObject(attribute) || name === model.idAttribute;
56 });
57 const formattedAttributes = model.format(whereAttributes);
58
59 if (model.idAttribute in attributes) {
60 formattedAttributes[model.idAttribute] = attributes[model.idAttribute];
61 }
62
63 if (!_.isEmpty(formattedAttributes)) query.where(this.prefixFields(formattedAttributes));
64 query.limit(1);
65
66 return this.select();
67 }),
68
69 // Runs a `count` query on the database, adding any necessary relational
70 // constraints. Returns a promise that resolves to an integer count.
71 count: Promise.method(function(column) {
72 const knex = this.query,
73 options = this.options,
74 relatedData = this.syncing.relatedData,
75 fks = {};
76
77 return Promise.bind(this)
78 .then(function() {
79 // Inject all appropriate select costraints dealing with the relation
80 // into the `knex` query builder for the current instance.
81 if (relatedData)
82 return Promise.try(function() {
83 if (relatedData.isThrough()) {
84 fks[relatedData.key('foreignKey')] = relatedData.parentFk;
85 const through = new relatedData.throughTarget(fks);
86 relatedData.pivotColumns = through.parse(relatedData.pivotColumns);
87 } else if (relatedData.type === 'hasMany') {
88 const fk = relatedData.key('foreignKey');
89 knex.where(fk, relatedData.parentFk);
90 }
91 });
92 })
93 .then(function() {
94 options.query = knex;
95
96 /**
97 * Counting event.
98 *
99 * Fired before a `count` query. A promise may be
100 * returned from the event handler for async behaviour.
101 *
102 * @event Model#counting
103 * @tutorial events
104 * @param {Model} model The model firing the event.
105 * @param {Object} options Options object passed to {@link Model#count count}.
106 * @returns {Promise}
107 */
108 return this.syncing.triggerThen('counting', this.syncing, options);
109 })
110 .then(function() {
111 return knex.count((column || '*') + ' as count');
112 })
113 .then(function(rows) {
114 return rows[0].count;
115 });
116 }),
117
118 // Runs a `select` query on the database, adding any necessary relational
119 // constraints, resetting the query when complete. If there are results and
120 // eager loaded relations, those are fetched and returned on the model before
121 // the promise is resolved. Any `success` handler passed in the
122 // options will be called - used by both models & collections.
123 select: Promise.method(function() {
124 const knex = this.query;
125 const options = this.options;
126 const relatedData = this.syncing.relatedData;
127 const fks = {};
128 let columns = null;
129
130 // Check if any `select` style statements have been called with column
131 // specifications. This could include `distinct()` with no arguments, which
132 // does not affect inform the columns returned.
133 const queryContainsColumns = _(knex._statements)
134 .filter({grouping: 'columns'})
135 .some('value.length');
136
137 return Promise.bind(this)
138 .then(function() {
139 // Set the query builder on the options, in-case we need to
140 // access in the `fetching` event handlers.
141 options.query = knex;
142
143 // Inject all appropriate select costraints dealing with the relation
144 // into the `knex` query builder for the current instance.
145 if (relatedData)
146 return Promise.try(function() {
147 if (relatedData.isThrough()) {
148 fks[relatedData.key('foreignKey')] = relatedData.parentFk;
149 const through = new relatedData.throughTarget(fks);
150
151 return through.triggerThen('fetching', through, relatedData.pivotColumns, options).then(function() {
152 relatedData.pivotColumns = through.parse(relatedData.pivotColumns);
153 });
154 }
155 });
156 })
157 .tap(() => {
158 // If this is a relation, apply the appropriate constraints.
159 if (relatedData) {
160 relatedData.selectConstraints(knex, options);
161 } else {
162 // Call the function, if one exists, to constrain the eager loaded query.
163 if (options._beforeFn) options._beforeFn.call(knex, knex);
164
165 if (options.columns) {
166 // Normalize single column name into array.
167 columns = Array.isArray(options.columns) ? options.columns : [options.columns];
168 } else if (!queryContainsColumns) {
169 // If columns have already been selected via the `query` method
170 // we will use them. Otherwise, select all columns in this table.
171 columns = [_.result(this.syncing, 'tableName') + '.*'];
172 }
173 }
174
175 // Set the query builder on the options, for access in the `fetching`
176 // event handlers.
177 options.query = knex;
178
179 /**
180 * Fired before a `fetch` operation. A promise may be returned from the event handler for
181 * async behaviour.
182 *
183 * @example
184 * const MyModel = bookshelf.model('MyModel', {
185 * initialize() {
186 * this.on('fetching', function(model, columns, options) {
187 * options.query.where('status', 'active')
188 * })
189 * }
190 * })
191 *
192 * @event Model#fetching
193 * @tutorial events
194 * @param {Model} model The model which is about to be fetched.
195 * @param {string[]} columns The columns to be retrieved by the query.
196 * @param {Object} options Options object passed to {@link Model#fetch fetch}.
197 * @param {QueryBuilder} options.query
198 * Query builder to be used for fetching. This can be used to modify or add to the query
199 * before it is executed. See example above.
200 * @return {Promise}
201 */
202 return this.syncing.triggerThen('fetching', this.syncing, columns, options);
203 })
204 .then(() => knex.select(columns));
205 }),
206
207 // Issues an `insert` command on the query - only used by models.
208 insert: Promise.method(function() {
209 const syncing = this.syncing;
210 return this.query.insert(
211 syncing.format(_.extend(Object.create(null), syncing.attributes)),
212 supportsReturning(this.query.client) ? '*' : null
213 );
214 }),
215
216 // Issues an `update` command on the query - only used by models.
217 update: Promise.method(function(attrs) {
218 const syncing = this.syncing,
219 query = this.query;
220 if (syncing.id != null) query.where(syncing.format({[syncing.idAttribute]: syncing.id}));
221 if (_.filter(query._statements, {grouping: 'where'}).length === 0) {
222 throw new Error('A model cannot be updated without a "where" clause or an idAttribute.');
223 }
224 var updating = syncing.format(_.extend(Object.create(null), attrs));
225 if (syncing.id === updating[syncing.idAttribute]) {
226 delete updating[syncing.idAttribute];
227 }
228 if (supportsReturning(query.client)) query.returning('*');
229 return query.update(updating);
230 }),
231
232 // Issues a `delete` command on the query.
233 del: Promise.method(function() {
234 const query = this.query,
235 syncing = this.syncing;
236 if (syncing.id != null) query.where(syncing.format({[syncing.idAttribute]: syncing.id}));
237 if (_.filter(query._statements, {grouping: 'where'}).length === 0) {
238 throw new Error('A model cannot be destroyed without a "where" clause or an idAttribute.');
239 }
240 return this.query.del();
241 })
242});
243
244module.exports = Sync;