UNPKG

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