@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/lib/connection.js CHANGED
@@ -1,8 +1,8 @@
1
- const { Pool } = require('pg');
2
- const { Client } = require('@opensearch-project/opensearch');
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.secretName = this.config.secretName;
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.pgPool = null;
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
- const secret = await this.getSecret();
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
- this.pgPool.on('remove', () => {
86
- logInfo('pegasus-sdk', 'PostgreSQL client removed from pool');
87
- });
34
+ logInfo('pegasus-sdk', 'RDS Data API client initialized');
88
35
 
89
- // Eagerly verify the connection is actually reachable before declaring
90
- // isConnected = true. Without this, new Pool() never opens a socket and
91
- // isConnected would be a lie — the first real query would pay the full
92
- // TCP+SSL+auth cost while racing the connectionTimeoutMillis.
93
- const verifyClient = await this.pgPool.connect();
94
- await verifyClient.query('SELECT 1');
95
- verifyClient.release();
96
- this.lastActivityAt = Date.now();
97
- logInfo('pegasus-sdk', 'PostgreSQL connection verified and ready');
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 Client({
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
- * Proactively verify the connection is alive before executing a batch of
141
- * queries. If the pool has been idle long enough that connections may have
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 reconnect happened (caller should
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.pgPool || !this.isConnected) {
74
+ if (!this.isConnected) {
151
75
  await this.connect();
152
76
  return true;
153
77
  }
78
+ return false;
79
+ }
154
80
 
155
- // If we used this connection recently, trust it's still alive.
156
- const FRESH_THRESHOLD_MS = 60_000;
157
- const idleSince = Date.now() - (this.lastActivityAt || 0);
158
- if (idleSince < FRESH_THRESHOLD_MS) {
159
- return false;
160
- }
161
-
162
- logInfo('pegasus-sdk', `[ensureConnected] Pool inactive for ${Math.round(idleSince / 1000)}s — running liveness check before query`);
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
- let client;
165
- try {
166
- // Use a short race timeout so a dead connection fails fast rather than
167
- // waiting the full connectionTimeoutMillis before we know to reconnect.
168
- client = await Promise.race([
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
- if (this.pgPool) {
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
- this.cachedSecret = null;
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.pgPool) {
225
- const client = await this.pgPool.connect();
226
- const result = await client.query('SELECT NOW() as current_time, version() as pg_version');
227
- client.release();
228
-
229
- const pgStatus = {
230
- connected: true,
231
- timestamp: result.rows[0].current_time,
232
- version: result.rows[0].pg_version,
233
- poolSize: this.pgPool.totalCount,
234
- idleConnections: this.pgPool.idleCount,
235
- waitingRequests: this.pgPool.waitingCount
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
- let osStatus = null;
239
- if (this.osClient) {
240
- try {
241
- const indexName = this.getOpenSearchIndex();
242
- const testSearch = await this.osClient.search({
243
- index: indexName,
244
- body: {
245
- size: 1,
246
- query: {
247
- match: { chemical_name: 'benzene' }
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
- osStatus = {
252
- connected: true,
253
- resultsFound: testSearch.body.hits.total.value || 0
254
- };
255
- } catch (osError) {
256
- osStatus = {
257
- connected: false,
258
- error: osError.message
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
- throw new Error('No active connections');
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
- const result = await pool.query(sql, params);
287
- logInfo('pegasus-sdk', `[SQL] rowCount: ${result.rowCount} duration: ${Date.now() - start}ms`);
288
- return result;
289
- }
290
-
291
- async getClient() {
292
- const pool = this.getPostgresClient();
293
- return pool.connect();
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
- async transaction(callback) {
297
- const client = await this.getClient();
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
- try {
300
- await client.query('BEGIN');
301
- const result = await callback(client);
302
- await client.query('COMMIT');
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
- const { drizzle } = require('drizzle-orm/node-postgres');
2
- const { logInfo } = require('@toxplanet/tphelper/logging');
3
- const schema = require('./schema');
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
- const logger = {
6
- logQuery(query, params) {
7
- logInfo('pegasus-sdk', `[SQL] ${query}${params?.length ? ` -- params: ${JSON.stringify(params)}` : ''}`);
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 getDrizzle(pgPool) {
12
- return drizzle(pgPool, { schema, logger });
18
+ function mapRecords(records = [], columnMetadata = []) {
19
+ return records.map(r => mapRecord(r, columnMetadata));
13
20
  }
14
21
 
15
22
  module.exports = {
16
- getDrizzle,
17
- schema
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.19",
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-secrets-manager": "^3.490.0",
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
- };