@bitblit/ratchet-rdbms 6.0.146-alpha → 6.0.147-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.
Files changed (56) hide show
  1. package/package.json +4 -3
  2. package/src/build/ratchet-rdbms-info.ts +19 -0
  3. package/src/model/connection-and-tunnel.ts +7 -0
  4. package/src/model/database-access-provider.ts +11 -0
  5. package/src/model/database-access.ts +30 -0
  6. package/src/model/database-config-list.ts +3 -0
  7. package/src/model/database-request-type.ts +6 -0
  8. package/src/model/group-by-count-result.ts +4 -0
  9. package/src/model/modify-results.ts +9 -0
  10. package/src/model/named-parameter-database-service-config.ts +13 -0
  11. package/src/model/paginated-results.ts +5 -0
  12. package/src/model/pagination-bounds.ts +12 -0
  13. package/src/model/paginator.ts +20 -0
  14. package/src/model/query-defaults.ts +4 -0
  15. package/src/model/query-text-provider.ts +4 -0
  16. package/src/model/request-results.ts +4 -0
  17. package/src/model/simple-query-text-provider.ts +18 -0
  18. package/src/model/sort-direction.ts +4 -0
  19. package/src/model/ssh/ssh-tunnel-config.ts +8 -0
  20. package/src/model/ssh/ssh-tunnel-container.ts +13 -0
  21. package/src/model/transaction-isolation-level.ts +4 -0
  22. package/src/mysql/model/mysql-db-config.ts +14 -0
  23. package/src/mysql/model/mysql-master-status.ts +6 -0
  24. package/src/mysql/model/mysql-slave-status.ts +52 -0
  25. package/src/mysql/mysql-style-database-access.ts +85 -0
  26. package/src/mysql/rds-mysql-style-connection-provider.ts +265 -0
  27. package/src/postgres/model/postgres-db-config.ts +8 -0
  28. package/src/postgres/postgres-style-connection-provider.ts +270 -0
  29. package/src/postgres/postgres-style-database-access.spec.ts +76 -0
  30. package/src/postgres/postgres-style-database-access.ts +110 -0
  31. package/src/query-builder/query-builder-result.ts +21 -0
  32. package/src/query-builder/query-builder.spec.ts +194 -0
  33. package/src/query-builder/query-builder.ts +445 -0
  34. package/src/query-builder/query-util.spec.ts +20 -0
  35. package/src/query-builder/query-util.ts +162 -0
  36. package/src/rds-data-api/model/rds-data-api-connection-config.ts +8 -0
  37. package/src/rds-data-api/rds-data-api-connection-provider.ts +39 -0
  38. package/src/rds-data-api/rds-data-api-database-access.spec.ts +139 -0
  39. package/src/rds-data-api/rds-data-api-database-access.ts +209 -0
  40. package/src/service/named-parameter-database-service.ts +421 -0
  41. package/src/service/ssh-tunnel-service.ts +62 -0
  42. package/src/service/transactional-named-parameter-database-service.ts +171 -0
  43. package/src/sqlite/model/fetch-remote-mode.ts +4 -0
  44. package/src/sqlite/model/flush-remote-mode.ts +4 -0
  45. package/src/sqlite/model/sqlite-connection-config-flag.ts +3 -0
  46. package/src/sqlite/model/sqlite-connection-config.ts +11 -0
  47. package/src/sqlite/model/sqlite-local-file-config.ts +3 -0
  48. package/src/sqlite/model/sqlite-remote-file-sync-config.ts +9 -0
  49. package/src/sqlite/sqlite-database-access.spec.ts +158 -0
  50. package/src/sqlite/sqlite-database-access.ts +126 -0
  51. package/src/sqlite/sqlite-remote-sync-database-access.ts +152 -0
  52. package/src/sqlite/sqlite-style-connection-provider.ts +181 -0
  53. package/src/util/aws-rds-cert-2023.ts +502 -0
  54. package/src/util/named-parameter-adapter/named-parameter-adapter.ts +51 -0
  55. package/src/util/named-parameter-adapter/query-and-params.ts +4 -0
  56. package/src/util/relational-database-utils.ts +54 -0
