@toxplanet/pegasus-sdk 1.1.18 → 1.1.20

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.
@@ -3,24 +3,13 @@ module.exports = {
3
3
  region: 'us-east-1',
4
4
  awsAccountId: '605134466764',
5
5
  sourceService: 'pegasus-sdk',
6
- secretName: 'arn:aws:secretsmanager:us-east-1:605134466764:secret:rds!cluster-9b502dde-5e2a-49db-b2c5-9801141ee40b-gkHbLm',
7
- openSearchEndpoint: 'https://1pbu0yqr197lq07hfcjh.us-east-1.aoss.amazonaws.com',
8
- openSearchIndex: 'chemicals',
6
+ secretArn: 'arn:aws:secretsmanager:us-east-1:605134466764:secret:rds!cluster-9b502dde-5e2a-49db-b2c5-9801141ee40b-gkHbLm',
7
+ clusterArn: 'arn:aws:rds:us-east-1:605134466764:cluster:cr-chemicals-acc',
9
8
  database: {
10
- host: 'cr-chemicals-acc.cluster-czgc4c20yerz.us-east-1.rds.amazonaws.com',
11
9
  name: 'chemicals'
12
10
  },
13
- postgres: {
14
- maxConnections: 2,
15
- minConnections: 0,
16
- idleTimeoutMillis: 30000,
17
- connectionTimeoutMillis: 5000,
18
- statementTimeout: 30000,
19
- queryTimeout: 30000,
20
- ssl: {
21
- rejectUnauthorized: false
22
- }
23
- },
11
+ openSearchEndpoint: 'https://1pbu0yqr197lq07hfcjh.us-east-1.aoss.amazonaws.com',
12
+ openSearchIndex: 'chemicals',
24
13
  indexRoutes: {
25
14
  chemicals: ['chemicals*'],
26
15
  documents: ['documents*'],
@@ -1,29 +1,18 @@
1
- module.exports = {
2
- environment: 'dev',
3
- region: 'us-east-1',
4
- awsAccountId: '292931567094',
5
- sourceService: 'pegasus-sdk',
6
- secretName: 'arn:aws:secretsmanager:us-east-1:292931567094:secret:rds!cluster-b851c3ce-58cc-41cd-aeae-05cc7f5e031a-ZYSjiI',
7
- openSearchEndpoint: 'https://war8lk73nzswquk8dcz1.us-east-1.aoss.amazonaws.com',
8
- openSearchIndex: 'chemicals',
9
- database: {
10
- host: 'cr-chemicals.cluster-cz0iqdg8irhb.us-east-1.rds.amazonaws.com',
11
- name: 'chemicals'
12
- },
13
- postgres: {
14
- maxConnections: 2,
15
- minConnections: 1,
16
- idleTimeoutMillis: 0,
17
- connectionTimeoutMillis: 15000,
18
- statementTimeout: 30000,
19
- queryTimeout: 30000,
20
- ssl: {
21
- rejectUnauthorized: false
22
- }
23
- },
24
- indexRoutes: {
25
- chemicals: ['chemicals*'],
26
- documents: ['documents*'],
27
- search: [/^(chemicals|substances|search)/]
28
- }
29
- };
1
+ module.exports = {
2
+ environment: 'dev',
3
+ region: 'us-east-1',
4
+ awsAccountId: '292931567094',
5
+ sourceService: 'pegasus-sdk',
6
+ secretArn: 'arn:aws:secretsmanager:us-east-1:292931567094:secret:rds!cluster-b851c3ce-58cc-41cd-aeae-05cc7f5e031a-ZYSjiI',
7
+ clusterArn: 'arn:aws:rds:us-east-1:292931567094:cluster:cr-chemicals',
8
+ database: {
9
+ name: 'chemicals'
10
+ },
11
+ openSearchEndpoint: 'https://war8lk73nzswquk8dcz1.us-east-1.aoss.amazonaws.com',
12
+ openSearchIndex: 'chemicals',
13
+ indexRoutes: {
14
+ chemicals: ['chemicals*'],
15
+ documents: ['documents*'],
16
+ search: [/^(chemicals|substances|search)/]
17
+ }
18
+ };
@@ -3,24 +3,13 @@ module.exports = {
3
3
  region: 'us-east-1',
4
4
  awsAccountId: '147997144422',
5
5
  sourceService: 'pegasus-sdk',
6
- secretName: 'rds!cluster-25483b3f-3758-43ed-9548-26c91de16c2d',
7
- openSearchEndpoint: 'https://odusb11s00j5hyy5r6.us-east-1.aoss.amazonaws.com',
8
- openSearchIndex: 'chemicals',
6
+ secretArn: 'arn:aws:secretsmanager:us-east-1:147997144422:secret:rds!cluster-25483b3f-3758-43ed-9548-26c91de16c2d',
7
+ clusterArn: 'arn:aws:rds:us-east-1:147997144422:cluster:cr-chemicals-prod',
9
8
  database: {
10
- host: 'cr-chemicals-qa.cluster-c7gakqksq9m4.us-east-1.rds.amazonaws.com',
11
9
  name: 'chemicals'
12
10
  },
13
- postgres: {
14
- maxConnections: 10,
15
- minConnections: 2,
16
- idleTimeoutMillis: 120000,
17
- connectionTimeoutMillis: 15000,
18
- statementTimeout: 120000,
19
- queryTimeout: 120000,
20
- ssl: {
21
- rejectUnauthorized: true
22
- }
23
- },
11
+ openSearchEndpoint: 'https://odusb11s00j5hyy5r6.us-east-1.aoss.amazonaws.com',
12
+ openSearchIndex: 'chemicals',
24
13
  indexRoutes: {
25
14
  chemicals: ['chemicals*'],
26
15
  documents: ['documents*'],
@@ -3,24 +3,13 @@ module.exports = {
3
3
  region: 'us-east-1',
4
4
  awsAccountId: '147997144422',
5
5
  sourceService: 'pegasus-sdk',
6
- secretName: 'arn:aws:secretsmanager:us-east-1:147997144422:secret:rds!cluster-25483b3f-3758-43ed-9548-26c91de16c2d-oYjysU',
7
- openSearchEndpoint: 'https://odusb11s00j5hyy5r6.us-east-1.aoss.amazonaws.com',
8
- openSearchIndex: 'chemicals',
6
+ secretArn: 'arn:aws:secretsmanager:us-east-1:147997144422:secret:rds!cluster-25483b3f-3758-43ed-9548-26c91de16c2d-oYjysU',
7
+ clusterArn: 'arn:aws:rds:us-east-1:147997144422:cluster:cr-chemicals-qa',
9
8
  database: {
10
- host: 'cr-chemicals-qa.cluster-c7gakqksq9m4.us-east-1.rds.amazonaws.com',
11
9
  name: 'chemicals'
12
10
  },
13
- postgres: {
14
- maxConnections: 5,
15
- minConnections: 1,
16
- idleTimeoutMillis: 60000,
17
- connectionTimeoutMillis: 10000,
18
- statementTimeout: 60000,
19
- queryTimeout: 60000,
20
- ssl: {
21
- rejectUnauthorized: true
22
- }
23
- },
11
+ openSearchEndpoint: 'https://odusb11s00j5hyy5r6.us-east-1.aoss.amazonaws.com',
12
+ openSearchIndex: 'chemicals',
24
13
  indexRoutes: {
25
14
  chemicals: ['chemicals*'],
26
15
  documents: ['documents*'],
package/lib/chemicals.js CHANGED
@@ -21,11 +21,9 @@ class ChemicalsService {
21
21
  this.sqsClient = null;
22
22
  }
23
23
 
24
- getDb() {
25
- if (!this.db) {
26
- this.db = getDrizzle(this.connection.pgPool);
27
- }
28
- return this.db;
24
+ async getDb() {
25
+ await this.connection.ensureConnected();
26
+ return this.connection.db;
29
27
  }
30
28
 
31
29
  async sendSqlWriteFailure({ sql, parameters, error, retryCount, failedAt }) {
@@ -75,28 +73,28 @@ class ChemicalsService {
75
73
  _buildChemicalUpsertSql(chemical) {
76
74
  const sql = [
77
75
  'INSERT INTO chemicals (source_id, chemical_name, chemical_meta, chemical_identifiers, chemical_synonyms, chemical_categories, created_at, updated_at)',
78
- 'VALUES (@source_id, @chemical_name, @chemical_meta::jsonb, @chemical_identifiers::jsonb, @chemical_synonyms, @chemical_categories, @created_at, @updated_at)',
76
+ 'VALUES (:source_id, :chemical_name, :chemical_meta::jsonb, :chemical_identifiers::jsonb, :chemical_synonyms, :chemical_categories, :created_at, :updated_at)',
79
77
  'ON CONFLICT (source_id) DO UPDATE SET',
80
- ' chemical_name = @chemical_name,',
81
- ' chemical_meta = @chemical_meta::jsonb,',
82
- ' chemical_identifiers = @chemical_identifiers::jsonb,',
83
- ' chemical_synonyms = @chemical_synonyms,',
84
- ' chemical_categories = @chemical_categories,',
85
- ' updated_at = @updated_at'
78
+ ' chemical_name = :chemical_name,',
79
+ ' chemical_meta = :chemical_meta::jsonb,',
80
+ ' chemical_identifiers = :chemical_identifiers::jsonb,',
81
+ ' chemical_synonyms = :chemical_synonyms,',
82
+ ' chemical_categories = :chemical_categories,',
83
+ ' updated_at = :updated_at'
86
84
  ].join('\n');
87
85
 
88
86
  const serializeDate = (d) => d instanceof Date ? d.toISOString() : d;
89
87
 
90
- const parameters = {
91
- '@source_id': chemical.sourceId,
92
- '@chemical_name': chemical.chemicalName,
93
- '@chemical_meta': JSON.stringify(chemical.chemicalMeta ?? {}),
94
- '@chemical_identifiers': JSON.stringify(chemical.chemicalIdentifiers ?? {}),
95
- '@chemical_synonyms': JSON.stringify(chemical.chemicalSynonyms ?? []),
96
- '@chemical_categories': JSON.stringify(chemical.chemicalCategories ?? []),
97
- '@created_at': serializeDate(chemical.createdAt),
98
- '@updated_at': serializeDate(chemical.updatedAt)
99
- };
88
+ const parameters = [
89
+ { name: 'source_id', value: { stringValue: chemical.sourceId } },
90
+ { name: 'chemical_name', value: { stringValue: chemical.chemicalName } },
91
+ { name: 'chemical_meta', value: { stringValue: JSON.stringify(chemical.chemicalMeta ?? {}) } },
92
+ { name: 'chemical_identifiers', value: { stringValue: JSON.stringify(chemical.chemicalIdentifiers ?? {}) } },
93
+ { name: 'chemical_synonyms', value: { stringValue: JSON.stringify(chemical.chemicalSynonyms ?? []) } },
94
+ { name: 'chemical_categories', value: { stringValue: JSON.stringify(chemical.chemicalCategories ?? []) } },
95
+ { name: 'created_at', value: { stringValue: serializeDate(chemical.createdAt) } },
96
+ { name: 'updated_at', value: { stringValue: serializeDate(chemical.updatedAt) } }
97
+ ];
100
98
 
101
99
  return { sql, parameters };
102
100
  }
@@ -142,15 +140,6 @@ class ChemicalsService {
142
140
  return { indexed: 0, errors: [], results: [] };
143
141
  }
144
142
 
145
- // Proactively validate the connection before any real query fires.
146
- // If idle too long, this reconnects first so the real query never faces
147
- // the full connectionTimeoutMillis wait on a stale pool.
148
- const reconnected = await this.connection.ensureConnected();
149
- if (reconnected) {
150
- this.db = null; // force getDb() to bind to the fresh pool
151
- }
152
-
153
- const db = this.getDb();
154
143
  const results = [];
155
144
  const errors = [];
156
145
 
@@ -183,14 +172,9 @@ class ChemicalsService {
183
172
  logInfo('pegasus-sdk', `[bulkIndexFielded] Prepared chemical object: sourceId=${chemical.sourceId}, chemicalName=${chemical.chemicalName}`);
184
173
  logInfo('pegasus-sdk', `[bulkIndexFielded] DEBUG SQL for document ${i}:\n${this._buildDebugSql(chemical)}`);
185
174
 
186
- const isConnectionError = (err) =>
187
- err.message?.toLowerCase().includes('timeout') ||
188
- err.message?.toLowerCase().includes('connection') ||
189
- err.code === 'ECONNREFUSED' ||
190
- err.code === 'ETIMEDOUT';
191
-
192
- const attemptUpsert = () =>
193
- db.insert(schema.chemicals)
175
+ const attemptUpsert = async () => {
176
+ const freshDb = await this.getDb();
177
+ return freshDb.insert(schema.chemicals)
194
178
  .values(chemical)
195
179
  .onConflictDoUpdate({
196
180
  target: schema.chemicals.sourceId,
@@ -207,6 +191,7 @@ class ChemicalsService {
207
191
  chemicalId: schema.chemicals.chemicalId,
208
192
  sourceId: schema.chemicals.sourceId
209
193
  });
194
+ };
210
195
 
211
196
  let lastError = null;
212
197
  let retryCount = 0;
@@ -215,39 +200,20 @@ class ChemicalsService {
215
200
  try {
216
201
  const [result] = await attemptUpsert();
217
202
  logInfo('pegasus-sdk', `[bulkIndexFielded] Document ${i} indexed successfully: ${result?.chemicalId || 'no ID returned'}`);
218
- this.connection.recordActivity();
219
203
  results.push({ index: i, success: true, result });
220
204
  continue;
221
205
  } catch (firstErr) {
222
206
  lastError = firstErr;
223
-
224
- if (isConnectionError(firstErr)) {
225
- // Stale pool — rebuild the connection and try once more before queuing
226
- logInfo('pegasus-sdk', `[bulkIndexFielded] Document ${i} connection error (${firstErr.message}), reconnecting pool and retrying`);
227
- try {
228
- await this.connection.reconnect();
229
- this.db = null; // force getDb() to bind to the new pool
230
- const [result] = await attemptUpsert();
231
- logInfo('pegasus-sdk', `[bulkIndexFielded] Document ${i} indexed successfully after reconnect: ${result?.chemicalId || 'no ID returned'}`);
232
- this.connection.recordActivity();
233
- results.push({ index: i, success: true, result });
234
- continue;
235
- } catch (reconnectErr) {
236
- lastError = reconnectErr;
237
- retryCount = 1;
238
- }
239
- } else {
240
- logInfo('pegasus-sdk', `[bulkIndexFielded] Document ${i} first attempt failed (${firstErr.message}), retrying once`);
241
- try {
242
- const [result] = await attemptUpsert();
243
- logInfo('pegasus-sdk', `[bulkIndexFielded] Document ${i} indexed successfully on retry: ${result?.chemicalId || 'no ID returned'}`);
244
- this.connection.recordActivity();
245
- results.push({ index: i, success: true, result });
246
- continue;
247
- } catch (retryErr) {
248
- lastError = retryErr;
249
- retryCount = 1;
250
- }
207
+ logInfo('pegasus-sdk', `[bulkIndexFielded] Document ${i} first attempt failed (${firstErr.message}), retrying once`);
208
+
209
+ try {
210
+ const [result] = await attemptUpsert();
211
+ logInfo('pegasus-sdk', `[bulkIndexFielded] Document ${i} indexed successfully on retry: ${result?.chemicalId || 'no ID returned'}`);
212
+ results.push({ index: i, success: true, result });
213
+ continue;
214
+ } catch (retryErr) {
215
+ lastError = retryErr;
216
+ retryCount = 1;
251
217
  }
252
218
  }
253
219
 
@@ -315,7 +281,7 @@ class ChemicalsService {
315
281
 
316
282
  async createChemical(chemical) {
317
283
  try {
318
- const db = this.getDb();
284
+ const db = await this.getDb();
319
285
 
320
286
  const [result] = await db
321
287
  .insert(schema.chemicals)
@@ -342,7 +308,7 @@ class ChemicalsService {
342
308
 
343
309
  async updateChemical(chemicalId, updates) {
344
310
  try {
345
- const db = this.getDb();
311
+ const db = await this.getDb();
346
312
 
347
313
  const updateData = {};
348
314
  if (updates.chemical_name) updateData.chemicalName = updates.chemical_name;
@@ -367,7 +333,7 @@ class ChemicalsService {
367
333
 
368
334
  async deleteChemical(chemicalId) {
369
335
  try {
370
- const db = this.getDb();
336
+ const db = await this.getDb();
371
337
 
372
338
  const [deleted] = await db
373
339
  .delete(schema.chemicals)
@@ -383,7 +349,7 @@ class ChemicalsService {
383
349
 
384
350
  async deleteBySourceId(sourceId) {
385
351
  try {
386
- const db = this.getDb();
352
+ const db = await this.getDb();
387
353
 
388
354
  const [deleted] = await db
389
355
  .delete(schema.chemicals)
@@ -399,7 +365,7 @@ class ChemicalsService {
399
365
 
400
366
  async deleteCollection(collectionName) {
401
367
  try {
402
- const db = this.getDb();
368
+ const db = await this.getDb();
403
369
 
404
370
  const deleted = await db
405
371
  .delete(schema.chemicals)
@@ -415,7 +381,7 @@ class ChemicalsService {
415
381
 
416
382
  async updateCollectionProperty(collectionName, propertyPath, newValue) {
417
383
  try {
418
- const db = this.getDb();
384
+ const db = await this.getDb();
419
385
  const pathArray = propertyPath.split('.');
420
386
  const valueJson = JSON.stringify(newValue);
421
387
 
@@ -437,7 +403,7 @@ class ChemicalsService {
437
403
 
438
404
  async bulkUpdateProperty(filter, propertyPath, newValue) {
439
405
  try {
440
- const db = this.getDb();
406
+ const db = await this.getDb();
441
407
 
442
408
  let whereCondition = sql`1=1`;
443
409
 
@@ -470,7 +436,7 @@ class ChemicalsService {
470
436
 
471
437
  async getChemicalById(chemicalId) {
472
438
  try {
473
- const db = this.getDb();
439
+ const db = await this.getDb();
474
440
 
475
441
  const [result] = await db
476
442
  .select()
@@ -487,7 +453,7 @@ class ChemicalsService {
487
453
 
488
454
  async getChemicalBySourceId(sourceId) {
489
455
  try {
490
- const db = this.getDb();
456
+ const db = await this.getDb();
491
457
 
492
458
  const [result] = await db
493
459
  .select()
@@ -504,7 +470,7 @@ class ChemicalsService {
504
470
 
505
471
  async getChemicalsByCAS(casNumber) {
506
472
  try {
507
- const db = this.getDb();
473
+ const db = await this.getDb();
508
474
 
509
475
  const results = await db
510
476
  .select()
@@ -524,7 +490,7 @@ class ChemicalsService {
524
490
  throw new Error(`Invalid identifier type: ${identifierType}`);
525
491
  }
526
492
 
527
- const db = this.getDb();
493
+ const db = await this.getDb();
528
494
 
529
495
  const results = await db
530
496
  .select()
@@ -540,7 +506,7 @@ class ChemicalsService {
540
506
 
541
507
  async countByCollection(collectionName) {
542
508
  try {
543
- const db = this.getDb();
509
+ const db = await this.getDb();
544
510
 
545
511
  const result = await db
546
512
  .select({ count: sql`count(*)::int` })
@@ -556,7 +522,7 @@ class ChemicalsService {
556
522
 
557
523
  async countByIdentifier(identifierValue) {
558
524
  try {
559
- const db = this.getDb();
525
+ const db = await this.getDb();
560
526
 
561
527
  const searchPattern = `%${escapeLikePattern(identifierValue)}%`;
562
528
  const result = await db
@@ -573,7 +539,7 @@ class ChemicalsService {
573
539
 
574
540
  async countByCAS(casNumber) {
575
541
  try {
576
- const db = this.getDb();
542
+ const db = await this.getDb();
577
543
 
578
544
  const result = await db
579
545
  .select({ count: sql`count(*)::int` })
@@ -589,7 +555,7 @@ class ChemicalsService {
589
555
 
590
556
  async getTotalSynonymCount() {
591
557
  try {
592
- const db = this.getDb();
558
+ const db = await this.getDb();
593
559
 
594
560
  const result = await db
595
561
  .select({ count: sql`sum(array_length(${schema.chemicals.chemicalSynonyms}, 1))::int` })
@@ -604,7 +570,7 @@ class ChemicalsService {
604
570
 
605
571
  async getSynonymCount(synonymTerm) {
606
572
  try {
607
- const db = this.getDb();
573
+ const db = await this.getDb();
608
574
 
609
575
  const result = await db
610
576
  .select({ count: sql`count(*)::int` })
@@ -620,7 +586,7 @@ class ChemicalsService {
620
586
 
621
587
  async convertIdentifier(fromIdentifier, toIdentifierType) {
622
588
  try {
623
- const db = this.getDb();
589
+ const db = await this.getDb();
624
590
 
625
591
  const searchPattern = `%${escapeLikePattern(fromIdentifier)}%`;
626
592
  const chemicals = await db
@@ -768,7 +734,7 @@ class ChemicalsService {
768
734
 
769
735
  async countAll() {
770
736
  try {
771
- const db = this.getDb();
737
+ const db = await this.getDb();
772
738
  const result = await db
773
739
  .select({ count: sql`count(*)::int` })
774
740
  .from(schema.chemicals);
@@ -781,7 +747,7 @@ class ChemicalsService {
781
747
 
782
748
  async findChemicalsWithoutDocuments(collectionName, searchTerm, pageSize = 100) {
783
749
  try {
784
- const db = this.getDb();
750
+ const db = await this.getDb();
785
751
 
786
752
  let whereConditions = [];
787
753
 
@@ -811,7 +777,7 @@ class ChemicalsService {
811
777
 
812
778
  async countChemicalsWithoutDocuments(collectionName) {
813
779
  try {
814
- const db = this.getDb();
780
+ const db = await this.getDb();
815
781
 
816
782
  const whereClause = collectionName
817
783
  ? arrayContains(schema.chemicals.chemicalCategories, [collectionName])
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 { getDrizzle, schema } = require('./db');
6
6
  const { loadConfig } = require('../config');
7
7
  const { logInfo, logError } = require('@toxplanet/tphelper/logging');
8
8
 
@@ -13,38 +13,16 @@ 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
+ this.db = null;
23
24
  this.osClient = null;
24
- this.secretsClient = null;
25
- this.cachedSecret = null;
26
25
  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
26
  }
49
27
 
50
28
  async connect() {
@@ -52,52 +30,32 @@ class PegasusConnection {
52
30
  return;
53
31
  }
54
32
 
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);
33
+ this.rdsDataClient = new RDSDataClient({ region: this.region });
75
34
 
76
- this.pgPool.on('error', (err) => {
77
- logError('pegasus-sdk', 'PegasusConnection', 'pgPool.error', err);
35
+ this.db = getDrizzle(this.rdsDataClient, {
36
+ resourceArn: this.clusterArn,
37
+ secretArn: this.secretArn,
38
+ database: this.databaseName
78
39
  });
79
40
 
80
- this.pgPool.on('connect', () => {
81
- this.lastActivityAt = Date.now();
82
- logInfo('pegasus-sdk', 'PostgreSQL client connected');
83
- });
41
+ logInfo('pegasus-sdk', 'RDS Data API client initialized');
84
42
 
85
- this.pgPool.on('remove', () => {
86
- logInfo('pegasus-sdk', 'PostgreSQL client removed from pool');
87
- });
88
-
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');
43
+ try {
44
+ const command = new ExecuteStatementCommand({
45
+ resourceArn: this.clusterArn,
46
+ secretArn: this.secretArn,
47
+ database: this.databaseName,
48
+ sql: 'SELECT 1'
49
+ });
50
+ await this.rdsDataClient.send(command);
51
+ logInfo('pegasus-sdk', 'RDS Data API connection verified and ready');
52
+ } catch (err) {
53
+ logError('pegasus-sdk', 'PegasusConnection', 'connect.verification', err);
54
+ throw err;
55
+ }
98
56
 
99
57
  if (this.openSearchEndpoint) {
100
- this.osClient = new Client({
58
+ this.osClient = new OpenSearchClient({
101
59
  ...AwsSigv4Signer({
102
60
  region: this.region,
103
61
  service: 'aoss',
@@ -113,77 +71,36 @@ class PegasusConnection {
113
71
  this.isConnected = true;
114
72
  }
115
73
 
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
74
  /**
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
- /**
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.
75
+ * With the RDS Data API, there are no stale connections or pool idleness.
76
+ * Each call is stateless. Just ensure the client is initialized.
145
77
  *
146
- * @returns {Promise<boolean>} true if a reconnect happened (caller should
147
- * reset any cached Drizzle db instance), false if connection is healthy.
78
+ * @returns {Promise<boolean>} true if a connect happened, false if already connected.
148
79
  */
149
80
  async ensureConnected() {
150
- if (!this.pgPool || !this.isConnected) {
81
+ if (!this.isConnected || !this.db) {
151
82
  await this.connect();
152
83
  return true;
153
84
  }
85
+ return false;
86
+ }
154
87
 
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`);
88
+ /**
89
+ * Data API is stateless, so reconnect simply means re-initializing.
90
+ * Called in fallback scenarios but not necessary for stale connections.
91
+ */
92
+ async reconnect() {
93
+ logInfo('pegasus-sdk', 'Reconnecting RDS Data API client');
94
+ this.db = null;
95
+ this.isConnected = false;
96
+ await this.connect();
97
+ }
163
98
 
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
- }
99
+ /**
100
+ * With stateless Data API, there is no meaningful "activity" tracking needed.
101
+ * This is a no-op for backward compatibility.
102
+ */
103
+ recordActivity() {
187
104
  }
188
105
 
189
106
  async disconnect() {
@@ -191,21 +108,18 @@ class PegasusConnection {
191
108
  return;
192
109
  }
193
110
 
194
- if (this.pgPool) {
195
- await this.pgPool.end();
196
- this.pgPool = null;
197
- }
198
-
111
+ this.db = null;
112
+ this.rdsDataClient = null;
199
113
  this.osClient = null;
200
114
  this.isConnected = false;
201
- this.cachedSecret = null;
115
+ logInfo('pegasus-sdk', 'RDS Data API client disconnected');
202
116
  }
203
117
 
204
118
  getPostgresClient() {
205
- if (!this.pgPool) {
206
- throw new Error('PostgreSQL connection not established. Call connect() first.');
119
+ if (!this.db) {
120
+ throw new Error('RDS Data API not initialized. Call connect() first.');
207
121
  }
208
- return this.pgPool;
122
+ return this.db;
209
123
  }
210
124
 
211
125
  getOpenSearchClient() {
@@ -221,54 +135,57 @@ class PegasusConnection {
221
135
 
222
136
  async testConnection() {
223
137
  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
- };
138
+ if (!this.db) {
139
+ throw new Error('RDS Data API not initialized');
140
+ }
141
+
142
+ const command = new ExecuteStatementCommand({
143
+ resourceArn: this.clusterArn,
144
+ secretArn: this.secretArn,
145
+ database: this.databaseName,
146
+ sql: 'SELECT NOW() as current_time, version() as pg_version'
147
+ });
148
+
149
+ const result = await this.rdsDataClient.send(command);
150
+ const row = result.records?.[0];
151
+
152
+ const pgStatus = {
153
+ connected: true,
154
+ timestamp: row?.[0]?.stringValue,
155
+ version: row?.[1]?.stringValue
156
+ };
237
157
 
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
- }
158
+ let osStatus = null;
159
+ if (this.osClient) {
160
+ try {
161
+ const indexName = this.getOpenSearchIndex();
162
+ const testSearch = await this.osClient.search({
163
+ index: indexName,
164
+ body: {
165
+ size: 1,
166
+ query: {
167
+ match: { chemical_name: 'benzene' }
249
168
  }
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
- }
169
+ }
170
+ });
171
+ osStatus = {
172
+ connected: true,
173
+ resultsFound: testSearch.body.hits.total.value || 0
174
+ };
175
+ } catch (osError) {
176
+ osStatus = {
177
+ connected: false,
178
+ error: osError.message
179
+ };
261
180
  }
262
-
263
- return {
264
- postgres: pgStatus,
265
- opensearch: osStatus,
266
- environment: this.environment,
267
- region: this.region
268
- };
269
181
  }
270
-
271
- throw new Error('No active connections');
182
+
183
+ return {
184
+ postgres: pgStatus,
185
+ opensearch: osStatus,
186
+ environment: this.environment,
187
+ region: this.region
188
+ };
272
189
  } catch (error) {
273
190
  return {
274
191
  postgres: { connected: false, error: error.message },
@@ -280,33 +197,34 @@ class PegasusConnection {
280
197
  }
281
198
 
282
199
  async query(sql, params) {
283
- const pool = this.getPostgresClient();
284
200
  const start = Date.now();
285
201
  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
- }
202
+
203
+ const command = new ExecuteStatementCommand({
204
+ resourceArn: this.clusterArn,
205
+ secretArn: this.secretArn,
206
+ database: this.databaseName,
207
+ sql,
208
+ parameters: params || []
209
+ });
290
210
 
291
- async getClient() {
292
- const pool = this.getPostgresClient();
293
- return pool.connect();
211
+ const result = await this.rdsDataClient.send(command);
212
+ logInfo('pegasus-sdk', `[SQL] rowCount: ${result.numberOfRecordsUpdated || 0} duration: ${Date.now() - start}ms`);
213
+
214
+ return {
215
+ rowCount: result.numberOfRecordsUpdated || result.records?.length || 0,
216
+ rows: result.records || [],
217
+ command: result
218
+ };
294
219
  }
295
220
 
296
221
  async transaction(callback) {
297
- const client = await this.getClient();
298
-
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();
222
+ if (!this.db) {
223
+ throw new Error('RDS Data API not initialized. Call connect() first.');
309
224
  }
225
+ return await this.db.transaction(async (trx) => {
226
+ return await callback(trx);
227
+ });
310
228
  }
311
229
  }
312
230
 
package/lib/db/index.js CHANGED
@@ -1,4 +1,4 @@
1
- const { drizzle } = require('drizzle-orm/node-postgres');
1
+ const { drizzle } = require('drizzle-orm/aws-data-api/pg');
2
2
  const { logInfo } = require('@toxplanet/tphelper/logging');
3
3
  const schema = require('./schema');
4
4
 
@@ -8,8 +8,8 @@ const logger = {
8
8
  }
9
9
  };
10
10
 
11
- function getDrizzle(pgPool) {
12
- return drizzle(pgPool, { schema, logger });
11
+ function getDrizzle(rdsDataClient, { resourceArn, secretArn, database }) {
12
+ return drizzle(rdsDataClient, { schema, logger, resourceArn, secretArn, database });
13
13
  }
14
14
 
15
15
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toxplanet/pegasus-sdk",
3
- "version": "1.1.18",
3
+ "version": "1.1.20",
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,9 @@
25
25
  "license": "MIT",
26
26
  "dependencies": {
27
27
  "@toxplanet/tphelper": "1.2.8",
28
- "pg": "^8.11.3",
29
28
  "drizzle-orm": "^0.30.0",
30
29
  "@opensearch-project/opensearch": "^2.5.0",
31
- "@aws-sdk/client-secrets-manager": "^3.490.0",
30
+ "@aws-sdk/client-rds-data": "^3.490.0",
32
31
  "@aws-sdk/client-sqs": "^3.490.0",
33
32
  "@aws-sdk/credential-providers": "^3.490.0"
34
33
  },