@toxplanet/pegasus-sdk 1.1.20 → 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/chemicals.js CHANGED
@@ -1,6 +1,4 @@
1
1
  const { logError, logInfo } = require('@toxplanet/tphelper/logging');
2
- const { getDrizzle, schema } = require('./db');
3
- const { eq, sql, and, inArray, arrayContains } = require('drizzle-orm');
4
2
  const { SQSClient, SendMessageCommand } = require('@aws-sdk/client-sqs');
5
3
 
6
4
  const SEARCH_BOOST_EXACT_PRIMARY = 100;
@@ -14,16 +12,66 @@ function escapeLikePattern(value) {
14
12
  return value.replace(/[%_\\]/g, '\\$&');
15
13
  }
16
14
 
15
+ function parsePostgresArray(str) {
16
+ if (!str || str === '{}') return [];
17
+ const trimmed = str.slice(1, -1);
18
+ if (!trimmed) return [];
19
+ const result = [];
20
+ let current = '';
21
+ let inQuotes = false;
22
+ for (let i = 0; i < trimmed.length; i++) {
23
+ const char = trimmed[i];
24
+ if (char === '"') {
25
+ if (inQuotes && trimmed[i + 1] === '"') {
26
+ current += '"';
27
+ i++;
28
+ } else {
29
+ inQuotes = !inQuotes;
30
+ }
31
+ } else if (char === ',' && !inQuotes) {
32
+ result.push(current);
33
+ current = '';
34
+ } else {
35
+ current += char;
36
+ }
37
+ }
38
+ if (current) result.push(current);
39
+ return result;
40
+ }
41
+
17
42
  class ChemicalsService {
18
43
  constructor(connection) {
19
44
  this.connection = connection;
20
- this.db = null;
21
45
  this.sqsClient = null;
22
46
  }
23
47
 
24
- async getDb() {
25
- await this.connection.ensureConnected();
26
- return this.connection.db;
48
+ _parsePostgresArray(str) {
49
+ return parsePostgresArray(str);
50
+ }
51
+
52
+ _toPostgresArray(arr) {
53
+ if (!Array.isArray(arr) || arr.length === 0) return '{}';
54
+ return '{' + arr.map(s => `"${String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`).join(',') + '}';
55
+ }
56
+
57
+ _serializeDate(d) {
58
+ return d instanceof Date ? d.toISOString() : (d || new Date().toISOString());
59
+ }
60
+
61
+ _mapChemicalRow(row) {
62
+ if (!row) return null;
63
+ return {
64
+ chemicalId: row.chemical_id,
65
+ sourceId: row.source_id,
66
+ chemicalName: row.chemical_name,
67
+ chemicalMeta: row.chemical_meta ? JSON.parse(row.chemical_meta) : null,
68
+ chemicalIdentifiers: row.chemical_identifiers ? JSON.parse(row.chemical_identifiers) : null,
69
+ chemicalSynonyms: this._parsePostgresArray(row.chemical_synonyms),
70
+ chemicalCategories: this._parsePostgresArray(row.chemical_categories),
71
+ createdAt: row.created_at,
72
+ updatedAt: row.updated_at,
73
+ importedAt: row.imported_at
74
+ };
27
75
  }
28
76
 
29
77
  async sendSqlWriteFailure({ sql, parameters, error, retryCount, failedAt }) {
@@ -71,32 +119,43 @@ class ChemicalsService {
71
119
  }
72
120
 
73
121
  _buildChemicalUpsertSql(chemical) {
74
- const sql = [
122
+ const upsertSql = [
75
123
  'INSERT INTO chemicals (source_id, chemical_name, chemical_meta, chemical_identifiers, 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)',
124
+ 'VALUES (:source_id, :chemical_name, :chemical_meta::jsonb, :chemical_identifiers::jsonb, :chemical_synonyms::text[], :chemical_categories::text[], :created_at, :updated_at)',
77
125
  'ON CONFLICT (source_id) DO UPDATE SET',
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'
126
+ ' chemical_name = EXCLUDED.chemical_name,',
127
+ ' chemical_meta = EXCLUDED.chemical_meta,',
128
+ ' chemical_identifiers = EXCLUDED.chemical_identifiers,',
129
+ ' chemical_synonyms = EXCLUDED.chemical_synonyms,',
130
+ ' chemical_categories = EXCLUDED.chemical_categories,',
131
+ ' updated_at = EXCLUDED.updated_at',
132
+ 'RETURNING chemical_id, source_id'
84
133
  ].join('\n');
85
134
 
86
- const serializeDate = (d) => d instanceof Date ? d.toISOString() : d;
87
-
88
135
  const parameters = [
89
136
  { name: 'source_id', value: { stringValue: chemical.sourceId } },
90
137
  { 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) } }
138
+ { name: 'chemical_meta', value: { stringValue: JSON.stringify(chemical.chemicalMeta ?? {}) }, typeHint: 'JSON' },
139
+ { name: 'chemical_identifiers', value: { stringValue: JSON.stringify(chemical.chemicalIdentifiers ?? {}) }, typeHint: 'JSON' },
140
+ { name: 'chemical_synonyms', value: { stringValue: this._toPostgresArray(chemical.chemicalSynonyms ?? []) } },
141
+ { name: 'chemical_categories', value: { stringValue: this._toPostgresArray(chemical.chemicalCategories ?? []) } },
142
+ { name: 'created_at', value: { stringValue: this._serializeDate(chemical.createdAt) }, typeHint: 'TIMESTAMP' },
143
+ { name: 'updated_at', value: { stringValue: this._serializeDate(chemical.updatedAt) }, typeHint: 'TIMESTAMP' }
97
144
  ];
98
145
 
99
- return { sql, parameters };
146
+ return { sql: upsertSql, parameters };
147
+ }
148
+
149
+ async _executeChemicalUpsert(chemical) {
150
+ await this.connection.ensureConnected();
151
+ const { sql, parameters } = this._buildChemicalUpsertSql(chemical);
152
+ const queryResult = await this.connection.query(sql, parameters);
153
+ const row = queryResult.rows?.[0];
154
+ if (!row) return null;
155
+ return {
156
+ chemicalId: row.chemical_id,
157
+ sourceId: row.source_id
158
+ };
100
159
  }
101
160
 
102
161
  _buildDebugSql(chemical) {
@@ -173,24 +232,8 @@ class ChemicalsService {
173
232
  logInfo('pegasus-sdk', `[bulkIndexFielded] DEBUG SQL for document ${i}:\n${this._buildDebugSql(chemical)}`);
174
233
 
175
234
  const attemptUpsert = async () => {
176
- const freshDb = await this.getDb();
177
- return freshDb.insert(schema.chemicals)
178
- .values(chemical)
179
- .onConflictDoUpdate({
180
- target: schema.chemicals.sourceId,
181
- set: {
182
- chemicalName: chemical.chemicalName,
183
- chemicalMeta: chemical.chemicalMeta,
184
- chemicalIdentifiers: chemical.chemicalIdentifiers,
185
- chemicalSynonyms: chemical.chemicalSynonyms,
186
- chemicalCategories: chemical.chemicalCategories,
187
- updatedAt: new Date()
188
- }
189
- })
190
- .returning({
191
- chemicalId: schema.chemicals.chemicalId,
192
- sourceId: schema.chemicals.sourceId
193
- });
235
+ const result = await this._executeChemicalUpsert(chemical);
236
+ return result ? [result] : [];
194
237
  };
195
238
 
196
239
  let lastError = null;
@@ -281,25 +324,36 @@ class ChemicalsService {
281
324
 
282
325
  async createChemical(chemical) {
283
326
  try {
284
- const db = await this.getDb();
327
+ await this.connection.ensureConnected();
285
328
 
286
- const [result] = await db
287
- .insert(schema.chemicals)
288
- .values({
289
- sourceId: chemical.source_id,
290
- chemicalName: chemical.chemical_name,
291
- chemicalMeta: chemical.chemical_meta,
292
- chemicalIdentifiers: chemical.chemical_identifiers,
293
- chemicalSynonyms: chemical.chemical_synonyms,
294
- chemicalCategories: chemical.chemical_categories,
295
- createdAt: chemical.created_at || new Date(),
296
- updatedAt: chemical.updated_at || new Date(),
297
- ...(chemical.imported_at && { importedAt: chemical.imported_at }),
298
- ...(chemical.chemical_id && { chemicalId: chemical.chemical_id })
299
- })
300
- .returning();
301
-
302
- return result;
329
+ const columns = ['source_id', 'chemical_name', 'chemical_meta', 'chemical_identifiers', 'chemical_synonyms', 'chemical_categories', 'created_at', 'updated_at'];
330
+ const values = [':source_id', ':chemical_name', ':chemical_meta::jsonb', ':chemical_identifiers::jsonb', ':chemical_synonyms::text[]', ':chemical_categories::text[]', ':created_at', ':updated_at'];
331
+ const params = [
332
+ { name: 'source_id', value: { stringValue: chemical.source_id } },
333
+ { name: 'chemical_name', value: { stringValue: chemical.chemical_name } },
334
+ { name: 'chemical_meta', value: { stringValue: JSON.stringify(chemical.chemical_meta || {}) }, typeHint: 'JSON' },
335
+ { name: 'chemical_identifiers', value: { stringValue: JSON.stringify(chemical.chemical_identifiers || {}) }, typeHint: 'JSON' },
336
+ { name: 'chemical_synonyms', value: { stringValue: this._toPostgresArray(chemical.chemical_synonyms || []) } },
337
+ { name: 'chemical_categories', value: { stringValue: this._toPostgresArray(chemical.chemical_categories || []) } },
338
+ { name: 'created_at', value: { stringValue: this._serializeDate(chemical.created_at || new Date()) }, typeHint: 'TIMESTAMP' },
339
+ { name: 'updated_at', value: { stringValue: this._serializeDate(chemical.updated_at || new Date()) }, typeHint: 'TIMESTAMP' }
340
+ ];
341
+
342
+ if (chemical.imported_at) {
343
+ columns.push('imported_at');
344
+ values.push(':imported_at');
345
+ params.push({ name: 'imported_at', value: { stringValue: this._serializeDate(chemical.imported_at) }, typeHint: 'TIMESTAMP' });
346
+ }
347
+
348
+ if (chemical.chemical_id) {
349
+ columns.push('chemical_id');
350
+ values.push(':chemical_id');
351
+ params.push({ name: 'chemical_id', value: { stringValue: chemical.chemical_id } });
352
+ }
353
+
354
+ const sql = `INSERT INTO chemicals (${columns.join(', ')}) VALUES (${values.join(', ')}) RETURNING *`;
355
+ const result = await this.connection.query(sql, params);
356
+ return this._mapChemicalRow(result.rows?.[0]);
303
357
  } catch (error) {
304
358
  logError('pegasus-sdk', 'ChemicalsService', 'createChemical', error);
305
359
  throw error;
@@ -308,23 +362,40 @@ class ChemicalsService {
308
362
 
309
363
  async updateChemical(chemicalId, updates) {
310
364
  try {
311
- const db = await this.getDb();
365
+ await this.connection.ensureConnected();
366
+
367
+ const setClauses = [];
368
+ const params = [];
369
+
370
+ if (updates.chemical_name) {
371
+ setClauses.push('chemical_name = :chemical_name');
372
+ params.push({ name: 'chemical_name', value: { stringValue: updates.chemical_name } });
373
+ }
374
+ if (updates.chemical_meta) {
375
+ setClauses.push('chemical_meta = :chemical_meta::jsonb');
376
+ params.push({ name: 'chemical_meta', value: { stringValue: JSON.stringify(updates.chemical_meta) }, typeHint: 'JSON' });
377
+ }
378
+ if (updates.chemical_identifiers) {
379
+ setClauses.push('chemical_identifiers = :chemical_identifiers::jsonb');
380
+ params.push({ name: 'chemical_identifiers', value: { stringValue: JSON.stringify(updates.chemical_identifiers) }, typeHint: 'JSON' });
381
+ }
382
+ if (updates.chemical_synonyms) {
383
+ setClauses.push('chemical_synonyms = :chemical_synonyms::text[]');
384
+ params.push({ name: 'chemical_synonyms', value: { stringValue: this._toPostgresArray(updates.chemical_synonyms) } });
385
+ }
386
+ if (updates.chemical_categories) {
387
+ setClauses.push('chemical_categories = :chemical_categories::text[]');
388
+ params.push({ name: 'chemical_categories', value: { stringValue: this._toPostgresArray(updates.chemical_categories) } });
389
+ }
390
+
391
+ setClauses.push('updated_at = :updated_at');
392
+ params.push({ name: 'updated_at', value: { stringValue: this._serializeDate(new Date()) }, typeHint: 'TIMESTAMP' });
312
393
 
313
- const updateData = {};
314
- if (updates.chemical_name) updateData.chemicalName = updates.chemical_name;
315
- if (updates.chemical_meta) updateData.chemicalMeta = updates.chemical_meta;
316
- if (updates.chemical_identifiers) updateData.chemicalIdentifiers = updates.chemical_identifiers;
317
- if (updates.chemical_synonyms) updateData.chemicalSynonyms = updates.chemical_synonyms;
318
- if (updates.chemical_categories) updateData.chemicalCategories = updates.chemical_categories;
319
- updateData.updatedAt = new Date();
320
-
321
- const [result] = await db
322
- .update(schema.chemicals)
323
- .set(updateData)
324
- .where(eq(schema.chemicals.chemicalId, chemicalId))
325
- .returning();
326
-
327
- return result || null;
394
+ params.push({ name: 'id', value: { stringValue: chemicalId } });
395
+
396
+ const sql = `UPDATE chemicals SET ${setClauses.join(', ')} WHERE chemical_id = :id::uuid RETURNING *`;
397
+ const result = await this.connection.query(sql, params);
398
+ return this._mapChemicalRow(result.rows?.[0]) || null;
328
399
  } catch (error) {
329
400
  logError('pegasus-sdk', 'ChemicalsService', 'updateChemical', error);
330
401
  throw error;
@@ -333,14 +404,12 @@ class ChemicalsService {
333
404
 
334
405
  async deleteChemical(chemicalId) {
335
406
  try {
336
- const db = await this.getDb();
407
+ await this.connection.ensureConnected();
337
408
 
338
- const [deleted] = await db
339
- .delete(schema.chemicals)
340
- .where(eq(schema.chemicals.chemicalId, chemicalId))
341
- .returning();
342
-
343
- return deleted || null;
409
+ const sql = 'DELETE FROM chemicals WHERE chemical_id = :id::uuid RETURNING *';
410
+ const params = [{ name: 'id', value: { stringValue: chemicalId } }];
411
+ const result = await this.connection.query(sql, params);
412
+ return this._mapChemicalRow(result.rows?.[0]) || null;
344
413
  } catch (error) {
345
414
  logError('pegasus-sdk', 'ChemicalsService', 'deleteChemical', error);
346
415
  throw error;
@@ -349,14 +418,12 @@ class ChemicalsService {
349
418
 
350
419
  async deleteBySourceId(sourceId) {
351
420
  try {
352
- const db = await this.getDb();
421
+ await this.connection.ensureConnected();
353
422
 
354
- const [deleted] = await db
355
- .delete(schema.chemicals)
356
- .where(eq(schema.chemicals.sourceId, sourceId))
357
- .returning();
358
-
359
- return deleted || null;
423
+ const sql = 'DELETE FROM chemicals WHERE source_id = :source_id RETURNING *';
424
+ const params = [{ name: 'source_id', value: { stringValue: sourceId } }];
425
+ const result = await this.connection.query(sql, params);
426
+ return this._mapChemicalRow(result.rows?.[0]) || null;
360
427
  } catch (error) {
361
428
  logError('pegasus-sdk', 'ChemicalsService', 'deleteBySourceId', error);
362
429
  throw error;
@@ -365,13 +432,12 @@ class ChemicalsService {
365
432
 
366
433
  async deleteCollection(collectionName) {
367
434
  try {
368
- const db = await this.getDb();
435
+ await this.connection.ensureConnected();
369
436
 
370
- const deleted = await db
371
- .delete(schema.chemicals)
372
- .where(arrayContains(schema.chemicals.chemicalCategories, [collectionName]))
373
- .returning();
374
-
437
+ const sql = 'DELETE FROM chemicals WHERE :collection_name = ANY(chemical_categories) RETURNING *';
438
+ const params = [{ name: 'collection_name', value: { stringValue: collectionName } }];
439
+ const result = await this.connection.query(sql, params);
440
+ const deleted = result.rows.map(row => this._mapChemicalRow(row));
375
441
  return { deletedCount: deleted.length, deleted };
376
442
  } catch (error) {
377
443
  logError('pegasus-sdk', 'ChemicalsService', 'deleteCollection', error);
@@ -381,20 +447,20 @@ class ChemicalsService {
381
447
 
382
448
  async updateCollectionProperty(collectionName, propertyPath, newValue) {
383
449
  try {
384
- const db = await this.getDb();
450
+ await this.connection.ensureConnected();
451
+
385
452
  const pathArray = propertyPath.split('.');
386
- const valueJson = JSON.stringify(newValue);
387
-
388
- const results = await db
389
- .update(schema.chemicals)
390
- .set({
391
- chemicalMeta: sql`jsonb_set(${schema.chemicals.chemicalMeta}, ${pathArray}::text[], ${valueJson}::jsonb)`,
392
- updatedAt: new Date()
393
- })
394
- .where(arrayContains(schema.chemicals.chemicalCategories, [collectionName]))
395
- .returning();
396
-
397
- return { updatedCount: results.length, updated: results };
453
+ const sql = 'UPDATE chemicals SET chemical_meta = jsonb_set(chemical_meta, :path::text[], :value::jsonb), updated_at = :updated_at WHERE :collection_name = ANY(chemical_categories) RETURNING *';
454
+ const params = [
455
+ { name: 'path', value: { stringValue: JSON.stringify(pathArray) }, typeHint: 'JSON' },
456
+ { name: 'value', value: { stringValue: JSON.stringify(newValue) }, typeHint: 'JSON' },
457
+ { name: 'updated_at', value: { stringValue: this._serializeDate(new Date()) }, typeHint: 'TIMESTAMP' },
458
+ { name: 'collection_name', value: { stringValue: collectionName } }
459
+ ];
460
+
461
+ const result = await this.connection.query(sql, params);
462
+ const updated = result.rows.map(row => this._mapChemicalRow(row));
463
+ return { updatedCount: updated.length, updated };
398
464
  } catch (error) {
399
465
  logError('pegasus-sdk', 'ChemicalsService', 'updateCollectionProperty', error);
400
466
  throw error;
@@ -403,31 +469,38 @@ class ChemicalsService {
403
469
 
404
470
  async bulkUpdateProperty(filter, propertyPath, newValue) {
405
471
  try {
406
- const db = await this.getDb();
472
+ await this.connection.ensureConnected();
407
473
 
408
- let whereCondition = sql`1=1`;
474
+ let whereClause = '1=1';
475
+ const params = [];
409
476
 
410
477
  if (filter.chemicalIds && filter.chemicalIds.length > 0) {
411
- whereCondition = inArray(schema.chemicals.chemicalId, filter.chemicalIds);
478
+ const ids = filter.chemicalIds.map((id, i) => `:cid_${i}`).join(',');
479
+ whereClause = `chemical_id = ANY(ARRAY[${ids}]::uuid[])`;
480
+ filter.chemicalIds.forEach((id, i) => {
481
+ params.push({ name: `cid_${i}`, value: { stringValue: id } });
482
+ });
412
483
  } else if (filter.sourceIds && filter.sourceIds.length > 0) {
413
- whereCondition = inArray(schema.chemicals.sourceId, filter.sourceIds);
484
+ const ids = filter.sourceIds.map((id, i) => `:sid_${i}`).join(',');
485
+ whereClause = `source_id = ANY(ARRAY[${ids}]::text[])`;
486
+ filter.sourceIds.forEach((id, i) => {
487
+ params.push({ name: `sid_${i}`, value: { stringValue: id } });
488
+ });
414
489
  } else if (filter.category) {
415
- whereCondition = arrayContains(schema.chemicals.chemicalCategories, [filter.category]);
490
+ whereClause = ':category = ANY(chemical_categories)';
491
+ params.push({ name: 'category', value: { stringValue: filter.category } });
416
492
  }
417
493
 
418
494
  const pathArray = propertyPath.split('.');
419
- const valueJson = JSON.stringify(newValue);
420
-
421
- const results = await db
422
- .update(schema.chemicals)
423
- .set({
424
- chemicalMeta: sql`jsonb_set(COALESCE(${schema.chemicals.chemicalMeta}, '{}'), ${pathArray}::text[], ${valueJson}::jsonb)`,
425
- updatedAt: new Date()
426
- })
427
- .where(whereCondition)
428
- .returning();
429
-
430
- return { updatedCount: results.length, updated: results };
495
+ const sql = `UPDATE chemicals SET chemical_meta = jsonb_set(COALESCE(chemical_meta, '{}'), :path::text[], :value::jsonb), updated_at = :updated_at WHERE ${whereClause} RETURNING *`;
496
+
497
+ params.push({ name: 'path', value: { stringValue: JSON.stringify(pathArray) }, typeHint: 'JSON' });
498
+ params.push({ name: 'value', value: { stringValue: JSON.stringify(newValue) }, typeHint: 'JSON' });
499
+ params.push({ name: 'updated_at', value: { stringValue: this._serializeDate(new Date()) }, typeHint: 'TIMESTAMP' });
500
+
501
+ const result = await this.connection.query(sql, params);
502
+ const updated = result.rows.map(row => this._mapChemicalRow(row));
503
+ return { updatedCount: updated.length, updated };
431
504
  } catch (error) {
432
505
  logError('pegasus-sdk', 'ChemicalsService', 'bulkUpdateProperty', error);
433
506
  throw error;
@@ -436,15 +509,12 @@ class ChemicalsService {
436
509
 
437
510
  async getChemicalById(chemicalId) {
438
511
  try {
439
- const db = await this.getDb();
512
+ await this.connection.ensureConnected();
440
513
 
441
- const [result] = await db
442
- .select()
443
- .from(schema.chemicals)
444
- .where(eq(schema.chemicals.chemicalId, chemicalId))
445
- .limit(1);
446
-
447
- return result || null;
514
+ const sql = 'SELECT * FROM chemicals WHERE chemical_id = :id::uuid LIMIT 1';
515
+ const params = [{ name: 'id', value: { stringValue: chemicalId } }];
516
+ const result = await this.connection.query(sql, params);
517
+ return this._mapChemicalRow(result.rows?.[0]) || null;
448
518
  } catch (error) {
449
519
  logError('pegasus-sdk', 'ChemicalsService', 'getChemicalById', error);
450
520
  throw error;
@@ -453,15 +523,12 @@ class ChemicalsService {
453
523
 
454
524
  async getChemicalBySourceId(sourceId) {
455
525
  try {
456
- const db = await this.getDb();
526
+ await this.connection.ensureConnected();
457
527
 
458
- const [result] = await db
459
- .select()
460
- .from(schema.chemicals)
461
- .where(eq(schema.chemicals.sourceId, sourceId))
462
- .limit(1);
463
-
464
- return result || null;
528
+ const sql = 'SELECT * FROM chemicals WHERE source_id = :source_id LIMIT 1';
529
+ const params = [{ name: 'source_id', value: { stringValue: sourceId } }];
530
+ const result = await this.connection.query(sql, params);
531
+ return this._mapChemicalRow(result.rows?.[0]) || null;
465
532
  } catch (error) {
466
533
  logError('pegasus-sdk', 'ChemicalsService', 'getChemicalBySourceId', error);
467
534
  throw error;
@@ -470,14 +537,12 @@ class ChemicalsService {
470
537
 
471
538
  async getChemicalsByCAS(casNumber) {
472
539
  try {
473
- const db = await this.getDb();
540
+ await this.connection.ensureConnected();
474
541
 
475
- const results = await db
476
- .select()
477
- .from(schema.chemicals)
478
- .where(sql`${schema.chemicals.chemicalIdentifiers}->>'CAS' = ${casNumber} OR ${schema.chemicals.chemicalIdentifiers}->'CAS' ? ${casNumber}`);
479
-
480
- return results;
542
+ const sql = "SELECT * FROM chemicals WHERE chemical_identifiers->>'CAS' = :cas OR chemical_identifiers->'CAS' ? :cas";
543
+ const params = [{ name: 'cas', value: { stringValue: casNumber } }];
544
+ const result = await this.connection.query(sql, params);
545
+ return result.rows.map(row => this._mapChemicalRow(row));
481
546
  } catch (error) {
482
547
  logError('pegasus-sdk', 'ChemicalsService', 'getChemicalsByCAS', error);
483
548
  throw error;
@@ -490,14 +555,12 @@ class ChemicalsService {
490
555
  throw new Error(`Invalid identifier type: ${identifierType}`);
491
556
  }
492
557
 
493
- const db = await this.getDb();
494
-
495
- const results = await db
496
- .select()
497
- .from(schema.chemicals)
498
- .where(sql`${schema.chemicals.chemicalIdentifiers}->>${identifierType} = ${identifierValue} OR ${schema.chemicals.chemicalIdentifiers}->${identifierType} ? ${identifierValue}`);
499
-
500
- return results;
558
+ await this.connection.ensureConnected();
559
+
560
+ const sql = `SELECT * FROM chemicals WHERE chemical_identifiers->>'${identifierType}' = :value OR chemical_identifiers->'${identifierType}' ? :value`;
561
+ const params = [{ name: 'value', value: { stringValue: identifierValue } }];
562
+ const result = await this.connection.query(sql, params);
563
+ return result.rows.map(row => this._mapChemicalRow(row));
501
564
  } catch (error) {
502
565
  logError('pegasus-sdk', 'ChemicalsService', 'getChemicalsByIdentifier', error);
503
566
  throw error;
@@ -506,14 +569,12 @@ class ChemicalsService {
506
569
 
507
570
  async countByCollection(collectionName) {
508
571
  try {
509
- const db = await this.getDb();
572
+ await this.connection.ensureConnected();
510
573
 
511
- const result = await db
512
- .select({ count: sql`count(*)::int` })
513
- .from(schema.chemicals)
514
- .where(arrayContains(schema.chemicals.chemicalCategories, [collectionName]));
515
-
516
- return { count: result[0].count };
574
+ const sql = 'SELECT count(*)::int AS count FROM chemicals WHERE :collection_name = ANY(chemical_categories)';
575
+ const params = [{ name: 'collection_name', value: { stringValue: collectionName } }];
576
+ const result = await this.connection.query(sql, params);
577
+ return { count: result.rows[0]?.count ?? 0 };
517
578
  } catch (error) {
518
579
  logError('pegasus-sdk', 'ChemicalsService', 'countByCollection', error);
519
580
  throw error;
@@ -522,15 +583,13 @@ class ChemicalsService {
522
583
 
523
584
  async countByIdentifier(identifierValue) {
524
585
  try {
525
- const db = await this.getDb();
586
+ await this.connection.ensureConnected();
526
587
 
527
588
  const searchPattern = `%${escapeLikePattern(identifierValue)}%`;
528
- const result = await db
529
- .select({ count: sql`count(*)::int` })
530
- .from(schema.chemicals)
531
- .where(sql`${schema.chemicals.chemicalIdentifiers}::text LIKE ${searchPattern}`);
532
-
533
- return { count: result[0].count };
589
+ const sql = 'SELECT count(*)::int AS count FROM chemicals WHERE chemical_identifiers::text LIKE :pattern';
590
+ const params = [{ name: 'pattern', value: { stringValue: searchPattern } }];
591
+ const result = await this.connection.query(sql, params);
592
+ return { count: result.rows[0]?.count ?? 0 };
534
593
  } catch (error) {
535
594
  logError('pegasus-sdk', 'ChemicalsService', 'countByIdentifier', error);
536
595
  throw error;
@@ -539,14 +598,12 @@ class ChemicalsService {
539
598
 
540
599
  async countByCAS(casNumber) {
541
600
  try {
542
- const db = await this.getDb();
601
+ await this.connection.ensureConnected();
543
602
 
544
- const result = await db
545
- .select({ count: sql`count(*)::int` })
546
- .from(schema.chemicals)
547
- .where(sql`${schema.chemicals.chemicalIdentifiers}->>'CAS' = ${casNumber} OR ${schema.chemicals.chemicalIdentifiers}->'CAS' ? ${casNumber}`);
548
-
549
- return { count: result[0].count };
603
+ const sql = "SELECT count(*)::int AS count FROM chemicals WHERE chemical_identifiers->>'CAS' = :cas OR chemical_identifiers->'CAS' ? :cas";
604
+ const params = [{ name: 'cas', value: { stringValue: casNumber } }];
605
+ const result = await this.connection.query(sql, params);
606
+ return { count: result.rows[0]?.count ?? 0 };
550
607
  } catch (error) {
551
608
  logError('pegasus-sdk', 'ChemicalsService', 'countByCAS', error);
552
609
  throw error;
@@ -555,13 +612,11 @@ class ChemicalsService {
555
612
 
556
613
  async getTotalSynonymCount() {
557
614
  try {
558
- const db = await this.getDb();
615
+ await this.connection.ensureConnected();
559
616
 
560
- const result = await db
561
- .select({ count: sql`sum(array_length(${schema.chemicals.chemicalSynonyms}, 1))::int` })
562
- .from(schema.chemicals);
563
-
564
- return { count: result[0].count || 0 };
617
+ const sql = 'SELECT sum(array_length(chemical_synonyms, 1))::int AS count FROM chemicals';
618
+ const result = await this.connection.query(sql, []);
619
+ return { count: result.rows[0]?.count || 0 };
565
620
  } catch (error) {
566
621
  logError('pegasus-sdk', 'ChemicalsService', 'getTotalSynonymCount', error);
567
622
  throw error;
@@ -570,14 +625,12 @@ class ChemicalsService {
570
625
 
571
626
  async getSynonymCount(synonymTerm) {
572
627
  try {
573
- const db = await this.getDb();
628
+ await this.connection.ensureConnected();
574
629
 
575
- const result = await db
576
- .select({ count: sql`count(*)::int` })
577
- .from(schema.chemicals)
578
- .where(arrayContains(schema.chemicals.chemicalSynonyms, [synonymTerm]));
579
-
580
- return { count: result[0].count };
630
+ const sql = 'SELECT count(*)::int AS count FROM chemicals WHERE :term = ANY(chemical_synonyms)';
631
+ const params = [{ name: 'term', value: { stringValue: synonymTerm } }];
632
+ const result = await this.connection.query(sql, params);
633
+ return { count: result.rows[0]?.count ?? 0 };
581
634
  } catch (error) {
582
635
  logError('pegasus-sdk', 'ChemicalsService', 'getSynonymCount', error);
583
636
  throw error;
@@ -586,19 +639,18 @@ class ChemicalsService {
586
639
 
587
640
  async convertIdentifier(fromIdentifier, toIdentifierType) {
588
641
  try {
589
- const db = await this.getDb();
642
+ await this.connection.ensureConnected();
590
643
 
591
644
  const searchPattern = `%${escapeLikePattern(fromIdentifier)}%`;
592
- const chemicals = await db
593
- .select()
594
- .from(schema.chemicals)
595
- .where(sql`${schema.chemicals.chemicalIdentifiers}::text LIKE ${searchPattern}`);
645
+ const sql = 'SELECT * FROM chemicals WHERE chemical_identifiers::text LIKE :pattern LIMIT 1';
646
+ const params = [{ name: 'pattern', value: { stringValue: searchPattern } }];
647
+ const result = await this.connection.query(sql, params);
596
648
 
597
- if (chemicals.length === 0) {
649
+ if (result.rows.length === 0) {
598
650
  return null;
599
651
  }
600
652
 
601
- const chemical = chemicals[0];
653
+ const chemical = this._mapChemicalRow(result.rows[0]);
602
654
  const identifiers = chemical.chemicalIdentifiers || {};
603
655
  const toIdentifier = identifiers[toIdentifierType];
604
656
 
@@ -734,11 +786,10 @@ class ChemicalsService {
734
786
 
735
787
  async countAll() {
736
788
  try {
737
- const db = await this.getDb();
738
- const result = await db
739
- .select({ count: sql`count(*)::int` })
740
- .from(schema.chemicals);
741
- return { count: result[0].count };
789
+ await this.connection.ensureConnected();
790
+ const sql = 'SELECT count(*)::int AS count FROM chemicals';
791
+ const result = await this.connection.query(sql, []);
792
+ return { count: result.rows[0]?.count ?? 0 };
742
793
  } catch (error) {
743
794
  logError('pegasus-sdk', 'ChemicalsService', 'countAll', error);
744
795
  throw error;
@@ -747,28 +798,28 @@ class ChemicalsService {
747
798
 
748
799
  async findChemicalsWithoutDocuments(collectionName, searchTerm, pageSize = 100) {
749
800
  try {
750
- const db = await this.getDb();
801
+ await this.connection.ensureConnected();
751
802
 
752
- let whereConditions = [];
803
+ let whereFragments = [];
804
+ const params = [];
753
805
 
754
806
  if (collectionName) {
755
- whereConditions.push(arrayContains(schema.chemicals.chemicalCategories, [collectionName]));
807
+ whereFragments.push(':collection_name = ANY(chemical_categories)');
808
+ params.push({ name: 'collection_name', value: { stringValue: collectionName } });
756
809
  }
757
810
 
758
811
  if (searchTerm) {
759
812
  const searchPattern = `%${escapeLikePattern(searchTerm)}%`;
760
- whereConditions.push(sql`${schema.chemicals.chemicalName} ILIKE ${searchPattern}`);
813
+ whereFragments.push('chemical_name ILIKE :search_term');
814
+ params.push({ name: 'search_term', value: { stringValue: searchPattern } });
761
815
  }
762
816
 
763
- const whereClause = whereConditions.length > 0 ? and(...whereConditions) : undefined;
817
+ const whereClause = whereFragments.length > 0 ? 'WHERE ' + whereFragments.join(' AND ') : '';
818
+ params.push({ name: 'page_size', value: { longValue: pageSize } });
764
819
 
765
- const results = await db
766
- .select()
767
- .from(schema.chemicals)
768
- .where(whereClause)
769
- .limit(pageSize);
770
-
771
- return results;
820
+ const sql = `SELECT * FROM chemicals ${whereClause} LIMIT :page_size`;
821
+ const result = await this.connection.query(sql, params);
822
+ return result.rows.map(row => this._mapChemicalRow(row));
772
823
  } catch (error) {
773
824
  logError('pegasus-sdk', 'ChemicalsService', 'findChemicalsWithoutDocuments', error);
774
825
  throw error;
@@ -777,18 +828,17 @@ class ChemicalsService {
777
828
 
778
829
  async countChemicalsWithoutDocuments(collectionName) {
779
830
  try {
780
- const db = await this.getDb();
831
+ await this.connection.ensureConnected();
781
832
 
782
- const whereClause = collectionName
783
- ? arrayContains(schema.chemicals.chemicalCategories, [collectionName])
784
- : undefined;
833
+ let sql = 'SELECT count(*)::int AS count FROM chemicals';
834
+ const params = [];
835
+ if (collectionName) {
836
+ sql += ' WHERE :collection_name = ANY(chemical_categories)';
837
+ params.push({ name: 'collection_name', value: { stringValue: collectionName } });
838
+ }
785
839
 
786
- const result = await db
787
- .select({ count: sql`count(*)::int` })
788
- .from(schema.chemicals)
789
- .where(whereClause);
790
-
791
- return { count: result[0].count };
840
+ const result = await this.connection.query(sql, params);
841
+ return { count: result.rows[0]?.count ?? 0 };
792
842
  } catch (error) {
793
843
  logError('pegasus-sdk', 'ChemicalsService', 'countChemicalsWithoutDocuments', error);
794
844
  throw error;
package/lib/connection.js CHANGED
@@ -2,7 +2,7 @@ const { Client: OpenSearchClient } = require('@opensearch-project/opensearch');
2
2
  const { RDSDataClient, ExecuteStatementCommand } = require('@aws-sdk/client-rds-data');
3
3
  const { AwsSigv4Signer } = require('@opensearch-project/opensearch/aws');
4
4
  const { fromNodeProviderChain } = require('@aws-sdk/credential-providers');
5
- const { getDrizzle, schema } = require('./db');
5
+ const { mapRecords } = require('./db');
6
6
  const { loadConfig } = require('../config');
7
7
  const { logInfo, logError } = require('@toxplanet/tphelper/logging');
8
8
 
@@ -20,7 +20,6 @@ class PegasusConnection {
20
20
  this.databaseName = this.config.database?.name;
21
21
 
22
22
  this.rdsDataClient = null;
23
- this.db = null;
24
23
  this.osClient = null;
25
24
  this.isConnected = false;
26
25
  }
@@ -32,12 +31,6 @@ class PegasusConnection {
32
31
 
33
32
  this.rdsDataClient = new RDSDataClient({ region: this.region });
34
33
 
35
- this.db = getDrizzle(this.rdsDataClient, {
36
- resourceArn: this.clusterArn,
37
- secretArn: this.secretArn,
38
- database: this.databaseName
39
- });
40
-
41
34
  logInfo('pegasus-sdk', 'RDS Data API client initialized');
42
35
 
43
36
  try {
@@ -78,7 +71,7 @@ class PegasusConnection {
78
71
  * @returns {Promise<boolean>} true if a connect happened, false if already connected.
79
72
  */
80
73
  async ensureConnected() {
81
- if (!this.isConnected || !this.db) {
74
+ if (!this.isConnected) {
82
75
  await this.connect();
83
76
  return true;
84
77
  }
@@ -91,7 +84,6 @@ class PegasusConnection {
91
84
  */
92
85
  async reconnect() {
93
86
  logInfo('pegasus-sdk', 'Reconnecting RDS Data API client');
94
- this.db = null;
95
87
  this.isConnected = false;
96
88
  await this.connect();
97
89
  }
@@ -108,20 +100,12 @@ class PegasusConnection {
108
100
  return;
109
101
  }
110
102
 
111
- this.db = null;
112
103
  this.rdsDataClient = null;
113
104
  this.osClient = null;
114
105
  this.isConnected = false;
115
106
  logInfo('pegasus-sdk', 'RDS Data API client disconnected');
116
107
  }
117
108
 
118
- getPostgresClient() {
119
- if (!this.db) {
120
- throw new Error('RDS Data API not initialized. Call connect() first.');
121
- }
122
- return this.db;
123
- }
124
-
125
109
  getOpenSearchClient() {
126
110
  if (!this.osClient) {
127
111
  throw new Error('OpenSearch connection not established. Call connect() first or provide openSearchEndpoint.');
@@ -135,7 +119,7 @@ class PegasusConnection {
135
119
 
136
120
  async testConnection() {
137
121
  try {
138
- if (!this.db) {
122
+ if (!this.rdsDataClient) {
139
123
  throw new Error('RDS Data API not initialized');
140
124
  }
141
125
 
@@ -143,16 +127,18 @@ class PegasusConnection {
143
127
  resourceArn: this.clusterArn,
144
128
  secretArn: this.secretArn,
145
129
  database: this.databaseName,
146
- sql: 'SELECT NOW() as current_time, version() as pg_version'
130
+ sql: 'SELECT NOW() as current_time, version() as pg_version',
131
+ includeResultMetadata: true
147
132
  });
148
133
 
149
134
  const result = await this.rdsDataClient.send(command);
150
- const row = result.records?.[0];
135
+ const rows = mapRecords(result.records, result.columnMetadata);
136
+ const row = rows?.[0];
151
137
 
152
138
  const pgStatus = {
153
139
  connected: true,
154
- timestamp: row?.[0]?.stringValue,
155
- version: row?.[1]?.stringValue
140
+ timestamp: row?.current_time,
141
+ version: row?.pg_version
156
142
  };
157
143
 
158
144
  let osStatus = null;
@@ -205,7 +191,8 @@ class PegasusConnection {
205
191
  secretArn: this.secretArn,
206
192
  database: this.databaseName,
207
193
  sql,
208
- parameters: params || []
194
+ parameters: params || [],
195
+ includeResultMetadata: true
209
196
  });
210
197
 
211
198
  const result = await this.rdsDataClient.send(command);
@@ -213,19 +200,9 @@ class PegasusConnection {
213
200
 
214
201
  return {
215
202
  rowCount: result.numberOfRecordsUpdated || result.records?.length || 0,
216
- rows: result.records || [],
217
- command: result
203
+ rows: mapRecords(result.records, result.columnMetadata)
218
204
  };
219
205
  }
220
-
221
- async transaction(callback) {
222
- if (!this.db) {
223
- throw new Error('RDS Data API not initialized. Call connect() first.');
224
- }
225
- return await this.db.transaction(async (trx) => {
226
- return await callback(trx);
227
- });
228
- }
229
206
  }
230
207
 
231
208
  module.exports = PegasusConnection;
package/lib/db/index.js CHANGED
@@ -1,18 +1,26 @@
1
- const { drizzle } = require('drizzle-orm/aws-data-api/pg');
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(rdsDataClient, { resourceArn, secretArn, database }) {
12
- return drizzle(rdsDataClient, { schema, logger, resourceArn, secretArn, database });
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.20",
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,7 +25,6 @@
25
25
  "license": "MIT",
26
26
  "dependencies": {
27
27
  "@toxplanet/tphelper": "1.2.8",
28
- "drizzle-orm": "^0.30.0",
29
28
  "@opensearch-project/opensearch": "^2.5.0",
30
29
  "@aws-sdk/client-rds-data": "^3.490.0",
31
30
  "@aws-sdk/client-sqs": "^3.490.0",
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
- };