@bitblit/ratchet-rdbms 4.0.115-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 (62) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/License.txt +13 -0
  3. package/README.md +41 -0
  4. package/lib/build/ratchet-rdbms-info.d.ts +5 -0
  5. package/lib/build/ratchet-rdbms-info.js +14 -0
  6. package/lib/model/connection-config.d.ts +4 -0
  7. package/lib/model/connection-config.js +1 -0
  8. package/lib/model/db-config.d.ts +9 -0
  9. package/lib/model/db-config.js +1 -0
  10. package/lib/model/group-by-count-result.d.ts +4 -0
  11. package/lib/model/group-by-count-result.js +1 -0
  12. package/lib/model/mysql/mysql-master-status.d.ts +6 -0
  13. package/lib/model/mysql/mysql-master-status.js +1 -0
  14. package/lib/model/mysql/mysql-results-wrapper.d.ts +5 -0
  15. package/lib/model/mysql/mysql-results-wrapper.js +1 -0
  16. package/lib/model/mysql/mysql-slave-status.d.ts +52 -0
  17. package/lib/model/mysql/mysql-slave-status.js +1 -0
  18. package/lib/model/mysql/mysql-style-connection-provider.d.ts +7 -0
  19. package/lib/model/mysql/mysql-style-connection-provider.js +1 -0
  20. package/lib/model/mysql/mysql-update-results.d.ts +9 -0
  21. package/lib/model/mysql/mysql-update-results.js +1 -0
  22. package/lib/model/paginated-results.d.ts +5 -0
  23. package/lib/model/paginated-results.js +1 -0
  24. package/lib/model/pagination-bounds.d.ts +6 -0
  25. package/lib/model/pagination-bounds.js +1 -0
  26. package/lib/model/paginator.d.ts +9 -0
  27. package/lib/model/paginator.js +1 -0
  28. package/lib/model/query-defaults.d.ts +4 -0
  29. package/lib/model/query-defaults.js +1 -0
  30. package/lib/model/query-text-provider.d.ts +4 -0
  31. package/lib/model/query-text-provider.js +1 -0
  32. package/lib/model/sort-direction.d.ts +4 -0
  33. package/lib/model/sort-direction.js +5 -0
  34. package/lib/model/ssh/ssh-tunnel-config.d.ts +7 -0
  35. package/lib/model/ssh/ssh-tunnel-config.js +1 -0
  36. package/lib/model/ssh/ssh-tunnel-container.d.ts +12 -0
  37. package/lib/model/ssh/ssh-tunnel-container.js +1 -0
  38. package/lib/model/ssh-config.d.ts +5 -0
  39. package/lib/model/ssh-config.js +1 -0
  40. package/lib/model/transaction-isolation-level.d.ts +4 -0
  41. package/lib/model/transaction-isolation-level.js +5 -0
  42. package/lib/named-parameter-maria-db-service.d.ts +41 -0
  43. package/lib/named-parameter-maria-db-service.js +251 -0
  44. package/lib/non-pooled-mysql-style-connection-provider.d.ts +8 -0
  45. package/lib/non-pooled-mysql-style-connection-provider.js +16 -0
  46. package/lib/query-builder/query-builder-result.d.ts +9 -0
  47. package/lib/query-builder/query-builder-result.js +12 -0
  48. package/lib/query-builder/query-builder.d.ts +52 -0
  49. package/lib/query-builder/query-builder.js +352 -0
  50. package/lib/query-builder/query-builder.spec.d.ts +1 -0
  51. package/lib/query-builder/query-builder.spec.js +126 -0
  52. package/lib/query-builder/query-util.d.ts +12 -0
  53. package/lib/query-builder/query-util.js +69 -0
  54. package/lib/rds-mysql-style-connection-provider.d.ts +23 -0
  55. package/lib/rds-mysql-style-connection-provider.js +159 -0
  56. package/lib/ssh-tunnel-service.d.ts +6 -0
  57. package/lib/ssh-tunnel-service.js +47 -0
  58. package/lib/transactional-named-parameter-maria-db-service.d.ts +23 -0
  59. package/lib/transactional-named-parameter-maria-db-service.js +129 -0
  60. package/lib/util/relational-database-utils.d.ts +4 -0
  61. package/lib/util/relational-database-utils.js +29 -0
  62. package/package.json +68 -0
