@toxplanet/pegasus-sdk 1.1.19 → 1.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/environment.acc.js +4 -15
- package/config/environment.dev.js +4 -15
- package/config/environment.prod.js +4 -15
- package/config/environment.qa.js +4 -15
- package/lib/chemicals.js +288 -267
- package/lib/connection.js +108 -213
- package/lib/db/index.js +20 -12
- package/package.json +2 -4
- package/lib/db/schema.js +0 -27
package/lib/connection.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
const {
|
|
2
|
-
const {
|
|
3
|
-
const { SecretsManagerClient, GetSecretValueCommand } = require('@aws-sdk/client-secrets-manager');
|
|
1
|
+
const { Client: OpenSearchClient } = require('@opensearch-project/opensearch');
|
|
2
|
+
const { RDSDataClient, ExecuteStatementCommand } = require('@aws-sdk/client-rds-data');
|
|
4
3
|
const { AwsSigv4Signer } = require('@opensearch-project/opensearch/aws');
|
|
5
4
|
const { fromNodeProviderChain } = require('@aws-sdk/credential-providers');
|
|
5
|
+
const { mapRecords } = require('./db');
|
|
6
6
|
const { loadConfig } = require('../config');
|
|
7
7
|
const { logInfo, logError } = require('@toxplanet/tphelper/logging');
|
|
8
8
|
|
|
@@ -13,38 +13,15 @@ class PegasusConnection {
|
|
|
13
13
|
this.config = { ...envConfig, ...config };
|
|
14
14
|
this.environment = this.config.environment;
|
|
15
15
|
this.region = this.config.region;
|
|
16
|
-
this.
|
|
16
|
+
this.secretArn = this.config.secretArn;
|
|
17
|
+
this.clusterArn = this.config.clusterArn;
|
|
17
18
|
this.openSearchEndpoint = this.config.openSearchEndpoint;
|
|
18
19
|
this.openSearchIndex = this.config.openSearchIndex;
|
|
19
|
-
this.databaseHost = this.config.database?.host;
|
|
20
20
|
this.databaseName = this.config.database?.name;
|
|
21
21
|
|
|
22
|
-
this.
|
|
22
|
+
this.rdsDataClient = null;
|
|
23
23
|
this.osClient = null;
|
|
24
|
-
this.secretsClient = null;
|
|
25
|
-
this.cachedSecret = null;
|
|
26
24
|
this.isConnected = false;
|
|
27
|
-
this.lastActivityAt = null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async getSecret() {
|
|
31
|
-
if (this.cachedSecret) {
|
|
32
|
-
return this.cachedSecret;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (!this.secretsClient) {
|
|
36
|
-
this.secretsClient = new SecretsManagerClient({ region: this.region });
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const command = new GetSecretValueCommand({ SecretId: this.secretName });
|
|
40
|
-
const response = await this.secretsClient.send(command);
|
|
41
|
-
|
|
42
|
-
if (response.SecretString) {
|
|
43
|
-
this.cachedSecret = JSON.parse(response.SecretString);
|
|
44
|
-
return this.cachedSecret;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
throw new Error(`Secret ${this.secretName} does not contain SecretString`);
|
|
48
25
|
}
|
|
49
26
|
|
|
50
27
|
async connect() {
|
|
@@ -52,52 +29,26 @@ class PegasusConnection {
|
|
|
52
29
|
return;
|
|
53
30
|
}
|
|
54
31
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const poolConfig = {
|
|
58
|
-
host: this.databaseHost,
|
|
59
|
-
port: 5432,
|
|
60
|
-
database: this.databaseName,
|
|
61
|
-
user: secret.username,
|
|
62
|
-
password: secret.password,
|
|
63
|
-
max: this.config.postgres.maxConnections,
|
|
64
|
-
min: this.config.postgres.minConnections,
|
|
65
|
-
idleTimeoutMillis: this.config.postgres.idleTimeoutMillis,
|
|
66
|
-
connectionTimeoutMillis: this.config.postgres.connectionTimeoutMillis,
|
|
67
|
-
keepAlive: true,
|
|
68
|
-
keepAliveInitialDelayMillis: 10000,
|
|
69
|
-
ssl: this.config.postgres.ssl,
|
|
70
|
-
statement_timeout: this.config.postgres.statementTimeout,
|
|
71
|
-
query_timeout: this.config.postgres.queryTimeout
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
this.pgPool = new Pool(poolConfig);
|
|
75
|
-
|
|
76
|
-
this.pgPool.on('error', (err) => {
|
|
77
|
-
logError('pegasus-sdk', 'PegasusConnection', 'pgPool.error', err);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
this.pgPool.on('connect', () => {
|
|
81
|
-
this.lastActivityAt = Date.now();
|
|
82
|
-
logInfo('pegasus-sdk', 'PostgreSQL client connected');
|
|
83
|
-
});
|
|
32
|
+
this.rdsDataClient = new RDSDataClient({ region: this.region });
|
|
84
33
|
|
|
85
|
-
|
|
86
|
-
logInfo('pegasus-sdk', 'PostgreSQL client removed from pool');
|
|
87
|
-
});
|
|
34
|
+
logInfo('pegasus-sdk', 'RDS Data API client initialized');
|
|
88
35
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
36
|
+
try {
|
|
37
|
+
const command = new ExecuteStatementCommand({
|
|
38
|
+
resourceArn: this.clusterArn,
|
|
39
|
+
secretArn: this.secretArn,
|
|
40
|
+
database: this.databaseName,
|
|
41
|
+
sql: 'SELECT 1'
|
|
42
|
+
});
|
|
43
|
+
await this.rdsDataClient.send(command);
|
|
44
|
+
logInfo('pegasus-sdk', 'RDS Data API connection verified and ready');
|
|
45
|
+
} catch (err) {
|
|
46
|
+
logError('pegasus-sdk', 'PegasusConnection', 'connect.verification', err);
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
98
49
|
|
|
99
50
|
if (this.openSearchEndpoint) {
|
|
100
|
-
this.osClient = new
|
|
51
|
+
this.osClient = new OpenSearchClient({
|
|
101
52
|
...AwsSigv4Signer({
|
|
102
53
|
region: this.region,
|
|
103
54
|
service: 'aoss',
|
|
@@ -113,77 +64,35 @@ class PegasusConnection {
|
|
|
113
64
|
this.isConnected = true;
|
|
114
65
|
}
|
|
115
66
|
|
|
116
|
-
async reconnect() {
|
|
117
|
-
logInfo('pegasus-sdk', 'Reconnecting PostgreSQL pool (stale connection detected)');
|
|
118
|
-
if (this.pgPool) {
|
|
119
|
-
try {
|
|
120
|
-
await this.pgPool.end();
|
|
121
|
-
} catch (err) {
|
|
122
|
-
logError('pegasus-sdk', 'PegasusConnection', 'reconnect.end', err);
|
|
123
|
-
}
|
|
124
|
-
this.pgPool = null;
|
|
125
|
-
}
|
|
126
|
-
this.isConnected = false;
|
|
127
|
-
this.lastActivityAt = null;
|
|
128
|
-
await this.connect();
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Call this after any successful database operation so ensureConnected()
|
|
133
|
-
* knows the connection was recently healthy and can skip the liveness check.
|
|
134
|
-
*/
|
|
135
|
-
recordActivity() {
|
|
136
|
-
this.lastActivityAt = Date.now();
|
|
137
|
-
}
|
|
138
|
-
|
|
139
67
|
/**
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* gone stale, a lightweight SELECT 1 is issued first. If that fails, the
|
|
143
|
-
* pool is rebuilt before the caller's real query fires — avoiding the full
|
|
144
|
-
* connectionTimeoutMillis wait on the real query.
|
|
68
|
+
* With the RDS Data API, there are no stale connections or pool idleness.
|
|
69
|
+
* Each call is stateless. Just ensure the client is initialized.
|
|
145
70
|
*
|
|
146
|
-
* @returns {Promise<boolean>} true if a
|
|
147
|
-
* reset any cached Drizzle db instance), false if connection is healthy.
|
|
71
|
+
* @returns {Promise<boolean>} true if a connect happened, false if already connected.
|
|
148
72
|
*/
|
|
149
73
|
async ensureConnected() {
|
|
150
|
-
if (!this.
|
|
74
|
+
if (!this.isConnected) {
|
|
151
75
|
await this.connect();
|
|
152
76
|
return true;
|
|
153
77
|
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
154
80
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Data API is stateless, so reconnect simply means re-initializing.
|
|
83
|
+
* Called in fallback scenarios but not necessary for stale connections.
|
|
84
|
+
*/
|
|
85
|
+
async reconnect() {
|
|
86
|
+
logInfo('pegasus-sdk', 'Reconnecting RDS Data API client');
|
|
87
|
+
this.isConnected = false;
|
|
88
|
+
await this.connect();
|
|
89
|
+
}
|
|
163
90
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
this.pgPool.connect(),
|
|
170
|
-
new Promise((_, reject) =>
|
|
171
|
-
setTimeout(() => reject(new Error('liveness check timeout')), 3000)
|
|
172
|
-
)
|
|
173
|
-
]);
|
|
174
|
-
await client.query('SELECT 1');
|
|
175
|
-
client.release();
|
|
176
|
-
this.lastActivityAt = Date.now();
|
|
177
|
-
logInfo('pegasus-sdk', '[ensureConnected] Liveness check passed');
|
|
178
|
-
return false;
|
|
179
|
-
} catch (err) {
|
|
180
|
-
if (client) {
|
|
181
|
-
try { client.release(true); } catch (_) {}
|
|
182
|
-
}
|
|
183
|
-
logInfo('pegasus-sdk', `[ensureConnected] Liveness check failed (${err.message}) — reconnecting pool proactively`);
|
|
184
|
-
await this.reconnect();
|
|
185
|
-
return true;
|
|
186
|
-
}
|
|
91
|
+
/**
|
|
92
|
+
* With stateless Data API, there is no meaningful "activity" tracking needed.
|
|
93
|
+
* This is a no-op for backward compatibility.
|
|
94
|
+
*/
|
|
95
|
+
recordActivity() {
|
|
187
96
|
}
|
|
188
97
|
|
|
189
98
|
async disconnect() {
|
|
@@ -191,21 +100,10 @@ class PegasusConnection {
|
|
|
191
100
|
return;
|
|
192
101
|
}
|
|
193
102
|
|
|
194
|
-
|
|
195
|
-
await this.pgPool.end();
|
|
196
|
-
this.pgPool = null;
|
|
197
|
-
}
|
|
198
|
-
|
|
103
|
+
this.rdsDataClient = null;
|
|
199
104
|
this.osClient = null;
|
|
200
105
|
this.isConnected = false;
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
getPostgresClient() {
|
|
205
|
-
if (!this.pgPool) {
|
|
206
|
-
throw new Error('PostgreSQL connection not established. Call connect() first.');
|
|
207
|
-
}
|
|
208
|
-
return this.pgPool;
|
|
106
|
+
logInfo('pegasus-sdk', 'RDS Data API client disconnected');
|
|
209
107
|
}
|
|
210
108
|
|
|
211
109
|
getOpenSearchClient() {
|
|
@@ -221,54 +119,59 @@ class PegasusConnection {
|
|
|
221
119
|
|
|
222
120
|
async testConnection() {
|
|
223
121
|
try {
|
|
224
|
-
if (this.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
122
|
+
if (!this.rdsDataClient) {
|
|
123
|
+
throw new Error('RDS Data API not initialized');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const command = new ExecuteStatementCommand({
|
|
127
|
+
resourceArn: this.clusterArn,
|
|
128
|
+
secretArn: this.secretArn,
|
|
129
|
+
database: this.databaseName,
|
|
130
|
+
sql: 'SELECT NOW() as current_time, version() as pg_version',
|
|
131
|
+
includeResultMetadata: true
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const result = await this.rdsDataClient.send(command);
|
|
135
|
+
const rows = mapRecords(result.records, result.columnMetadata);
|
|
136
|
+
const row = rows?.[0];
|
|
137
|
+
|
|
138
|
+
const pgStatus = {
|
|
139
|
+
connected: true,
|
|
140
|
+
timestamp: row?.current_time,
|
|
141
|
+
version: row?.pg_version
|
|
142
|
+
};
|
|
237
143
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
144
|
+
let osStatus = null;
|
|
145
|
+
if (this.osClient) {
|
|
146
|
+
try {
|
|
147
|
+
const indexName = this.getOpenSearchIndex();
|
|
148
|
+
const testSearch = await this.osClient.search({
|
|
149
|
+
index: indexName,
|
|
150
|
+
body: {
|
|
151
|
+
size: 1,
|
|
152
|
+
query: {
|
|
153
|
+
match: { chemical_name: 'benzene' }
|
|
249
154
|
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
osStatus = {
|
|
158
|
+
connected: true,
|
|
159
|
+
resultsFound: testSearch.body.hits.total.value || 0
|
|
160
|
+
};
|
|
161
|
+
} catch (osError) {
|
|
162
|
+
osStatus = {
|
|
163
|
+
connected: false,
|
|
164
|
+
error: osError.message
|
|
165
|
+
};
|
|
261
166
|
}
|
|
262
|
-
|
|
263
|
-
return {
|
|
264
|
-
postgres: pgStatus,
|
|
265
|
-
opensearch: osStatus,
|
|
266
|
-
environment: this.environment,
|
|
267
|
-
region: this.region
|
|
268
|
-
};
|
|
269
167
|
}
|
|
270
|
-
|
|
271
|
-
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
postgres: pgStatus,
|
|
171
|
+
opensearch: osStatus,
|
|
172
|
+
environment: this.environment,
|
|
173
|
+
region: this.region
|
|
174
|
+
};
|
|
272
175
|
} catch (error) {
|
|
273
176
|
return {
|
|
274
177
|
postgres: { connected: false, error: error.message },
|
|
@@ -280,33 +183,25 @@ class PegasusConnection {
|
|
|
280
183
|
}
|
|
281
184
|
|
|
282
185
|
async query(sql, params) {
|
|
283
|
-
const pool = this.getPostgresClient();
|
|
284
186
|
const start = Date.now();
|
|
285
187
|
logInfo('pegasus-sdk', `[SQL] ${sql}${params ? ` -- params: ${JSON.stringify(params)}` : ''}`);
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
188
|
+
|
|
189
|
+
const command = new ExecuteStatementCommand({
|
|
190
|
+
resourceArn: this.clusterArn,
|
|
191
|
+
secretArn: this.secretArn,
|
|
192
|
+
database: this.databaseName,
|
|
193
|
+
sql,
|
|
194
|
+
parameters: params || [],
|
|
195
|
+
includeResultMetadata: true
|
|
196
|
+
});
|
|
295
197
|
|
|
296
|
-
|
|
297
|
-
|
|
198
|
+
const result = await this.rdsDataClient.send(command);
|
|
199
|
+
logInfo('pegasus-sdk', `[SQL] rowCount: ${result.numberOfRecordsUpdated || 0} duration: ${Date.now() - start}ms`);
|
|
298
200
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
return result;
|
|
304
|
-
} catch (error) {
|
|
305
|
-
await client.query('ROLLBACK');
|
|
306
|
-
throw error;
|
|
307
|
-
} finally {
|
|
308
|
-
client.release();
|
|
309
|
-
}
|
|
201
|
+
return {
|
|
202
|
+
rowCount: result.numberOfRecordsUpdated || result.records?.length || 0,
|
|
203
|
+
rows: mapRecords(result.records, result.columnMetadata)
|
|
204
|
+
};
|
|
310
205
|
}
|
|
311
206
|
}
|
|
312
207
|
|
package/lib/db/index.js
CHANGED
|
@@ -1,18 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
function getFieldValue(field) {
|
|
2
|
+
if (!field || field.isNull) return null;
|
|
3
|
+
if ('stringValue' in field) return field.stringValue;
|
|
4
|
+
if ('longValue' in field) return field.longValue;
|
|
5
|
+
if ('doubleValue' in field) return field.doubleValue;
|
|
6
|
+
if ('booleanValue' in field) return field.booleanValue;
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
4
9
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
};
|
|
10
|
+
function mapRecord(record, columnMetadata) {
|
|
11
|
+
const obj = {};
|
|
12
|
+
columnMetadata.forEach((col, i) => {
|
|
13
|
+
obj[col.name] = getFieldValue(record[i]);
|
|
14
|
+
});
|
|
15
|
+
return obj;
|
|
16
|
+
}
|
|
10
17
|
|
|
11
|
-
function
|
|
12
|
-
return
|
|
18
|
+
function mapRecords(records = [], columnMetadata = []) {
|
|
19
|
+
return records.map(r => mapRecord(r, columnMetadata));
|
|
13
20
|
}
|
|
14
21
|
|
|
15
22
|
module.exports = {
|
|
16
|
-
|
|
17
|
-
|
|
23
|
+
getFieldValue,
|
|
24
|
+
mapRecord,
|
|
25
|
+
mapRecords
|
|
18
26
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toxplanet/pegasus-sdk",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.21",
|
|
4
4
|
"description": "SDK for migrating chemical data to Pegasus PostgreSQL + OpenSearch architecture with Elasticsearch client compatibility",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -25,10 +25,8 @@
|
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@toxplanet/tphelper": "1.2.8",
|
|
28
|
-
"pg": "^8.11.3",
|
|
29
|
-
"drizzle-orm": "^0.30.0",
|
|
30
28
|
"@opensearch-project/opensearch": "^2.5.0",
|
|
31
|
-
"@aws-sdk/client-
|
|
29
|
+
"@aws-sdk/client-rds-data": "^3.490.0",
|
|
32
30
|
"@aws-sdk/client-sqs": "^3.490.0",
|
|
33
31
|
"@aws-sdk/credential-providers": "^3.490.0"
|
|
34
32
|
},
|
package/lib/db/schema.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
const { pgTable, uuid, text, jsonb, timestamp, index } = require('drizzle-orm/pg-core');
|
|
2
|
-
const { sql } = require('drizzle-orm');
|
|
3
|
-
|
|
4
|
-
const chemicals = pgTable('chemicals', {
|
|
5
|
-
chemicalId: uuid('chemical_id').defaultRandom().primaryKey(),
|
|
6
|
-
sourceId: text('source_id').notNull().unique(),
|
|
7
|
-
chemicalName: text('chemical_name').notNull(),
|
|
8
|
-
chemicalMeta: jsonb('chemical_meta'),
|
|
9
|
-
chemicalIdentifiers: jsonb('chemical_identifiers'),
|
|
10
|
-
chemicalSynonyms: text('chemical_synonyms').array(),
|
|
11
|
-
chemicalCategories: text('chemical_categories').array(),
|
|
12
|
-
createdAt: timestamp('created_at', { withTimezone: true }).notNull(),
|
|
13
|
-
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull(),
|
|
14
|
-
importedAt: timestamp('imported_at', { withTimezone: true }).defaultNow()
|
|
15
|
-
}, (table) => {
|
|
16
|
-
return {
|
|
17
|
-
nameIdx: index('idx_chemicals_name').on(table.chemicalName),
|
|
18
|
-
createdAtIdx: index('idx_chemicals_created_at').on(table.createdAt),
|
|
19
|
-
updatedAtIdx: index('idx_chemicals_updated_at').on(table.updatedAt),
|
|
20
|
-
identifiersGinIdx: index('idx_chemicals_identifiers_gin').on(table.chemicalIdentifiers),
|
|
21
|
-
synonymsGinIdx: index('idx_chemicals_synonyms_gin').on(table.chemicalSynonyms)
|
|
22
|
-
};
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
module.exports = {
|
|
26
|
-
chemicals
|
|
27
|
-
};
|