@bitblit/ratchet-rdbms 6.0.146-alpha → 6.0.148-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -3
- package/src/build/ratchet-rdbms-info.ts +19 -0
- package/src/model/connection-and-tunnel.ts +7 -0
- package/src/model/database-access-provider.ts +11 -0
- package/src/model/database-access.ts +30 -0
- package/src/model/database-config-list.ts +3 -0
- package/src/model/database-request-type.ts +6 -0
- package/src/model/group-by-count-result.ts +4 -0
- package/src/model/modify-results.ts +9 -0
- package/src/model/named-parameter-database-service-config.ts +13 -0
- package/src/model/paginated-results.ts +5 -0
- package/src/model/pagination-bounds.ts +12 -0
- package/src/model/paginator.ts +20 -0
- package/src/model/query-defaults.ts +4 -0
- package/src/model/query-text-provider.ts +4 -0
- package/src/model/request-results.ts +4 -0
- package/src/model/simple-query-text-provider.ts +18 -0
- package/src/model/sort-direction.ts +4 -0
- package/src/model/ssh/ssh-tunnel-config.ts +8 -0
- package/src/model/ssh/ssh-tunnel-container.ts +13 -0
- package/src/model/transaction-isolation-level.ts +4 -0
- package/src/mysql/model/mysql-db-config.ts +14 -0
- package/src/mysql/model/mysql-master-status.ts +6 -0
- package/src/mysql/model/mysql-slave-status.ts +52 -0
- package/src/mysql/mysql-style-database-access.ts +85 -0
- package/src/mysql/rds-mysql-style-connection-provider.ts +265 -0
- package/src/postgres/model/postgres-db-config.ts +8 -0
- package/src/postgres/postgres-style-connection-provider.ts +270 -0
- package/src/postgres/postgres-style-database-access.spec.ts +76 -0
- package/src/postgres/postgres-style-database-access.ts +110 -0
- package/src/query-builder/query-builder-result.ts +21 -0
- package/src/query-builder/query-builder.spec.ts +194 -0
- package/src/query-builder/query-builder.ts +445 -0
- package/src/query-builder/query-util.spec.ts +20 -0
- package/src/query-builder/query-util.ts +162 -0
- package/src/rds-data-api/model/rds-data-api-connection-config.ts +8 -0
- package/src/rds-data-api/rds-data-api-connection-provider.ts +39 -0
- package/src/rds-data-api/rds-data-api-database-access.spec.ts +139 -0
- package/src/rds-data-api/rds-data-api-database-access.ts +209 -0
- package/src/service/named-parameter-database-service.ts +421 -0
- package/src/service/ssh-tunnel-service.ts +62 -0
- package/src/service/transactional-named-parameter-database-service.ts +171 -0
- package/src/sqlite/model/fetch-remote-mode.ts +4 -0
- package/src/sqlite/model/flush-remote-mode.ts +4 -0
- package/src/sqlite/model/sqlite-connection-config-flag.ts +3 -0
- package/src/sqlite/model/sqlite-connection-config.ts +11 -0
- package/src/sqlite/model/sqlite-local-file-config.ts +3 -0
- package/src/sqlite/model/sqlite-remote-file-sync-config.ts +9 -0
- package/src/sqlite/sqlite-database-access.spec.ts +158 -0
- package/src/sqlite/sqlite-database-access.ts +126 -0
- package/src/sqlite/sqlite-remote-sync-database-access.ts +152 -0
- package/src/sqlite/sqlite-style-connection-provider.ts +181 -0
- package/src/util/aws-rds-cert-2023.ts +502 -0
- package/src/util/named-parameter-adapter/named-parameter-adapter.ts +51 -0
- package/src/util/named-parameter-adapter/query-and-params.ts +4 -0
- package/src/util/relational-database-utils.ts +54 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { Logger } from '@bitblit/ratchet-common/logger/logger';
|
|
2
|
+
import { ErrorRatchet } from '@bitblit/ratchet-common/lang/error-ratchet';
|
|
3
|
+
import { PromiseRatchet } from '@bitblit/ratchet-common/lang/promise-ratchet';
|
|
4
|
+
import { StopWatch } from '@bitblit/ratchet-common/lang/stop-watch';
|
|
5
|
+
import { TimeoutToken } from '@bitblit/ratchet-common/lang/timeout-token';
|
|
6
|
+
import { DurationRatchet } from '@bitblit/ratchet-common/lang/duration-ratchet';
|
|
7
|
+
import { QueryUtil } from '../query-builder/query-util.js';
|
|
8
|
+
import { TransactionIsolationLevel } from '../model/transaction-isolation-level.js';
|
|
9
|
+
import { QueryBuilder } from '../query-builder/query-builder.js';
|
|
10
|
+
import { GroupByCountResult } from '../model/group-by-count-result.js';
|
|
11
|
+
import { QueryDefaults } from '../model/query-defaults.js';
|
|
12
|
+
import { QueryTextProvider } from '../model/query-text-provider.js';
|
|
13
|
+
import { ModifyResults } from '../model/modify-results.js';
|
|
14
|
+
import { DatabaseAccessProvider } from '../model/database-access-provider.js';
|
|
15
|
+
import { DatabaseAccess } from '../model/database-access.js';
|
|
16
|
+
import { RequestResults } from '../model/request-results.js';
|
|
17
|
+
import { DatabaseRequestType } from '../model/database-request-type.js';
|
|
18
|
+
import { NamedParameterDatabaseServiceConfig } from '../model/named-parameter-database-service-config.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Service to simplify talking to any Mysql dialect system
|
|
22
|
+
*
|
|
23
|
+
* 2021-05-31 : This is a refactor of MariaDbService that supports named parameters
|
|
24
|
+
* and conditional blocks, but does NOT support string substitution. Using this approach
|
|
25
|
+
* removes the possibility of SQL injection. I am putting this is as a separate class to
|
|
26
|
+
* make it easy to transition - as a dao gets refactored to use the new system, it can simply
|
|
27
|
+
* change which service gets injected. Once nothing is injecting the old service, we can
|
|
28
|
+
* remove it entirely.
|
|
29
|
+
*
|
|
30
|
+
* Conditional sections use the <<xx>>yyy<</>> syntax
|
|
31
|
+
* If your conditional section uses the <<:zz>>yyy<</>> then the section is included if named parameter zz is set not null/undefined and if it is an array the length is greater than zero.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
export class NamedParameterDatabaseService {
|
|
35
|
+
constructor(private cfg: NamedParameterDatabaseServiceConfig) {
|
|
36
|
+
cfg.serviceName = cfg.serviceName ?? 'NamedParameterDatabaseService';
|
|
37
|
+
cfg.logger = cfg.logger || Logger.getLogger();
|
|
38
|
+
Logger.info('NamedParameterDatabaseService using logger %s at %s', cfg.logger.guid, cfg.logger.level);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public getConfig(): NamedParameterDatabaseServiceConfig {
|
|
42
|
+
return this.cfg;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public nonPooledExtraConfiguration(): Record<string, any> {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public nonPooledMode(): boolean {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public get databaseAccessProvider(): DatabaseAccessProvider {
|
|
54
|
+
return this.cfg.connectionProvider;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public getQueryDefaults(): QueryDefaults {
|
|
58
|
+
return this.cfg.queryDefaults;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public getQueryProvider(): QueryTextProvider {
|
|
62
|
+
return this.cfg.queryProvider;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public async createNonPooledDatabaseAccess(
|
|
66
|
+
queryDefaults: QueryDefaults,
|
|
67
|
+
additionalConfig?: Record<string, any>,
|
|
68
|
+
): Promise<DatabaseAccess> {
|
|
69
|
+
this.cfg.logger.info('createTransactional : %s : %j', queryDefaults, additionalConfig);
|
|
70
|
+
if (!this.cfg.connectionProvider.createNonPooledDatabaseAccess) {
|
|
71
|
+
throw new Error(`Connection provider does not implement createNonPooledDatabaseAccess`);
|
|
72
|
+
}
|
|
73
|
+
const newConn: DatabaseAccess = await this.cfg.connectionProvider.createNonPooledDatabaseAccess(queryDefaults, additionalConfig);
|
|
74
|
+
if (!newConn) {
|
|
75
|
+
throw new Error(`Connection could not be created for DB type ${queryDefaults}`);
|
|
76
|
+
}
|
|
77
|
+
return newConn;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public fetchQueryRawTextByName(queryPath: string): string {
|
|
81
|
+
return this.cfg.queryProvider.fetchQuery(queryPath);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public queryBuilder(queryPath?: string): QueryBuilder {
|
|
85
|
+
const queryBuilder = new QueryBuilder(this.cfg.queryProvider);
|
|
86
|
+
if (queryPath) {
|
|
87
|
+
queryBuilder.withNamedQuery(queryPath);
|
|
88
|
+
}
|
|
89
|
+
return queryBuilder;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
public async executeUpdateOrInsertByName(
|
|
93
|
+
queryPath: string,
|
|
94
|
+
params?: object,
|
|
95
|
+
timeoutMS: number = this.cfg.queryDefaults.timeoutMS,
|
|
96
|
+
): Promise<ModifyResults> {
|
|
97
|
+
const builder = this.queryBuilder(queryPath).withParams(params ?? {});
|
|
98
|
+
return this.buildAndExecuteUpdateOrInsert(builder, timeoutMS);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public async buildAndExecuteUpdateOrInsert(
|
|
102
|
+
queryBuilder: QueryBuilder,
|
|
103
|
+
timeoutMS: number = this.cfg.queryDefaults.timeoutMS,
|
|
104
|
+
): Promise<ModifyResults> {
|
|
105
|
+
const build = queryBuilder.build();
|
|
106
|
+
const resp = await this.executeQueryWithMeta<ModifyResults>(
|
|
107
|
+
DatabaseRequestType.Modify,
|
|
108
|
+
build.transactionIsolationLevel,
|
|
109
|
+
build.query,
|
|
110
|
+
build.namedParams,
|
|
111
|
+
timeoutMS,
|
|
112
|
+
);
|
|
113
|
+
const rval = resp.results;
|
|
114
|
+
return rval;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public async buildAndExecuteUpdateOrInsertWithRetry(
|
|
118
|
+
queryBuilder: QueryBuilder,
|
|
119
|
+
maxRetries: number,
|
|
120
|
+
timeoutMS: number = this.cfg.queryDefaults.timeoutMS,
|
|
121
|
+
): Promise<ModifyResults> {
|
|
122
|
+
let retry = 0;
|
|
123
|
+
let res: ModifyResults | undefined;
|
|
124
|
+
while (!res && retry < maxRetries) {
|
|
125
|
+
retry++;
|
|
126
|
+
try {
|
|
127
|
+
res = await this.buildAndExecuteUpdateOrInsert(queryBuilder, timeoutMS);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
this.cfg.logger.info('Caught problem while trying to update/insert : %d : %s ', retry, err);
|
|
130
|
+
await PromiseRatchet.wait(retry * 2000);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!res) {
|
|
135
|
+
throw new Error(`Failed to execute update after ${maxRetries} retries`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return res;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
public async executeQueryByName<Row>(
|
|
142
|
+
queryPath: string,
|
|
143
|
+
params?: object,
|
|
144
|
+
timeoutMS: number = this.cfg.queryDefaults.timeoutMS,
|
|
145
|
+
): Promise<Row[]> {
|
|
146
|
+
const builder = this.queryBuilder(queryPath).withParams(params);
|
|
147
|
+
const resp: Row[] = await this.buildAndExecute(builder, timeoutMS);
|
|
148
|
+
return resp;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
public async executeQueryByNameSingle<Row>(
|
|
152
|
+
queryPath: string,
|
|
153
|
+
params?: object,
|
|
154
|
+
timeoutMS: number = this.cfg.queryDefaults.timeoutMS,
|
|
155
|
+
): Promise<Row | null> {
|
|
156
|
+
const builder = this.queryBuilder(queryPath).withParams(params);
|
|
157
|
+
const resp = await this.buildAndExecute<Row>(builder, timeoutMS);
|
|
158
|
+
return resp.length === 1 ? resp[0] : null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
public async buildAndExecute<Row>(queryBuilder: QueryBuilder, timeoutMS: number = this.cfg.queryDefaults.timeoutMS): Promise<Row[]> {
|
|
162
|
+
const build = queryBuilder.build();
|
|
163
|
+
const resp = await this.executeQueryWithMeta<Row[]>(
|
|
164
|
+
DatabaseRequestType.Query,
|
|
165
|
+
build.transactionIsolationLevel,
|
|
166
|
+
build.query,
|
|
167
|
+
build.namedParams,
|
|
168
|
+
timeoutMS,
|
|
169
|
+
queryBuilder.getDebugComment(),
|
|
170
|
+
);
|
|
171
|
+
return resp.results;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
public async buildAndExecuteSingle<Row>(
|
|
175
|
+
queryBuilder: QueryBuilder,
|
|
176
|
+
timeoutMS: number = this.cfg.queryDefaults.timeoutMS,
|
|
177
|
+
): Promise<Row | null> {
|
|
178
|
+
const build = queryBuilder.build();
|
|
179
|
+
const resp = await this.executeQueryWithMeta<Row[]>(
|
|
180
|
+
DatabaseRequestType.Query,
|
|
181
|
+
build.transactionIsolationLevel,
|
|
182
|
+
build.query,
|
|
183
|
+
build.namedParams,
|
|
184
|
+
timeoutMS,
|
|
185
|
+
queryBuilder.getDebugComment(),
|
|
186
|
+
);
|
|
187
|
+
return resp.results.length === 1 ? resp.results[0] : null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
public async buildAndExecuteFetchTotalRows(
|
|
191
|
+
queryBuilder: QueryBuilder,
|
|
192
|
+
groupBy = '',
|
|
193
|
+
timeoutMS: number = this.cfg.queryDefaults.timeoutMS,
|
|
194
|
+
): Promise<GroupByCountResult[]> {
|
|
195
|
+
const buildUnfiltered = queryBuilder.buildUnfiltered();
|
|
196
|
+
let query = buildUnfiltered.query.replace('COUNT(*)', `${groupBy} as groupByField, COUNT(*) as count`);
|
|
197
|
+
query = `${query} GROUP BY ${groupBy}`;
|
|
198
|
+
|
|
199
|
+
this.cfg.logger.info('Unfiltered query %s', buildUnfiltered.query);
|
|
200
|
+
const resp = await this.executeQueryWithMeta<GroupByCountResult[]>(
|
|
201
|
+
DatabaseRequestType.Query,
|
|
202
|
+
buildUnfiltered.transactionIsolationLevel,
|
|
203
|
+
query,
|
|
204
|
+
buildUnfiltered.namedParams,
|
|
205
|
+
timeoutMS,
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
return resp.results;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
public async executeQueryWithMeta<Row>(
|
|
212
|
+
requestType: DatabaseRequestType,
|
|
213
|
+
transactionIsolationLevel: TransactionIsolationLevel,
|
|
214
|
+
query: string,
|
|
215
|
+
fields: object = {},
|
|
216
|
+
timeoutMS: number = this.cfg.queryDefaults.timeoutMS,
|
|
217
|
+
debugComment?: string,
|
|
218
|
+
): Promise<RequestResults<Row>> {
|
|
219
|
+
const sw: StopWatch = new StopWatch();
|
|
220
|
+
if (!timeoutMS) {
|
|
221
|
+
timeoutMS = this.cfg.queryDefaults.timeoutMS;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
await this.changeNextQueryTransactionIsolationLevel(transactionIsolationLevel);
|
|
225
|
+
|
|
226
|
+
const result = await PromiseRatchet.timeout<RequestResults<Row>>(
|
|
227
|
+
this.innerExecutePreparedAsPromiseWithRetryCloseConnection<Row>(requestType, query, fields, undefined),
|
|
228
|
+
'Query:' + query,
|
|
229
|
+
timeoutMS,
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
if (TimeoutToken.isTimeoutToken(result)) {
|
|
233
|
+
this.cfg.logger.warn('Timed out (after %s): %j', DurationRatchet.colonFormatMsDuration(timeoutMS), result);
|
|
234
|
+
const duration = DurationRatchet.colonFormatMsDuration(timeoutMS);
|
|
235
|
+
throw new Error(`Timed out (after ${duration}) waiting for query : ${query}`);
|
|
236
|
+
}
|
|
237
|
+
const rval = result as RequestResults<Row>;
|
|
238
|
+
if (!rval.results) {
|
|
239
|
+
this.cfg.logger.error('DB:executeQueryWithMeta:Failure: %j', rval);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (debugComment && sw.elapsedMS() > this.cfg.longQueryTimeMs) {
|
|
243
|
+
this.cfg.logger.info('NamedParameterDatabaseService long query: %s, %s', debugComment, sw.dump());
|
|
244
|
+
}
|
|
245
|
+
return rval;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
public async shutdown(): Promise<boolean> {
|
|
249
|
+
this.cfg.logger.info('Shutting down %s service', this.cfg.serviceName);
|
|
250
|
+
let rval: boolean;
|
|
251
|
+
try {
|
|
252
|
+
rval = await this.cfg.connectionProvider.clearDatabaseAccessCache();
|
|
253
|
+
} catch (err) {
|
|
254
|
+
this.cfg.logger.error('Failure trying to shutdown : %s', err, err);
|
|
255
|
+
rval = false;
|
|
256
|
+
}
|
|
257
|
+
return rval;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
public async testDbFailure(): Promise<void> {
|
|
261
|
+
// This just executes a query guaranteed to fail to generate a db query failure
|
|
262
|
+
await this.executeQueryWithMeta(DatabaseRequestType.Query, TransactionIsolationLevel.Default, 'this is a bad query');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
public async changeNextQueryTransactionIsolationLevel<Row>(tx: TransactionIsolationLevel | null): Promise<RequestResults<Row> | null> {
|
|
266
|
+
if (tx && tx !== TransactionIsolationLevel.Default) {
|
|
267
|
+
this.cfg.logger.debug('Setting tx to %s', tx);
|
|
268
|
+
return await this.innerExecutePreparedAsPromiseWithRetryCloseConnection(
|
|
269
|
+
DatabaseRequestType.Meta,
|
|
270
|
+
'SET TRANSACTION ISOLATION LEVEL ' + tx,
|
|
271
|
+
{},
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
public async forceCloseConnectionForTesting(): Promise<boolean> {
|
|
278
|
+
this.cfg.logger.warn('Forcing connection closed for testing');
|
|
279
|
+
const conn: DatabaseAccess = await this.getDB();
|
|
280
|
+
try {
|
|
281
|
+
await conn.close();
|
|
282
|
+
this.cfg.logger.info('Connection has been ended, but not set to null');
|
|
283
|
+
return true;
|
|
284
|
+
} catch (err) {
|
|
285
|
+
this.cfg.logger.error('Error closing connection : %s', err, err);
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private async innerExecutePreparedAsPromiseWithRetryCloseConnection<Row>(
|
|
291
|
+
requestType: DatabaseRequestType,
|
|
292
|
+
query: string,
|
|
293
|
+
fields: object = {},
|
|
294
|
+
retryCount = 1,
|
|
295
|
+
): Promise<RequestResults<Row>> {
|
|
296
|
+
try {
|
|
297
|
+
const result: RequestResults<Row> = await this.innerExecutePreparedAsPromise<Row>(requestType, query, fields);
|
|
298
|
+
return result;
|
|
299
|
+
} catch (errIn) {
|
|
300
|
+
const err: Error = ErrorRatchet.asErr(errIn);
|
|
301
|
+
if (
|
|
302
|
+
err.message.includes('closed state') ||
|
|
303
|
+
err.message.includes('This socket has been ended by the other party') ||
|
|
304
|
+
err.message.includes('ETIMEDOUT') ||
|
|
305
|
+
err.message.includes('RatchetNoConnection') ||
|
|
306
|
+
err.message.includes('ER_LOCK_WAIT_TIMEOUT')
|
|
307
|
+
) {
|
|
308
|
+
const wait: number = Math.min(1000 * retryCount);
|
|
309
|
+
this.cfg.logger.warn(
|
|
310
|
+
'Found closed connection or lock timeout - clearing and attempting retry after %d (try %d of 3) (%s)',
|
|
311
|
+
wait,
|
|
312
|
+
retryCount,
|
|
313
|
+
err.message,
|
|
314
|
+
);
|
|
315
|
+
if (retryCount < 4) {
|
|
316
|
+
const cleared: boolean = await this.cfg.connectionProvider.clearDatabaseAccessCache();
|
|
317
|
+
this.cfg.logger.info('Clear connection cache returned %s', cleared);
|
|
318
|
+
await PromiseRatchet.wait(wait);
|
|
319
|
+
return this.innerExecutePreparedAsPromiseWithRetryCloseConnection(requestType, query, fields, retryCount + 1);
|
|
320
|
+
} else {
|
|
321
|
+
this.cfg.logger.warn('Ran out of retries');
|
|
322
|
+
throw new Error('Connection closed and cannot retry any more - dying horribly');
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
this.cfg.logger.error('Named Param DB Query Failed : Err: %s Query: %s Params: %j', err, query, fields, err);
|
|
326
|
+
try {
|
|
327
|
+
const conn: DatabaseAccess = await this.getDB();
|
|
328
|
+
this.cfg.logger.error(
|
|
329
|
+
'-----\nFor paste into tooling only: \n\n%s\n\n',
|
|
330
|
+
QueryUtil.renderQueryStringForPasteIntoTool(query, fields, (v) => conn.escape(v)),
|
|
331
|
+
);
|
|
332
|
+
} catch (err2) {
|
|
333
|
+
this.cfg.logger.error('Really bad - failed trying to get the conn for logging : %s', err2);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Just rethrow anything else
|
|
337
|
+
throw err;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private async innerExecutePreparedAsPromise<Row>(
|
|
343
|
+
requestType: DatabaseRequestType,
|
|
344
|
+
query: string,
|
|
345
|
+
fields: object = {},
|
|
346
|
+
): Promise<RequestResults<Row>> {
|
|
347
|
+
const conn: DatabaseAccess = await this.getDB();
|
|
348
|
+
if (conn.preQuery) {
|
|
349
|
+
await conn.preQuery();
|
|
350
|
+
}
|
|
351
|
+
const sw: StopWatch = new StopWatch();
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
let output: RequestResults<Row | ModifyResults>;
|
|
355
|
+
if (requestType === DatabaseRequestType.Modify) {
|
|
356
|
+
this.cfg.logger.debug('Executing modify on underlying db : %s / %j', query, fields);
|
|
357
|
+
output = await conn.modify(query, fields);
|
|
358
|
+
} else {
|
|
359
|
+
this.cfg.logger.debug('Executing query on underlying db : %s / %j', query, fields);
|
|
360
|
+
output = await conn.query<Row>(query, fields);
|
|
361
|
+
}
|
|
362
|
+
// If we reached here we were ok
|
|
363
|
+
this.cfg.logger.debug(
|
|
364
|
+
'Success : Finished query : %s\n%s\n\nParams : %j',
|
|
365
|
+
sw.dump(),
|
|
366
|
+
QueryUtil.reformatQueryForLogging(query),
|
|
367
|
+
fields,
|
|
368
|
+
);
|
|
369
|
+
this.cfg.logger.debug(
|
|
370
|
+
'-----\nFor paste into tooling only : \n\n%s\n\n',
|
|
371
|
+
QueryUtil.renderQueryStringForPasteIntoTool(query, fields, (v) => conn.escape(v)),
|
|
372
|
+
);
|
|
373
|
+
if (conn.onRequestSuccessOnly) {
|
|
374
|
+
await conn.onRequestSuccessOnly(requestType);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return output as RequestResults<Row>;
|
|
378
|
+
} catch (err) {
|
|
379
|
+
if (conn.onRequestFailureOnly) {
|
|
380
|
+
await conn.onRequestFailureOnly(requestType);
|
|
381
|
+
}
|
|
382
|
+
throw err;
|
|
383
|
+
} finally {
|
|
384
|
+
if (conn.onRequestSuccessOrFailure) {
|
|
385
|
+
await conn.onRequestSuccessOrFailure(requestType);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Creates a promise if there isn't already a cached one or if it is closed
|
|
391
|
+
public async getDB(): Promise<DatabaseAccess> {
|
|
392
|
+
const conn: DatabaseAccess | undefined = this.nonPooledMode()
|
|
393
|
+
? await this.cfg.connectionProvider.createNonPooledDatabaseAccess(this.cfg.queryDefaults, this.nonPooledExtraConfiguration())
|
|
394
|
+
: await this.cfg.connectionProvider.getDatabaseAccess(this.cfg.queryDefaults.databaseName);
|
|
395
|
+
if (!conn) {
|
|
396
|
+
// If we just couldn't connect to the DB
|
|
397
|
+
throw new Error('RatchetNoConnection : getConnection returned null - likely failed to get connection from db');
|
|
398
|
+
}
|
|
399
|
+
return conn;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
public async testConnection(logTestResults?: boolean): Promise<number> {
|
|
403
|
+
const db: DatabaseAccess = await this.getDB();
|
|
404
|
+
return db.testConnection(logTestResults);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
public async resetConnection(): Promise<boolean> {
|
|
408
|
+
let rval = false;
|
|
409
|
+
this.cfg.logger.info('Resetting connection');
|
|
410
|
+
try {
|
|
411
|
+
await this.cfg.connectionProvider.clearDatabaseAccessCache();
|
|
412
|
+
const db: DatabaseAccess = await this.getDB();
|
|
413
|
+
const tmpValue = await db.testConnection(true);
|
|
414
|
+
rval = !!tmpValue;
|
|
415
|
+
this.cfg.logger.info('Reset connection returning %s', rval);
|
|
416
|
+
} catch (err) {
|
|
417
|
+
this.cfg.logger.error('Failed to reset connection : %s', err);
|
|
418
|
+
}
|
|
419
|
+
return rval;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as TunnelSsh from 'tunnel-ssh';
|
|
2
|
+
import { ForwardOptions, ServerOptions, TunnelOptions } from 'tunnel-ssh';
|
|
3
|
+
import { Logger } from '@bitblit/ratchet-common/logger/logger';
|
|
4
|
+
import { SshTunnelContainer } from '../model/ssh/ssh-tunnel-container.js';
|
|
5
|
+
import { SshTunnelConfig } from '../model/ssh/ssh-tunnel-config.js';
|
|
6
|
+
|
|
7
|
+
export class SshTunnelService {
|
|
8
|
+
public async shutdown(ssh: SshTunnelContainer): Promise<boolean> {
|
|
9
|
+
if (!ssh.connection) {
|
|
10
|
+
Logger.info('Not shutting down tunnel - non-tunnel passed');
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
Logger.info('Shutting down SSH Tunnel');
|
|
15
|
+
ssh.connection.end();
|
|
16
|
+
ssh.server.close();
|
|
17
|
+
return true;
|
|
18
|
+
} catch (err) {
|
|
19
|
+
Logger.error('Error closing ssh tunnel : %s', err);
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public async createSSHTunnel(
|
|
25
|
+
sshOptions: SshTunnelConfig,
|
|
26
|
+
dstHost: string,
|
|
27
|
+
dstPort: number,
|
|
28
|
+
localPort: number,
|
|
29
|
+
): Promise<SshTunnelContainer> {
|
|
30
|
+
const tunnelOptions: TunnelOptions = {
|
|
31
|
+
autoClose: true,
|
|
32
|
+
reconnectOnError: true,
|
|
33
|
+
};
|
|
34
|
+
const serverOptions: ServerOptions = {
|
|
35
|
+
port: localPort,
|
|
36
|
+
};
|
|
37
|
+
const forwardOptions: ForwardOptions = {
|
|
38
|
+
srcAddr: 'localhost', //'0.0.0.0',
|
|
39
|
+
srcPort: localPort,
|
|
40
|
+
dstAddr: dstHost,
|
|
41
|
+
dstPort: dstPort,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const [server, connection] = await TunnelSsh.createTunnel(tunnelOptions, serverOptions, sshOptions, forwardOptions);
|
|
45
|
+
|
|
46
|
+
server.on('error', (err: Error) => {
|
|
47
|
+
Logger.warn('SSH Server Error : %s', err);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const rval: SshTunnelContainer = {
|
|
51
|
+
localPort: localPort,
|
|
52
|
+
tunnelOptions: tunnelOptions,
|
|
53
|
+
serverOptions: serverOptions,
|
|
54
|
+
sshOptions: sshOptions,
|
|
55
|
+
forwardOptions: forwardOptions,
|
|
56
|
+
server: server,
|
|
57
|
+
connection: connection,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return rval;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { NamedParameterDatabaseService } from './named-parameter-database-service.js';
|
|
2
|
+
import { Logger } from '@bitblit/ratchet-common/logger/logger';
|
|
3
|
+
import { ErrorRatchet } from '@bitblit/ratchet-common/lang/error-ratchet';
|
|
4
|
+
import { StringRatchet } from '@bitblit/ratchet-common/lang/string-ratchet';
|
|
5
|
+
import { QueryBuilder } from '../query-builder/query-builder.js';
|
|
6
|
+
import { ModifyResults } from '../model/modify-results.js';
|
|
7
|
+
import { NamedParameterDatabaseServiceConfig } from '../model/named-parameter-database-service-config.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Extends NamedParameterDatabaseService to add transactional functionality
|
|
11
|
+
*
|
|
12
|
+
* Typical usage would look like:
|
|
13
|
+
*
|
|
14
|
+
* (assume db is a NamedParameterDatabaseService)
|
|
15
|
+
* const tx: TransactionalNamedParameterDatabaseService = await TransactionalNamedParameterDatabaseService.create(db);
|
|
16
|
+
* const result: ModifyResults = await tx.buildAndExecuteUpdateOrInsertInTransaction(queryBuilder);
|
|
17
|
+
* ...
|
|
18
|
+
* (best practice is to clean-up the connection after)
|
|
19
|
+
* await tx.cleanShutdown();
|
|
20
|
+
*
|
|
21
|
+
* If the user needs to execute multiple statements, they can by calling
|
|
22
|
+
* startTransaction
|
|
23
|
+
* exec
|
|
24
|
+
* exec
|
|
25
|
+
* commit/rollback
|
|
26
|
+
* --
|
|
27
|
+
* manually
|
|
28
|
+
*
|
|
29
|
+
* (Then the cleanShutdown)
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
export class TransactionalNamedParameterDatabaseService extends NamedParameterDatabaseService {
|
|
33
|
+
private currentTxFlag?: string;
|
|
34
|
+
|
|
35
|
+
constructor(
|
|
36
|
+
private transCfg: NamedParameterDatabaseServiceConfig,
|
|
37
|
+
private additionalConfig: Record<string, any>,
|
|
38
|
+
) {
|
|
39
|
+
super(transCfg);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public nonPooledExtraConfiguration(): Record<string, any> {
|
|
43
|
+
return this.additionalConfig;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public nonPooledMode(): boolean {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public async cleanShutdown(): Promise<void> {
|
|
51
|
+
Logger.info('cleanShutdown');
|
|
52
|
+
try {
|
|
53
|
+
const conn = await this.transCfg.connectionProvider.getDatabaseAccess();
|
|
54
|
+
if (conn) {
|
|
55
|
+
Logger.info('Shutting down connection');
|
|
56
|
+
await conn.close();
|
|
57
|
+
}
|
|
58
|
+
} catch (err) {
|
|
59
|
+
Logger.info('Failure shutting down single-use connection : %s', err);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public async startTransaction(): Promise<void> {
|
|
64
|
+
if (!this.currentTxFlag) {
|
|
65
|
+
this.currentTxFlag = StringRatchet.createRandomHexString(10);
|
|
66
|
+
Logger.info('Starting a transaction : %s', this.currentTxFlag);
|
|
67
|
+
const conn = await this.transCfg.connectionProvider.getDatabaseAccess();
|
|
68
|
+
await conn?.beginTransaction();
|
|
69
|
+
} else {
|
|
70
|
+
ErrorRatchet.throwFormattedErr('Tried to start a new transaction while one is already in progress : %s', this.currentTxFlag);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public async commitTransaction(): Promise<void> {
|
|
75
|
+
if (this.currentTxFlag) {
|
|
76
|
+
Logger.info('commit a transaction : %s', this.currentTxFlag);
|
|
77
|
+
const conn = await this.transCfg.connectionProvider.getDatabaseAccess();
|
|
78
|
+
await conn?.commitTransaction();
|
|
79
|
+
this.currentTxFlag = undefined;
|
|
80
|
+
} else {
|
|
81
|
+
ErrorRatchet.throwFormattedErr('Cannot commit transaction - none in process');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public async rollBackTransaction(): Promise<void> {
|
|
86
|
+
if (this.currentTxFlag) {
|
|
87
|
+
Logger.info('rollBack a transaction : %s', this.currentTxFlag);
|
|
88
|
+
const conn = await this.transCfg.connectionProvider.getDatabaseAccess();
|
|
89
|
+
await conn?.rollbackTransaction();
|
|
90
|
+
this.currentTxFlag = undefined;
|
|
91
|
+
} else {
|
|
92
|
+
ErrorRatchet.throwFormattedErr('Cannot rollBack transaction - none in process');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public async buildAndExecuteUpdateOrInsertInTransaction(
|
|
97
|
+
queryBuilder: QueryBuilder,
|
|
98
|
+
timeoutMS: number = this.transCfg.queryDefaults.timeoutMS,
|
|
99
|
+
): Promise<ModifyResults | null> {
|
|
100
|
+
Logger.info('buildAndExecuteUpdateOrInsertInTransaction');
|
|
101
|
+
await this.startTransaction();
|
|
102
|
+
try {
|
|
103
|
+
const rval = await this.buildAndExecuteUpdateOrInsert(queryBuilder, timeoutMS);
|
|
104
|
+
await this.commitTransaction();
|
|
105
|
+
return rval;
|
|
106
|
+
} catch (err) {
|
|
107
|
+
Logger.error('Failed - rolling back transaction : %s', err, err);
|
|
108
|
+
await this.rollBackTransaction();
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public async buildAndExecuteInTransaction<T>(
|
|
114
|
+
queryBuilder: QueryBuilder,
|
|
115
|
+
timeoutMS: number = this.transCfg.queryDefaults.timeoutMS,
|
|
116
|
+
): Promise<T[] | null> {
|
|
117
|
+
Logger.info('buildAndExecuteInTransaction');
|
|
118
|
+
await this.startTransaction();
|
|
119
|
+
try {
|
|
120
|
+
const rval = await this.buildAndExecute<T>(queryBuilder, timeoutMS);
|
|
121
|
+
await this.commitTransaction();
|
|
122
|
+
return rval;
|
|
123
|
+
} catch (err) {
|
|
124
|
+
Logger.error('Failed - rolling back transaction : %s', err, err);
|
|
125
|
+
await this.rollBackTransaction();
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
public static async oneStepBuildAndExecuteUpdateOrInsertInTransaction(
|
|
131
|
+
src: NamedParameterDatabaseService,
|
|
132
|
+
queryBuilder: QueryBuilder,
|
|
133
|
+
timeoutMS: number = src.getQueryDefaults().timeoutMS,
|
|
134
|
+
additionalConfig?: Record<string, any>,
|
|
135
|
+
): Promise<ModifyResults | null> {
|
|
136
|
+
let handler: TransactionalNamedParameterDatabaseService | undefined;
|
|
137
|
+
let rval: ModifyResults | null = null;
|
|
138
|
+
try {
|
|
139
|
+
handler = new TransactionalNamedParameterDatabaseService(src.getConfig(), additionalConfig);
|
|
140
|
+
rval = await handler.buildAndExecuteUpdateOrInsertInTransaction(queryBuilder, timeoutMS);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
Logger.error('Failure in oneStepBuildAndExecuteUpdateOrInsertInTransaction : %j : %s', queryBuilder, err, err);
|
|
143
|
+
} finally {
|
|
144
|
+
if (handler) {
|
|
145
|
+
await handler.cleanShutdown();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return rval;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
public static async oneStepBuildAndExecuteInTransaction<S, R>(
|
|
152
|
+
src: NamedParameterDatabaseService,
|
|
153
|
+
queryBuilder: QueryBuilder,
|
|
154
|
+
timeoutMS: number = src.getQueryDefaults().timeoutMS,
|
|
155
|
+
additionalConfig?: R,
|
|
156
|
+
): Promise<S[] | null> {
|
|
157
|
+
let handler: TransactionalNamedParameterDatabaseService | undefined;
|
|
158
|
+
let rval: S[] | null = null;
|
|
159
|
+
try {
|
|
160
|
+
handler = new TransactionalNamedParameterDatabaseService(src.getConfig(), additionalConfig);
|
|
161
|
+
rval = await handler.buildAndExecuteInTransaction(queryBuilder, timeoutMS);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
Logger.error('Failure in oneStepbuildAndExecuteInTransaction : %j : %s', queryBuilder, err, err);
|
|
164
|
+
} finally {
|
|
165
|
+
if (handler) {
|
|
166
|
+
await handler.cleanShutdown();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return rval;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { SqliteRemoteFileSyncConfig } from './sqlite-remote-file-sync-config.js';
|
|
2
|
+
import { SqliteConnectionConfigFlag } from './sqlite-connection-config-flag.js';
|
|
3
|
+
import { SqliteLocalFileConfig } from './sqlite-local-file-config.js';
|
|
4
|
+
|
|
5
|
+
// If neither localFile nor remoteFileSync are provided, a memory database is used
|
|
6
|
+
export interface SqliteConnectionConfig {
|
|
7
|
+
label: string;
|
|
8
|
+
localFile?: SqliteLocalFileConfig;
|
|
9
|
+
remoteFileSync?: SqliteRemoteFileSyncConfig;
|
|
10
|
+
flags?: SqliteConnectionConfigFlag[];
|
|
11
|
+
}
|