@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/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,19 +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
- const reconnected = await this.connection.ensureConnected();
26
- if (reconnected || !this.db) {
27
- this.db = getDrizzle(this.connection.pgPool);
28
- }
29
- return this.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
+ };
30
75
  }
31
76
 
32
77
  async sendSqlWriteFailure({ sql, parameters, error, retryCount, failedAt }) {
@@ -74,32 +119,43 @@ class ChemicalsService {
74
119
  }
75
120
 
76
121
  _buildChemicalUpsertSql(chemical) {
77
- const sql = [
122
+ const upsertSql = [
78
123
  'INSERT INTO chemicals (source_id, chemical_name, chemical_meta, chemical_identifiers, chemical_synonyms, chemical_categories, created_at, updated_at)',
79
- '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)',
80
125
  'ON CONFLICT (source_id) DO UPDATE SET',
81
- ' chemical_name = @chemical_name,',
82
- ' chemical_meta = @chemical_meta::jsonb,',
83
- ' chemical_identifiers = @chemical_identifiers::jsonb,',
84
- ' chemical_synonyms = @chemical_synonyms,',
85
- ' chemical_categories = @chemical_categories,',
86
- ' 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'
87
133
  ].join('\n');
88
134
 
89
- const serializeDate = (d) => d instanceof Date ? d.toISOString() : d;
90
-
91
- const parameters = {
92
- '@source_id': chemical.sourceId,
93
- '@chemical_name': chemical.chemicalName,
94
- '@chemical_meta': JSON.stringify(chemical.chemicalMeta ?? {}),
95
- '@chemical_identifiers': JSON.stringify(chemical.chemicalIdentifiers ?? {}),
96
- '@chemical_synonyms': JSON.stringify(chemical.chemicalSynonyms ?? []),
97
- '@chemical_categories': JSON.stringify(chemical.chemicalCategories ?? []),
98
- '@created_at': serializeDate(chemical.createdAt),
99
- '@updated_at': serializeDate(chemical.updatedAt)
100
- };
135
+ const parameters = [
136
+ { name: 'source_id', value: { stringValue: chemical.sourceId } },
137
+ { name: 'chemical_name', value: { stringValue: chemical.chemicalName } },
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' }
144
+ ];
145
+
146
+ return { sql: upsertSql, parameters };
147
+ }
101
148
 
102
- return { sql, parameters };
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
+ };
103
159
  }
104
160
 
