UNPKG

11.7 kBJavaScriptView Raw
1const Bluebird = require('bluebird');
2
3const Raw = require('./raw');
4const Ref = require('./ref');
5const Runner = require('./runner');
6const Formatter = require('./formatter');
7const Transaction = require('./transaction');
8
9const QueryBuilder = require('./query/builder');
10const QueryCompiler = require('./query/compiler');
11
12const SchemaBuilder = require('./schema/builder');
13const SchemaCompiler = require('./schema/compiler');
14const TableBuilder = require('./schema/tablebuilder');
15const TableCompiler = require('./schema/tablecompiler');
16const ColumnBuilder = require('./schema/columnbuilder');
17const ColumnCompiler = require('./schema/columncompiler');
18
19const { Pool, TimeoutError } = require('tarn');
20const inherits = require('inherits');
21const { EventEmitter } = require('events');
22const { promisify } = require('util');
23
24const { makeEscape } = require('./query/string');
25const { uniqueId, cloneDeep, defaults } = require('lodash');
26
27const Logger = require('./logger');
28
29const debug = require('debug')('knex:client');
30const _debugQuery = require('debug')('knex:query');
31const debugBindings = require('debug')('knex:bindings');
32
33const debugQuery = (sql, txId) => _debugQuery(sql.replace(/%/g, '%%'), txId);
34
35const { POOL_CONFIG_OPTIONS } = require('./constants');
36
37// The base client provides the general structure
38// for a dialect specific client object.
39function Client(config = {}) {
40 this.config = config;
41 this.logger = new Logger(config);
42
43 //Client is a required field, so throw error if it's not supplied.
44 //If 'this.dialect' is set, then this is a 'super()' call, in which case
45 //'client' does not have to be set as it's already assigned on the client prototype.
46
47 if (this.dialect && !this.config.client) {
48 this.logger.warn(
49 `Using 'this.dialect' to identify the client is deprecated and support for it will be removed in the future. Please use configuration option 'client' instead.`
50 );
51 }
52 const dbClient = this.config.client || this.dialect;
53 if (!dbClient) {
54 throw new Error(`knex: Required configuration option 'client' is missing.`);
55 }
56
57 if (config.version) {
58 this.version = config.version;
59 }
60
61 if (config.connection && config.connection instanceof Function) {
62 this.connectionConfigProvider = config.connection;
63 this.connectionConfigExpirationChecker = () => true; // causes the provider to be called on first use
64 } else {
65 this.connectionSettings = cloneDeep(config.connection || {});
66 this.connectionConfigExpirationChecker = null;
67 }
68 if (this.driverName && config.connection) {
69 this.initializeDriver();
70 if (!config.pool || (config.pool && config.pool.max !== 0)) {
71 this.initializePool(config);
72 }
73 }
74 this.valueForUndefined = this.raw('DEFAULT');
75 if (config.useNullAsDefault) {
76 this.valueForUndefined = null;
77 }
78}
79
80inherits(Client, EventEmitter);
81
82Object.assign(Client.prototype, {
83 formatter(builder) {
84 return new Formatter(this, builder);
85 },
86
87 queryBuilder() {
88 return new QueryBuilder(this);
89 },
90
91 queryCompiler(builder) {
92 return new QueryCompiler(this, builder);
93 },
94
95 schemaBuilder() {
96 return new SchemaBuilder(this);
97 },
98
99 schemaCompiler(builder) {
100 return new SchemaCompiler(this, builder);
101 },
102
103 tableBuilder(type, tableName, fn) {
104 return new TableBuilder(this, type, tableName, fn);
105 },
106
107 tableCompiler(tableBuilder) {
108 return new TableCompiler(this, tableBuilder);
109 },
110
111 columnBuilder(tableBuilder, type, args) {
112 return new ColumnBuilder(this, tableBuilder, type, args);
113 },
114
115 columnCompiler(tableBuilder, columnBuilder) {
116 return new ColumnCompiler(this, tableBuilder, columnBuilder);
117 },
118
119 runner(builder) {
120 return new Runner(this, builder);
121 },
122
123 transaction(container, config, outerTx) {
124 return new Transaction(this, container, config, outerTx);
125 },
126
127 raw() {
128 return new Raw(this).set(...arguments);
129 },
130
131 ref() {
132 return new Ref(this, ...arguments);
133 },
134
135 _formatQuery(sql, bindings, timeZone) {
136 bindings = bindings == null ? [] : [].concat(bindings);
137 let index = 0;
138 return sql.replace(/\\?\?/g, (match) => {
139 if (match === '\\?') {
140 return '?';
141 }
142 if (index === bindings.length) {
143 return match;
144 }
145 const value = bindings[index++];
146 return this._escapeBinding(value, { timeZone });
147 });
148 },
149
150 _escapeBinding: makeEscape({
151 escapeString(str) {
152 return `'${str.replace(/'/g, "''")}'`;
153 },
154 }),
155
156 query(connection, obj) {
157 if (typeof obj === 'string') obj = { sql: obj };
158 obj.bindings = this.prepBindings(obj.bindings);
159
160 const { __knexUid, __knexTxId } = connection;
161
162 this.emit('query', Object.assign({ __knexUid, __knexTxId }, obj));
163 debugQuery(obj.sql, __knexTxId);
164 debugBindings(obj.bindings, __knexTxId);
165
166 obj.sql = this.positionBindings(obj.sql);
167
168 return this._query(connection, obj).catch((err) => {
169 err.message =
170 this._formatQuery(obj.sql, obj.bindings) + ' - ' + err.message;
171 this.emit(
172 'query-error',
173 err,
174 Object.assign({ __knexUid, __knexTxId }, obj)
175 );
176 throw err;
177 });
178 },
179
180 stream(connection, obj, stream, options) {
181 if (typeof obj === 'string') obj = { sql: obj };
182 obj.bindings = this.prepBindings(obj.bindings);
183
184 const { __knexUid, __knexTxId } = connection;
185
186 this.emit('query', Object.assign({ __knexUid, __knexTxId }, obj));
187 debugQuery(obj.sql, __knexTxId);
188 debugBindings(obj.bindings, __knexTxId);
189
190 obj.sql = this.positionBindings(obj.sql);
191
192 return this._stream(connection, obj, stream, options);
193 },
194
195 prepBindings(bindings) {
196 return bindings;
197 },
198
199 positionBindings(sql) {
200 return sql;
201 },
202
203 postProcessResponse(resp, queryContext) {
204 if (this.config.postProcessResponse) {
205 return this.config.postProcessResponse(resp, queryContext);
206 }
207 return resp;
208 },
209
210 wrapIdentifier(value, queryContext) {
211 return this.customWrapIdentifier(
212 value,
213 this.wrapIdentifierImpl,
214 queryContext
215 );
216 },
217
218 customWrapIdentifier(value, origImpl, queryContext) {
219 if (this.config.wrapIdentifier) {
220 return this.config.wrapIdentifier(value, origImpl, queryContext);
221 }
222 return origImpl(value);
223 },
224
225 wrapIdentifierImpl(value) {
226 return value !== '*' ? `"${value.replace(/"/g, '""')}"` : '*';
227 },
228
229 initializeDriver() {
230 try {
231 this.driver = this._driver();
232 } catch (e) {
233 const message = `Knex: run\n$ npm install ${this.driverName} --save`;
234 this.logger.error(`${message}\n${e.message}\n${e.stack}`);
235 throw new Error(`${message}\n${e.message}`);
236 }
237 },
238
239 poolDefaults() {
240 return { min: 2, max: 10, propagateCreateError: true };
241 },
242
243 getPoolSettings(poolConfig) {
244 poolConfig = defaults({}, poolConfig, this.poolDefaults());
245
246 POOL_CONFIG_OPTIONS.forEach((option) => {
247 if (option in poolConfig) {
248 this.logger.warn(
249 [
250 `Pool config option "${option}" is no longer supported.`,
251 `See https://github.com/Vincit/tarn.js for possible pool config options.`,
252 ].join(' ')
253 );
254 }
255 });
256
257 const timeouts = [
258 this.config.acquireConnectionTimeout || 60000,
259 poolConfig.acquireTimeoutMillis,
260 ].filter((timeout) => timeout !== undefined);
261
262 // acquire connection timeout can be set on config or config.pool
263 // choose the smallest, positive timeout setting and set on poolConfig
264 poolConfig.acquireTimeoutMillis = Math.min(...timeouts);
265
266 const updatePoolConnectionSettingsFromProvider = async () => {
267 if (!this.connectionConfigProvider) {
268 return; // static configuration, nothing to update
269 }
270 if (
271 !this.connectionConfigExpirationChecker ||
272 !this.connectionConfigExpirationChecker()
273 ) {
274 return; // not expired, reuse existing connection
275 }
276 const providerResult = await this.connectionConfigProvider();
277 if (providerResult.expirationChecker) {
278 this.connectionConfigExpirationChecker =
279 providerResult.expirationChecker;
280 delete providerResult.expirationChecker; // MySQL2 driver warns on receiving extra properties
281 } else {
282 this.connectionConfigExpirationChecker = null;
283 }
284 this.connectionSettings = providerResult;
285 };
286
287 return Object.assign(poolConfig, {
288 create: async () => {
289 await updatePoolConnectionSettingsFromProvider();
290 const connection = await this.acquireRawConnection();
291 connection.__knexUid = uniqueId('__knexUid');
292 if (poolConfig.afterCreate) {
293 await promisify(poolConfig.afterCreate)(connection);
294 }
295 return connection;
296 },
297
298 destroy: (connection) => {
299 if (connection !== void 0) {
300 return this.destroyRawConnection(connection);
301 }
302 },
303
304 validate: (connection) => {
305 if (connection.__knex__disposed) {
306 this.logger.warn(`Connection Error: ${connection.__knex__disposed}`);
307 return false;
308 }
309
310 return this.validateConnection(connection);
311 },
312 });
313 },
314
315 initializePool(config = this.config) {
316 if (this.pool) {
317 this.logger.warn('The pool has already been initialized');
318 return;
319 }
320
321 const tarnPoolConfig = {
322 ...this.getPoolSettings(config.pool),
323 };
324 // afterCreate is an internal knex param, tarn.js does not support it
325 if (tarnPoolConfig.afterCreate) {
326 delete tarnPoolConfig.afterCreate;
327 }
328
329 this.pool = new Pool(tarnPoolConfig);
330 },
331
332 validateConnection(connection) {
333 return true;
334 },
335
336 // Acquire a connection from the pool.
337 acquireConnection() {
338 if (!this.pool) {
339 return Bluebird.reject(new Error('Unable to acquire a connection'));
340 }
341 try {
342 return Bluebird.try(() => this.pool.acquire().promise)
343 .then((connection) => {
344 debug('acquired connection from pool: %s', connection.__knexUid);
345 return connection;
346 })
347 .catch(TimeoutError, () => {
348 throw new Bluebird.TimeoutError(
349 'Knex: Timeout acquiring a connection. The pool is probably full. ' +
350 'Are you missing a .transacting(trx) call?'
351 );
352 });
353 } catch (e) {
354 return Bluebird.reject(e);
355 }
356 },
357
358 // Releases a connection back to the connection pool,
359 // returning a promise resolved when the connection is released.
360 releaseConnection(connection) {
361 debug('releasing connection to pool: %s', connection.__knexUid);
362 const didRelease = this.pool.release(connection);
363
364 if (!didRelease) {
365 debug('pool refused connection: %s', connection.__knexUid);
366 }
367
368 return Bluebird.resolve();
369 },
370
371 // Destroy the current connection pool for the client.
372 destroy(callback) {
373 const maybeDestroy = this.pool && this.pool.destroy();
374
375 return Bluebird.resolve(maybeDestroy)
376 .then(() => {
377 this.pool = void 0;
378
379 if (typeof callback === 'function') {
380 callback();
381 }
382 })
383 .catch((err) => {
384 if (typeof callback === 'function') {
385 callback(err);
386 }
387
388 return Bluebird.reject(err);
389 });
390 },
391
392 // Return the database being used by this client.
393 database() {
394 return this.connectionSettings.database;
395 },
396
397 toString() {
398 return '[object KnexClient]';
399 },
400
401 canCancelQuery: false,
402
403 assertCanCancelQuery() {
404 if (!this.canCancelQuery) {
405 throw new Error('Query cancelling not supported for this dialect');
406 }
407 },
408
409 cancelQuery() {
410 throw new Error('Query cancelling not supported for this dialect');
411 },
412});
413
414module.exports = Client;