UNPKG

8.89 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 * @param {Model} model The model firing the event.
99 * @param {Object} options Options object passed to {@link Model#count count}.
100 * @returns {Promise}
101 */
102 return this.syncing.triggerThen('counting', this.syncing, options);
103 })
104 .then(function() {
105 return knex.count((column || '*') + ' as count');
106 })
107 .then(function(rows) {
108 return rows[0].count;
109 });
110 }),
111
112 // Runs a `select` query on the database, adding any necessary relational
113 // constraints, resetting the query when complete. If there are results and
114 // eager loaded relations, those are fetched and returned on the model before
115 // the promise is resolved. Any `success` handler passed in the
116 // options will be called - used by both models & collections.
117 select: Promise.method(function() {
118 const knex = this.query;
119 const options = this.options;
120 const relatedData = this.syncing.relatedData;
121 const fks = {};
122 let columns = null;
123
124 // Check if any `select` style statements have been called with column
125 // specifications. This could include `distinct()` with no arguments, which
126 // does not affect inform the columns returned.
127 const queryContainsColumns = _(knex._statements)
128 .filter({grouping: 'columns'})
129 .some('value.length');
130
131 return Promise.bind(this)
132 .then(function() {
133 // Set the query builder on the options, in-case we need to
134 // access in the `fetching` event handlers.
135 options.query = knex;
136
137 // Inject all appropriate select costraints dealing with the relation
138 // into the `knex` query builder for the current instance.
139 if (relatedData)
140 return Promise.try(function() {
141 if (relatedData.isThrough()) {
142 fks[relatedData.key('foreignKey')] = relatedData.parentFk;
143 const through = new relatedData.throughTarget(fks);
144
145 return through.triggerThen('fetching', through, relatedData.pivotColumns, options).then(function() {
146 relatedData.pivotColumns = through.parse(relatedData.pivotColumns);
147 });
148 }
149 });
150 })
151 .tap(() => {
152 // If this is a relation, apply the appropriate constraints.
153 if (relatedData) {
154 relatedData.selectConstraints(knex, options);
155 } else {
156 // Call the function, if one exists, to constrain the eager loaded query.
157 if (options._beforeFn) options._beforeFn.call(knex, knex);
158
159 if (options.columns) {
160 // Normalize single column name into array.
161 columns = Array.isArray(options.columns) ? options.columns : [options.columns];
162 } else if (!queryContainsColumns) {
163 // If columns have already been selected via the `query` method
164 // we will use them. Otherwise, select all columns in this table.
165 columns = [_.result(this.syncing, 'tableName') + '.*'];
166 }
167 }
168
169 // Set the query builder on the options, for access in the `fetching`
170 // event handlers.
171 options.query = knex;
172
173 /**
174 * Fired before a `fetch` operation. A promise may be returned from the
175 * event handler for async behaviour.
176 *
177 * @event Model#fetching
178 * @param {Model} model
179 * The model which is about to be fetched.
180 * @param {string[]} columns
181 * The columns to be retrieved by the query.
182 * @param {Object} options
183 * Options object passed to {@link Model#fetch fetch}.
184 * @param {QueryBuilder} options.query
185 * Query builder to be used for fetching. This can be modified to
186 * change the query before it is executed.
187 *
188 * @returns {Promise}
189 */
190 return this.syncing.triggerThen('fetching', this.syncing, columns, options);
191 })
192 .then(() => knex.select(columns));
193 }),
194
195 // Issues an `insert` command on the query - only used by models.
196 insert: Promise.method(function() {
197 const syncing = this.syncing;
198 return this.query.insert(
199 syncing.format(_.extend(Object.create(null), syncing.attributes)),
200 supportsReturning(this.query.client.config.client) ? syncing.idAttribute : null
201 );
202 }),
203
204 // Issues an `update` command on the query - only used by models.
205 update: Promise.method(function(attrs) {
206 const syncing = this.syncing,
207 query = this.query;
208 if (syncing.id != null) query.where(syncing.format({[syncing.idAttribute]: syncing.id}));
209 if (_.filter(query._statements, {grouping: 'where'}).length === 0) {
210 throw new Error('A model cannot be updated without a "where" clause or an idAttribute.');
211 }
212 var updating = syncing.format(_.extend(Object.create(null), attrs));
213 if (syncing.id === updating[syncing.idAttribute]) {
214 delete updating[syncing.idAttribute];
215 }
216 return query.update(updating);
217 }),
218
219 // Issues a `delete` command on the query.
220 del: Promise.method(function() {
221 const query = this.query,
222 syncing = this.syncing;
223 if (syncing.id != null) query.where(syncing.format({[syncing.idAttribute]: syncing.id}));
224 if (_.filter(query._statements, {grouping: 'where'}).length === 0) {
225 throw new Error('A model cannot be destroyed without a "where" clause or an idAttribute.');
226 }
227 return this.query.del();
228 })
229});
230
231module.exports = Sync;