@@ -0,0 +1,8 @@
1
+ import { Connection } from 'mysql2/promise';
2
+ import { MysqlStyleConnectionProvider } from './model/mysql/mysql-style-connection-provider.js';
3
+ export declare class NonPooledMysqlStyleConnectionProvider implements MysqlStyleConnectionProvider {
4
+ private connection;
5
+ constructor(connection: Connection);
6
+ getConnection(): Promise<Connection>;
7
+ clearConnectionCache(): Promise<boolean>;
8
+ }
@@ -0,0 +1,16 @@
1
+ import { RequireRatchet } from '@bitblit/ratchet-common/lib/lang/require-ratchet.js';
2
+ import { Logger } from '@bitblit/ratchet-common/lib/logger/logger.js';
3
+ export class NonPooledMysqlStyleConnectionProvider {
4
+ connection;
5
+ constructor(connection) {
6
+ this.connection = connection;
7
+ RequireRatchet.notNullOrUndefined(connection);
8
+ }
9
+ async getConnection() {
10
+ return this.connection;
11
+ }
12
+ async clearConnectionCache() {
13
+ Logger.info('clearConnectionCache ignored - not pooled');
14
+ return true;
15
+ }
16
+ }
@@ -0,0 +1,9 @@
1
+ import { TransactionIsolationLevel } from '../model/transaction-isolation-level.js';
2
+ import { Paginator } from '../model/paginator.js';
3
+ export declare class QueryBuilderResult {
4
+ query: string;
5
+ namedParams: Record<string, unknown>;
6
+ paginator?: Paginator<any>;
7
+ transactionIsolationLevel: TransactionIsolationLevel;
8
+ constructor(query: string, namedParams: Record<string, unknown>, paginator: Paginator<any> | undefined, transactionIsolationLevel: TransactionIsolationLevel);
9
+ }
@@ -0,0 +1,12 @@
1
+ export class QueryBuilderResult {
2
+ query;
3
+ namedParams;
4
+ paginator;
5
+ transactionIsolationLevel;
6
+ constructor(query, namedParams, paginator, transactionIsolationLevel) {
7
+ this.query = query;
8
+ this.namedParams = namedParams;
9
+ this.paginator = paginator;
10
+ this.transactionIsolationLevel = transactionIsolationLevel;
11
+ }
12
+ }
@@ -0,0 +1,52 @@
1
+ import { QueryBuilderResult } from './query-builder-result.js';
2
+ import { TransactionIsolationLevel } from '../model/transaction-isolation-level.js';
3
+ import { QueryTextProvider } from '../model/query-text-provider.js';
4
+ import { Paginator } from '../model/paginator.js';
5
+ export declare class QueryBuilder {
6
+ static readonly ALLOWED_SQL_CONSTRUCT: RegExp;
7
+ private readonly queryProvider;
8
+ private query?;
9
+ meta: {
10
+ queryPath?: string;
11
+ };
12
+ private sqlConstructs;
13
+ private namedParams;
14
+ private conditionals;
15
+ private debugComment;
16
+ private paginator?;
17
+ private debugAnnotateMode;
18
+ private transactionIsolationLevel;
19
+ constructor(queryProvider: QueryTextProvider);
20
+ clone(): QueryBuilder;
21
+ withTransactionIsolationLevel(level: TransactionIsolationLevel): QueryBuilder;
22
+ withDebugComment(comment: string): QueryBuilder;
23
+ appendDebugComment(comment: string): QueryBuilder;
24
+ withNamedQuery(queryPath: string): QueryBuilder;
25
+ withBaseQuery(baseQuery: string): void;
26
+ withSqlConstruct(key: string, value: unknown): QueryBuilder;
27
+ withSqlConstructs(params: Record<string, unknown>): QueryBuilder;
28
+ removeParam(key: string): QueryBuilder;
29
+ paramNames(): string[];
30
+ withParam(key: string, value: unknown): QueryBuilder;
31
+ withParams(params: unknown): QueryBuilder;
32
+ withExpandedParam(keyPrefix: string, values: unknown[], extendIfExists: boolean): QueryBuilder;
33
+ withConditional(tag: string, state?: boolean): QueryBuilder;
34
+ withConditionals(params: Record<string, boolean>): QueryBuilder;
35
+ withPaginator(paginator: Paginator<any>): QueryBuilder;
36
+ fetchCopyOfParam<T>(paramName: string): T | undefined;
37
+ fetchCopyOfConditional<T>(conditionalName: string): T | undefined;
38
+ containsParam(paramName: string): boolean;
39
+ getDebugComment(): string;
40
+ containsConditional(conditionalName: string): boolean;
41
+ build(): QueryBuilderResult;
42
+ buildUnfiltered(): QueryBuilderResult;
43
+ protected internalBuild(unfiltered: boolean): QueryBuilderResult;
44
+ private stripNonAsciiParams;
45
+ private runQueryChecks;
46
+ private applyComments;
47
+ applySqlConstructs(): void;
48
+ private applyRepeatBlocks;
49
+ private applyQueryFragments;
50
+ private applyPagination;
51
+ private applyConditionalBlocks;
52
+ }
@@ -0,0 +1,352 @@
1
+ import { ErrorRatchet } from '@bitblit/ratchet-common/lib/lang/error-ratchet.js';
2
+ import { Logger } from '@bitblit/ratchet-common/lib/logger/logger.js';
3
+ import { RequireRatchet } from '@bitblit/ratchet-common/lib/lang/require-ratchet.js';
4
+ import { StringRatchet } from '@bitblit/ratchet-common/lib/lang/string-ratchet.js';
5
+ import _ from 'lodash';
6
+ import { QueryBuilderResult } from './query-builder-result.js';
7
+ import { TransactionIsolationLevel } from '../model/transaction-isolation-level.js';
8
+ import { SortDirection } from '../model/sort-direction.js';
9
+ export class QueryBuilder {
10
+ static ALLOWED_SQL_CONSTRUCT = /^[a-z0-9_.`]+$/i;
11
+ queryProvider;
12
+ query;
13
+ meta = Object.freeze({});
14
+ sqlConstructs = {};
15
+ namedParams = {};
16
+ conditionals = {};
17
+ debugComment = '';
18
+ paginator;
19
+ debugAnnotateMode = false;
20
+ transactionIsolationLevel = TransactionIsolationLevel.Default;
21
+ constructor(queryProvider) {
22
+ this.queryProvider = queryProvider;
23
+ }
24
+ clone() {
25
+ const clone = new QueryBuilder(this.queryProvider);
26
+ if (this.query) {
27
+ clone.withBaseQuery(this.query);
28
+ }
29
+ clone.sqlConstructs = _.clone(this.sqlConstructs);
30
+ clone.namedParams = _.clone(this.namedParams);
31
+ clone.conditionals = _.clone(this.conditionals);
32
+ clone.paginator = _.clone(this.paginator);
33
+ clone.debugComment = this.debugComment;
34
+ clone.transactionIsolationLevel = this.transactionIsolationLevel;
35
+ return clone;
36
+ }
37
+ withTransactionIsolationLevel(level) {
38
+ this.transactionIsolationLevel = level;
39
+ return this;
40
+ }
41
+ withDebugComment(comment) {
42
+ this.debugComment = comment;
43
+ return this;
44
+ }
45
+ appendDebugComment(comment) {
46
+ this.debugComment = this.debugComment + comment;
47
+ return this;
48
+ }
49
+ withNamedQuery(queryPath) {
50
+ this.query = this.queryProvider.fetchQuery(queryPath);
51
+ if (!StringRatchet.trimToNull(this.query)) {
52
+ ErrorRatchet.throwFormattedErr('Requested query that does not exist : %s', queryPath);
53
+ }
54
+ this.meta = Object.freeze({ queryPath: queryPath });
55
+ this.withDebugComment(' ' + queryPath + ' ');
56
+ return this;
57
+ }
58
+ withBaseQuery(baseQuery) {
59
+ this.query = baseQuery;
60
+ }
61
+ withSqlConstruct(key, value) {
62
+ this.sqlConstructs[key] = value;
63
+ return this;
64
+ }
65
+ withSqlConstructs(params) {
66
+ this.sqlConstructs = Object.assign(this.sqlConstructs, params);
67
+ return this;
68
+ }
69
+ removeParam(key) {
70
+ delete this.namedParams[key];
71
+ return this;
72
+ }
73
+ paramNames() {
74
+ return Object.keys(this.namedParams);
75
+ }
76
+ withParam(key, value) {
77
+ this.namedParams[key] = value;
78
+ return this;
79
+ }
80
+ withParams(params) {
81
+ this.namedParams = Object.assign(this.namedParams, params);
82
+ return this;
83
+ }
84
+ withExpandedParam(keyPrefix, values, extendIfExists) {
85
+ const lengthParamName = keyPrefix + 'Length';
86
+ let oldSize = this.fetchCopyOfParam(lengthParamName) ?? 0;
87
+ if (oldSize > 0 && !extendIfExists) {
88
+ Logger.silly('Old item found and not extending - removing old params');
89
+ const toRemove = this.paramNames().filter((s) => s.startsWith(keyPrefix));
90
+ toRemove.forEach((s) => this.removeParam(s));
91
+ oldSize = 0;
92
+ }
93
+ this.withParam(lengthParamName, values.length + oldSize);
94
+ for (let i = 0; i < values.length; i++) {
95
+ const value = values[i];
96
+ if (typeof value === 'object' && !!value) {
97
+ for (const key of Object.keys(value)) {
98
+ const paramKey = keyPrefix + key.charAt(0).toUpperCase() + key.slice(1) + (i + oldSize);
99
+ this.withParam(paramKey, value[key]);
100
+ }
101
+ }
102
+ else {
103
+ const paramKey = keyPrefix + i;
104
+ this.withParam(paramKey, value);
105
+ }
106
+ }
107
+ return this;
108
+ }
109
+ withConditional(tag, state = true) {
110
+ this.conditionals[tag] = state;
111
+ return this;
112
+ }
113
+ withConditionals(params) {
114
+ this.conditionals = Object.assign(this.conditionals, params);
115
+ return this;
116
+ }
117
+ withPaginator(paginator) {
118
+ RequireRatchet.notNullOrUndefined(paginator, 'paginator');
119
+ RequireRatchet.notNullOrUndefined(paginator.cn, 'paginator.cn');
120
+ RequireRatchet.true(paginator.min || paginator.max || paginator.l, 'paginator must have some limit');
121
+ paginator.s = paginator.s ?? SortDirection.Asc;
122
+ this.paginator = paginator;
123
+ return this;
124
+ }
125
+ fetchCopyOfParam(paramName) {
126
+ return this.namedParams[paramName];
127
+ }
128
+ fetchCopyOfConditional(conditionalName) {
129
+ return this.conditionals[conditionalName];
130
+ }
131
+ containsParam(paramName) {
132
+ return this.namedParams[paramName] != undefined;
133
+ }
134
+ getDebugComment() {
135
+ return this.debugComment;
136
+ }
137
+ containsConditional(conditionalName) {
138
+ return this.conditionals[conditionalName] != undefined;
139
+ }
140
+ build() {
141
+ const build = this.clone();
142
+ return build.internalBuild(false);
143
+ }
144
+ buildUnfiltered() {
145
+ const builder = this.clone();
146
+ return builder.internalBuild(true);
147
+ }
148
+ internalBuild(unfiltered) {
149
+ this.applyQueryFragments();
150
+ this.applyConditionalBlocks();
151
+ this.applyRepeatBlocks();
152
+ this.applyPagination(unfiltered);
153
+ this.applySqlConstructs();
154
+ this.applyComments();
155
+ this.runQueryChecks();
156
+ this.stripNonAsciiParams();
157
+ return new QueryBuilderResult((this.query ?? '').trim(), this.namedParams, this.paginator, this.transactionIsolationLevel);
158
+ }
159
+ stripNonAsciiParams() {
160
+ const reduced = StringRatchet.stripNonAscii(JSON.stringify(this.namedParams));
161
+ this.namedParams = JSON.parse(reduced);
162
+ }
163
+ runQueryChecks() {
164
+ const quotedNamedParams = [...(this.query?.matchAll(/['"]:[A-z-]*['"]/gm) ?? [])];
165
+ if (quotedNamedParams.length > 0) {
166
+ Logger.warn('The resulting query contains quoted named params check this this is intended. Instances found: %s', quotedNamedParams.join(', '));
167
+ }
168
+ }
169
+ applyComments() {
170
+ if (this.debugComment.length && this.query) {
171
+ const firstSpaceIndex = this.query.indexOf(' ');
172
+ const comment = this.debugComment;
173
+ this.query = this.query.substring(0, firstSpaceIndex + 1) + `/*${comment}*/` + this.query.substring(firstSpaceIndex + 1);
174
+ }
175
+ }
176
+ applySqlConstructs() {
177
+ for (const key of Object.keys(this.sqlConstructs)) {
178
+ let value;
179
+ const val = this.sqlConstructs[key];
180
+ if (Array.isArray(val)) {
181
+ val.forEach((v) => {
182
+ if (typeof v !== 'string' || !v.match(QueryBuilder.ALLOWED_SQL_CONSTRUCT)) {
183
+ throw new Error(`sql construct entry ${v} is invalid value must be alphanumeric only.`);
184
+ }
185
+ });
186
+ value = val.join(', ');
187
+ }
188
+ else {
189
+ value = StringRatchet.safeString(val);
190
+ if (value.length > 0 && !value.match(QueryBuilder.ALLOWED_SQL_CONSTRUCT)) {
191
+ throw new Error(`sql construct ${value} is invalid value must be alphanumeric only.`);
192
+ }
193
+ }
194
+ const sqlReservedWords = ['update', 'insert', 'delete', 'drop', 'select'];
195
+ for (const word of sqlReservedWords) {
196
+ if (value.toLowerCase().includes(word)) {
197
+ throw new Error(`sql construct ${value} is invalid value must not contain reserved word ${word}.`);
198
+ }
199
+ }
200
+ const rawKey = `##sqlConstruct:${key}##`;
201
+ while (this.query?.includes(rawKey)) {
202
+ this.query = this.query.replace(rawKey, value);
203
+ }
204
+ }
205
+ }
206
+ applyRepeatBlocks() {
207
+ const startSymbol = '<repeat';
208
+ const endSymbol = '>';
209
+ while (true) {
210
+ const startIndex = this.query?.indexOf(startSymbol);
211
+ if (startIndex === -1 || !this.query || typeof startIndex !== 'number') {
212
+ return;
213
+ }
214
+ const endIndex = this.query.indexOf(endSymbol, startIndex);
215
+ if (endIndex == -1) {
216
+ throw new Error(`Invalid query when finding end symbol matching ${endSymbol} in ${this.query} check that you have closed all your tags correctly.`);
217
+ }
218
+ const content = this.query.substring(startIndex + startSymbol.length, endIndex).trim();
219
+ const countSymbol = 'count=';
220
+ let countParam = '';
221
+ const joinSymbol = 'join=';
222
+ let joinString;
223
+ const params = content.split(' ');
224
+ for (const param of params) {
225
+ if (param.includes(countSymbol)) {
226
+ countParam = param.substring(param.indexOf(countSymbol) + countSymbol.length);
227
+ }
228
+ if (param.includes(joinSymbol)) {
229
+ joinString = param.substring(param.indexOf(joinSymbol) + joinSymbol.length);
230
+ }
231
+ }
232
+ const endTag = `</repeat>`;
233
+ const endTagIndex = this.query.indexOf(endTag);
234
+ const repeatedContent = this.query.substring(endIndex + endSymbol.length, endTagIndex);
235
+ this.query = this.query.substring(0, startIndex) + this.query.substring(endTagIndex + endTag.length);
236
+ const count = this.namedParams[countParam.substring(1)];
237
+ for (let i = 0; i < count; i++) {
238
+ let indexedContent = repeatedContent;
239
+ if (joinString && i != 0) {
240
+ indexedContent += joinString;
241
+ }
242
+ let startParamTagIndex = indexedContent.indexOf(`::`);
243
+ while (startParamTagIndex != -1) {
244
+ const endParamTagIndex = indexedContent.indexOf(`::`, startParamTagIndex + 2);
245
+ if (endParamTagIndex == -1) {
246
+ throw new Error(`Invalid query when finding end symbol matching :: check that you have closed all your tags correctly. Query: ${this.query} `);
247
+ }
248
+ const param = indexedContent.substring(startParamTagIndex + 2, endParamTagIndex);
249
+ indexedContent = indexedContent.replace('::' + param + '::', ':' + param + i);
250
+ startParamTagIndex = indexedContent.indexOf(`::`);
251
+ }
252
+ this.query = this.query.substring(0, startIndex) + indexedContent + this.query.substring(startIndex);
253
+ }
254
+ }
255
+ }
256
+ applyQueryFragments() {
257
+ const startSymbol = '[[';
258
+ const endSymbol = ']]';
259
+ while (true) {
260
+ const startIndex = this.query?.indexOf(startSymbol);
261
+ if (startIndex == -1 || !this.query || typeof startIndex !== 'number') {
262
+ return;
263
+ }
264
+ const endIndex = this.query.indexOf(endSymbol, startIndex);
265
+ if (endIndex == -1) {
266
+ throw new Error(`Invalid query when finding end symbol matching ${endSymbol} in ${this.query} check that you have closed all your tags correctly.`);
267
+ }
268
+ const rawName = this.query.substring(startIndex + startSymbol.length, endIndex);
269
+ const namedQueryElement = this.queryProvider.fetchQuery(rawName);
270
+ if (!namedQueryElement) {
271
+ throw new Error(`Invalid query, query fragment ${rawName} not found in named queries.`);
272
+ }
273
+ this.query = this.query.replace(`[[${rawName}]]`, namedQueryElement);
274
+ }
275
+ }
276
+ applyPagination(unfiltered) {
277
+ const paginationRawKey = '##pagination##';
278
+ if (!unfiltered && this.paginator) {
279
+ const sortDirEnum = this.paginator.s == SortDirection.Desc ? SortDirection.Desc : SortDirection.Asc;
280
+ const sortDir = StringRatchet.safeString(sortDirEnum);
281
+ if (this.paginator.min || this.paginator.max) {
282
+ let wc = 'WHERE ##sqlConstruct:queryBuilderPaginatorWhere##';
283
+ this.withSqlConstruct('queryBuilderPaginatorWhere', this.paginator.cn);
284
+ if (this.paginator.min) {
285
+ wc += '>= :queryBuilderPaginatorWhereMin';
286
+ this.withParam('queryBuilderPaginatorWhereMin', this.paginator.min);
287
+ }
288
+ if (this.paginator.max) {
289
+ if (this.paginator.min) {
290
+ wc += ' AND ##sqlConstruct:queryBuilderPaginatorWhere##';
291
+ }
292
+ wc += '< :queryBuilderPaginatorWhereMax';
293
+ this.withParam('queryBuilderPaginatorWhereMax', this.paginator.max);
294
+ }
295
+ this.query += wc;
296
+ }
297
+ this.query += ` ORDER BY ##sqlConstruct:queryBuilderOrderBy## ${sortDir}`;
298
+ this.withSqlConstruct('queryBuilderOrderBy', this.paginator.cn);
299
+ if (this.paginator.l) {
300
+ this.query += ' LIMIT :queryBuilderLimit';
301
+ this.withParam('queryBuilderLimit', this.paginator.l);
302
+ }
303
+ }
304
+ if (unfiltered && this.query) {
305
+ const paginationSplitIndex = this.query.indexOf(paginationRawKey);
306
+ if (paginationSplitIndex != -1) {
307
+ this.query = 'SELECT COUNT(*) ' + this.query.substring(paginationSplitIndex + paginationRawKey.length);
308
+ }
309
+ }
310
+ while (this.query?.includes(paginationRawKey)) {
311
+ this.query = this.query.replace(paginationRawKey, '');
312
+ }
313
+ }
314
+ applyConditionalBlocks() {
315
+ const startSymbol = '<<';
316
+ const endSymbol = '>>';
317
+ while (true) {
318
+ const startIndex = this.query?.indexOf(startSymbol);
319
+ if (startIndex == -1 || !this.query || typeof startIndex !== 'number') {
320
+ return;
321
+ }
322
+ const endIndex = this.query.indexOf(endSymbol, startIndex);
323
+ if (endIndex == -1) {
324
+ throw new Error(`Invalid query when finding end symbol matching ${endSymbol} in ${this.query} check that you have closed all your tags correctly.`);
325
+ }
326
+ const rawTag = this.query.substring(startIndex + startSymbol.length, endIndex);
327
+ const tag = rawTag.replace(':', '');
328
+ const endTag = `<</${rawTag}>>`;
329
+ const endTagIndex = this.query.indexOf(endTag);
330
+ if (endTagIndex == -1) {
331
+ throw new Error(`Invalid query when finding conditional end tag matching ${endTag} in ${this.query} check that your query contains an exact match of this tag.`);
332
+ }
333
+ let replacement = this.query.substring(endIndex + endSymbol.length, endTagIndex);
334
+ if (rawTag.startsWith(':')) {
335
+ const param = this.namedParams[tag];
336
+ if (param == null || (Array.isArray(param) && param.length == 0)) {
337
+ replacement = '';
338
+ }
339
+ }
340
+ else {
341
+ const conditional = this.conditionals[tag.replace('!', '')];
342
+ if (conditional == undefined || conditional == tag.startsWith('!')) {
343
+ replacement = '';
344
+ }
345
+ }
346
+ if (this.debugAnnotateMode) {
347
+ replacement = '/* conditional ' + tag + '*/';
348
+ }
349
+ this.query = this.query.substring(0, startIndex) + replacement + this.query.substring(endTagIndex + endTag.length);
350
+ }
351
+ }
352
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,126 @@
1
+ import { NamedParameterMariaDbService } from '../named-parameter-maria-db-service.js';
2
+ import { JestRatchet } from '@bitblit/ratchet-jest/lib/jest/jest-ratchet.js';
3
+ import { SortDirection } from '../model/sort-direction.js';
4
+ import { jest } from '@jest/globals';
5
+ const prov = {
6
+ fetchQuery(queryPath) {
7
+ return this.fetchAllQueries()[queryPath];
8
+ },
9
+ fetchAllQueries() {
10
+ return {
11
+ 'named_parameter_tests.named_fragment_1': 'SELECT b.`id`, b.`owner_id`, b.`name` FROM boards b',
12
+ 'named_parameter_tests.named_test_1': '[[named_parameter_tests.named_fragment_1]] WHERE id=:boardId OR owner_id=:ownerId',
13
+ 'named_parameter_tests.conditional_section_test': 'SELECT b.`id` FROM boards b WHERE b.id < 1000<<COND1>> AND b.owner_id = :ownerId <</COND1>>',
14
+ 'named_parameter_tests.pagination_count_test': 'SELECT b.`id` ##pagination## FROM boards b WHERE b.id < 1000 UNION (SELECT x.`id` ##pagination## FROM somewherelse WHERE x.`id` = 3)',
15
+ 'named_parameter_tests.negated_conditional_section_test': 'SELECT b.`id` FROM boards b WHERE b.id < 1000 <<!COND1>> AND b.owner_id = :ownerId <</!COND1>>',
16
+ 'named_parameter_tests.conditional_param_section_test': 'SELECT b.`id` FROM boards b WHERE b.id < 1000 <<:ownerId>> AND b.owner_id = :ownerId <</:ownerId>>',
17
+ 'named_parameter_tests.new_repeat_section_test': '<repeat count=:clsLength join=AND> repeated ::clsW:: ::clsH:: clause </repeat>',
18
+ 'named_parameter_tests.sql_construct_test': 'GROUP BY x.##sqlConstruct:groupingColumn##',
19
+ 'named_parameter_tests.constant_test': 'SELECT b.`id`, :constantName as constant FROM boards b where b.`id` = 52',
20
+ };
21
+ },
22
+ };
23
+ const mariaDb = new NamedParameterMariaDbService(prov, JestRatchet.mock(jest.fn), { databaseName: 'test', timeoutMS: 2_000 });
24
+ describe('query-builder', () => {
25
+ it('builds filtered', () => {
26
+ const queryBuilder = mariaDb.queryBuilder('named_parameter_tests.pagination_count_test');
27
+ const build = queryBuilder.build();
28
+ expect(build.query).toBe('SELECT /* named_parameter_tests.pagination_count_test */b.`id` FROM boards b WHERE b.id < 1000 UNION (SELECT x.`id` FROM somewherelse WHERE x.`id` = 3)');
29
+ });
30
+ it('builds unfiltered', () => {
31
+ const queryBuilder = mariaDb.queryBuilder('named_parameter_tests.pagination_count_test');
32
+ const build = queryBuilder.buildUnfiltered();
33
+ expect(build.query).toBe('SELECT /* named_parameter_tests.pagination_count_test */COUNT(*) FROM boards b WHERE b.id < 1000 UNION (SELECT x.`id` FROM somewherelse WHERE x.`id` = 3)');
34
+ });
35
+ it('fails if param is missing', () => {
36
+ const queryBuilder = mariaDb.queryBuilder('named_parameter_tests.pagination_count_test');
37
+ const build = queryBuilder.build();
38
+ expect(build.query).toBe('SELECT /* named_parameter_tests.pagination_count_test */b.`id` FROM boards b WHERE b.id < 1000 UNION (SELECT x.`id` FROM somewherelse WHERE x.`id` = 3)');
39
+ });
40
+ it('removes conditional blocks when missing', () => {
41
+ const queryBuilder = mariaDb.queryBuilder('named_parameter_tests.conditional_section_test');
42
+ const build = queryBuilder.build();
43
+ expect(build.query).toBe('SELECT /* named_parameter_tests.conditional_section_test */b.`id` FROM boards b WHERE b.id < 1000');
44
+ });
45
+ it('handles negated conditional blocks true state', () => {
46
+ const queryBuilder = mariaDb.queryBuilder('named_parameter_tests.negated_conditional_section_test');
47
+ queryBuilder.withConditional('COND1');
48
+ const build = queryBuilder.build();
49
+ expect(build.query).toBe('SELECT /* named_parameter_tests.negated_conditional_section_test */b.`id` FROM boards b WHERE b.id < 1000');
50
+ });
51
+ it('handles negated conditional blocks false state', () => {
52
+ const queryBuilder = mariaDb.queryBuilder('named_parameter_tests.negated_conditional_section_test');
53
+ queryBuilder.withConditional('COND1', false);
54
+ const build = queryBuilder.build();
55
+ expect(build.query).toBe('SELECT /* named_parameter_tests.negated_conditional_section_test */b.`id` FROM boards b WHERE b.id < 1000 AND b.owner_id = :ownerId');
56
+ });
57
+ it('leaves conditional blocks when present', () => {
58
+ const queryBuilder = mariaDb.queryBuilder('named_parameter_tests.conditional_section_test');
59
+ queryBuilder.withConditional('COND1');
60
+ const build = queryBuilder.build();
61
+ expect(build.query).toBe('SELECT /* named_parameter_tests.conditional_section_test */b.`id` FROM boards b WHERE b.id < 1000 AND b.owner_id = :ownerId');
62
+ });
63
+ it('leaves param-conditional blocks when param exists', () => {
64
+ const queryBuilder = mariaDb.queryBuilder('named_parameter_tests.conditional_param_section_test');
65
+ queryBuilder.withParam('ownerId', 'testvalue');
66
+ const build = queryBuilder.build();
67
+ expect(build.query).toBe('SELECT /* named_parameter_tests.conditional_param_section_test */b.`id` FROM boards b WHERE b.id < 1000 AND b.owner_id = :ownerId');
68
+ });
69
+ it('removes param-conditional blocks when param missing', () => {
70
+ const queryBuilder = mariaDb.queryBuilder('named_parameter_tests.conditional_param_section_test');
71
+ const build = queryBuilder.build();
72
+ expect(build.query).toBe('SELECT /* named_parameter_tests.conditional_param_section_test */b.`id` FROM boards b WHERE b.id < 1000');
73
+ });
74
+ it('removes param-conditional blocks when param is an empty array', () => {
75
+ const queryBuilder = mariaDb.queryBuilder('named_parameter_tests.conditional_param_section_test');
76
+ queryBuilder.withParam('ownerId', []);
77
+ const build = queryBuilder.build();
78
+ expect(build.query).toBe('SELECT /* named_parameter_tests.conditional_param_section_test */b.`id` FROM boards b WHERE b.id < 1000');
79
+ });
80
+ it('applies query fragments', () => {
81
+ const queryBuilder = mariaDb.queryBuilder('named_parameter_tests.named_test_1');
82
+ const build = queryBuilder.build();
83
+ expect(build.query).toBe('SELECT /* named_parameter_tests.named_test_1 */b.`id`, b.`owner_id`, b.`name` FROM boards b WHERE id=:boardId OR owner_id=:ownerId');
84
+ });
85
+ it('applies sql constructs', () => {
86
+ const queryBuilder = mariaDb.queryBuilder('named_parameter_tests.sql_construct_test');
87
+ queryBuilder.withSqlConstruct('groupingColumn', 'test_column');
88
+ const build = queryBuilder.build();
89
+ expect(build.query).toBe('GROUP /* named_parameter_tests.sql_construct_test */BY x.test_column');
90
+ });
91
+ it('expands object arrays', () => {
92
+ const queryBuilder = mariaDb.queryBuilder('named_parameter_tests.new_repeat_section_test');
93
+ queryBuilder.withExpandedParam('cls', [
94
+ { w: 1, h: 1 },
95
+ { w: 2, h: 2 },
96
+ ], true);
97
+ expect(queryBuilder.fetchCopyOfParam('clsW0')).toBe(1);
98
+ expect(queryBuilder.fetchCopyOfParam('clsW1')).toBe(2);
99
+ expect(queryBuilder.fetchCopyOfParam('clsH0')).toBe(1);
100
+ expect(queryBuilder.fetchCopyOfParam('clsH1')).toBe(2);
101
+ });
102
+ it('expands string arrays', () => {
103
+ const queryBuilder = mariaDb.queryBuilder('named_parameter_tests.new_repeat_section_test');
104
+ queryBuilder.withExpandedParam('cls', ['s0', 's1'], true);
105
+ expect(queryBuilder.fetchCopyOfParam('cls0')).toBe('s0');
106
+ expect(queryBuilder.fetchCopyOfParam('cls1')).toBe('s1');
107
+ });
108
+ it('applies repeat blocks', () => {
109
+ const queryBuilder = mariaDb.queryBuilder('named_parameter_tests.new_repeat_section_test');
110
+ queryBuilder.withExpandedParam('cls', [
111
+ { w: 1, h: 1 },
112
+ { w: 2, h: 2 },
113
+ ], true);
114
+ const build = queryBuilder.build();
115
+ expect(build.query).toBe('/* named_parameter_tests.new_repeat_section_test */repeated :clsW1 :clsH1 clause AND repeated :clsW0 :clsH0 clause');
116
+ });
117
+ xit('applies pagination', () => {
118
+ const queryBuilder = mariaDb.queryBuilder('named_parameter_tests.conditional_param_section_test');
119
+ queryBuilder.withPaginator({ s: SortDirection.Desc, cn: 'b.id', max: 1000, l: 25 });
120
+ const build = queryBuilder.build();
121
+ expect(build.query).toBe('SELECT /* named_parameter_tests.conditional_param_section_test */b.`id` FROM boards b WHERE b.id < 1000 ORDER BY id Desc LIMIT :queryBuilderLimit');
122
+ expect(build.namedParams).toStrictEqual({
123
+ queryBuilderLimit: 20,
124
+ });
125
+ });
126
+ });
@@ -0,0 +1,12 @@
1
+ export declare class QueryUtil {
2
+ private fields;
3
+ private replacements;
4
+ addReplacement(replacement: Record<string, unknown>, ...fields: string[]): void;
5
+ appendReplacement(replacementKey: string, appendValue: string, ...fields: string[]): void;
6
+ addFields(...fields: string[]): void;
7
+ getFields(): string[];
8
+ getReplacements(): Record<string, unknown>;
9
+ static sqlInjectionUnsafeParamRenderer(value: unknown): string;
10
+ static renderQueryStringForPasteIntoTool(query: string, inFields: object | null, transform?: (x: unknown) => string): string | null;
11
+ static reformatQueryForLogging(qry: string, inMaxLineLength?: number): string | null;
12
+ }
@@ -0,0 +1,69 @@
1
+ import { StringRatchet } from '@bitblit/ratchet-common/lib/lang/string-ratchet.js';
2
+ import { Logger } from '@bitblit/ratchet-common/lib/logger/logger.js';
3
+ export class QueryUtil {
4
+ fields = [];
5
+ replacements = {};
6
+ addReplacement(replacement, ...fields) {
7
+ this.replacements = Object.assign(this.replacements, replacement);
8
+ this.addFields(...fields);
9
+ }
10
+ appendReplacement(replacementKey, appendValue, ...fields) {
11
+ this.replacements[replacementKey] = this.replacements[replacementKey] + appendValue;
12
+ this.addFields(...fields);
13
+ }
14
+ addFields(...fields) {
15
+ this.fields = this.fields.concat(...fields);
16
+ }
17
+ getFields() {
18
+ return this.fields;
19
+ }
20
+ getReplacements() {
21
+ return this.replacements;
22
+ }
23
+ static sqlInjectionUnsafeParamRenderer(value) {
24
+ const rFn = (val) => (typeof val === 'string' ? '"' + val + '"' : StringRatchet.safeString(val));
25
+ const repl = Array.isArray(value) ? value.map((s) => rFn(s)).join(',') : rFn(value);
26
+ return repl;
27
+ }
28
+ static renderQueryStringForPasteIntoTool(query, inFields, transform = QueryUtil.sqlInjectionUnsafeParamRenderer) {
29
+ const fields = inFields ?? {};
30
+ let rval = QueryUtil.reformatQueryForLogging(query);
31
+ if (rval) {
32
+ const keys = Object.keys(fields);
33
+ keys.sort((b, a) => a.length - b.length);
34
+ for (const key of keys) {
35
+ const val = fields[key];
36
+ const find = ':' + key;
37
+ const repl = transform(val);
38
+ rval = rval.split(find).join(repl);
39
+ }
40
+ if (!rval.endsWith(';')) {
41
+ rval += ';';
42
+ }
43
+ }
44
+ return rval;
45
+ }
46
+ static reformatQueryForLogging(qry, inMaxLineLength = 80) {
47
+ let maxLineLength = inMaxLineLength;
48
+ if (!StringRatchet.trimToNull(qry)) {
49
+ return null;
50
+ }
51
+ let loggableQuery = '';
52
+ let cleaned = StringRatchet.trimToEmpty(qry).split('\n').join(' ').split('\r').join(' ');
53
+ while (cleaned.length > maxLineLength) {
54
+ let idx = Math.min(cleaned.length, maxLineLength);
55
+ while (idx > 0 && ![' ', ','].includes(cleaned.charAt(idx))) {
56
+ idx--;
57
+ }
58
+ if (idx > 0) {
59
+ loggableQuery += cleaned.substring(0, idx) + '\n';
60
+ cleaned = StringRatchet.trimToEmpty(cleaned.substring(idx));
61
+ }
62
+ else {
63
+ Logger.silly('Input contains a string longer than the max line length - bumping');
64
+ maxLineLength += 2;
65
+ }
66
+ }
67
+ return loggableQuery + (cleaned.length > 0 ? cleaned : '');
68
+ }
69
+ }