@toxplanet/pegasus-sdk 1.1.19 → 1.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/environment.acc.js +4 -15
- package/config/environment.dev.js +4 -15
- package/config/environment.prod.js +4 -15
- package/config/environment.qa.js +4 -15
- package/lib/chemicals.js +288 -267
- package/lib/connection.js +108 -213
- package/lib/db/index.js +20 -12
- package/package.json +2 -4
- package/lib/db/schema.js +0 -27
package/lib/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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return
|
|
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
|
|
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 (
|
|
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 =
|
|
82
|
-
' chemical_meta =
|
|
83
|
-
' chemical_identifiers =
|
|
84
|
-
' chemical_synonyms =
|
|
85
|
-
' chemical_categories =
|
|
86
|
-
' 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
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
'
|
|
93
|
-
'
|
|
94
|
-
'
|
|
95
|
-
'
|
|
96
|
-
'
|
|
97
|
-
'
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
|
188
|
-
return
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
327
|
+
await this.connection.ensureConnected();
|
|
314
328
|
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
.
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
365
|
+
await this.connection.ensureConnected();
|
|
341
366
|
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if (updates.
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
.
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
.
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
407
|
+
await this.connection.ensureConnected();
|
|
366
408
|
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
421
|
+
await this.connection.ensureConnected();
|
|
382
422
|
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
435
|
+
await this.connection.ensureConnected();
|
|
398
436
|
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
|
|
450
|
+
await this.connection.ensureConnected();
|
|
451
|
+
|
|
414
452
|
const pathArray = propertyPath.split('.');
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
.
|
|
419
|
-
.
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
472
|
+
await this.connection.ensureConnected();
|
|
436
473
|
|
|
437
|
-
let
|
|
474
|
+
let whereClause = '1=1';
|
|
475
|
+
const params = [];
|
|
438
476
|
|
|
439
477
|
if (filter.chemicalIds && filter.chemicalIds.length > 0) {
|
|
440
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
512
|
+
await this.connection.ensureConnected();
|
|
469
513
|
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
526
|
+
await this.connection.ensureConnected();
|
|
486
527
|
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
|
|
540
|
+
await this.connection.ensureConnected();
|
|
503
541
|
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
|
|
523
|
-
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
-
|
|
572
|
+
await this.connection.ensureConnected();
|
|
539
573
|
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
-
|
|
586
|
+
await this.connection.ensureConnected();
|
|
555
587
|
|
|
556
588
|
const searchPattern = `%${escapeLikePattern(identifierValue)}%`;
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
-
|
|
601
|
+
await this.connection.ensureConnected();
|
|
572
602
|
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
-
|
|
615
|
+
await this.connection.ensureConnected();
|
|
588
616
|
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
|
|
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
|
-
|
|
628
|
+
await this.connection.ensureConnected();
|
|
603
629
|
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
-
|
|
642
|
+
await this.connection.ensureConnected();
|
|
619
643
|
|
|
620
644
|
const searchPattern = `%${escapeLikePattern(fromIdentifier)}%`;
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
|
|
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 (
|
|
649
|
+
if (result.rows.length === 0) {
|
|
627
650
|
return null;
|
|
628
651
|
}
|
|
629
652
|
|
|
630
|
-
const chemical =
|
|
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
|
-
|
|
767
|
-
const
|
|
768
|
-
|
|
769
|
-
|
|
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
|
-
|
|
801
|
+
await this.connection.ensureConnected();
|
|
780
802
|
|
|
781
|
-
let
|
|
803
|
+
let whereFragments = [];
|
|
804
|
+
const params = [];
|
|
782
805
|
|
|
783
806
|
if (collectionName) {
|
|
784
|
-
|
|
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
|
-
|
|
813
|
+
whereFragments.push('chemical_name ILIKE :search_term');
|
|
814
|
+
params.push({ name: 'search_term', value: { stringValue: searchPattern } });
|
|
790
815
|
}
|
|
791
816
|
|
|
792
|
-
const whereClause =
|
|
817
|
+
const whereClause = whereFragments.length > 0 ? 'WHERE ' + whereFragments.join(' AND ') : '';
|
|
818
|
+
params.push({ name: 'page_size', value: { longValue: pageSize } });
|
|
793
819
|
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
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
|
-
|
|
831
|
+
await this.connection.ensureConnected();
|
|
810
832
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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
|
|
816
|
-
|
|
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;
|