105
161
  _buildDebugSql(chemical) {
@@ -175,33 +231,9 @@ class ChemicalsService {
175
231
  logInfo('pegasus-sdk', `[bulkIndexFielded] Prepared chemical object: sourceId=${chemical.sourceId}, chemicalName=${chemical.chemicalName}`);
176
232
  logInfo('pegasus-sdk', `[bulkIndexFielded] DEBUG SQL for document ${i}:\n${this._buildDebugSql(chemical)}`);
177
233
 
178
- const isConnectionError = (err) =>
179
- err.message?.toLowerCase().includes('timeout') ||
180
- err.message?.toLowerCase().includes('connection') ||
181
- err.code === 'ECONNREFUSED' ||
182
- err.code === 'ETIMEDOUT';
183
-
184
- // Use this.getDb() on each attempt so a reconnect mid-loop automatically
185
- // gets a fresh Drizzle instance bound to the new pool.
186
234
  const attemptUpsert = async () => {
187
- const freshDb = await this.getDb();
188
- return freshDb.insert(schema.chemicals)
189
- .values(chemical)
190
- .onConflictDoUpdate({
191
- target: schema.chemicals.sourceId,
192
- set: {
193
- chemicalName: chemical.chemicalName,
194
- chemicalMeta: chemical.chemicalMeta,
195
- chemicalIdentifiers: chemical.chemicalIdentifiers,
196
- chemicalSynonyms: chemical.chemicalSynonyms,
197
- chemicalCategories: chemical.chemicalCategories,
198
- updatedAt: new Date()
199
- }
200
- })
201
- .returning({
202
- chemicalId: schema.chemicals.chemicalId,
203
- sourceId: schema.chemicals.sourceId
204
- });
235
+ const result = await this._executeChemicalUpsert(chemical);
236
+ return result ? [result] : [];
205
237
  };
206
238
 
207
239
  let lastError = null;
@@ -211,38 +243,20 @@ class ChemicalsService {
211
243
  try {
212
244
  const [result] = await attemptUpsert();
213
245
  logInfo('pegasus-sdk', `[bulkIndexFielded] Document ${i} indexed successfully: ${result?.chemicalId || 'no ID returned'}`);
214
- this.connection.recordActivity();
215
246
  results.push({ index: i, success: true, result });
216
247
  continue;
217
248
  } catch (firstErr) {
218
249
  lastError = firstErr;
219
-
220
- if (isConnectionError(firstErr)) {
221
- // Stale pool — rebuild the connection and try once more before queuing
222
- logInfo('pegasus-sdk', `[bulkIndexFielded] Document ${i} connection error (${firstErr.message}), reconnecting pool and retrying`);
223
- try {
224
- await this.connection.reconnect();
225
- const [result] = await attemptUpsert();
226
- logInfo('pegasus-sdk', `[bulkIndexFielded] Document ${i} indexed successfully after reconnect: ${result?.chemicalId || 'no ID returned'}`);
227
- this.connection.recordActivity();
228
- results.push({ index: i, success: true, result });
229
- continue;
230
- } catch (reconnectErr) {
231
- lastError = reconnectErr;
232
- retryCount = 1;
233
- }
234
- } else {
235
- logInfo('pegasus-sdk', `[bulkIndexFielded] Document ${i} first attempt failed (${firstErr.message}), retrying once`);
236
- try {
237
- const [result] = await attemptUpsert();
238
- logInfo('pegasus-sdk', `[bulkIndexFielded] Document ${i} indexed successfully on retry: ${result?.chemicalId || 'no ID returned'}`);
239
- this.connection.recordActivity();
240
- results.push({ index: i, success: true, result });
241
- continue;
242
- } catch (retryErr) {
243
- lastError = retryErr;
244
- retryCount = 1;
245
- }
250
+ logInfo('pegasus-sdk', `[bulkIndexFielded] Document ${i} first attempt failed (${firstErr.message}), retrying once`);
251
+
252
+ try {
253
+ const [result] = await attemptUpsert();
254
+ logInfo('pegasus-sdk', `[bulkIndexFielded] Document ${i} indexed successfully on retry: ${result?.chemicalId || 'no ID returned'}`);
255
+ results.push({ index: i, success: true, result });
256
+ continue;
257
+ } catch (retryErr) {
258
+ lastError = retryErr;
259
+ retryCount = 1;
246
260
  }
247
261
  }
248
262
 
@@ -310,25 +324,36 @@ class ChemicalsService {
310
324
 
311
325
  async createChemical(chemical) {
312
326
  try {
313
- const db = await this.getDb();
327
+ await this.connection.ensureConnected();
314
328
 
315
- const [result] = await db
316
- .insert(schema.chemicals)
317
- .values({
318
- sourceId: chemical.source_id,
319
- chemicalName: chemical.chemical_name,
320
- chemicalMeta: chemical.chemical_meta,
321
- chemicalIdentifiers: chemical.chemical_identifiers,
322
- chemicalSynonyms: chemical.chemical_synonyms,
323
- chemicalCategories: chemical.chemical_categories,
324
- createdAt: chemical.created_at || new Date(),
325
- updatedAt: chemical.updated_at || new Date(),
326
- ...(chemical.imported_at && { importedAt: chemical.imported_at }),
327
- ...(chemical.chemical_id && { chemicalId: chemical.chemical_id })
328
- })
329
- .returning();
330
-
331
- 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]);
332
357
  } catch (error) {
333
358
  logError('pegasus-sdk', 'ChemicalsService', 'createChemical', error);
334
359
  throw error;
@@ -337,23 +362,40 @@ class ChemicalsService {
337
362
 
338
363
  async updateChemical(chemicalId, updates) {
339
364
  try {
340
- const db = await this.getDb();
365
+ await this.connection.ensureConnected();
341
366
 
342
- const updateData = {};
343
- if (updates.chemical_name) updateData.chemicalName = updates.chemical_name;
344
- if (updates.chemical_meta) updateData.chemicalMeta = updates.chemical_meta;
345
- if (updates.chemical_identifiers) updateData.chemicalIdentifiers = updates.chemical_identifiers;
346
- if (updates.chemical_synonyms) updateData.chemicalSynonyms = updates.chemical_synonyms;
347
- if (updates.chemical_categories) updateData.chemicalCategories = updates.chemical_categories;
348
- updateData.updatedAt = new Date();
349
-
350
- const [result] = await db
351
- .update(schema.chemicals)
352
- .set(updateData)
353
- .where(eq(schema.chemicals.chemicalId, chemicalId))
354
- .returning();
355
-
356
- return result || null;
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' });
393
+
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;
357
399
  } catch (error) {
358
400
  logError('pegasus-sdk', 'ChemicalsService', 'updateChemical', error);
359
401
  throw error;
@@ -362,14 +404,12 @@ class ChemicalsService {
362
404
 
363
405
  async deleteChemical(chemicalId) {
364
406
  try {
365
- const db = await this.getDb();
407
+ await this.connection.ensureConnected();
366
408
 
367
- const [deleted] = await db
368
- .delete(schema.chemicals)
369
- .where(eq(schema.chemicals.chemicalId, chemicalId))
370
- .returning();
371
-
372
- 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;
373
413
  } catch (error) {
374
414
  logError('pegasus-sdk', 'ChemicalsService', 'deleteChemical', error);
375
415
  throw error;
@@ -378,14 +418,12 @@ class ChemicalsService {
378
418
 
379
419
  async deleteBySourceId(sourceId) {
380
420
  try {
381
- const db = await this.getDb();
421
+ await this.connection.ensureConnected();
382
422
 
383
- const [deleted] = await db
384
- .delete(schema.chemicals)
385
- .where(eq(schema.chemicals.sourceId, sourceId))
386
- .returning();
387
-
388
- 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;
389
427
  } catch (error) {
390
428
  logError('pegasus-sdk', 'ChemicalsService', 'deleteBySourceId', error);
391
429
  throw error;
@@ -394,13 +432,12 @@ class ChemicalsService {
394
432
 
395
433
  async deleteCollection(collectionName) {
396
434
  try {
397
- const db = await this.getDb();
435
+ await this.connection.ensureConnected();
398
436
 
399
- const deleted = await db
400
- .delete(schema.chemicals)
401
- .where(arrayContains(schema.chemicals.chemicalCategories, [collectionName]))
402
- .returning();
403
-
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));
404
441
  return { deletedCount: deleted.length, deleted };
405
442
  } catch (error) {
406
443
  logError('pegasus-sdk', 'ChemicalsService', 'deleteCollection', error);
@@ -410,20 +447,20 @@ class ChemicalsService {
410
447
 
411
448
  async updateCollectionProperty(collectionName, propertyPath, newValue) {
412
449
  try {
413
- const db = await this.getDb();
450
+ await this.connection.ensureConnected();
451
+
414
452
  const pathArray = propertyPath.split('.');
415
- const valueJson = JSON.stringify(newValue);
416
-
417
- const results = await db
418
- .update(schema.chemicals)
419
- .set({
420
- chemicalMeta: sql`jsonb_set(${schema.chemicals.chemicalMeta}, ${pathArray}::text[], ${valueJson}::jsonb)`,
421
- updatedAt: new Date()
422
- })
423
- .where(arrayContains(schema.chemicals.chemicalCategories, [collectionName]))
424
- .returning();
425
-
426
- 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 };
427
464
  } catch (error) {
428
465
  logError('pegasus-sdk', 'ChemicalsService', 'updateCollectionProperty', error);
429
466
  throw error;
@@ -432,31 +469,38 @@ class ChemicalsService {
432
469
 
433
470
  async bulkUpdateProperty(filter, propertyPath, newValue) {
434
471
  try {
435
- const db = await this.getDb();
472
+ await this.connection.ensureConnected();
436
473
 
437
- let whereCondition = sql`1=1`;
474
+ let whereClause = '1=1';
475
+ const params = [];
438
476
 
439
477
  if (filter.chemicalIds && filter.chemicalIds.length > 0) {
440
- 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
+ });
441
483
  } else if (filter.sourceIds && filter.sourceIds.length > 0) {
442
- 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
+ });
443
489
  } else if (filter.category) {
444
- whereCondition = arrayContains(schema.chemicals.chemicalCategories, [filter.category]);
490
+ whereClause = ':category = ANY(chemical_categories)';
491
+ params.push({ name: 'category', value: { stringValue: filter.category } });
445
492
  }
446
493
 
447
494
  const pathArray = propertyPath.split('.');
448
- const valueJson = JSON.stringify(newValue);
449
-
450
- const results = await db
451
- .update(schema.chemicals)
452
- .set({
453
- chemicalMeta: sql`jsonb_set(COALESCE(${schema.chemicals.chemicalMeta}, '{}'), ${pathArray}::text[], ${valueJson}::jsonb)`,
454
- updatedAt: new Date()
455
- })
456
- .where(whereCondition)
457
- .returning();
458
-
459
- 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 };
460
504
  } catch (error) {
461
505
  logError('pegasus-sdk', 'ChemicalsService', 'bulkUpdateProperty', error);
462
506
  throw error;
@@ -465,15 +509,12 @@ class ChemicalsService {
465
509
 
466
510
  async getChemicalById(chemicalId) {
467
511
  try {
468
- const db = await this.getDb();
512
+ await this.connection.ensureConnected();
469
513
 
470
- const [result] = await db
471
- .select()
472
- .from(schema.chemicals)
473
- .where(eq(schema.chemicals.chemicalId, chemicalId))
474
- .limit(1);
475
-
476
- 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;
477
518
  } catch (error) {
478
519
  logError('pegasus-sdk', 'ChemicalsService', 'getChemicalById', error);
479
520
  throw error;
@@ -482,15 +523,12 @@ class ChemicalsService {
482
523
 
483
524
  async getChemicalBySourceId(sourceId) {
484
525
  try {
485
- const db = await this.getDb();
526
+ await this.connection.ensureConnected();
486
527
 
487
- const [result] = await db
488
- .select()
489
- .from(schema.chemicals)
490
- .where(eq(schema.chemicals.sourceId, sourceId))
491
- .limit(1);
492
-
493
- 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;
494
532
  } catch (error) {
495
533
  logError('pegasus-sdk', 'ChemicalsService', 'getChemicalBySourceId', error);
496
534
  throw error;
@@ -499,14 +537,12 @@ class ChemicalsService {
499
537
 
500
538
  async getChemicalsByCAS(casNumber) {
501
539
  try {
502
- const db = await this.getDb();
540
+ await this.connection.ensureConnected();
503
541
 
504
- const results = await db
505
- .select()
506
- .from(schema.chemicals)
507
- .where(sql`${schema.chemicals.chemicalIdentifiers}->>'CAS' = ${casNumber} OR ${schema.chemicals.chemicalIdentifiers}->'CAS' ? ${casNumber}`);
508
-
509
- 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));
510
546
  } catch (error) {
511
547
  logError('pegasus-sdk', 'ChemicalsService', 'getChemicalsByCAS', error);
512
548
  throw error;
@@ -519,14 +555,12 @@ class ChemicalsService {
519
555
  throw new Error(`Invalid identifier type: ${identifierType}`);
520
556
  }
521
557
 
522
- const db = await this.getDb();
523
-
524
- const results = await db
525
- .select()
526
- .from(schema.chemicals)
527
- .where(sql`${schema.chemicals.chemicalIdentifiers}->>${identifierType} = ${identifierValue} OR ${schema.chemicals.chemicalIdentifiers}->${identifierType} ? ${identifierValue}`);
528
-
529
- 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));
530
564
  } catch (error) {
531
565
  logError('pegasus-sdk', 'ChemicalsService', 'getChemicalsByIdentifier', error);
532
566
  throw error;
@@ -535,14 +569,12 @@ class ChemicalsService {
535
569
 
536
570
  async countByCollection(collectionName) {
537
571
  try {
538
- const db = await this.getDb();
572
+ await this.connection.ensureConnected();
539
573
 
540
- const result = await db
541
- .select({ count: sql`count(*)::int` })
542
- .from(schema.chemicals)
543
- .where(arrayContains(schema.chemicals.chemicalCategories, [collectionName]));
544
-
545
- 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 };
546
578
  } catch (error) {
547
579
  logError('pegasus-sdk', 'ChemicalsService', 'countByCollection', error);
548
580
  throw error;
@@ -551,15 +583,13 @@ class ChemicalsService {
551
583
 
552
584
  async countByIdentifier(identifierValue) {
553
585
  try {
554
- const db = await this.getDb();
586
+ await this.connection.ensureConnected();
555
587
 
556
588
  const searchPattern = `%${escapeLikePattern(identifierValue)}%`;
557
- const result = await db
558
- .select({ count: sql`count(*)::int` })
559
- .from(schema.chemicals)
560
- .where(sql`${schema.chemicals.chemicalIdentifiers}::text LIKE ${searchPattern}`);
561
-
562
- 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 };
563
593
  } catch (error) {
564
594
  logError('pegasus-sdk', 'ChemicalsService', 'countByIdentifier', error);
565
595
  throw error;
@@ -568,14 +598,12 @@ class ChemicalsService {
568
598
 
569
599
  async countByCAS(casNumber) {
570
600
  try {
571
- const db = await this.getDb();
601
+ await this.connection.ensureConnected();
572
602
 
573
- const result = await db
574
- .select({ count: sql`count(*)::int` })
575
- .from(schema.chemicals)
576
- .where(sql`${schema.chemicals.chemicalIdentifiers}->>'CAS' = ${casNumber} OR ${schema.chemicals.chemicalIdentifiers}->'CAS' ? ${casNumber}`);
577
-
578
- 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 };
579
607
  } catch (error) {
580
608
  logError('pegasus-sdk', 'ChemicalsService', 'countByCAS', error);
581
609
  throw error;
@@ -584,13 +612,11 @@ class ChemicalsService {
584
612
 
585
613
  async getTotalSynonymCount() {
586
614
  try {
587
- const db = await this.getDb();
615
+ await this.connection.ensureConnected();
588
616
 
589
- const result = await db
590
- .select({ count: sql`sum(array_length(${schema.chemicals.chemicalSynonyms}, 1))::int` })
591
- .from(schema.chemicals);
592
-
593
- 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 };
594
620
  } catch (error) {
595
621
  logError('pegasus-sdk', 'ChemicalsService', 'getTotalSynonymCount', error);
596
622
  throw error;
@@ -599,14 +625,12 @@ class ChemicalsService {
599
625
 
600
626
  async getSynonymCount(synonymTerm) {
601
627
  try {
602
- const db = await this.getDb();
628
+ await this.connection.ensureConnected();
603
629
 
604
- const result = await db
605
- .select({ count: sql`count(*)::int` })
606
- .from(schema.chemicals)
607
- .where(arrayContains(schema.chemicals.chemicalSynonyms, [synonymTerm]));
608
-
609
- 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 };
610
634
  } catch (error) {
611
635
  logError('pegasus-sdk', 'ChemicalsService', 'getSynonymCount', error);
612
636
  throw error;
@@ -615,19 +639,18 @@ class ChemicalsService {
615
639
 
616
640
  async convertIdentifier(fromIdentifier, toIdentifierType) {
617
641
  try {
618
- const db = await this.getDb();
642
+ await this.connection.ensureConnected();
619
643
 
620
644
  const searchPattern = `%${escapeLikePattern(fromIdentifier)}%`;
621
- const chemicals = await db
622
- .select()
623
- .from(schema.chemicals)
624
- .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);
625
648
 
626
- if (chemicals.length === 0) {
649
+ if (result.rows.length === 0) {
627
650
  return null;
628
651
  }
629
652
 
630
- const chemical = chemicals[0];
653
+ const chemical = this._mapChemicalRow(result.rows[0]);
631
654
  const identifiers = chemical.chemicalIdentifiers || {};
632
655
  const toIdentifier = identifiers[toIdentifierType];
633
656
 
@@ -763,11 +786,10 @@ class ChemicalsService {
763
786
 
764
787
  async countAll() {
765
788
  try {
766
- const db = await this.getDb();
767
- const result = await db
768
- .select({ count: sql`count(*)::int` })
769
- .from(schema.chemicals);
770
- 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 };
771
793
  } catch (error) {
772
794
  logError('pegasus-sdk', 'ChemicalsService', 'countAll', error);
773
795
  throw error;
@@ -776,28 +798,28 @@ class ChemicalsService {
776
798
 
777
799
  async findChemicalsWithoutDocuments(collectionName, searchTerm, pageSize = 100) {
778
800
  try {
779
- const db = await this.getDb();
801
+ await this.connection.ensureConnected();
780
802
 
781
- let whereConditions = [];
803
+ let whereFragments = [];
804
+ const params = [];
782
805
 
783
806
  if (collectionName) {
784
- whereConditions.push(arrayContains(schema.chemicals.chemicalCategories, [collectionName]));
807
+ whereFragments.push(':collection_name = ANY(chemical_categories)');
808
+ params.push({ name: 'collection_name', value: { stringValue: collectionName } });
785
809
  }
786
810
 
787
811
  if (searchTerm) {
788
812
  const searchPattern = `%${escapeLikePattern(searchTerm)}%`;
789
- 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 } });
790
815
  }
791
816
 
792
- 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 } });
793
819
 
794
- const results = await db
795
- .select()
796
- .from(schema.chemicals)
797
- .where(whereClause)
798
- .limit(pageSize);
799
-
800
- 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));
801
823
  } catch (error) {
802
824
  logError('pegasus-sdk', 'ChemicalsService', 'findChemicalsWithoutDocuments', error);
803
825
  throw error;
@@ -806,18 +828,17 @@ class ChemicalsService {
806
828
 
807
829
  async countChemicalsWithoutDocuments(collectionName) {
808
830
  try {
809
- const db = await this.getDb();
831
+ await this.connection.ensureConnected();
810
832
 
811
- const whereClause = collectionName
812
- ? arrayContains(schema.chemicals.chemicalCategories, [collectionName])
813
- : 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
+ }
814
839
 
815
- const result = await db
816
- .select({ count: sql`count(*)::int` })
817
- .from(schema.chemicals)
818
- .where(whereClause);
819
-
820
- return { count: result[0].count };
840
+ const result = await this.connection.query(sql, params);
841
+ return { count: result.rows[0]?.count ?? 0 };
821
842
  } catch (error) {
822
843
  logError('pegasus-sdk', 'ChemicalsService', 'countChemicalsWithoutDocuments', error);
823
844
  throw error;