@@ -0,0 +1,445 @@
1
+ import { RequireRatchet } from '@bitblit/ratchet-common/lang/require-ratchet';
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
+
6
+ import { QueryBuilderResult } from './query-builder-result.js';
7
+ import { TransactionIsolationLevel } from '../model/transaction-isolation-level.js';
8
+ import { QueryTextProvider } from '../model/query-text-provider.js';
9
+ import { Paginator } from '../model/paginator.js';
10
+ import { SortDirection } from '../model/sort-direction.js';
11
+
12
+ export class QueryBuilder {
13
+ public static readonly ALLOWED_SQL_CONSTRUCT: RegExp = /^[a-z0-9_.`]+$/i;
14
+ private readonly queryProvider: QueryTextProvider;
15
+
16
+ private query?: string;
17
+
18
+ // Meta data
19
+ public meta: { queryPath?: string } = Object.freeze({});
20
+
21
+ private sqlConstructs: Record<string, unknown> = {};
22
+ private namedParams: Record<string, unknown> = {};
23
+ private conditionals: Record<string, unknown> = {};
24
+
25
+ private debugComment = '';
26
+
27
+ private paginator?: Paginator<any>;
28
+
29
+ private debugAnnotateMode = false;
30
+ private transactionIsolationLevel: TransactionIsolationLevel = TransactionIsolationLevel.Default;
31
+
32
+ constructor(queryProvider: QueryTextProvider) {
33
+ this.queryProvider = queryProvider;
34
+ }
35
+
36
+ public clone(): QueryBuilder {
37
+ const clone: QueryBuilder = new QueryBuilder(this.queryProvider);
38
+ if (this.query) {
39
+ clone.withBaseQuery(this.query);
40
+ }
41
+ clone.sqlConstructs = structuredClone(this.sqlConstructs);
42
+ clone.namedParams = structuredClone(this.namedParams);
43
+ clone.conditionals = structuredClone(this.conditionals);
44
+ clone.paginator = structuredClone(this.paginator);
45
+ clone.debugComment = this.debugComment;
46
+ clone.transactionIsolationLevel = this.transactionIsolationLevel;
47
+ return clone;
48
+ }
49
+
50
+ public withTransactionIsolationLevel(level: TransactionIsolationLevel): QueryBuilder {
51
+ this.transactionIsolationLevel = level;
52
+ return this;
53
+ }
54
+
55
+ public withDebugComment(comment: string): QueryBuilder {
56
+ this.debugComment = comment;
57
+ return this;
58
+ }
59
+
60
+ public appendDebugComment(comment: string): QueryBuilder {
61
+ this.debugComment = this.debugComment + comment;
62
+ return this;
63
+ }
64
+
65
+ public withNamedQuery(queryPath: string): QueryBuilder {
66
+ this.query = this.queryProvider.fetchQuery(queryPath);
67
+ if (!StringRatchet.trimToNull(this.query)) {
68
+ ErrorRatchet.throwFormattedErr('Requested query that does not exist : %s', queryPath);
69
+ }
70
+ this.meta = Object.freeze({ queryPath: queryPath });
71
+ this.withDebugComment(' ' + queryPath + ' ');
72
+ return this;
73
+ }
74
+
75
+ public withBaseQuery(baseQuery: string): void {
76
+ this.query = baseQuery;
77
+ }
78
+
79
+ public withSqlConstruct(key: string, value: unknown): QueryBuilder {
80
+ this.sqlConstructs[key] = value;
81
+ return this;
82
+ }
83
+
84
+ public withSqlConstructs(params: Record<string, unknown>): QueryBuilder {
85
+ this.sqlConstructs = Object.assign(this.sqlConstructs, params);
86
+ return this;
87
+ }
88
+
89
+ public removeParam(key: string): QueryBuilder {
90
+ //eslint-disable-next-line @typescript-eslint/no-dynamic-delete
91
+ delete this.namedParams[key];
92
+ return this;
93
+ }
94
+
95
+ public paramNames(): string[] {
96
+ return Object.keys(this.namedParams);
97
+ }
98
+
99
+ public withParam(key: string, value: unknown): QueryBuilder {
100
+ this.namedParams[key] = value;
101
+ return this;
102
+ }
103
+
104
+ public withParams(params: unknown): QueryBuilder {
105
+ this.namedParams = Object.assign(this.namedParams, params ?? {});
106
+ return this;
107
+ }
108
+
109
+ public withExpandedParam(keyPrefix: string, values: unknown[], extendIfExists: boolean): QueryBuilder {
110
+ const lengthParamName: string = keyPrefix + 'Length';
111
+ let oldSize: number = this.fetchCopyOfParam<number>(lengthParamName) ?? 0;
112
+ if (oldSize > 0 && !extendIfExists) {
113
+ Logger.silly('Old item found and not extending - removing old params');
114
+ const toRemove: string[] = this.paramNames().filter((s) => s.startsWith(keyPrefix));
115
+ toRemove.forEach((s) => this.removeParam(s));
116
+ oldSize = 0;
117
+ }
118
+
119
+ this.withParam(lengthParamName, values.length + oldSize);
120
+ for (let i = 0; i < values.length; i++) {
121
+ const value = values[i];
122
+
123
+ if (typeof value === 'object' && !!value) {
124
+ for (const key of Object.keys(value)) {
125
+ const paramKey = keyPrefix + key.charAt(0).toUpperCase() + key.slice(1) + (i + oldSize);
126
+ this.withParam(paramKey, value[key as keyof typeof value]);
127
+ }
128
+ } else {
129
+ const paramKey = keyPrefix + i;
130
+ this.withParam(paramKey, value);
131
+ }
132
+ }
133
+ return this;
134
+ }
135
+
136
+ public withConditional(tag: string, state = true): QueryBuilder {
137
+ this.conditionals[tag] = state;
138
+ return this;
139
+ }
140
+
141
+ public withConditionals(params: Record<string, boolean>): QueryBuilder {
142
+ this.conditionals = Object.assign(this.conditionals, params);
143
+ return this;
144
+ }
145
+
146
+ public withPaginator(paginator: Paginator<any>): QueryBuilder {
147
+ RequireRatchet.notNullOrUndefined(paginator, 'paginator');
148
+ RequireRatchet.notNullOrUndefined(paginator.cn, 'paginator.cn');
149
+ RequireRatchet.true(paginator.min || paginator.max || paginator.l, 'paginator must have some limit');
150
+ paginator.s = paginator.s ?? SortDirection.Asc; // Default to asc sort
151
+
152
+ this.paginator = paginator;
153
+ return this;
154
+ }
155
+
156
+ public fetchCopyOfParam<T>(paramName: string): T | undefined {
157
+ return this.namedParams[paramName] as T | undefined;
158
+ }
159
+
160
+ public fetchCopyOfConditional<T>(conditionalName: string): T | undefined {
161
+ return this.conditionals[conditionalName] as T | undefined;
162
+ }
163
+
164
+ public containsParam(paramName: string): boolean {
165
+ return this.namedParams[paramName] != undefined;
166
+ }
167
+
168
+ public getDebugComment(): string {
169
+ return this.debugComment;
170
+ }
171
+
172
+ public containsConditional(conditionalName: string): boolean {
173
+ return this.conditionals[conditionalName] != undefined;
174
+ }
175
+
176
+ public build(): QueryBuilderResult {
177
+ const build = this.clone();
178
+ return build.internalBuild(false);
179
+ }
180
+
181
+ public buildUnfiltered(): QueryBuilderResult {
182
+ const builder: QueryBuilder = this.clone();
183
+ return builder.internalBuild(true);
184
+ }
185
+
186
+ protected internalBuild(unfiltered: boolean): QueryBuilderResult {
187
+ this.applyQueryFragments();
188
+ this.applyConditionalBlocks();
189
+ this.applyRepeatBlocks();
190
+ this.applyPagination(unfiltered);
191
+ this.applySqlConstructs();
192
+ this.applyComments();
193
+ this.runQueryChecks();
194
+ this.stripNonAsciiParams();
195
+
196
+ return new QueryBuilderResult((this.query ?? '').trim(), this.namedParams, this.paginator, this.transactionIsolationLevel);
197
+ }
198
+
199
+ private stripNonAsciiParams(): void {
200
+ const reduced = StringRatchet.stripNonAscii(JSON.stringify(this.namedParams));
201
+ this.namedParams = JSON.parse(reduced) as Record<string, unknown>;
202
+ }
203
+
204
+ private runQueryChecks(): void {
205
+ const quotedNamedParams = [...(this.query?.matchAll(/['"]:[A-z-]*['"]/gm) ?? [])];
206
+ if (quotedNamedParams.length > 0) {
207
+ Logger.warn(
208
+ 'The resulting query contains quoted named params check this this is intended. Instances found: %s',
209
+ quotedNamedParams.join(', '),
210
+ );
211
+ }
212
+ }
213
+
214
+ private applyComments(): void {
215
+ if (this.debugComment.length && this.query) {
216
+ const firstSpaceIndex = this.query.indexOf(' ');
217
+ //const requestId = ContextUtil.currentRequestId(); TODO: Reimplement?
218
+ const comment = this.debugComment; // + (requestId ? ` ${requestId}` : '');
219
+ this.query = this.query.substring(0, firstSpaceIndex + 1) + `/*${comment}*/` + this.query.substring(firstSpaceIndex + 1);
220
+ }
221
+ }
222
+
223
+ public applySqlConstructs(): void {
224
+ for (const key of Object.keys(this.sqlConstructs)) {
225
+ let value: string;
226
+ const val = this.sqlConstructs[key];
227
+ // If an array, check each individually, then join with comma
228
+ if (Array.isArray(val)) {
229
+ val.forEach((v: unknown) => {
230
+ if (typeof v !== 'string' || !v.match(QueryBuilder.ALLOWED_SQL_CONSTRUCT)) {
231
+ throw new Error(`sql construct entry ${v as string} is invalid value must be alphanumeric only.`);
232
+ }
233
+ });
234
+ value = val.join(', ');
235
+ } else {
236
+ value = StringRatchet.safeString(val);
237
+ if (value.length > 0 && !value.match(QueryBuilder.ALLOWED_SQL_CONSTRUCT)) {
238
+ throw new Error(`sql construct ${value} is invalid value must be alphanumeric only.`);
239
+ }
240
+ }
241
+
242
+ const sqlReservedWords: string[] = ['update', 'insert', 'delete', 'drop', 'select'];
243
+ for (const word of sqlReservedWords) {
244
+ if (value.toLowerCase().includes(word)) {
245
+ throw new Error(`sql construct ${value} is invalid value must not contain reserved word ${word}.`);
246
+ }
247
+ }
248
+
249
+ const rawKey = `##sqlConstruct:${key}##`;
250
+ while (this.query?.includes(rawKey)) {
251
+ this.query = this.query.replace(rawKey, value);
252
+ }
253
+ }
254
+ }
255
+
256
+ private applyRepeatBlocks(): void {
257
+ const startSymbol = '<repeat';
258
+ const endSymbol = '>';
259
+
260
+ while (true) {
261
+ const startIndex = this.query?.indexOf(startSymbol);
262
+ if (startIndex === -1 || !this.query || typeof startIndex !== 'number') {
263
+ return;
264
+ }
265
+
266
+ const endIndex = this.query.indexOf(endSymbol, startIndex);
267
+ if (endIndex == -1) {
268
+ throw new Error(
269
+ `Invalid query when finding end symbol matching ${endSymbol} in ${this.query} check that you have closed all your tags correctly.`,
270
+ );
271
+ }
272
+
273
+ const content = this.query.substring(startIndex + startSymbol.length, endIndex).trim();
274
+
275
+ const countSymbol = 'count=';
276
+ let countParam = '';
277
+
278
+ const joinSymbol = 'join=';
279
+ let joinString: string | undefined;
280
+
281
+ const params = content.split(' ');
282
+ for (const param of params) {
283
+ if (param.includes(countSymbol)) {
284
+ countParam = param.substring(param.indexOf(countSymbol) + countSymbol.length);
285
+ }
286
+
287
+ if (param.includes(joinSymbol)) {
288
+ joinString = param.substring(param.indexOf(joinSymbol) + joinSymbol.length);
289
+ }
290
+ }
291
+
292
+ const endTag = `</repeat>`;
293
+ const endTagIndex = this.query.indexOf(endTag);
294
+
295
+ const repeatedContent: string = this.query.substring(endIndex + endSymbol.length, endTagIndex);
296
+
297
+ this.query = this.query.substring(0, startIndex) + this.query.substring(endTagIndex + endTag.length);
298
+
299
+ const count = this.namedParams[countParam.substring(1)] as number;
300
+ for (let i = 0; i < count; i++) {
301
+ let indexedContent = repeatedContent;
302
+ if (joinString && i != 0) {
303
+ indexedContent += joinString;
304
+ }
305
+
306
+ let startParamTagIndex = indexedContent.indexOf(`::`);
307
+ while (startParamTagIndex != -1) {
308
+ const endParamTagIndex = indexedContent.indexOf(`::`, startParamTagIndex + 2);
309
+
310
+ if (endParamTagIndex == -1) {
311
+ throw new Error(
312
+ `Invalid query when finding end symbol matching :: check that you have closed all your tags correctly. Query: ${this.query} `,
313
+ );
314
+ }
315
+
316
+ const param = indexedContent.substring(startParamTagIndex + 2, endParamTagIndex);
317
+ indexedContent = indexedContent.replace('::' + param + '::', ':' + param + i);
318
+
319
+ startParamTagIndex = indexedContent.indexOf(`::`);
320
+ }
321
+
322
+ this.query = this.query.substring(0, startIndex) + indexedContent + this.query.substring(startIndex);
323
+ }
324
+ }
325
+ }
326
+
327
+ private applyQueryFragments(): void {
328
+ const startSymbol = '[[';
329
+ const endSymbol = ']]';
330
+
331
+ while (true) {
332
+ const startIndex = this.query?.indexOf(startSymbol);
333
+ if (startIndex == -1 || !this.query || typeof startIndex !== 'number') {
334
+ return;
335
+ }
336
+
337
+ const endIndex = this.query.indexOf(endSymbol, startIndex);
338
+ if (endIndex == -1) {
339
+ throw new Error(
340
+ `Invalid query when finding end symbol matching ${endSymbol} in ${this.query} check that you have closed all your tags correctly.`,
341
+ );
342
+ }
343
+
344
+ const rawName = this.query.substring(startIndex + startSymbol.length, endIndex);
345
+ const namedQueryElement = this.queryProvider.fetchQuery(rawName);
346
+ if (!namedQueryElement) {
347
+ throw new Error(`Invalid query, query fragment ${rawName} not found in named queries.`);
348
+ }
349
+ this.query = this.query.replace(`[[${rawName}]]`, namedQueryElement);
350
+ }
351
+ }
352
+
353
+ private applyPagination(unfiltered: boolean): void {
354
+ const paginationRawKey = '##pagination##';
355
+
356
+ if (!unfiltered && this.paginator) {
357
+ const sortDirEnum: SortDirection = this.paginator.s == SortDirection.Desc ? SortDirection.Desc : SortDirection.Asc;
358
+ const sortDir: string = StringRatchet.safeString(sortDirEnum);
359
+
360
+ if (this.paginator.min || this.paginator.max) {
361
+ let wc: string = 'WHERE ##sqlConstruct:queryBuilderPaginatorWhere##';
362
+ this.withSqlConstruct('queryBuilderPaginatorWhere', this.paginator.cn);
363
+ if (this.paginator.min) {
364
+ wc += '>= :queryBuilderPaginatorWhereMin';
365
+ this.withParam('queryBuilderPaginatorWhereMin', this.paginator.min);
366
+ }
367
+ if (this.paginator.max) {
368
+ if (this.paginator.min) {
369
+ wc += ' AND ##sqlConstruct:queryBuilderPaginatorWhere##';
370
+ }
371
+ wc += '< :queryBuilderPaginatorWhereMax';
372
+ this.withParam('queryBuilderPaginatorWhereMax', this.paginator.max);
373
+ }
374
+ this.query += wc;
375
+ }
376
+
377
+ this.query += ` ORDER BY ##sqlConstruct:queryBuilderOrderBy## ${sortDir}`;
378
+ this.withSqlConstruct('queryBuilderOrderBy', this.paginator.cn);
379
+
380
+ if (this.paginator.l) {
381
+ this.query += ' LIMIT :queryBuilderLimit';
382
+ this.withParam('queryBuilderLimit', this.paginator.l);
383
+ }
384
+ }
385
+
386
+ if (unfiltered && this.query) {
387
+ const paginationSplitIndex = this.query.indexOf(paginationRawKey);
388
+ if (paginationSplitIndex != -1) {
389
+ this.query = 'SELECT COUNT(*) ' + this.query.substring(paginationSplitIndex + paginationRawKey.length);
390
+ }
391
+ }
392
+
393
+ while (this.query?.includes(paginationRawKey)) {
394
+ this.query = this.query.replace(paginationRawKey, '');
395
+ }
396
+ }
397
+
398
+ private applyConditionalBlocks(): void {
399
+ const startSymbol = '<<';
400
+ const endSymbol = '>>';
401
+
402
+ while (true) {
403
+ const startIndex = this.query?.indexOf(startSymbol);
404
+ if (startIndex == -1 || !this.query || typeof startIndex !== 'number') {
405
+ return;
406
+ }
407
+
408
+ const endIndex = this.query.indexOf(endSymbol, startIndex);
409
+ if (endIndex == -1) {
410
+ throw new Error(
411
+ `Invalid query when finding end symbol matching ${endSymbol} in ${this.query} check that you have closed all your tags correctly.`,
412
+ );
413
+ }
414
+
415
+ const rawTag = this.query.substring(startIndex + startSymbol.length, endIndex);
416
+ const tag = rawTag.replace(':', '');
417
+ const endTag = `<</${rawTag}>>`;
418
+ const endTagIndex = this.query.indexOf(endTag);
419
+ if (endTagIndex == -1) {
420
+ throw new Error(
421
+ `Invalid query when finding conditional end tag matching ${endTag} in ${this.query} check that your query contains an exact match of this tag.`,
422
+ );
423
+ }
424
+
425
+ let replacement = this.query.substring(endIndex + endSymbol.length, endTagIndex);
426
+ if (rawTag.startsWith(':')) {
427
+ const param = this.namedParams[tag];
428
+ if (param == null || (Array.isArray(param) && param.length == 0)) {
429
+ replacement = '';
430
+ }
431
+ } else {
432
+ const conditional = this.conditionals[tag.replace('!', '')];
433
+ if (conditional == undefined || conditional == tag.startsWith('!')) {
434
+ replacement = '';
435
+ }
436
+ }
437
+
438
+ if (this.debugAnnotateMode) {
439
+ replacement = '/* conditional ' + tag + '*/';
440
+ }
441
+
442
+ this.query = this.query.substring(0, startIndex) + replacement + this.query.substring(endTagIndex + endTag.length);
443
+ }
444
+ }
445
+ }
@@ -0,0 +1,20 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { QueryUtil } from './query-util.js';
3
+
4
+ describe('query-util', () => {
5
+ test('extract used params', () => {
6
+ const test: string =
7
+ 'REPLACE /* TicketDao.saveDeliveryInformation */INTO delivery_information (delivery_information_guid, contact_phone, street_1, street_2, city, state, zip) VALUES (:deliveryInformationGuid, :contactPhone, :street1, :street2, :city, :state, :zip)';
8
+
9
+ const used: string[] = QueryUtil.extractUsedNamedParams(test);
10
+
11
+ expect(used.length).toBe(7);
12
+ expect(used.includes(':deliveryInformationGuid')).toBeTruthy;
13
+ expect(used.includes(':contactPhone')).toBeTruthy;
14
+ expect(used.includes(':street1')).toBeTruthy;
15
+ expect(used.includes(':street2')).toBeTruthy;
16
+ expect(used.includes(':city')).toBeTruthy;
17
+ expect(used.includes(':state')).toBeTruthy;
18
+ expect(used.includes(':zip')).toBeTruthy;
19
+ });
20
+ });
@@ -0,0 +1,162 @@
1
+ import { Logger } from '@bitblit/ratchet-common/logger/logger';
2
+ import { ErrorRatchet } from '@bitblit/ratchet-common/lang/error-ratchet';
3
+ import { StringRatchet } from '@bitblit/ratchet-common/lang/string-ratchet';
4
+
5
+ export class QueryUtil {
6
+ private fields: string[] = [];
7
+ private replacements: Record<string, unknown> = {};
8
+
9
+ public addReplacement(replacement: Record<string, unknown>, ...fields: string[]): void {
10
+ this.replacements = Object.assign(this.replacements, replacement);
11
+ this.addFields(...fields);
12
+ }
13
+
14
+ public appendReplacement(replacementKey: string, appendValue: string, ...fields: string[]): void {
15
+ this.replacements[replacementKey] = this.replacements[replacementKey] + appendValue;
16
+ this.addFields(...fields);
17
+ }
18
+
19
+ public addFields(...fields: string[]): void {
20
+ this.fields = this.fields.concat(...fields);
21
+ }
22
+
23
+ public getFields(): string[] {
24
+ return this.fields;
25
+ }
26
+
27
+ public getReplacements(): Record<string, unknown> {
28
+ return this.replacements;
29
+ }
30
+
31
+ public static sqlInjectionUnsafeParamRenderer(value: unknown): string {
32
+ const rFn = (val: unknown): string => (typeof val === 'string' ? '"' + val + '"' : StringRatchet.safeString(val));
33
+ const repl: string = Array.isArray(value) ? value.map((s: unknown) => rFn(s)).join(',') : rFn(value);
34
+ return repl;
35
+ }
36
+
37
+ public static renderQueryStringForPasteIntoTool(
38
+ query: string,
39
+ inFields: object | null,
40
+ transform: (x: unknown) => string = QueryUtil.sqlInjectionUnsafeParamRenderer,
41
+ ): string | null {
42
+ const fields = inFields ?? {};
43
+ // This is not safe from sql injection at all so it really should only be used for
44
+ let rval = QueryUtil.reformatQueryForLogging(query);
45
+ if (rval) {
46
+ const keys = Object.keys(fields);
47
+ // Sort longest first so that it replaces correctly
48
+ keys.sort((b, a) => a.length - b.length);
49
+
50
+ for (const key of keys) {
51
+ const val: unknown = fields[key as keyof typeof fields];
52
+ const find: string = ':' + key;
53
+ const repl: string = transform(val);
54
+ rval = rval.split(find).join(repl);
55
+ }
56
+ if (!rval.endsWith(';')) {
57
+ rval += ';';
58
+ }
59
+ }
60
+ return rval;
61
+ }
62
+
63
+ public static reformatQueryForLogging(qry: string, inMaxLineLength = 80): string | null {
64
+ let maxLineLength: number = inMaxLineLength;
65
+ if (!StringRatchet.trimToNull(qry)) {
66
+ return null;
67
+ }
68
+ let loggableQuery = '';
69
+ // First, remove any built in CR/LF returns
70
+ let cleaned: string = StringRatchet.trimToEmpty(qry).split('\n').join(' ').split('\r').join(' ');
71
+ while (cleaned.length > maxLineLength) {
72
+ let idx: number = Math.min(cleaned.length, maxLineLength);
73
+ // Scan back until we hit a space or a comma
74
+ while (idx > 0 && ![' ', ','].includes(cleaned.charAt(idx))) {
75
+ idx--;
76
+ }
77
+ if (idx > 0) {
78
+ loggableQuery += cleaned.substring(0, idx) + '\n';
79
+ cleaned = StringRatchet.trimToEmpty(cleaned.substring(idx));
80
+ } else {
81
+ Logger.silly('Input contains a string longer than the max line length - bumping');
82
+ maxLineLength += 2;
83
+ }
84
+ }
85
+ return loggableQuery + (cleaned.length > 0 ? cleaned : '');
86
+ }
87
+
88
+ // Used by dbs like sqlite3 that want the prefix in the supplied record
89
+ public static addPrefixToFieldNames(fields: Record<string, any>, prefix: string = ':'): Record<string, any> {
90
+ const rval: Record<string, any> = {};
91
+ Object.keys(fields).forEach((k) => {
92
+ rval[prefix + k] = fields[k];
93
+ });
94
+ return rval;
95
+ }
96
+
97
+ // If any supplied fields are null/undefined, replaces their variable in the source query
98
+ // Needed by dbs like sqlite3 that dont handle null injection well
99
+ public static replaceNullReplacementsInQuery(query: string, fields: Record<string, any>): string {
100
+ const rval: string = query;
101
+ Object.keys(fields).forEach((k) => {
102
+ if (fields[k] === null || fields[k] === undefined) {
103
+ rval.replaceAll(k, 'null');
104
+ }
105
+ });
106
+ return rval;
107
+ }
108
+
109
+ // Return a record that only contains fields that are actually used in the query
110
+ public static removeUnusedFields(query: string, fields: Record<string, any>, prefix?: string): Record<string, any> {
111
+ const usedFields: string[] = QueryUtil.extractUsedNamedParams(query);
112
+ const rval: Record<string, any> = {};
113
+ Object.keys(fields).forEach((k) => {
114
+ if (usedFields.includes(k) || (prefix && usedFields.includes(prefix + k))) {
115
+ rval[k] = fields[k];
116
+ }
117
+ });
118
+ return rval;
119
+ }
120
+
121
+ public static extractUsedNamedParams(query: string): string[] {
122
+ // TODO: Cmon, this really should be a regex...
123
+ //const usedParams: string[] = [...query.matchAll(/:[a-z0-9]+/i)].map((s) => StringRatchet.safeString(s));
124
+ let state: number = 0;
125
+ let idx: number = 0;
126
+ const frags: string[] = []; // debug only
127
+ const usedParams: string[] = [];
128
+ let curString: string = '';
129
+ while (idx < query.length) {
130
+ const nextChar: string = query.charAt(idx++);
131
+ if (state === 0) {
132
+ // Not in a var
133
+ if (nextChar === ':') {
134
+ frags.push(curString);
135
+ curString = ':';
136
+ state = 1;
137
+ } else {
138
+ curString += nextChar;
139
+ }
140
+ } else if (state === 1) {
141
+ // In a var
142
+ if (!StringRatchet.stringContainsOnlyAlphanumeric(nextChar)) {
143
+ usedParams.push(curString);
144
+ curString = nextChar;
145
+ state = 0;
146
+ } else {
147
+ curString += nextChar;
148
+ }
149
+ } else {
150
+ throw ErrorRatchet.fErr('Cant happen - invalid state');
151
+ }
152
+ }
153
+ // Whatever was left
154
+ if (state === 0) {
155
+ frags.push(curString);
156
+ } else {
157
+ usedParams.push(curString);
158
+ }
159
+
160
+ return usedParams;
161
+ }
162
+ }
@@ -0,0 +1,8 @@
1
+ export interface RdsDataApiConnectionConfig {
2
+ label: string;
3
+ resourceArn: string;
4
+ secretArn: string;
5
+ database: string;
6
+ maximumWaitForDbResumeInMillis?: number;
7
+ dbResumePingTimeMillis?: number;
8
+ }
@@ -0,0 +1,39 @@
1
+ import { DatabaseAccessProvider } from "../model/database-access-provider.js";
2
+ import { DatabaseAccess } from "../model/database-access.js";
3
+ import { QueryDefaults } from "../model/query-defaults.ts";
4
+ import { RDSDataClient } from "@aws-sdk/client-rds-data";
5
+ import { RdsDataApiConnectionConfig } from "./model/rds-data-api-connection-config.ts";
6
+ import { RdsDataApiDatabaseAccess } from "./rds-data-api-database-access.ts";
7
+ import { DatabaseConfigList } from "../model/database-config-list.ts";
8
+ import { ErrorRatchet } from "@bitblit/ratchet-common/lang/error-ratchet";
9
+
10
+ /**
11
+ */
12
+ export class RdsDataApiConnectionProvider implements DatabaseAccessProvider {
13
+
14
+ constructor(
15
+ private client: RDSDataClient,
16
+ private configPromiseProvider: () => Promise<DatabaseConfigList<RdsDataApiConnectionConfig>>,
17
+ ) {}
18
+
19
+ public async getDatabaseAccess(name?: string): Promise<DatabaseAccess | undefined> {
20
+ const configs: DatabaseConfigList<RdsDataApiConnectionConfig> = await this.configPromiseProvider();
21
+ let cfg: RdsDataApiConnectionConfig = null;
22
+ if (name) {
23
+ cfg = configs.dbList.find(s=>s.label===name);
24
+ } else if (configs.dbList.length===1) {
25
+ cfg = configs.dbList[0];
26
+ } else {
27
+ throw ErrorRatchet.fErr('Database Access not found for name, or no entries found');
28
+ }
29
+ return new RdsDataApiDatabaseAccess(this.client, cfg);
30
+ }
31
+
32
+ public async clearDatabaseAccessCache(): Promise<boolean> {
33
+ return true; // We don't really cache these
34
+ }
35
+
36
+ public async createNonPooledDatabaseAccess(queryDefaults: QueryDefaults): Promise<DatabaseAccess | undefined> {
37
+ return this.getDatabaseAccess(queryDefaults.databaseName);
38
+ }
39
+ }