@bitblit/ratchet-rdbms 6.0.146-alpha → 6.0.148-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,9 @@
1
+ import { FlushRemoteMode } from './flush-remote-mode.js';
2
+ import { FetchRemoteMode } from './fetch-remote-mode.js';
3
+ import { RemoteFileTracker } from '@bitblit/ratchet-common/network/remote-file-tracker/remote-file-tracker';
4
+
5
+ export interface SqliteRemoteFileSyncConfig {
6
+ remoteFileTracker: RemoteFileTracker<any>;
7
+ flushRemoteMode?: FlushRemoteMode;
8
+ fetchRemoteMode?: FetchRemoteMode;
9
+ }
@@ -0,0 +1,158 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { SqliteStyleConnectionProvider } from './sqlite-style-connection-provider.js';
3
+ import { NamedParameterDatabaseService } from '../service/named-parameter-database-service.js';
4
+ import { Logger } from '@bitblit/ratchet-common/logger/logger';
5
+ import { SimpleQueryTextProvider } from '../model/simple-query-text-provider.js';
6
+ import { ModifyResults } from '../model/modify-results.js';
7
+ import { RequestResults } from '../model/request-results.js';
8
+ import fs from 'fs';
9
+ import { ErrorRatchet } from '@bitblit/ratchet-common/lang/error-ratchet';
10
+ import path from 'path';
11
+
12
+ describe('sqlite-database-access', () => {
13
+ const testQueries: SimpleQueryTextProvider = new SimpleQueryTextProvider({
14
+ create: 'create table testable (val varchar(255))',
15
+ singleIns: 'insert into testable (val) values (:val)',
16
+ csvIns: 'insert into testable (val) values (:valCsv)',
17
+ counter: 'select count(1) as cnt from testable',
18
+ multi: 'insert into testable (val) values :multiVal',
19
+ });
20
+
21
+ test.skip('builds filtered', async () => {
22
+ const prov: SqliteStyleConnectionProvider = new SqliteStyleConnectionProvider(() => {
23
+ const pth: string = path.resolve(path.join(process.env['SQLITE_HOME'], 'ratchet-test.db'));
24
+ if (!fs.existsSync(pth)) {
25
+ throw ErrorRatchet.fErr('Cannot find file %s', pth);
26
+ }
27
+
28
+ return Promise.resolve({
29
+ dbList: [
30
+ {
31
+ label: 'test',
32
+ localFile: {
33
+ filePath: pth,
34
+ },
35
+ },
36
+ ],
37
+ });
38
+ });
39
+ const ns: NamedParameterDatabaseService = new NamedParameterDatabaseService({
40
+ serviceName: 'Test',
41
+ queryProvider: new SimpleQueryTextProvider({ default: 'SELECT * FROM TICKETS WHERE ticket_state IN (:ticketStateList)' }),
42
+ connectionProvider: prov,
43
+ queryDefaults: { databaseName: 'test', timeoutMS: 20_000 },
44
+ longQueryTimeMs: 8_500,
45
+ });
46
+
47
+ const res: any[] = await ns.buildAndExecute<any>(ns.queryBuilder('default').withParams({ ticketStateList: ['New', 'Complete'] }));
48
+
49
+ Logger.info('Get: %j', res);
50
+
51
+ expect(res).not.toBeNull;
52
+ });
53
+
54
+ test.skip('runs test query', async () => {
55
+ // Memory database
56
+ const prov: SqliteStyleConnectionProvider = new SqliteStyleConnectionProvider(() => {
57
+ return Promise.resolve({
58
+ dbList: [
59
+ {
60
+ label: 'test',
61
+ },
62
+ ],
63
+ });
64
+ });
65
+ const ns: NamedParameterDatabaseService = new NamedParameterDatabaseService({
66
+ serviceName: 'Test',
67
+ queryProvider: testQueries,
68
+ connectionProvider: prov,
69
+ queryDefaults: { databaseName: 'test', timeoutMS: 20_000 },
70
+ longQueryTimeMs: 8_500,
71
+ });
72
+ const val: any = await ns.testConnection(true);
73
+ Logger.info('Val was : %j', val);
74
+ });
75
+
76
+ test('handles path with sub', async () => {
77
+ // Memory database
78
+ const prov: SqliteStyleConnectionProvider = new SqliteStyleConnectionProvider(() => {
79
+ return Promise.resolve({
80
+ dbList: [
81
+ {
82
+ label: 'test',
83
+ },
84
+ ],
85
+ });
86
+ });
87
+ const ns: NamedParameterDatabaseService = new NamedParameterDatabaseService({
88
+ serviceName: 'Test',
89
+ queryProvider: testQueries,
90
+ connectionProvider: prov,
91
+ queryDefaults: { databaseName: 'test', timeoutMS: 20_000 },
92
+ longQueryTimeMs: 8_500,
93
+ });
94
+
95
+ // Create a table
96
+ const _createRes: any = await ns.executeUpdateOrInsertByName('create');
97
+
98
+ const myOb: Record<string, any> = {
99
+ val: ['t1', 't2'],
100
+ };
101
+
102
+ // Test insert
103
+ const singleIns: ModifyResults = await ns.buildAndExecuteUpdateOrInsert(
104
+ ns.queryBuilder('csvIns').withParams(myOb).withParam('valCsv', myOb['val'].join(',')),
105
+ );
106
+ expect(singleIns.affectedRows).toBeGreaterThan(0);
107
+
108
+ const singleCount: RequestResults<any> = await ns.executeQueryByNameSingle('counter', {});
109
+
110
+ expect(singleCount['cnt']).toEqual(1);
111
+
112
+ Logger.info('Get: %j', singleCount);
113
+ }, 30_000);
114
+
115
+ test('handles apostrophes in multi-value inserts', async () => {
116
+ // Memory database
117
+ const prov: SqliteStyleConnectionProvider = new SqliteStyleConnectionProvider(() => {
118
+ return Promise.resolve({
119
+ dbList: [
120
+ {
121
+ label: 'test',
122
+ },
123
+ ],
124
+ });
125
+ });
126
+ const ns: NamedParameterDatabaseService = new NamedParameterDatabaseService({
127
+ serviceName: 'Test',
128
+ queryProvider: testQueries,
129
+ connectionProvider: prov,
130
+ queryDefaults: { databaseName: 'test', timeoutMS: 20_000 },
131
+ longQueryTimeMs: 8_500,
132
+ });
133
+
134
+ // Create a table
135
+ const _createRes: any = await ns.executeUpdateOrInsertByName('create');
136
+
137
+ // Test single
138
+ const _singleIns: ModifyResults = await ns.executeUpdateOrInsertByName('singleIns', { val: 'val1' });
139
+
140
+ const singleCount: RequestResults<any> = await ns.executeQueryByNameSingle('counter', {});
141
+
142
+ expect(singleCount['cnt']).toEqual(1);
143
+
144
+ const _multiIns: ModifyResults = await ns.executeUpdateOrInsertByName('multi', { multiVal: [["val's are 2"], ['val3']] });
145
+
146
+ const multiCount: RequestResults<any> = await ns.executeQueryByNameSingle('counter', {});
147
+
148
+ expect(multiCount['cnt']).toEqual(3);
149
+
150
+ const _multiIns2: ModifyResults = await ns.executeUpdateOrInsertByName('multi', { multiVal: [["multi's apo's"]] });
151
+
152
+ const multiCount2: RequestResults<any> = await ns.executeQueryByNameSingle('counter', {});
153
+
154
+ expect(multiCount2['cnt']).toEqual(4);
155
+
156
+ Logger.info('Get: %j', singleCount);
157
+ }, 30_000);
158
+ });
@@ -0,0 +1,126 @@
1
+ import { Logger } from '@bitblit/ratchet-common/logger/logger';
2
+ import { ErrorRatchet } from '@bitblit/ratchet-common/lang/error-ratchet';
3
+ import SqlString from 'sqlstring';
4
+ import { DatabaseAccess } from '../model/database-access.js';
5
+ import { DatabaseRequestType } from '../model/database-request-type.js';
6
+ import { ModifyResults } from '../model/modify-results.js';
7
+ import { RequestResults } from '../model/request-results.js';
8
+ import { SqliteConnectionConfigFlag } from './model/sqlite-connection-config-flag.js';
9
+ import { Database, RunResult, Statement } from 'better-sqlite3';
10
+ import { NumberRatchet } from '@bitblit/ratchet-common/lang/number-ratchet';
11
+ import { NamedParameterAdapter } from "../util/named-parameter-adapter/named-parameter-adapter.ts";
12
+ import { QueryAndParams } from "../util/named-parameter-adapter/query-and-params.ts";
13
+
14
+ export class SqliteDatabaseAccess implements DatabaseAccess {
15
+ constructor(
16
+ private conn: Database,
17
+ private flags: SqliteConnectionConfigFlag[],
18
+ private extraConfig: Record<string, any>,
19
+ ) {}
20
+
21
+ get connection(): Database {
22
+ return this.conn;
23
+ }
24
+
25
+ beginTransaction(): Promise<void> {
26
+ throw ErrorRatchet.fErr('Transactions not supported in Sqlite');
27
+ }
28
+
29
+ async close(): Promise<boolean> {
30
+ try {
31
+ this.conn.close();
32
+ return true;
33
+ } catch (err) {
34
+ Logger.error('Failed to close : %s', err, err);
35
+ return false;
36
+ }
37
+ }
38
+
39
+ commitTransaction(): Promise<void> {
40
+ throw ErrorRatchet.fErr('Transactions not supported in Sqlite');
41
+ }
42
+
43
+ escape(value: any): string {
44
+ let rval: string = SqlString.escape(value);
45
+ rval = rval.replaceAll("\\'", "''"); // For some reason sqlite uses '' as the escape for '...
46
+ return rval;
47
+ }
48
+
49
+ async onRequestFailureOnly(_type: DatabaseRequestType): Promise<void> {
50
+ return Promise.resolve(undefined);
51
+ }
52
+
53
+ async onRequestSuccessOnly(_type: DatabaseRequestType): Promise<void> {
54
+ return Promise.resolve(undefined);
55
+ }
56
+
57
+ async onRequestSuccessOrFailure(_type: DatabaseRequestType): Promise<void> {
58
+ return Promise.resolve(undefined);
59
+ }
60
+
61
+ async preQuery(): Promise<void> {
62
+ return Promise.resolve(undefined);
63
+ }
64
+
65
+ async modify(query: string, fields: Record<string, any>): Promise<RequestResults<ModifyResults>> {
66
+ const qap: QueryAndParams = this.preprocessQuery({ query: query, params: fields });
67
+
68
+ const stmt: Statement = this.conn.prepare(qap.query);
69
+ const tmp: RunResult = stmt.run(qap.params);
70
+
71
+ const update: ModifyResults = {
72
+ changedRows: tmp.changes,
73
+ insertId: NumberRatchet.safeNumber(tmp.lastInsertRowid),
74
+ fieldCount: undefined,
75
+ affectedRows: tmp.changes,
76
+ info: undefined,
77
+ serverStatus: undefined,
78
+ warningStatus: undefined,
79
+ };
80
+ const res: RequestResults<ModifyResults> = {
81
+ results: update,
82
+ fields: null,
83
+ };
84
+ return res;
85
+ }
86
+
87
+ preprocessQuery(qap: QueryAndParams): QueryAndParams {
88
+ const rval: QueryAndParams = NamedParameterAdapter.applyNamedValuesToQuery(
89
+ qap,
90
+ (this.flags || []).includes(SqliteConnectionConfigFlag.AlwaysCollateNoCase) ? ' COLLATE NOCASE' : '',
91
+ this.escape
92
+ );
93
+ return rval;
94
+ }
95
+
96
+ async query<S>(inQuery: string, inFields: Record<string, any>): Promise<RequestResults<S>> {
97
+ const qap: QueryAndParams = this.preprocessQuery({ query: inQuery, params: inFields });
98
+
99
+ const stmt: Statement = this.conn.prepare(qap.query);
100
+ const res: S[] = stmt.all(qap.params) as S[];
101
+
102
+ const rval: RequestResults<S> = {
103
+ results: res as S,
104
+ fields: null,
105
+ };
106
+ return rval;
107
+ }
108
+
109
+ public async testConnection(logTestResults?: boolean): Promise<number | null> {
110
+ if (logTestResults) {
111
+ Logger.info('Running connection test');
112
+ }
113
+
114
+ const res: RequestResults<any> = await this.query('SELECT unixepoch()*1000 AS test', {});
115
+ const rows = res.results as { test: number }[];
116
+ const timestamp = rows.length === 1 ? rows[0].test : null;
117
+ if (logTestResults) {
118
+ Logger.info('Test returned : %j', timestamp);
119
+ }
120
+ return timestamp;
121
+ }
122
+
123
+ async rollbackTransaction(): Promise<void> {
124
+ throw ErrorRatchet.fErr('Transactions not supported in Sqlite');
125
+ }
126
+ }
@@ -0,0 +1,152 @@
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 SqlString from 'sqlstring';
5
+ import { SqliteDatabaseAccess } from './sqlite-database-access.js';
6
+ import { DatabaseAccess } from '../model/database-access.js';
7
+ import { DatabaseRequestType } from '../model/database-request-type.js';
8
+ import { RequestResults } from '../model/request-results.js';
9
+ import { ModifyResults } from '../model/modify-results.js';
10
+ import { SqliteRemoteFileSyncConfig } from './model/sqlite-remote-file-sync-config.js';
11
+ import { FlushRemoteMode } from './model/flush-remote-mode.js';
12
+ import { SqliteConnectionConfigFlag } from './model/sqlite-connection-config-flag.js';
13
+ import { FetchRemoteMode } from './model/fetch-remote-mode.js';
14
+ import DatabaseConstructor, { Database } from 'better-sqlite3';
15
+ import { RemoteStatusDataAndContent } from '@bitblit/ratchet-common/network/remote-file-tracker/remote-status-data-and-content';
16
+ import { RemoteFileTracker } from '@bitblit/ratchet-common/network/remote-file-tracker/remote-file-tracker';
17
+ import { RemoteStatusData } from '@bitblit/ratchet-common/network/remote-file-tracker/remote-status-data';
18
+
19
+ export class SqliteRemoteSyncDatabaseAccess implements DatabaseAccess {
20
+ private cacheDb: Promise<SqliteDatabaseAccess>;
21
+
22
+ constructor(
23
+ private cfg: SqliteRemoteFileSyncConfig,
24
+ private flags: SqliteConnectionConfigFlag[],
25
+ private extraConfig: Record<string, any>,
26
+ ) {
27
+ this.cfg.flushRemoteMode = this.cfg.flushRemoteMode || FlushRemoteMode.Auto;
28
+ this.cfg.fetchRemoteMode = this.cfg.fetchRemoteMode || FetchRemoteMode.EveryQuery;
29
+ }
30
+
31
+ private async db(): Promise<SqliteDatabaseAccess> {
32
+ if (!this.cacheDb) {
33
+ this.cacheDb = this.createDb();
34
+ }
35
+ return this.cacheDb;
36
+ }
37
+
38
+ public changeFlushRemoteMode(newMode: FlushRemoteMode): void {
39
+ RequireRatchet.notNullOrUndefined(newMode, 'newMode');
40
+ Logger.info('Changing flush remote mode from %s to %s', this.cfg.flushRemoteMode, newMode);
41
+ this.cfg.flushRemoteMode = newMode;
42
+ }
43
+
44
+ public changeFetchRemoteMode(newMode: FetchRemoteMode): void {
45
+ RequireRatchet.notNullOrUndefined(newMode, 'newMode');
46
+ Logger.info('Changing fetch remote mode from %s to %s', this.cfg.fetchRemoteMode, newMode);
47
+ this.cfg.fetchRemoteMode = newMode;
48
+ }
49
+
50
+ public async flushLocalToRemote(): Promise<void> {
51
+ Logger.info('Flushing to remote (Flush mode is %s)', this.cfg.flushRemoteMode);
52
+ const access: SqliteDatabaseAccess = await this.db();
53
+ const asBuffer: Buffer = access.connection.serialize({});
54
+ const result: RemoteStatusData<any> = await this.cfg.remoteFileTracker.pushUint8ArrayToRemote(asBuffer, { force: false, backup: true });
55
+ Logger.info('Result is %j', result);
56
+ }
57
+
58
+ public async reloadRemoteToLocalIfNeeded(): Promise<SqliteDatabaseAccess> {
59
+ Logger.info('Reloading remote to local (Fetch mode is %s)', this.cfg.fetchRemoteMode);
60
+ const needed: boolean = await this.cfg.remoteFileTracker.modifiedSinceLastSync();
61
+ if (needed) {
62
+ Logger.info('Reloading, remote is newer');
63
+ this.cacheDb = this.createDb(); // The old one is a memory db, just let it get garbage collected?
64
+ await this.cacheDb; // We want to pause in this case
65
+ } else {
66
+ Logger.info('Skipping - remote is not modified');
67
+ }
68
+ return this.cacheDb;
69
+ }
70
+
71
+ private async createDb(): Promise<SqliteDatabaseAccess> {
72
+ Logger.info('Pulling file local');
73
+ const data: RemoteStatusDataAndContent<any> = await this.cfg.remoteFileTracker.pullRemoteData();
74
+ Logger.info('Reading file as array');
75
+
76
+ const uint: Uint8Array = await RemoteFileTracker.dataAsUint8Array(data);
77
+ Logger.info('Converting to buffer (%d bytes)', uint.length);
78
+ const asBuffer: Buffer = Buffer.from(uint);
79
+ Logger.info('Got data %j %d', data.status, asBuffer.length);
80
+ Logger.info('Creating database');
81
+ const db: Database = new DatabaseConstructor(asBuffer);
82
+ const rval: SqliteDatabaseAccess = new SqliteDatabaseAccess(db, this.flags, this.extraConfig);
83
+ return rval;
84
+ }
85
+
86
+ beginTransaction(): Promise<void> {
87
+ throw ErrorRatchet.fErr('Transactions not supported in Sqlite');
88
+ }
89
+
90
+ async close(): Promise<boolean> {
91
+ try {
92
+ const db: SqliteDatabaseAccess = await this.db();
93
+ await db.close();
94
+ return true;
95
+ } catch (err) {
96
+ Logger.error('Failed to close : %s', err, err);
97
+ return false;
98
+ }
99
+ }
100
+
101
+ commitTransaction(): Promise<void> {
102
+ throw ErrorRatchet.fErr('Transactions not supported in Sqlite');
103
+ }
104
+
105
+ escape(value: any): string {
106
+ return SqlString.format('?', value);
107
+ //return StringRatchet.safeString(value);
108
+ }
109
+
110
+ async onRequestSuccessOnly(type: DatabaseRequestType): Promise<void> {
111
+ if (type === DatabaseRequestType.Modify && this.cfg.flushRemoteMode === FlushRemoteMode.Auto) {
112
+ Logger.info('Successful modification with auto mode - flushing remote');
113
+ await this.flushLocalToRemote();
114
+ }
115
+ }
116
+
117
+ /*
118
+ async onRequestFailureOnly(type: DatabaseRequestType): Promise<void>{}
119
+
120
+ async onRequestSuccessOrFailure(type: DatabaseRequestType): Promise<void> {
121
+ return Promise.resolve(undefined);
122
+ }
123
+
124
+
125
+ */
126
+
127
+ public async preQuery(): Promise<void> {
128
+ if (this?.cfg?.fetchRemoteMode === FetchRemoteMode.EveryQuery) {
129
+ Logger.debug('EveryQuery mode - checking remote');
130
+ await this.reloadRemoteToLocalIfNeeded();
131
+ }
132
+ }
133
+
134
+ async modify(query: string, fields: Record<string, any>): Promise<RequestResults<ModifyResults>> {
135
+ const db: SqliteDatabaseAccess = await this.db();
136
+ return db.modify(query, fields);
137
+ }
138
+
139
+ async query<S>(inQuery: string, inFields: Record<string, any>): Promise<RequestResults<S>> {
140
+ const db: SqliteDatabaseAccess = await this.db();
141
+ return db.query(inQuery, inFields);
142
+ }
143
+
144
+ public async testConnection(logTestResults?: boolean): Promise<number | null> {
145
+ const db: SqliteDatabaseAccess = await this.db();
146
+ return db.testConnection(logTestResults);
147
+ }
148
+
149
+ async rollbackTransaction(): Promise<void> {
150
+ throw ErrorRatchet.fErr('Transactions not supported in Sqlite');
151
+ }
152
+ }
@@ -0,0 +1,181 @@
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
+ import fs from 'fs';
6
+ import { SqliteDatabaseAccess } from './sqlite-database-access.js';
7
+ import { SqliteRemoteSyncDatabaseAccess } from './sqlite-remote-sync-database-access.js';
8
+ import { DatabaseAccessProvider } from '../model/database-access-provider.js';
9
+ import { DatabaseAccess } from '../model/database-access.js';
10
+ import { DatabaseConfigList } from '../model/database-config-list.js';
11
+ import { SqliteConnectionConfig } from './model/sqlite-connection-config.js';
12
+ import DatabaseConstructor, { Database } from 'better-sqlite3';
13
+
14
+ /**
15
+ */
16
+ export class SqliteStyleConnectionProvider implements DatabaseAccessProvider {
17
+ private connectionCache: Map<string, Promise<DatabaseAccess>> = new Map<string, Promise<DatabaseAccess>>(); //// Cache the promises to make it a single connection
18
+ private cacheConfigPromise: Promise<DatabaseConfigList<SqliteConnectionConfig>>;
19
+ constructor(
20
+ private configPromiseProvider: () => Promise<DatabaseConfigList<SqliteConnectionConfig>>,
21
+ private additionalConfig: Record<string, any> = {},
22
+ ) {
23
+ this.cacheConfigPromise = this.createSqliteConnectionConfig(); // Sets up tunnels, etc
24
+ Logger.info('Added shutdown handler to the process (Only once per instantiation)');
25
+ this.addShutdownHandlerToProcess();
26
+ }
27
+
28
+ private addShutdownHandlerToProcess(): void {
29
+ process.on('exit', () => {
30
+ Logger.info('Process is shutting down, closing connections');
31
+ this.clearDatabaseAccessCache().catch((err) => {
32
+ Logger.error('Shutdown connection failed : %s', err);
33
+ });
34
+ });
35
+ }
36
+
37
+ public async clearDatabaseAccessCache(): Promise<boolean> {
38
+ const rval = false;
39
+ Logger.info('Clearing connection cache for SqliteStyleConnectionProvider');
40
+ // First, clear the connection caches so that subsequent connection attempts start fresh
41
+ const oldConnections: Promise<DatabaseAccess>[] = Array.from(this.connectionCache.values());
42
+ //const oldDbHooks = this.dbPromise;
43
+ //const oldSshTunnels = this.tunnels;
44
+ this.cacheConfigPromise = null; // Re-read config in case the password expired, etc
45
+ this.connectionCache = new Map();
46
+ //this.tunnels = new Map();
47
+ // Resolve any leftover DB connections & end them
48
+ if (oldConnections.length > 0) {
49
+ for (let i = 0; i < oldConnections.length; i++) {
50
+ Logger.info('Shutting down old connection %d of %d', i, oldConnections.length);
51
+ try {
52
+ const conn: DatabaseAccess = await oldConnections[i];
53
+ Logger.info('Conn %d', i);
54
+ if (conn) {
55
+ Logger.info('Stopping connection to database');
56
+ try {
57
+ await conn.close();
58
+ Logger.info('Database connection closed');
59
+ } catch (err) {
60
+ if (ErrorRatchet.asErr(err).message.includes('closed state')) {
61
+ // DB was already closed, ignore
62
+ } else {
63
+ Logger.error('Something went wrong closing the database connection : %s', err);
64
+ }
65
+ }
66
+ }
67
+ } catch (err) {
68
+ Logger.warn('Shutdown failed : %s ', err, err);
69
+ }
70
+ }
71
+ }
72
+ Logger.info('Old db and tunnels removed');
73
+ return rval;
74
+ }
75
+
76
+ public async getDatabaseAccess(name: string): Promise<DatabaseAccess | undefined> {
77
+ Logger.silly('getConnectionAndTunnel : %s', name);
78
+ if (!this.connectionCache.has(name)) {
79
+ Logger.info('No connectionCache found for %s - creating new one', name);
80
+ const dbConfig: SqliteConnectionConfig = await this.getDbConfig(name);
81
+ const connection: Promise<DatabaseAccess> = this.createAsyncDatabase(dbConfig, this.additionalConfig, true);
82
+ this.connectionCache.set(name, connection);
83
+ Logger.info('Added connectionCache for %s', name);
84
+ }
85
+ return this.connectionCache.get(name);
86
+ }
87
+
88
+ private async getDbConfig(name: string): Promise<SqliteConnectionConfig> {
89
+ Logger.info('SqliteStyleConnectionProvider:getDbConfig:Initiating promise for %s', name);
90
+ const cfgs: DatabaseConfigList<SqliteConnectionConfig> = await this.configPromise();
91
+ const finder: string = StringRatchet.trimToEmpty(name).toLowerCase();
92
+ const dbConfig: SqliteConnectionConfig = cfgs.dbList.find((s) => StringRatchet.trimToEmpty(s.label).toLowerCase() === finder);
93
+ if (!dbConfig) {
94
+ throw ErrorRatchet.fErr(
95
+ 'Cannot find any connection config named %s (Available are %j)',
96
+ name,
97
+ cfgs.dbList.map((d) => d.label),
98
+ );
99
+ }
100
+ return dbConfig;
101
+ }
102
+
103
+ // Always creates a promise
104
+ private async createAsyncDatabase(
105
+ dbCfg: SqliteConnectionConfig,
106
+ _additionalConfig: Record<string, any> = {},
107
+ clearCacheOnConnectionFailure: boolean,
108
+ ): Promise<DatabaseAccess | undefined> {
109
+ Logger.info('In SqliteStyleConnectionProvider:createAsyncDatabase : %s', dbCfg.label);
110
+ RequireRatchet.notNullOrUndefined(dbCfg, 'dbCfg');
111
+
112
+ Logger.debug('Opening connection for SqliteStyleConnectionProvider');
113
+ let rval: DatabaseAccess;
114
+ try {
115
+ if (dbCfg.remoteFileSync) {
116
+ rval = new SqliteRemoteSyncDatabaseAccess(dbCfg.remoteFileSync, dbCfg.flags, _additionalConfig);
117
+ } else if (dbCfg.localFile) {
118
+ if (!fs.existsSync(dbCfg.localFile.filePath)) {
119
+ throw ErrorRatchet.fErr('Requested file does not exist : %s', dbCfg.localFile.filePath);
120
+ }
121
+ const db: Database = new DatabaseConstructor(dbCfg.localFile.filePath);
122
+ rval = new SqliteDatabaseAccess(db, dbCfg.flags, _additionalConfig);
123
+ } else {
124
+ Logger.info('Neither remote nor local file specified, using memory');
125
+ const db: Database = new DatabaseConstructor(':memory:');
126
+ rval = new SqliteDatabaseAccess(db, dbCfg.flags, _additionalConfig);
127
+ }
128
+ } catch (err) {
129
+ Logger.info('Failed trying to create connection : %s : clearing for retry', err);
130
+ if (clearCacheOnConnectionFailure) {
131
+ this.connectionCache = new Map<string, Promise<DatabaseAccess>>();
132
+ }
133
+ return undefined;
134
+ }
135
+
136
+ return rval;
137
+ }
138
+
139
+ private configPromise(): Promise<DatabaseConfigList<SqliteConnectionConfig>> {
140
+ if (!this.cacheConfigPromise) {
141
+ this.cacheConfigPromise = this.createSqliteConnectionConfig();
142
+ }
143
+ return this.cacheConfigPromise;
144
+ }
145
+
146
+ private async createSqliteConnectionConfig(): Promise<DatabaseConfigList<SqliteConnectionConfig>> {
147
+ RequireRatchet.notNullOrUndefined(this.configPromiseProvider, 'input');
148
+ const inputPromise: Promise<DatabaseConfigList<SqliteConnectionConfig>> = this.configPromiseProvider();
149
+ Logger.info('Creating connection config');
150
+ const cfg: DatabaseConfigList<SqliteConnectionConfig> = await inputPromise;
151
+ RequireRatchet.true(cfg.dbList.length > 0, 'input.dbList');
152
+
153
+ cfg.dbList.forEach((db) => {
154
+ const errors: string[] = SqliteStyleConnectionProvider.validDbConfig(db);
155
+ if (errors?.length) {
156
+ throw ErrorRatchet.fErr('Errors found in db config : %j', errors);
157
+ }
158
+ });
159
+ return cfg;
160
+ }
161
+
162
+ public static validDbConfig(cfg: SqliteConnectionConfig): string[] {
163
+ let rval: string[] = [];
164
+ if (!cfg) {
165
+ rval.push('The config is null');
166
+ } else {
167
+ if (cfg.localFile && cfg.remoteFileSync) {
168
+ rval.push('May not define both filePath and remoteFileSync');
169
+ }
170
+ if (cfg.localFile && !StringRatchet.trimToNull(cfg.localFile.filePath)) {
171
+ rval.push('Localfile provided but filepath is not');
172
+ }
173
+ if (cfg.remoteFileSync && !cfg.remoteFileSync.remoteFileTracker) {
174
+ rval.push('remoteFileTracker provided but remoteFileTracker value within is not');
175
+ }
176
+ rval.push(StringRatchet.trimToNull(cfg.label) ? null : 'label is required and non-empty');
177
+ }
178
+ rval = rval.filter((s) => !!s);
179
+ return rval;
180
+ }
181
+ }