@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,265 @@
1
+ import maria, { Connection, ConnectionOptions } from 'mysql2/promise';
2
+
3
+ import { RequireRatchet } from '@bitblit/ratchet-common/lang/require-ratchet';
4
+ import { Logger } from '@bitblit/ratchet-common/logger/logger';
5
+ import { ErrorRatchet } from '@bitblit/ratchet-common/lang/error-ratchet';
6
+ import { StringRatchet } from '@bitblit/ratchet-common/lang/string-ratchet';
7
+ import { SshTunnelService } from '../service/ssh-tunnel-service.js';
8
+ import { DatabaseConfigList } from '../model/database-config-list.js';
9
+ import getPort from 'get-port';
10
+ import { SshTunnelContainer } from '../model/ssh/ssh-tunnel-container.js';
11
+ import { QueryDefaults } from '../model/query-defaults.js';
12
+ import { ConnectionAndTunnel } from '../model/connection-and-tunnel.js';
13
+ import { DatabaseAccessProvider } from '../model/database-access-provider.js';
14
+ import { DatabaseAccess } from '../model/database-access.js';
15
+ import { MysqlStyleDatabaseAccess } from './mysql-style-database-access.js';
16
+ import { MysqlDbConfig } from './model/mysql-db-config.js';
17
+
18
+ /**
19
+ */
20
+ export class RdsMysqlStyleConnectionProvider implements DatabaseAccessProvider {
21
+ // While this _technically_ expands the possible scope of Sql injection, we already
22
+ // tightly limit to Named parameters, so multiple statements is more useful than not!
23
+ public static DEFAULT_CONNECTION_OPTIONS: ConnectionOptions = { multipleStatements: true };
24
+
25
+ private connectionCache = new Map<string, Promise<ConnectionAndTunnel<Connection, MysqlDbConfig>>>(); //// Cache the promises to make it a single connection
26
+ //private tunnels = new Map<string, SshTunnelContainer>();
27
+ //private dbPromise = new Map<string, Promise<Connection | undefined>>(); // Cache the promises to make it a single connection
28
+ private cacheConfigPromise: Promise<DatabaseConfigList<MysqlDbConfig>>;
29
+ constructor(
30
+ private configPromiseProvider: () => Promise<DatabaseConfigList<MysqlDbConfig>>,
31
+ private additionalConfig: ConnectionOptions = RdsMysqlStyleConnectionProvider.DEFAULT_CONNECTION_OPTIONS,
32
+ private ssh?: SshTunnelService,
33
+ ) {
34
+ this.cacheConfigPromise = this.createConnectionConfig(); // Sets up tunnels, etc
35
+ Logger.info('Added shutdown handler to the process (Only once per instantiation)');
36
+ this.addShutdownHandlerToProcess();
37
+ }
38
+
39
+ public get usingSshTunnel(): boolean {
40
+ return !!this.ssh;
41
+ }
42
+
43
+ private addShutdownHandlerToProcess(): void {
44
+ process.on('exit', () => {
45
+ Logger.info('Process is shutting down, closing connections');
46
+ this.clearDatabaseAccessCache().catch((err) => {
47
+ Logger.error('Shutdown connection failed : %s', err);
48
+ });
49
+ });
50
+ }
51
+
52
+ public async clearDatabaseAccessCache(): Promise<boolean> {
53
+ const rval = false;
54
+ Logger.info('Clearing connection cache for RdsMysqlConnectionProvider');
55
+ // First, clear the connection caches so that subsequent connection attempts start fresh
56
+ const oldConnections: Promise<ConnectionAndTunnel<Connection, MysqlDbConfig>>[] = Array.from(this.connectionCache.values());
57
+ //const oldDbHooks = this.dbPromise;
58
+ //const oldSshTunnels = this.tunnels;
59
+ this.cacheConfigPromise = null; // Re-read config in case the password expired, etc
60
+ this.connectionCache = new Map();
61
+ //this.tunnels = new Map();
62
+ // Resolve any leftover DB connections & end them
63
+ if (oldConnections.length > 0) {
64
+ for (let i = 0; i < oldConnections.length; i++) {
65
+ Logger.info('Shutting down old connection %d of %d', i, oldConnections.length);
66
+ try {
67
+ const conn: ConnectionAndTunnel<Connection, MysqlDbConfig> = await oldConnections[i];
68
+ Logger.info('Conn %d is %s', i, conn?.config?.label);
69
+ if (conn.db) {
70
+ Logger.info('Stopping connection to database');
71
+ try {
72
+ conn.db.destroy();
73
+ Logger.info('Database connection closed');
74
+ } catch (err) {
75
+ if (ErrorRatchet.asErr(err).message.includes('closed state')) {
76
+ // DB was already closed, ignore
77
+ } else {
78
+ Logger.error('Something went wrong closing the database connection : %s', err);
79
+ }
80
+ }
81
+ }
82
+ if (conn.ssh) {
83
+ try {
84
+ Logger.info('Stopping ssh tunnel');
85
+ await this.ssh.shutdown(conn.ssh);
86
+ Logger.info('Ssh tunnel stopped');
87
+ } catch (err) {
88
+ Logger.warn('Failed to stop ssh tunnel : %s', err, err);
89
+ }
90
+ }
91
+ } catch (err) {
92
+ Logger.warn('Shutdown failed : %s ', err, err);
93
+ }
94
+ }
95
+ }
96
+ Logger.info('Old db and tunnels removed');
97
+ return rval;
98
+ }
99
+
100
+ public async getConnectionAndTunnel(name: string): Promise<ConnectionAndTunnel<Connection, MysqlDbConfig>> {
101
+ Logger.silly('getConnectionAndTunnel : %s', name);
102
+ if (!this.connectionCache.has(name)) {
103
+ Logger.info('No connectionCache found for %s - creating new one', name);
104
+ const dbConfig = await this.getDbConfig(name);
105
+ const connection = this.createConnectionAndTunnel(dbConfig, this.additionalConfig, true);
106
+ this.connectionCache.set(name, connection);
107
+ Logger.info('Added connectionCache for %s', name);
108
+ }
109
+ return this.connectionCache.get(name);
110
+ }
111
+
112
+ public async getDatabaseAccess(name: string): Promise<DatabaseAccess | undefined> {
113
+ Logger.silly('getConnection : %s', name);
114
+ const conn: ConnectionAndTunnel<Connection, MysqlDbConfig> = await this.getConnectionAndTunnel(name);
115
+ const rval: DatabaseAccess = conn?.db ? new MysqlStyleDatabaseAccess(conn.db, this.additionalConfig) : null;
116
+ return rval;
117
+ }
118
+
119
+ public async createNonPooledConnectionAndTunnel(
120
+ queryDefaults: QueryDefaults,
121
+ additionalConfig: ConnectionOptions = RdsMysqlStyleConnectionProvider.DEFAULT_CONNECTION_OPTIONS,
122
+ ): Promise<ConnectionAndTunnel<Connection, MysqlDbConfig>> {
123
+ Logger.info('Creating non-pooled connection for %s', queryDefaults.databaseName);
124
+ const dbConfig = await this.getDbConfig(queryDefaults.databaseName);
125
+ const rval = await this.createConnectionAndTunnel(dbConfig, additionalConfig, false);
126
+ return rval;
127
+ }
128
+
129
+ public async createNonPooledDatabaseConnection(
130
+ queryDefaults: QueryDefaults,
131
+ additionalConfig: ConnectionOptions = RdsMysqlStyleConnectionProvider.DEFAULT_CONNECTION_OPTIONS,
132
+ ): Promise<Connection | undefined> {
133
+ const conTunnel: ConnectionAndTunnel<Connection, MysqlDbConfig> = await this.createNonPooledConnectionAndTunnel(
134
+ queryDefaults,
135
+ additionalConfig,
136
+ );
137
+ return conTunnel?.db;
138
+ }
139
+
140
+ private async getDbConfig(name: string): Promise<MysqlDbConfig> {
141
+ Logger.info('RdsMysqlStyleConnectionProvider:getDbConfig:Initiating promise for %s', name);
142
+ const cfgs: DatabaseConfigList<MysqlDbConfig> = await this.configPromise();
143
+ const finder: string = StringRatchet.trimToEmpty(name).toLowerCase();
144
+ const dbConfig = cfgs.dbList.find((s) => StringRatchet.trimToEmpty(s.label).toLowerCase() === finder);
145
+ if (!dbConfig) {
146
+ throw ErrorRatchet.fErr(
147
+ 'Cannot find any connection config named %s (Available are %j)',
148
+ name,
149
+ cfgs.dbList.map((d) => d.label),
150
+ );
151
+ }
152
+ return dbConfig;
153
+ }
154
+
155
+ // Always creates a promise
156
+ private async createConnectionAndTunnel(
157
+ dbCfg: MysqlDbConfig,
158
+ additionalConfig: ConnectionOptions = RdsMysqlStyleConnectionProvider.DEFAULT_CONNECTION_OPTIONS,
159
+ clearCacheOnConnectionFailure: boolean,
160
+ ): Promise<ConnectionAndTunnel<Connection, MysqlDbConfig> | undefined> {
161
+ Logger.info('In RdsMysqlStyleConnectionProvider:createConnectionAndTunnel : %s', dbCfg.label);
162
+ RequireRatchet.notNullOrUndefined(dbCfg, 'dbCfg');
163
+
164
+ let tunnel: SshTunnelContainer = null;
165
+ if (dbCfg.sshTunnelConfig) {
166
+ const localPort: number = dbCfg.sshTunnelConfig.forceLocalPort || (await getPort());
167
+ Logger.debug(
168
+ 'SSH tunnel config found, opening tunnel to %s / %s to using local port %s',
169
+ dbCfg.sshTunnelConfig.host,
170
+ dbCfg.sshTunnelConfig.port,
171
+ localPort,
172
+ );
173
+ tunnel = await this.ssh.createSSHTunnel(dbCfg.sshTunnelConfig, dbCfg.host, dbCfg.port, localPort);
174
+ Logger.debug('SSH Tunnel open');
175
+ } else {
176
+ Logger.debug('No ssh configuration - skipping tunnel');
177
+ }
178
+
179
+ Logger.debug('Opening connection for RdsMysqlStyleConnectionProvider');
180
+ let connection: Connection;
181
+ try {
182
+ const cfgCopy: MysqlDbConfig = structuredClone(dbCfg);
183
+ delete cfgCopy.label;
184
+ delete cfgCopy.sshTunnelConfig;
185
+ if (tunnel) {
186
+ cfgCopy.host = 'localhost';
187
+ cfgCopy.port = tunnel.localPort;
188
+ }
189
+
190
+ connection = await maria.createConnection({ ...additionalConfig, ...cfgCopy });
191
+ } catch (err) {
192
+ Logger.info('Failed trying to create connection : %s : clearing for retry', err);
193
+ if (clearCacheOnConnectionFailure) {
194
+ this.connectionCache = new Map<string, Promise<ConnectionAndTunnel<Connection, MysqlDbConfig>>>();
195
+ }
196
+ return undefined;
197
+ }
198
+ connection.on('error', (err) => {
199
+ Logger.info('An error was detected on the connection : %s : Clearing', err);
200
+ this.clearDatabaseAccessCache()
201
+ .then((cleared) => {
202
+ Logger.info('Connection cleared: %s', cleared);
203
+ })
204
+ .catch((err) => Logger.error('Failed to clear RDS connection cache: %j', err));
205
+ });
206
+ Logger.info(
207
+ 'Added error handler to db, there are now %d error handlers and %d shutdown handlers',
208
+ connection.rawListeners('error').length,
209
+ process.rawListeners('exit').length,
210
+ );
211
+
212
+ const rval: ConnectionAndTunnel<Connection, MysqlDbConfig> = {
213
+ config: dbCfg,
214
+ db: connection,
215
+ ssh: tunnel,
216
+ };
217
+
218
+ return rval;
219
+ }
220
+
221
+ private configPromise(): Promise<DatabaseConfigList<MysqlDbConfig>> {
222
+ if (!this.cacheConfigPromise) {
223
+ this.cacheConfigPromise = this.createConnectionConfig();
224
+ }
225
+ return this.cacheConfigPromise;
226
+ }
227
+
228
+ private async createConnectionConfig(): Promise<DatabaseConfigList<MysqlDbConfig>> {
229
+ RequireRatchet.notNullOrUndefined(this.configPromiseProvider, 'input');
230
+ const inputPromise: Promise<DatabaseConfigList<MysqlDbConfig>> = this.configPromiseProvider();
231
+ Logger.info('Creating connection config');
232
+ const cfg: DatabaseConfigList<MysqlDbConfig> = await inputPromise;
233
+ RequireRatchet.true(cfg.dbList.length > 0, 'input.dbList');
234
+
235
+ cfg.dbList.forEach((db) => {
236
+ const errors: string[] = RdsMysqlStyleConnectionProvider.validDbConfig(db);
237
+ if (errors?.length) {
238
+ throw ErrorRatchet.fErr('Errors found in db config : %j', errors);
239
+ }
240
+ });
241
+ return cfg;
242
+ }
243
+
244
+ public static validDbConfig(cfg: MysqlDbConfig): string[] {
245
+ let rval: string[] = [];
246
+ if (!cfg) {
247
+ rval.push('The config is null');
248
+ } else {
249
+ rval.push(StringRatchet.trimToNull(cfg.host) ? null : 'host is required and non-empty');
250
+ rval.push(StringRatchet.trimToNull(cfg.label) ? null : 'label is required and non-empty');
251
+ rval.push(StringRatchet.trimToNull(cfg.database) ? null : 'database is required and non-empty');
252
+ rval.push(StringRatchet.trimToNull(cfg.user) ? null : 'user is required and non-empty');
253
+ rval.push(StringRatchet.trimToNull(cfg.password) ? null : 'password is required and non-empty');
254
+ rval.push(cfg.port ? null : 'port is required and non-empty');
255
+ }
256
+ if (cfg.sshTunnelConfig) {
257
+ rval.push(
258
+ StringRatchet.trimToNull(cfg.sshTunnelConfig.host) ? null : 'If sshTunnelConfig is non-null, host is required and non-empty',
259
+ );
260
+ rval.push(cfg.sshTunnelConfig.port ? null : 'If sshTunnelConfig is non-null, port is required and non-empty');
261
+ }
262
+ rval = rval.filter((s) => !!s);
263
+ return rval;
264
+ }
265
+ }
@@ -0,0 +1,8 @@
1
+ import { SshTunnelConfig } from "../../model/ssh/ssh-tunnel-config.ts";
2
+ import { Config } from 'pg';
3
+
4
+ export interface PostgresDbConfig {
5
+ label: string;
6
+ dbConfig: Config;
7
+ sshTunnelConfig?: SshTunnelConfig;
8
+ }
@@ -0,0 +1,270 @@
1
+
2
+ import { RequireRatchet } from '@bitblit/ratchet-common/lang/require-ratchet';
3
+ import { Logger } from '@bitblit/ratchet-common/logger/logger';
4
+ import { ErrorRatchet } from '@bitblit/ratchet-common/lang/error-ratchet';
5
+ import { StringRatchet } from '@bitblit/ratchet-common/lang/string-ratchet';
6
+ import { SshTunnelService } from '../service/ssh-tunnel-service.js';
7
+ import { DatabaseConfigList } from '../model/database-config-list.js';
8
+ import getPort from 'get-port';
9
+ import { SshTunnelContainer } from '../model/ssh/ssh-tunnel-container.js';
10
+ import { QueryDefaults } from '../model/query-defaults.js';
11
+ import { ConnectionAndTunnel } from '../model/connection-and-tunnel.js';
12
+ import { DatabaseAccessProvider } from '../model/database-access-provider.js';
13
+ import { DatabaseAccess } from '../model/database-access.js';
14
+ import { Client } from 'pg';
15
+ import { PostgresDbConfig } from "./model/postgres-db-config.ts";
16
+ import { PostgresStyleDatabaseAccess } from "./postgres-style-database-access.ts";
17
+
18
+ /**
19
+ */
20
+ export class PostgresStyleConnectionProvider implements DatabaseAccessProvider {
21
+
22
+ private connectionCache = new Map<string, Promise<ConnectionAndTunnel<Client, PostgresDbConfig>>>(); //// Cache the promises to make it a single connection
23
+ //private tunnels = new Map<string, SshTunnelContainer>();
24
+ //private dbPromise = new Map<string, Promise<Connection | undefined>>(); // Cache the promises to make it a single connection
25
+ private cacheConfigPromise: Promise<DatabaseConfigList<PostgresDbConfig>>;
26
+ constructor(
27
+ private configPromiseProvider: () => Promise<DatabaseConfigList<PostgresDbConfig>>,
28
+ private ssh?: SshTunnelService,
29
+ ) {
30
+ this.cacheConfigPromise = this.createConnectionConfig(); // Sets up tunnels, etc
31
+ Logger.info('Added shutdown handler to the process (Only once per instantiation)');
32
+ this.addShutdownHandlerToProcess();
33
+ }
34
+
35
+ public get usingSshTunnel(): boolean {
36
+ return !!this.ssh;
37
+ }
38
+
39
+ private addShutdownHandlerToProcess(): void {
40
+ process.on('exit', () => {
41
+ Logger.info('Process is shutting down, closing connections');
42
+ this.clearDatabaseAccessCache().catch((err) => {
43
+ Logger.error('Shutdown connection failed : %s', err);
44
+ });
45
+ });
46
+ }
47
+
48
+ public async clearDatabaseAccessCache(): Promise<boolean> {
49
+ const rval = false;
50
+ Logger.info('Clearing connection cache for PostgresStyleConnectionProvider');
51
+ // First, clear the connection caches so that subsequent connection attempts start fresh
52
+ const oldConnections: Promise<ConnectionAndTunnel<Client, PostgresDbConfig>>[] = Array.from(this.connectionCache.values());
53
+ //const oldDbHooks = this.dbPromise;
54
+ //const oldSshTunnels = this.tunnels;
55
+ this.cacheConfigPromise = null; // Re-read config in case the password expired, etc
56
+ this.connectionCache = new Map();
57
+ //this.tunnels = new Map();
58
+ // Resolve any leftover DB connections & end them
59
+ if (oldConnections.length > 0) {
60
+ for (let i = 0; i < oldConnections.length; i++) {
61
+ Logger.info('Shutting down old connection %d of %d', i, oldConnections.length);
62
+ try {
63
+ const conn: ConnectionAndTunnel<Client, PostgresDbConfig> = await oldConnections[i];
64
+ Logger.info('Conn %d is %s', i, conn?.config?.label);
65
+ if (conn.db) {
66
+ Logger.info('Stopping connection to database');
67
+ try {
68
+ await conn.db.end();
69
+ Logger.info('Database connection closed');
70
+ } catch (err) {
71
+ if (ErrorRatchet.asErr(err).message.includes('closed state')) {
72
+ // DB was already closed, ignore
73
+ } else {
74
+ Logger.error('Something went wrong closing the database connection : %s', err);
75
+ }
76
+ }
77
+ }
78
+ if (conn.ssh) {
79
+ try {
80
+ Logger.info('Stopping ssh tunnel');
81
+ await this.ssh.shutdown(conn.ssh);
82
+ Logger.info('Ssh tunnel stopped');
83
+ } catch (err) {
84
+ Logger.warn('Failed to stop ssh tunnel : %s', err, err);
85
+ }
86
+ }
87
+ } catch (err) {
88
+ Logger.warn('Shutdown failed : %s ', err, err);
89
+ }
90
+ }
91
+ }
92
+ Logger.info('Old db and tunnels removed');
93
+ return rval;
94
+ }
95
+
96
+ public async getConnectionAndTunnel(name: string): Promise<ConnectionAndTunnel<Client, PostgresDbConfig>> {
97
+ Logger.silly('getConnectionAndTunnel : %s', name);
98
+ if (!this.connectionCache.has(name)) {
99
+ Logger.info('No connectionCache found for %s - creating new one', name);
100
+ const dbConfig = await this.getDbConfig(name);
101
+ const connection = this.createConnectionAndTunnel(dbConfig, true);
102
+ this.connectionCache.set(name, connection);
103
+ Logger.info('Added connectionCache for %s', name);
104
+ }
105
+ return this.connectionCache.get(name);
106
+ }
107
+
108
+ public async getDatabaseAccess(name: string): Promise<DatabaseAccess | undefined> {
109
+ Logger.silly('getConnection : %s', name);
110
+
111
+ const conn: ConnectionAndTunnel<Client, PostgresDbConfig> = await this.getConnectionAndTunnel(name);
112
+ const dbConfig = await this.getDbConfig(name);
113
+ const rval: DatabaseAccess = conn?.db ? new PostgresStyleDatabaseAccess(conn.db, dbConfig) : null;
114
+ return rval;
115
+ }
116
+
117
+ public async createNonPooledConnectionAndTunnel(
118
+ queryDefaults: QueryDefaults
119
+ ): Promise<ConnectionAndTunnel<Client, PostgresDbConfig>> {
120
+ Logger.info('Creating non-pooled connection for %s', queryDefaults.databaseName);
121
+ const dbConfig = await this.getDbConfig(queryDefaults.databaseName);
122
+ const rval = await this.createConnectionAndTunnel(dbConfig, false);
123
+ return rval;
124
+ }
125
+
126
+ public async createNonPooledDatabaseConnection(
127
+ queryDefaults: QueryDefaults
128
+ ): Promise<Client | undefined> {
129
+ const conTunnel: ConnectionAndTunnel<Client, PostgresDbConfig> = await this.createNonPooledConnectionAndTunnel(
130
+ queryDefaults
131
+ );
132
+ return conTunnel?.db;
133
+ }
134
+
135
+ private async getDbConfig(name: string): Promise<PostgresDbConfig> {
136
+ Logger.info('PostgresStyleConnectionProvider:getDbConfig:Initiating promise for %s', name);
137
+ const cfgs: DatabaseConfigList<PostgresDbConfig> = await this.configPromise();
138
+ const finder: string = StringRatchet.trimToEmpty(name).toLowerCase();
139
+ const dbConfig = cfgs.dbList.find((s) => StringRatchet.trimToEmpty(s.label).toLowerCase() === finder);
140
+ if (!dbConfig) {
141
+ throw ErrorRatchet.fErr(
142
+ 'Cannot find any connection config named %s (Available are %j)',
143
+ name,
144
+ cfgs.dbList.map((d) => d.label),
145
+ );
146
+ }
147
+ return dbConfig;
148
+ }
149
+
150
+ // Always creates a promise
151
+ private async createConnectionAndTunnel(
152
+ dbCfg: PostgresDbConfig,
153
+ clearCacheOnConnectionFailure: boolean,
154
+ ): Promise<ConnectionAndTunnel<Client, PostgresDbConfig> | undefined> {
155
+ Logger.info('In PostgresStyleConnectionProvider:createConnectionAndTunnel : %s', dbCfg.label);
156
+ RequireRatchet.notNullOrUndefined(dbCfg, 'dbCfg');
157
+
158
+ let tunnel: SshTunnelContainer = null;
159
+ if (dbCfg.sshTunnelConfig) {
160
+ const localPort: number = dbCfg.sshTunnelConfig.forceLocalPort || (await getPort());
161
+ Logger.debug(
162
+ 'SSH tunnel config found, opening tunnel to %s / %s to using local port %s',
163
+ dbCfg.sshTunnelConfig.host,
164
+ dbCfg.sshTunnelConfig.port,
165
+ localPort,
166
+ );
167
+ tunnel = await this.ssh.createSSHTunnel(dbCfg.sshTunnelConfig, dbCfg.dbConfig.host, dbCfg.dbConfig.port, localPort);
168
+ Logger.debug('SSH Tunnel open');
169
+ } else {
170
+ Logger.debug('No ssh configuration - skipping tunnel');
171
+ }
172
+
173
+ Logger.debug('Opening connection for PostgresStyleConnectionProvider');
174
+ let connection: Client;
175
+ try {
176
+ const cfgCopy: PostgresDbConfig = structuredClone(dbCfg);
177
+ delete cfgCopy.label;
178
+ delete cfgCopy.sshTunnelConfig;
179
+ if (tunnel) {
180
+ cfgCopy.dbConfig.host = 'localhost';
181
+ cfgCopy.dbConfig.port = tunnel.localPort;
182
+ }
183
+
184
+ connection = new Client(cfgCopy.dbConfig);
185
+ await connection.connect();
186
+ } catch (err) {
187
+ Logger.info('Failed trying to create connection : %s : clearing for retry', err);
188
+ if (clearCacheOnConnectionFailure) {
189
+ this.connectionCache = new Map<string, Promise<ConnectionAndTunnel<Client, PostgresDbConfig>>>();
190
+ }
191
+ return undefined;
192
+ }
193
+ /*
194
+ TODO: Does postgres have error handlers like this (eg, like mysql driver?)
195
+ connection.on('error', (err) => {
196
+ Logger.info('An error was detected on the connection : %s : Clearing', err);
197
+ this.clearDatabaseAccessCache()
198
+ .then((cleared) => {
199
+ Logger.info('Connection cleared: %s', cleared);
200
+ })
201
+ .catch((err) => Logger.error('Failed to clear connection cache: %j', err));
202
+ });
203
+ Logger.info(
204
+ 'Added error handler to db, there are now %d error handlers and %d shutdown handlers',
205
+ connection.rawListeners('error').length,
206
+ process.rawListeners('exit').length,
207
+ );
208
+
209
+ */
210
+
211
+ const rval: ConnectionAndTunnel<Client, PostgresDbConfig> = {
212
+ config: dbCfg,
213
+ db: connection,
214
+ ssh: tunnel,
215
+ };
216
+
217
+ return rval;
218
+ }
219
+
220
+ private configPromise(): Promise<DatabaseConfigList<PostgresDbConfig>> {
221
+ if (!this.cacheConfigPromise) {
222
+ this.cacheConfigPromise = this.createConnectionConfig();
223
+ }
224
+ return this.cacheConfigPromise;
225
+ }
226
+
227
+ private async createConnectionConfig(): Promise<DatabaseConfigList<PostgresDbConfig>> {
228
+ RequireRatchet.notNullOrUndefined(this.configPromiseProvider, 'input');
229
+ const inputPromise: Promise<DatabaseConfigList<PostgresDbConfig>> = this.configPromiseProvider();
230
+ Logger.info('Creating connection config');
231
+ const cfg: DatabaseConfigList<PostgresDbConfig> = await inputPromise;
232
+ RequireRatchet.true(cfg.dbList.length > 0, 'input.dbList');
233
+
234
+ cfg.dbList.forEach((db) => {
235
+ const errors: string[] = PostgresStyleConnectionProvider.validDbConfig(db);
236
+ if (errors?.length) {
237
+ throw ErrorRatchet.fErr('Errors found in db config : %j', errors);
238
+ }
239
+ });
240
+ return cfg;
241
+ }
242
+
243
+ public static validDbConfig(cfg: PostgresDbConfig): string[] {
244
+ let rval: string[] = [];
245
+ if (!cfg) {
246
+ rval.push('The config is null');
247
+ } else if (!cfg.dbConfig) {
248
+ rval.push('The options field is null');
249
+ } else {
250
+ if (StringRatchet.trimToNull(cfg?.dbConfig?.connectionString)) {
251
+ // TODO: validate connection string here
252
+ } else {
253
+ rval.push(StringRatchet.trimToNull(cfg.dbConfig.host) ? null : 'host is required and non-empty');
254
+ rval.push(StringRatchet.trimToNull(cfg.label) ? null : 'label is required and non-empty');
255
+ rval.push(StringRatchet.trimToNull(cfg.dbConfig.database) ? null : 'database is required and non-empty');
256
+ rval.push(StringRatchet.trimToNull(cfg.dbConfig.user) ? null : 'user is required and non-empty');
257
+ rval.push(cfg.dbConfig.password ? null : 'password is required and non-empty');
258
+ rval.push(cfg.dbConfig.port ? null : 'port is required and non-empty');
259
+ }
260
+ }
261
+ if (cfg.sshTunnelConfig) {
262
+ rval.push(
263
+ StringRatchet.trimToNull(cfg.sshTunnelConfig.host) ? null : 'If sshTunnelConfig is non-null, host is required and non-empty',
264
+ );
265
+ rval.push(cfg.sshTunnelConfig.port ? null : 'If sshTunnelConfig is non-null, port is required and non-empty');
266
+ }
267
+ rval = rval.filter((s) => !!s);
268
+ return rval;
269
+ }
270
+ }
@@ -0,0 +1,76 @@
1
+ import { describe, test } from "vitest";
2
+ import { NamedParameterDatabaseService } from "../service/named-parameter-database-service.js";
3
+ import { Logger } from "@bitblit/ratchet-common/logger/logger";
4
+ import { SimpleQueryTextProvider } from "../model/simple-query-text-provider.js";
5
+ import { PostgresStyleConnectionProvider } from "./postgres-style-connection-provider";
6
+ import { DatabaseConfigList } from "../model/database-config-list";
7
+ import { PostgresDbConfig } from "./model/postgres-db-config";
8
+ import { StringRatchet } from "@bitblit/ratchet-common/lang/string-ratchet";
9
+ import { ModifyResults } from "../model/modify-results";
10
+
11
+ describe('postgres-style-database-access', () => {
12
+ const testQueries: SimpleQueryTextProvider = new SimpleQueryTextProvider({
13
+ create: 'create table testable (id SERIAL PRIMARY KEY, val varchar(255))',
14
+ fetchWithParam: 'select * from testable where val = :val',
15
+ singleIns: 'insert into testable (val) values (:val) RETURNING id',
16
+ singleDelete: 'delete from testable where val=:val',
17
+ csvIns: 'insert into testable (val) values (:valCsv)',
18
+ counter: 'select count(1) as cnt from testable',
19
+ multi: 'insert into testable (val) values :multiVal',
20
+ multiSelect: 'select * from testable',
21
+ });
22
+
23
+ test('runs test query', async () => {
24
+ const ratchetPostgresTestDatabaseConnectionString = StringRatchet.trimToNull(process.env['RATCHET_POSTGRES_TEST_DATABASE_CONNECT_STRING']);
25
+ if (ratchetPostgresTestDatabaseConnectionString) {
26
+ const cfgProvider = async () =>{
27
+ const rval: DatabaseConfigList<PostgresDbConfig> = {
28
+ dbList: [
29
+ {
30
+ label: 'test',
31
+ sshTunnelConfig: null,
32
+ dbConfig: {
33
+ connectionString: ratchetPostgresTestDatabaseConnectionString,
34
+ ssl: { rejectUnauthorized: false }
35
+ }
36
+ }
37
+ ]
38
+ };
39
+ return rval;
40
+ };
41
+
42
+ // Memory database
43
+ const prov: PostgresStyleConnectionProvider = new PostgresStyleConnectionProvider(cfgProvider);
44
+ const ns: NamedParameterDatabaseService = new NamedParameterDatabaseService({
45
+ serviceName: 'Test',
46
+ queryProvider: testQueries,
47
+ connectionProvider: prov,
48
+ queryDefaults: { databaseName: 'test', timeoutMS: 20_000 },
49
+ longQueryTimeMs: 8_500,
50
+ });
51
+ const testConnVal: any = await ns.testConnection(true);
52
+ Logger.info('TestConnection returned : %j', testConnVal);
53
+ let selectCounter: any = await ns.executeQueryByNameSingle('counter');
54
+ Logger.info('selectCounter returned : %j', selectCounter);
55
+ const multiSelect: any[] = await ns.executeQueryByName('multiSelect');
56
+ Logger.info('multiSelect returned : %j', multiSelect);
57
+ const testVal: string = 'TestValue:'+Date.now();
58
+ const insert: ModifyResults = await ns.executeUpdateOrInsertByName('singleIns', {val: testVal});
59
+ Logger.info('insert returned : %j', insert);
60
+ selectCounter = await ns.executeQueryByNameSingle('counter');
61
+ Logger.info('selectCounter returned : %j', selectCounter);
62
+
63
+ const fetch: any = await ns.executeQueryByNameSingle('fetchWithParam', {val: testVal});
64
+ Logger.info('fetch returned : %j', fetch);
65
+
66
+
67
+ const del: ModifyResults = await ns.executeUpdateOrInsertByName('singleDelete', {val: testVal});
68
+ Logger.info('del returned : %j', del);
69
+ selectCounter = await ns.executeQueryByNameSingle('counter');
70
+ Logger.info('selectCounter returned : %j', selectCounter);
71
+
72
+ } else {
73
+ Logger.info('Skipping - no connection string found')
74
+ }
75
+ }, 30_000);
76
+ });