@toxplanet/pegasus-sdk 1.0.1 → 1.0.2
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 +27 -0
- package/config/environment.dev.js +6 -1
- package/config/environment.prod.js +6 -1
- package/config/environment.qa.js +5 -0
- package/config/index.js +1 -1
- package/index.js +7 -0
- package/lib/chemicals.js +66 -2
- package/lib/documents.js +48 -1
- package/lib/elasticsearch.js +404 -0
- package/lib/search.js +29 -0
- package/lib/sync.js +3 -1
- package/lib/utils.js +3 -1
- package/package.json +27 -4
- package/env.example +0 -21
- package/index.d.ts +0 -252
- package/tests/chemicals.js +0 -165
- package/tests/search.js +0 -138
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
environment: 'acc',
|
|
3
|
+
region: 'us-east-1',
|
|
4
|
+
secretName: 'arn:aws:secretsmanager:us-east-1:292931567094:secret:rds!cluster-b851c3ce-58cc-41cd-aeae-05cc7f5e031a-ZYSjiI',
|
|
5
|
+
openSearchEndpoint: 'https://war8lk73nzswquk8dcz1.us-east-1.aoss.amazonaws.com',
|
|
6
|
+
openSearchIndex: 'chemicals',
|
|
7
|
+
database: {
|
|
8
|
+
host: 'cr-chemicals.cluster-cz0iqdg8irhb.us-east-1.rds.amazonaws.com',
|
|
9
|
+
name: 'chemicals'
|
|
10
|
+
},
|
|
11
|
+
postgres: {
|
|
12
|
+
maxConnections: 2,
|
|
13
|
+
minConnections: 0,
|
|
14
|
+
idleTimeoutMillis: 30000,
|
|
15
|
+
connectionTimeoutMillis: 5000,
|
|
16
|
+
statementTimeout: 30000,
|
|
17
|
+
queryTimeout: 30000,
|
|
18
|
+
ssl: {
|
|
19
|
+
rejectUnauthorized: false
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
indexRoutes: {
|
|
23
|
+
chemicals: ['chemicals*'],
|
|
24
|
+
documents: ['documents*'],
|
|
25
|
+
search: [/^(chemicals|substances|search)/]
|
|
26
|
+
}
|
|
27
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
module.exports = {
|
|
2
|
-
environment: '
|
|
2
|
+
environment: 'dev',
|
|
3
3
|
region: 'us-east-1',
|
|
4
4
|
secretName: 'arn:aws:secretsmanager:us-east-1:292931567094:secret:rds!cluster-b851c3ce-58cc-41cd-aeae-05cc7f5e031a-ZYSjiI',
|
|
5
5
|
openSearchEndpoint: 'https://war8lk73nzswquk8dcz1.us-east-1.aoss.amazonaws.com',
|
|
@@ -18,5 +18,10 @@ module.exports = {
|
|
|
18
18
|
ssl: {
|
|
19
19
|
rejectUnauthorized: false
|
|
20
20
|
}
|
|
21
|
+
},
|
|
22
|
+
indexRoutes: {
|
|
23
|
+
chemicals: ['chemicals*'],
|
|
24
|
+
documents: ['documents*'],
|
|
25
|
+
search: [/^(chemicals|substances|search)/]
|
|
21
26
|
}
|
|
22
27
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
module.exports = {
|
|
2
|
-
environment: '
|
|
2
|
+
environment: 'prod',
|
|
3
3
|
region: 'us-east-1',
|
|
4
4
|
secretName: 'pegasus/production/database',
|
|
5
5
|
openSearchEndpoint: null,
|
|
@@ -18,5 +18,10 @@ module.exports = {
|
|
|
18
18
|
ssl: {
|
|
19
19
|
rejectUnauthorized: true
|
|
20
20
|
}
|
|
21
|
+
},
|
|
22
|
+
indexRoutes: {
|
|
23
|
+
chemicals: ['chemicals*'],
|
|
24
|
+
documents: ['documents*'],
|
|
25
|
+
search: [/^(chemicals|substances|search)/]
|
|
21
26
|
}
|
|
22
27
|
};
|
package/config/environment.qa.js
CHANGED
package/config/index.js
CHANGED
package/index.js
CHANGED
|
@@ -4,15 +4,21 @@ const DocumentsService = require('./lib/documents');
|
|
|
4
4
|
const SearchService = require('./lib/search');
|
|
5
5
|
const SyncService = require('./lib/sync');
|
|
6
6
|
const UtilsService = require('./lib/utils');
|
|
7
|
+
const ElasticsearchService = require('./lib/elasticsearch');
|
|
7
8
|
|
|
8
9
|
class PegasusSDK {
|
|
9
10
|
constructor(config) {
|
|
10
11
|
this.connection = new PegasusConnection(config);
|
|
12
|
+
this.elasticsearch = new ElasticsearchService(this.connection);
|
|
11
13
|
this.chemicals = new ChemicalsService(this.connection);
|
|
12
14
|
this.documents = new DocumentsService(this.connection);
|
|
13
15
|
this.search = new SearchService(this.connection);
|
|
14
16
|
this.sync = new SyncService(this.connection);
|
|
15
17
|
this.utils = new UtilsService(this.connection);
|
|
18
|
+
|
|
19
|
+
this.chemicals.registerElasticsearchHandlers(this.elasticsearch);
|
|
20
|
+
this.documents.registerElasticsearchHandlers(this.elasticsearch);
|
|
21
|
+
this.search.registerElasticsearchHandlers(this.elasticsearch);
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
async connect() {
|
|
@@ -35,3 +41,4 @@ module.exports.DocumentsService = DocumentsService;
|
|
|
35
41
|
module.exports.SearchService = SearchService;
|
|
36
42
|
module.exports.SyncService = SyncService;
|
|
37
43
|
module.exports.UtilsService = UtilsService;
|
|
44
|
+
module.exports.ElasticsearchService = ElasticsearchService;
|
package/lib/chemicals.js
CHANGED
|
@@ -97,8 +97,6 @@ class ChemicalsService {
|
|
|
97
97
|
|
|
98
98
|
async getChemicalsByIdentifier(identifierType, identifierValue) {}
|
|
99
99
|
|
|
100
|
-
async countAll() {}
|
|
101
|
-
|
|
102
100
|
async countByCollection(collectionName) {}
|
|
103
101
|
|
|
104
102
|
async countByIdentifier(identifierValue) {}
|
|
@@ -221,9 +219,75 @@ class ChemicalsService {
|
|
|
221
219
|
}
|
|
222
220
|
}
|
|
223
221
|
|
|
222
|
+
async countAll() {
|
|
223
|
+
try {
|
|
224
|
+
const db = this.getDb();
|
|
225
|
+
const result = await db.select({ count: sql`count(*)::int` }).from(schema.chemicals);
|
|
226
|
+
return { count: result[0].count };
|
|
227
|
+
} catch (error) {
|
|
228
|
+
logError('pegasus-sdk', 'ChemicalsService', 'countAll', error);
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
224
233
|
async findChemicalsWithoutDocuments(collectionName, searchTerm, pageSize) {}
|
|
225
234
|
|
|
226
235
|
async countChemicalsWithoutDocuments(collectionName) {}
|
|
236
|
+
|
|
237
|
+
registerElasticsearchHandlers(elasticsearchService) {
|
|
238
|
+
const indexPatterns = this.connection.config.indexRoutes?.chemicals || ['chemicals*'];
|
|
239
|
+
|
|
240
|
+
indexPatterns.forEach(pattern => {
|
|
241
|
+
elasticsearchService.registerIndexRoute(pattern, {
|
|
242
|
+
index: async (params) => {
|
|
243
|
+
const chemical = params.body;
|
|
244
|
+
return await this.createChemical(chemical);
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
bulk: async (params) => {
|
|
248
|
+
const operations = params.body || params.operations;
|
|
249
|
+
const documents = [];
|
|
250
|
+
|
|
251
|
+
for (let i = 0; i < operations.length; i += 2) {
|
|
252
|
+
const action = operations[i];
|
|
253
|
+
const document = operations[i + 1];
|
|
254
|
+
|
|
255
|
+
if (action.index || action.create) {
|
|
256
|
+
documents.push(document);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return await this.bulkIndexFielded(documents);
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
get: async (params) => {
|
|
264
|
+
return await this.getChemicalById(params.id);
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
update: async (params) => {
|
|
268
|
+
return await this.updateChemical(params.id, params.body);
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
delete: async (params) => {
|
|
272
|
+
return await this.deleteChemical(params.id);
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
search: async (params) => {
|
|
276
|
+
const query = params.body?.query;
|
|
277
|
+
const searchTerm = query?.match?.chemical_name ||
|
|
278
|
+
query?.term?.chemical_name ||
|
|
279
|
+
query?.query_string?.query || '';
|
|
280
|
+
const limit = params.body?.size || 10;
|
|
281
|
+
|
|
282
|
+
return await this.searchByName(searchTerm, limit);
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
count: async (params) => {
|
|
286
|
+
return await this.countAll();
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
}
|
|
227
291
|
}
|
|
228
292
|
|
|
229
293
|
module.exports = ChemicalsService;
|
package/lib/documents.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
class DocumentsService {
|
|
2
|
-
constructor(connection) {
|
|
2
|
+
constructor(connection) {
|
|
3
|
+
this.connection = connection;
|
|
4
|
+
}
|
|
3
5
|
|
|
4
6
|
async createDocument(documentPath, casNumbers) {}
|
|
5
7
|
|
|
@@ -42,6 +44,51 @@ class DocumentsService {
|
|
|
42
44
|
async extractCASFromText(text) {}
|
|
43
45
|
|
|
44
46
|
async processDocument(documentPath, documentData) {}
|
|
47
|
+
|
|
48
|
+
registerElasticsearchHandlers(elasticsearchService) {
|
|
49
|
+
const indexPatterns = this.connection.config.indexRoutes?.documents || ['documents*'];
|
|
50
|
+
|
|
51
|
+
indexPatterns.forEach(pattern => {
|
|
52
|
+
elasticsearchService.registerIndexRoute(pattern, {
|
|
53
|
+
index: async (params) => {
|
|
54
|
+
const document = params.body;
|
|
55
|
+
return await this.createDocument(document.document_path, document.cas_numbers || []);
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
bulk: async (params) => {
|
|
59
|
+
const operations = params.body || params.operations;
|
|
60
|
+
const documents = [];
|
|
61
|
+
|
|
62
|
+
for (let i = 0; i < operations.length; i += 2) {
|
|
63
|
+
const action = operations[i];
|
|
64
|
+
const document = operations[i + 1];
|
|
65
|
+
|
|
66
|
+
if (action.index || action.create) {
|
|
67
|
+
documents.push(document);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return await this.bulkCreateDocuments(documents);
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
get: async (params) => {
|
|
75
|
+
return await this.getDocumentById(params.id);
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
update: async (params) => {
|
|
79
|
+
return await this.updateDocument(params.id, params.body);
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
delete: async (params) => {
|
|
83
|
+
return await this.deleteDocument(params.id);
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
count: async (params) => {
|
|
87
|
+
return await this.countDocuments();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
45
92
|
}
|
|
46
93
|
|
|
47
94
|
module.exports = DocumentsService;
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
class ElasticsearchService {
|
|
2
|
+
constructor(connection) {
|
|
3
|
+
this.connection = connection;
|
|
4
|
+
this.indexRoutes = new Map();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Returns an Elasticsearch client interface that routes operations through registered handlers.
|
|
9
|
+
* This method provides compatibility with legacy elasticsearch client usage patterns.
|
|
10
|
+
* @returns {ElasticsearchService} The service instance itself, which implements the client interface
|
|
11
|
+
*/
|
|
12
|
+
client() {
|
|
13
|
+
return this;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
registerIndexRoute(indexPattern, handler) {
|
|
17
|
+
this.indexRoutes.set(indexPattern, handler);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
getRouteHandler(indexName) {
|
|
21
|
+
for (const [pattern, handler] of this.indexRoutes.entries()) {
|
|
22
|
+
if (typeof pattern === 'string' && indexName === pattern) {
|
|
23
|
+
return handler;
|
|
24
|
+
}
|
|
25
|
+
if (pattern instanceof RegExp && pattern.test(indexName)) {
|
|
26
|
+
return handler;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
// example flow:
|
|
34
|
+
// external application
|
|
35
|
+
// 1. initialize the sdk
|
|
36
|
+
// 2. call elastic search search
|
|
37
|
+
// 3. sdk will call the appropriate handler based on the index name
|
|
38
|
+
// 4. handler will be called with the appropriate parameters
|
|
39
|
+
// 5. handler will return the result
|
|
40
|
+
// 6. sdk will return the result to the external application
|
|
41
|
+
|
|
42
|
+
async search(params) {
|
|
43
|
+
const handler = this.getRouteHandler(params.index);
|
|
44
|
+
if (handler && handler.search) {
|
|
45
|
+
return await handler.search(params);
|
|
46
|
+
}
|
|
47
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async msearch(params) {
|
|
51
|
+
const handler = this.getRouteHandler(params.index);
|
|
52
|
+
if (handler && handler.msearch) {
|
|
53
|
+
return await handler.msearch(params);
|
|
54
|
+
}
|
|
55
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async index(params) {
|
|
59
|
+
const handler = this.getRouteHandler(params.index);
|
|
60
|
+
if (handler && handler.index) {
|
|
61
|
+
return await handler.index(params);
|
|
62
|
+
}
|
|
63
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async bulk(params) {
|
|
67
|
+
const operations = params.body || params.operations;
|
|
68
|
+
if (!Array.isArray(operations) || operations.length === 0) {
|
|
69
|
+
throw new Error('Bulk operations must be a non-empty array');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const firstOp = operations[0];
|
|
73
|
+
const indexName = firstOp?.index?._index || firstOp?.index?.index || firstOp?.create?._index || firstOp?.create?.index || params.index;
|
|
74
|
+
|
|
75
|
+
if (!indexName) {
|
|
76
|
+
throw new Error('Could not determine index from bulk operations');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const handler = this.getRouteHandler(indexName);
|
|
80
|
+
if (handler && handler.bulk) {
|
|
81
|
+
return await handler.bulk(params);
|
|
82
|
+
}
|
|
83
|
+
throw new Error(`No handler registered for index: ${indexName}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async get(params) {
|
|
87
|
+
const handler = this.getRouteHandler(params.index);
|
|
88
|
+
if (handler && handler.get) {
|
|
89
|
+
return await handler.get(params);
|
|
90
|
+
}
|
|
91
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async mget(params) {
|
|
95
|
+
const handler = this.getRouteHandler(params.index);
|
|
96
|
+
if (handler && handler.mget) {
|
|
97
|
+
return await handler.mget(params);
|
|
98
|
+
}
|
|
99
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async update(params) {
|
|
103
|
+
const handler = this.getRouteHandler(params.index);
|
|
104
|
+
if (handler && handler.update) {
|
|
105
|
+
return await handler.update(params);
|
|
106
|
+
}
|
|
107
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async updateByQuery(params) {
|
|
111
|
+
const handler = this.getRouteHandler(params.index);
|
|
112
|
+
if (handler && handler.updateByQuery) {
|
|
113
|
+
return await handler.updateByQuery(params);
|
|
114
|
+
}
|
|
115
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async delete(params) {
|
|
119
|
+
const handler = this.getRouteHandler(params.index);
|
|
120
|
+
if (handler && handler.delete) {
|
|
121
|
+
return await handler.delete(params);
|
|
122
|
+
}
|
|
123
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async deleteByQuery(params) {
|
|
127
|
+
const handler = this.getRouteHandler(params.index);
|
|
128
|
+
if (handler && handler.deleteByQuery) {
|
|
129
|
+
return await handler.deleteByQuery(params);
|
|
130
|
+
}
|
|
131
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async exists(params) {
|
|
135
|
+
const handler = this.getRouteHandler(params.index);
|
|
136
|
+
if (handler && handler.exists) {
|
|
137
|
+
return await handler.exists(params);
|
|
138
|
+
}
|
|
139
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async count(params) {
|
|
143
|
+
const handler = this.getRouteHandler(params.index);
|
|
144
|
+
if (handler && handler.count) {
|
|
145
|
+
return await handler.count(params);
|
|
146
|
+
}
|
|
147
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async scroll(params) {
|
|
151
|
+
const handler = this.getRouteHandler(params.index);
|
|
152
|
+
if (handler && handler.scroll) {
|
|
153
|
+
return await handler.scroll(params);
|
|
154
|
+
}
|
|
155
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async clearScroll(params) {
|
|
159
|
+
const handler = this.getRouteHandler(params.index);
|
|
160
|
+
if (handler && handler.clearScroll) {
|
|
161
|
+
return await handler.clearScroll(params);
|
|
162
|
+
}
|
|
163
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async reindex(params) {
|
|
167
|
+
const sourceIndex = params.body?.source?.index;
|
|
168
|
+
const destIndex = params.body?.dest?.index;
|
|
169
|
+
|
|
170
|
+
const handler = this.getRouteHandler(destIndex || sourceIndex);
|
|
171
|
+
if (handler && handler.reindex) {
|
|
172
|
+
return await handler.reindex(params);
|
|
173
|
+
}
|
|
174
|
+
throw new Error(`No handler registered for index: ${destIndex || sourceIndex}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
indices() {
|
|
178
|
+
return {
|
|
179
|
+
create: async (params) => {
|
|
180
|
+
const handler = this.getRouteHandler(params.index);
|
|
181
|
+
if (handler && handler.indices && handler.indices.create) {
|
|
182
|
+
return await handler.indices.create(params);
|
|
183
|
+
}
|
|
184
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
185
|
+
},
|
|
186
|
+
delete: async (params) => {
|
|
187
|
+
const handler = this.getRouteHandler(params.index);
|
|
188
|
+
if (handler && handler.indices && handler.indices.delete) {
|
|
189
|
+
return await handler.indices.delete(params);
|
|
190
|
+
}
|
|
191
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
192
|
+
},
|
|
193
|
+
exists: async (params) => {
|
|
194
|
+
const handler = this.getRouteHandler(params.index);
|
|
195
|
+
if (handler && handler.indices && handler.indices.exists) {
|
|
196
|
+
return await handler.indices.exists(params);
|
|
197
|
+
}
|
|
198
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
199
|
+
},
|
|
200
|
+
refresh: async (params) => {
|
|
201
|
+
const handler = this.getRouteHandler(params.index);
|
|
202
|
+
if (handler && handler.indices && handler.indices.refresh) {
|
|
203
|
+
return await handler.indices.refresh(params);
|
|
204
|
+
}
|
|
205
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
206
|
+
},
|
|
207
|
+
flush: async (params) => {
|
|
208
|
+
const handler = this.getRouteHandler(params.index);
|
|
209
|
+
if (handler && handler.indices && handler.indices.flush) {
|
|
210
|
+
return await handler.indices.flush(params);
|
|
211
|
+
}
|
|
212
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
213
|
+
},
|
|
214
|
+
putMapping: async (params) => {
|
|
215
|
+
const handler = this.getRouteHandler(params.index);
|
|
216
|
+
if (handler && handler.indices && handler.indices.putMapping) {
|
|
217
|
+
return await handler.indices.putMapping(params);
|
|
218
|
+
}
|
|
219
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
220
|
+
},
|
|
221
|
+
getMapping: async (params) => {
|
|
222
|
+
const handler = this.getRouteHandler(params.index);
|
|
223
|
+
if (handler && handler.indices && handler.indices.getMapping) {
|
|
224
|
+
return await handler.indices.getMapping(params);
|
|
225
|
+
}
|
|
226
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
227
|
+
},
|
|
228
|
+
putSettings: async (params) => {
|
|
229
|
+
const handler = this.getRouteHandler(params.index);
|
|
230
|
+
if (handler && handler.indices && handler.indices.putSettings) {
|
|
231
|
+
return await handler.indices.putSettings(params);
|
|
232
|
+
}
|
|
233
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
234
|
+
},
|
|
235
|
+
getSettings: async (params) => {
|
|
236
|
+
const handler = this.getRouteHandler(params.index);
|
|
237
|
+
if (handler && handler.indices && handler.indices.getSettings) {
|
|
238
|
+
return await handler.indices.getSettings(params);
|
|
239
|
+
}
|
|
240
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
241
|
+
},
|
|
242
|
+
stats: async (params) => {
|
|
243
|
+
const handler = this.getRouteHandler(params.index);
|
|
244
|
+
if (handler && handler.indices && handler.indices.stats) {
|
|
245
|
+
return await handler.indices.stats(params);
|
|
246
|
+
}
|
|
247
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
248
|
+
},
|
|
249
|
+
get: async (params) => {
|
|
250
|
+
const handler = this.getRouteHandler(params.index);
|
|
251
|
+
if (handler && handler.indices && handler.indices.get) {
|
|
252
|
+
return await handler.indices.get(params);
|
|
253
|
+
}
|
|
254
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
255
|
+
},
|
|
256
|
+
putAlias: async (params) => {
|
|
257
|
+
const handler = this.getRouteHandler(params.index);
|
|
258
|
+
if (handler && handler.indices && handler.indices.putAlias) {
|
|
259
|
+
return await handler.indices.putAlias(params);
|
|
260
|
+
}
|
|
261
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
262
|
+
},
|
|
263
|
+
deleteAlias: async (params) => {
|
|
264
|
+
const handler = this.getRouteHandler(params.index);
|
|
265
|
+
if (handler && handler.indices && handler.indices.deleteAlias) {
|
|
266
|
+
return await handler.indices.deleteAlias(params);
|
|
267
|
+
}
|
|
268
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
269
|
+
},
|
|
270
|
+
getAlias: async (params) => {
|
|
271
|
+
const handler = this.getRouteHandler(params.index);
|
|
272
|
+
if (handler && handler.indices && handler.indices.getAlias) {
|
|
273
|
+
return await handler.indices.getAlias(params);
|
|
274
|
+
}
|
|
275
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
276
|
+
},
|
|
277
|
+
analyze: async (params) => {
|
|
278
|
+
const handler = this.getRouteHandler(params.index);
|
|
279
|
+
if (handler && handler.indices && handler.indices.analyze) {
|
|
280
|
+
return await handler.indices.analyze(params);
|
|
281
|
+
}
|
|
282
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
283
|
+
},
|
|
284
|
+
close: async (params) => {
|
|
285
|
+
const handler = this.getRouteHandler(params.index);
|
|
286
|
+
if (handler && handler.indices && handler.indices.close) {
|
|
287
|
+
return await handler.indices.close(params);
|
|
288
|
+
}
|
|
289
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
290
|
+
},
|
|
291
|
+
open: async (params) => {
|
|
292
|
+
const handler = this.getRouteHandler(params.index);
|
|
293
|
+
if (handler && handler.indices && handler.indices.open) {
|
|
294
|
+
return await handler.indices.open(params);
|
|
295
|
+
}
|
|
296
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
cat() {
|
|
302
|
+
return {
|
|
303
|
+
indices: async (params) => {
|
|
304
|
+
throw new Error('cat.indices not implemented');
|
|
305
|
+
},
|
|
306
|
+
health: async (params) => {
|
|
307
|
+
throw new Error('cat.health not implemented');
|
|
308
|
+
},
|
|
309
|
+
count: async (params) => {
|
|
310
|
+
throw new Error('cat.count not implemented');
|
|
311
|
+
},
|
|
312
|
+
aliases: async (params) => {
|
|
313
|
+
throw new Error('cat.aliases not implemented');
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
cluster() {
|
|
319
|
+
return {
|
|
320
|
+
health: async (params) => {
|
|
321
|
+
throw new Error('cluster.health not implemented');
|
|
322
|
+
},
|
|
323
|
+
stats: async (params) => {
|
|
324
|
+
throw new Error('cluster.stats not implemented');
|
|
325
|
+
},
|
|
326
|
+
putSettings: async (params) => {
|
|
327
|
+
throw new Error('cluster.putSettings not implemented');
|
|
328
|
+
},
|
|
329
|
+
getSettings: async (params) => {
|
|
330
|
+
throw new Error('cluster.getSettings not implemented');
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async ping() {
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async info() {
|
|
340
|
+
return {
|
|
341
|
+
name: 'pegasus-sdk',
|
|
342
|
+
version: '1.0.0',
|
|
343
|
+
cluster_name: 'pegasus'
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async explain(params) {
|
|
348
|
+
const handler = this.getRouteHandler(params.index);
|
|
349
|
+
if (handler && handler.explain) {
|
|
350
|
+
return await handler.explain(params);
|
|
351
|
+
}
|
|
352
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async termvectors(params) {
|
|
356
|
+
const handler = this.getRouteHandler(params.index);
|
|
357
|
+
if (handler && handler.termvectors) {
|
|
358
|
+
return await handler.termvectors(params);
|
|
359
|
+
}
|
|
360
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async mtermvectors(params) {
|
|
364
|
+
const handler = this.getRouteHandler(params.index);
|
|
365
|
+
if (handler && handler.mtermvectors) {
|
|
366
|
+
return await handler.mtermvectors(params);
|
|
367
|
+
}
|
|
368
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async searchTemplate(params) {
|
|
372
|
+
const handler = this.getRouteHandler(params.index);
|
|
373
|
+
if (handler && handler.searchTemplate) {
|
|
374
|
+
return await handler.searchTemplate(params);
|
|
375
|
+
}
|
|
376
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async renderSearchTemplate(params) {
|
|
380
|
+
throw new Error('renderSearchTemplate not implemented');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async fieldCaps(params) {
|
|
384
|
+
const handler = this.getRouteHandler(params.index);
|
|
385
|
+
if (handler && handler.fieldCaps) {
|
|
386
|
+
return await handler.fieldCaps(params);
|
|
387
|
+
}
|
|
388
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async rankEval(params) {
|
|
392
|
+
const handler = this.getRouteHandler(params.index);
|
|
393
|
+
if (handler && handler.rankEval) {
|
|
394
|
+
return await handler.rankEval(params);
|
|
395
|
+
}
|
|
396
|
+
throw new Error(`No handler registered for index: ${params.index}`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async scriptsPainlessExecute(params) {
|
|
400
|
+
throw new Error('scriptsPainlessExecute not implemented');
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
module.exports = ElasticsearchService;
|
package/lib/search.js
CHANGED
|
@@ -302,6 +302,35 @@ class SearchService {
|
|
|
302
302
|
async getSearchSuggestions(partialTerm, limit) {}
|
|
303
303
|
|
|
304
304
|
async findSimilarChemicals(chemicalId, limit) {}
|
|
305
|
+
|
|
306
|
+
registerElasticsearchHandlers(elasticsearchService) {
|
|
307
|
+
const indexPatterns = this.connection.config.indexRoutes?.search || [/^(chemicals|substances|search)/];
|
|
308
|
+
|
|
309
|
+
indexPatterns.forEach(pattern => {
|
|
310
|
+
elasticsearchService.registerIndexRoute(pattern, {
|
|
311
|
+
search: async (params) => {
|
|
312
|
+
const query = params.body?.query;
|
|
313
|
+
const searchTerm = query?.match?.chemical_name ||
|
|
314
|
+
query?.term?.chemical_name ||
|
|
315
|
+
query?.query_string?.query ||
|
|
316
|
+
query?.match_all ? '*' : '';
|
|
317
|
+
const limit = params.body?.size || 10;
|
|
318
|
+
|
|
319
|
+
return await this.searchChemicals(searchTerm, { limit });
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
count: async (params) => {
|
|
323
|
+
const query = params.body?.query;
|
|
324
|
+
const searchTerm = query?.match?.chemical_name ||
|
|
325
|
+
query?.term?.chemical_name ||
|
|
326
|
+
query?.query_string?.query || '';
|
|
327
|
+
|
|
328
|
+
const results = await this.searchChemicals(searchTerm, { limit: 10000 });
|
|
329
|
+
return { count: results.results.length };
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
}
|
|
305
334
|
}
|
|
306
335
|
|
|
307
336
|
module.exports = SearchService;
|
package/lib/sync.js
CHANGED
package/lib/utils.js
CHANGED
package/package.json
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toxplanet/pegasus-sdk",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "SDK for migrating chemical data to Pegasus PostgreSQL + OpenSearch architecture",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "SDK for migrating chemical data to Pegasus PostgreSQL + OpenSearch architecture with Elasticsearch client compatibility",
|
|
5
5
|
"main": "index.js",
|
|
6
|
+
"type": "commonjs",
|
|
6
7
|
"scripts": {
|
|
7
|
-
"test
|
|
8
|
-
"test:
|
|
8
|
+
"test": "vitest run",
|
|
9
|
+
"test:watch": "vitest",
|
|
10
|
+
"test:ui": "vitest --ui"
|
|
9
11
|
},
|
|
10
12
|
"keywords": [
|
|
13
|
+
"elasticsearch",
|
|
14
|
+
"opensearch",
|
|
15
|
+
"postgresql",
|
|
16
|
+
"aws",
|
|
17
|
+
"chemicals",
|
|
18
|
+
"database",
|
|
19
|
+
"search",
|
|
20
|
+
"sdk",
|
|
21
|
+
"pegasus",
|
|
22
|
+
"migration"
|
|
11
23
|
],
|
|
12
24
|
"author": "Chemical Research Development Team",
|
|
13
25
|
"license": "MIT",
|
|
@@ -21,5 +33,16 @@
|
|
|
21
33
|
},
|
|
22
34
|
"engines": {
|
|
23
35
|
"node": ">=18.0.0"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"index.js",
|
|
39
|
+
"lib/",
|
|
40
|
+
"config/",
|
|
41
|
+
"README.md",
|
|
42
|
+
"ELASTICSEARCH_CLIENT.md",
|
|
43
|
+
"LICENSE"
|
|
44
|
+
],
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"vitest": "^1.2.0"
|
|
24
47
|
}
|
|
25
48
|
}
|
package/env.example
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
NODE_ENV=development
|
|
2
|
-
|
|
3
|
-
# Pegasus SDK Configuration Overrides
|
|
4
|
-
PEGASUS_SDK_DB_SECRET_ARN=arn:aws:secretsmanager:us-east-1:292931567094:secret:rds!cluster-b851c3ce-58cc-41cd-aeae-05cc7f5e031a-ZYSjiI
|
|
5
|
-
PEGASUS_SDK_OPENSEARCH_ENDPOINT=https://war8lk73nzswquk8dcz1.us-east-1.aoss.amazonaws.com
|
|
6
|
-
PEGASUS_SDK_AWS_REGION=us-east-1
|
|
7
|
-
|
|
8
|
-
# Database Configuration
|
|
9
|
-
PEGASUS_SDK_DATABASE_HOST=cr-chemicals.cluster-cz0iqdg8irhb.us-east-1.rds.amazonaws.com
|
|
10
|
-
PEGASUS_SDK_DATABASE_NAME=chemicals
|
|
11
|
-
|
|
12
|
-
# OpenSearch Configuration
|
|
13
|
-
PEGASUS_SDK_OPENSEARCH_INDEX=chemicals
|
|
14
|
-
|
|
15
|
-
# PostgreSQL Connection Pool Settings (optional)
|
|
16
|
-
PEGASUS_SDK_MAX_CONNECTIONS=2
|
|
17
|
-
PEGASUS_SDK_MIN_CONNECTIONS=0
|
|
18
|
-
PEGASUS_SDK_IDLE_TIMEOUT=30000
|
|
19
|
-
PEGASUS_SDK_CONNECTION_TIMEOUT=5000
|
|
20
|
-
PEGASUS_SDK_STATEMENT_TIMEOUT=30000
|
|
21
|
-
PEGASUS_SDK_QUERY_TIMEOUT=30000
|
package/index.d.ts
DELETED
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
export interface PostgresConfig {
|
|
2
|
-
maxConnections?: number;
|
|
3
|
-
minConnections?: number;
|
|
4
|
-
idleTimeoutMillis?: number;
|
|
5
|
-
connectionTimeoutMillis?: number;
|
|
6
|
-
statementTimeout?: number;
|
|
7
|
-
queryTimeout?: number;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface DatabaseConfig {
|
|
11
|
-
host?: string;
|
|
12
|
-
name?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface PegasusConfig {
|
|
16
|
-
environment?: string;
|
|
17
|
-
region?: string;
|
|
18
|
-
secretName?: string;
|
|
19
|
-
openSearchEndpoint?: string;
|
|
20
|
-
openSearchIndex?: string;
|
|
21
|
-
database?: DatabaseConfig;
|
|
22
|
-
postgres?: PostgresConfig;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface Chemical {
|
|
26
|
-
chemical_id?: string;
|
|
27
|
-
source_id: string;
|
|
28
|
-
chemical_name: string;
|
|
29
|
-
chemical_identifiers: Identifier[];
|
|
30
|
-
chemical_synonyms: string[];
|
|
31
|
-
chemical_categories: string[];
|
|
32
|
-
chemical_meta: Record<string, any>;
|
|
33
|
-
created_at?: Date;
|
|
34
|
-
updated_at?: Date;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface Identifier {
|
|
38
|
-
type: string;
|
|
39
|
-
value: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface Document {
|
|
43
|
-
document_id?: string;
|
|
44
|
-
document_path: string;
|
|
45
|
-
processed_at?: Date;
|
|
46
|
-
cas_count?: number;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface OutboxEntry {
|
|
50
|
-
outbox_id: number;
|
|
51
|
-
chemical_id: string;
|
|
52
|
-
operation: 'INSERT' | 'UPDATE' | 'DELETE';
|
|
53
|
-
created_at: Date;
|
|
54
|
-
processed_at?: Date;
|
|
55
|
-
retry_count: number;
|
|
56
|
-
last_error?: string;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export interface ConnectionStatus {
|
|
60
|
-
postgres: {
|
|
61
|
-
connected: boolean;
|
|
62
|
-
timestamp?: Date;
|
|
63
|
-
version?: string;
|
|
64
|
-
poolSize?: number;
|
|
65
|
-
idleConnections?: number;
|
|
66
|
-
waitingRequests?: number;
|
|
67
|
-
error?: string;
|
|
68
|
-
};
|
|
69
|
-
opensearch: {
|
|
70
|
-
connected: boolean;
|
|
71
|
-
version?: string;
|
|
72
|
-
cluster?: string;
|
|
73
|
-
error?: string;
|
|
74
|
-
} | null;
|
|
75
|
-
environment: string;
|
|
76
|
-
region: string;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export declare class PegasusConnection {
|
|
80
|
-
constructor(config?: PegasusConfig);
|
|
81
|
-
getSecret(): Promise<any>;
|
|
82
|
-
connect(): Promise<void>;
|
|
83
|
-
disconnect(): Promise<void>;
|
|
84
|
-
getPostgresClient(): any;
|
|
85
|
-
getOpenSearchClient(): any;
|
|
86
|
-
getOpenSearchIndex(): string;
|
|
87
|
-
testConnection(): Promise<ConnectionStatus>;
|
|
88
|
-
query(sql: string, params?: any[]): Promise<any>;
|
|
89
|
-
getClient(): Promise<any>;
|
|
90
|
-
transaction(callback: (client: any) => Promise<any>): Promise<any>;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export declare class ChemicalsService {
|
|
94
|
-
constructor(connection: PegasusConnection);
|
|
95
|
-
bulkIndexFielded(documents: any[]): Promise<void>;
|
|
96
|
-
bulkIndexFulltext(documents: any[]): Promise<void>;
|
|
97
|
-
bulkIndexSubstances(substances: any[]): Promise<void>;
|
|
98
|
-
createChemical(chemical: Chemical): Promise<Chemical>;
|
|
99
|
-
updateChemical(chemicalId: string, updates: Partial<Chemical>): Promise<Chemical>;
|
|
100
|
-
deleteChemical(chemicalId: string): Promise<void>;
|
|
101
|
-
deleteBySourceId(sourceId: string): Promise<void>;
|
|
102
|
-
deleteCollection(collectionName: string): Promise<number>;
|
|
103
|
-
updateCollectionProperty(collectionName: string, propertyPath: string, newValue: any): Promise<number>;
|
|
104
|
-
bulkUpdateProperty(filter: any, propertyPath: string, newValue: any): Promise<number>;
|
|
105
|
-
getChemicalById(chemicalId: string): Promise<Chemical>;
|
|
106
|
-
getChemicalBySourceId(sourceId: string): Promise<Chemical>;
|
|
107
|
-
getChemicalsByCAS(casNumber: string): Promise<Chemical[]>;
|
|
108
|
-
getChemicalsByIdentifier(identifierType: string, identifierValue: string): Promise<Chemical[]>;
|
|
109
|
-
countAll(): Promise<number>;
|
|
110
|
-
countByCollection(collectionName: string): Promise<number>;
|
|
111
|
-
countByIdentifier(identifierValue: string): Promise<number>;
|
|
112
|
-
countByCAS(casNumber: string): Promise<number>;
|
|
113
|
-
getTotalSynonymCount(): Promise<number>;
|
|
114
|
-
getSynonymCount(synonymTerm: string): Promise<number>;
|
|
115
|
-
convertIdentifier(fromIdentifier: string, toIdentifierType: string): Promise<any>;
|
|
116
|
-
convertIdentifiersBatch(fromIdentifiers: string[], toIdentifierType: string): Promise<any[]>;
|
|
117
|
-
searchByName(searchTerm: string, limit?: number): Promise<SearchResults>;
|
|
118
|
-
searchBySynonym(synonymTerm: string, limit?: number): Promise<SearchResults>;
|
|
119
|
-
findChemicalsWithoutDocuments(collectionName: string, searchTerm: string, pageSize: number): Promise<Chemical[]>;
|
|
120
|
-
countChemicalsWithoutDocuments(collectionName: string): Promise<number>;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export declare class DocumentsService {
|
|
124
|
-
constructor(connection: PegasusConnection);
|
|
125
|
-
createDocument(documentPath: string, casNumbers: string[]): Promise<Document>;
|
|
126
|
-
bulkCreateDocuments(documents: any[]): Promise<void>;
|
|
127
|
-
updateDocument(documentId: string, updates: Partial<Document>): Promise<Document>;
|
|
128
|
-
deleteDocument(documentId: string): Promise<void>;
|
|
129
|
-
deleteDocumentByPath(documentPath: string): Promise<void>;
|
|
130
|
-
getDocumentById(documentId: string): Promise<Document>;
|
|
131
|
-
getDocumentByPath(documentPath: string): Promise<Document>;
|
|
132
|
-
getDocumentsByCAS(casNumber: string): Promise<Document[]>;
|
|
133
|
-
getCASByDocument(documentId: string): Promise<string[]>;
|
|
134
|
-
getCASByDocumentPath(documentPath: string): Promise<string[]>;
|
|
135
|
-
addCASToDocument(documentId: string, casNumber: string): Promise<void>;
|
|
136
|
-
addCASToDocumentBatch(documentId: string, casNumbers: string[]): Promise<void>;
|
|
137
|
-
removeCASFromDocument(documentId: string, casNumber: string): Promise<void>;
|
|
138
|
-
findDocumentsWithMultipleCAS(casNumbers: string[], requireAll: boolean): Promise<Document[]>;
|
|
139
|
-
countDocuments(): Promise<number>;
|
|
140
|
-
countDocumentsByCAS(casNumber: string): Promise<number>;
|
|
141
|
-
countUniqueCAS(): Promise<number>;
|
|
142
|
-
getTopCASByDocumentCount(limit: number): Promise<any[]>;
|
|
143
|
-
extractTextFromPDF(pdfBuffer: Buffer): Promise<string>;
|
|
144
|
-
extractCASFromText(text: string): Promise<string[]>;
|
|
145
|
-
processDocument(documentPath: string, documentData: any): Promise<Document>;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export interface SearchOptions {
|
|
149
|
-
limit?: number;
|
|
150
|
-
casExact?: number;
|
|
151
|
-
casPrefix?: number;
|
|
152
|
-
nameExact?: number;
|
|
153
|
-
namePrefix?: number;
|
|
154
|
-
identifierExact?: number;
|
|
155
|
-
identifierPrefix?: number;
|
|
156
|
-
synonymExact?: number;
|
|
157
|
-
synonymPrefix?: number;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export interface ChemicalSearchResult {
|
|
161
|
-
id: number;
|
|
162
|
-
name: string;
|
|
163
|
-
cas: string[];
|
|
164
|
-
identifiers: string[];
|
|
165
|
-
synonyms: string[];
|
|
166
|
-
score: number;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export interface SearchResults {
|
|
170
|
-
results: ChemicalSearchResult[];
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export declare class SearchService {
|
|
174
|
-
constructor(connection: PegasusConnection);
|
|
175
|
-
searchChemicals(query: string, options?: SearchOptions): Promise<SearchResults>;
|
|
176
|
-
searchStartsWith(searchTerm: string, limit?: number): Promise<SearchResults>;
|
|
177
|
-
searchContains(searchTerm: string, limit?: number): Promise<SearchResults>;
|
|
178
|
-
searchExact(searchTerm: string, limit?: number): Promise<SearchResults>;
|
|
179
|
-
searchByCAS(casNumber: string, searchType?: string): Promise<SearchResults>;
|
|
180
|
-
searchByIdentifier(identifierValue: string, searchType?: string): Promise<SearchResults>;
|
|
181
|
-
searchBySynonym(synonymTerm: string, searchType?: string): Promise<SearchResults>;
|
|
182
|
-
advancedSearch(queryBuilder: any): Promise<any[]>;
|
|
183
|
-
searchWithFilters(searchTerm: string, filters: any, limit: number): Promise<any[]>;
|
|
184
|
-
searchByCollection(collectionName: string, searchTerm: string, limit: number): Promise<any[]>;
|
|
185
|
-
aggregateByCategory(): Promise<any>;
|
|
186
|
-
aggregateByIdentifierType(): Promise<any>;
|
|
187
|
-
getSearchSuggestions(partialTerm: string, limit: number): Promise<string[]>;
|
|
188
|
-
findSimilarChemicals(chemicalId: string, limit: number): Promise<any[]>;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export declare class SyncService {
|
|
192
|
-
constructor(connection: PegasusConnection);
|
|
193
|
-
syncBatch(batchSize: number): Promise<number>;
|
|
194
|
-
syncAll(): Promise<number>;
|
|
195
|
-
syncContinuous(intervalMs: number): Promise<void>;
|
|
196
|
-
stopContinuousSync(): Promise<void>;
|
|
197
|
-
getPendingCount(): Promise<number>;
|
|
198
|
-
getOldestPending(): Promise<Date>;
|
|
199
|
-
getSyncLag(): Promise<number>;
|
|
200
|
-
getFailedEntries(minRetryCount: number): Promise<OutboxEntry[]>;
|
|
201
|
-
retryFailed(outboxId: number): Promise<void>;
|
|
202
|
-
retryAllFailed(): Promise<number>;
|
|
203
|
-
markAsProcessed(outboxId: number): Promise<void>;
|
|
204
|
-
deleteProcessedOlderThan(days: number): Promise<number>;
|
|
205
|
-
cleanupOutbox(daysToKeep: number): Promise<number>;
|
|
206
|
-
getSyncStats(timeWindowMinutes: number): Promise<any>;
|
|
207
|
-
getSyncThroughput(): Promise<number>;
|
|
208
|
-
verifySync(chemicalId: string): Promise<boolean>;
|
|
209
|
-
forceResync(chemicalId: string): Promise<void>;
|
|
210
|
-
getOutboxHealth(): Promise<any>;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
export declare class UtilsService {
|
|
215
|
-
constructor(connection: PegasusConnection);
|
|
216
|
-
executeBatch(operations: any[], batchSize: number, concurrency: number): Promise<void>;
|
|
217
|
-
withTransaction(callback: () => Promise<any>): Promise<any>;
|
|
218
|
-
withRetry(operation: () => Promise<any>, maxRetries: number, backoffMs: number): Promise<any>;
|
|
219
|
-
validateChemical(chemical: Chemical): boolean;
|
|
220
|
-
validateDocument(document: Document): boolean;
|
|
221
|
-
validateIdentifier(identifier: Identifier): boolean;
|
|
222
|
-
validateCAS(casNumber: string): boolean;
|
|
223
|
-
transformForOpenSearch(chemical: Chemical): any;
|
|
224
|
-
transformFromElasticsearch(esDocument: any): Chemical;
|
|
225
|
-
transformFromDynamoDB(dynamoItem: any): Chemical;
|
|
226
|
-
buildOpenSearchQuery(searchTerm: string, searchType: string): any;
|
|
227
|
-
buildPostgresFilter(filters: any): any;
|
|
228
|
-
parseChemicalIdentifiers(identifiers: any): Identifier[];
|
|
229
|
-
parseSynonyms(synonyms: any): string[];
|
|
230
|
-
extractCASFromText(text: string): string[];
|
|
231
|
-
sanitizeSearchTerm(term: string): string;
|
|
232
|
-
generateSourceId(chemical: Chemical): string;
|
|
233
|
-
calculateChecksum(data: any): string;
|
|
234
|
-
formatError(error: Error): any;
|
|
235
|
-
logOperation(operation: string, duration: number, metadata: any): void;
|
|
236
|
-
getTimestamp(): Date;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
export default class PegasusSDK {
|
|
241
|
-
connection: PegasusConnection;
|
|
242
|
-
chemicals: ChemicalsService;
|
|
243
|
-
documents: DocumentsService;
|
|
244
|
-
search: SearchService;
|
|
245
|
-
sync: SyncService;
|
|
246
|
-
utils: UtilsService;
|
|
247
|
-
|
|
248
|
-
constructor(config: PegasusConfig);
|
|
249
|
-
connect(): Promise<void>;
|
|
250
|
-
disconnect(): Promise<void>;
|
|
251
|
-
healthCheck(): Promise<any>;
|
|
252
|
-
}
|
package/tests/chemicals.js
DELETED
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
const PegasusSDK = require('../index');
|
|
2
|
-
const { logInfo, logError } = require('@toxplanet/tphelper/logging');
|
|
3
|
-
|
|
4
|
-
const ICONS = {
|
|
5
|
-
PASS: '[OK]',
|
|
6
|
-
FAIL: '[!!]',
|
|
7
|
-
WARN: '[?]'
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
async function runChemicalTests() {
|
|
11
|
-
const sdk = new PegasusSDK();
|
|
12
|
-
let testsPassed = 0;
|
|
13
|
-
let testsFailed = 0;
|
|
14
|
-
|
|
15
|
-
const log = (test, status, details = '') => {
|
|
16
|
-
logInfo('pegasus-sdk-tests', `${test}: ${status} ${details}`);
|
|
17
|
-
if (status === ICONS.PASS) testsPassed++;
|
|
18
|
-
else if (status === ICONS.FAIL) testsFailed++;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const print = (message) => {
|
|
22
|
-
logInfo('pegasus-sdk-tests', message);
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
let createdChemicalId = null;
|
|
26
|
-
|
|
27
|
-
try {
|
|
28
|
-
await sdk.connect();
|
|
29
|
-
log('Connection', ICONS.PASS);
|
|
30
|
-
|
|
31
|
-
const testChemical = {
|
|
32
|
-
source_id: `test-chemical-${Date.now()}`,
|
|
33
|
-
chemical_name: 'Test Chemical (Benzene)',
|
|
34
|
-
chemical_meta: {
|
|
35
|
-
test: true,
|
|
36
|
-
description: 'This is a test chemical record'
|
|
37
|
-
},
|
|
38
|
-
chemical_identifiers: [
|
|
39
|
-
{ type: 'cas', value: '71-43-2' },
|
|
40
|
-
{ type: 'pubchem_cid', value: 'CID241' }
|
|
41
|
-
],
|
|
42
|
-
chemical_synonyms: ['Test Synonym 1', 'Test Synonym 2'],
|
|
43
|
-
chemical_categories: ['test', 'aromatic'],
|
|
44
|
-
created_at: new Date(),
|
|
45
|
-
updated_at: new Date()
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
print('\n' + '='.repeat(60));
|
|
49
|
-
print('TEST 1: Create Chemical');
|
|
50
|
-
print('='.repeat(60));
|
|
51
|
-
|
|
52
|
-
const created = await sdk.chemicals.createChemical(testChemical);
|
|
53
|
-
createdChemicalId = created.chemicalId;
|
|
54
|
-
|
|
55
|
-
if (created && created.chemicalId) {
|
|
56
|
-
log('Create Chemical', ICONS.PASS, `(ID: ${created.chemicalId.substring(0, 8)}...)`);
|
|
57
|
-
print(` Source ID: ${created.sourceId}`);
|
|
58
|
-
print(` Name: ${created.chemicalName}`);
|
|
59
|
-
print(` Identifiers: ${created.chemicalIdentifiers.length} found`);
|
|
60
|
-
print(` Synonyms: ${created.chemicalSynonyms.length} found`);
|
|
61
|
-
} else {
|
|
62
|
-
log('Create Chemical', ICONS.FAIL, 'No ID returned');
|
|
63
|
-
testsFailed++;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
print('\n' + '='.repeat(60));
|
|
67
|
-
print('TEST 2: Get Chemical By ID');
|
|
68
|
-
print('='.repeat(60));
|
|
69
|
-
|
|
70
|
-
const retrieved = await sdk.chemicals.getChemicalById(createdChemicalId);
|
|
71
|
-
|
|
72
|
-
if (retrieved && retrieved.chemicalId === createdChemicalId) {
|
|
73
|
-
log('Get Chemical By ID', ICONS.PASS);
|
|
74
|
-
print(` Retrieved Name: ${retrieved.chemicalName}`);
|
|
75
|
-
print(` Source ID: ${retrieved.sourceId}`);
|
|
76
|
-
|
|
77
|
-
if (retrieved.chemicalName === testChemical.chemical_name) {
|
|
78
|
-
log('Name Match', ICONS.PASS);
|
|
79
|
-
} else {
|
|
80
|
-
log('Name Match', ICONS.FAIL, `Expected: ${testChemical.chemical_name}, Got: ${retrieved.chemicalName}`);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (retrieved.sourceId === testChemical.source_id) {
|
|
84
|
-
log('Source ID Match', ICONS.PASS);
|
|
85
|
-
} else {
|
|
86
|
-
log('Source ID Match', ICONS.FAIL);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (Array.isArray(retrieved.chemicalIdentifiers) && retrieved.chemicalIdentifiers.length === 2) {
|
|
90
|
-
log('Identifiers Match', ICONS.PASS, `(${retrieved.chemicalIdentifiers.length} identifiers)`);
|
|
91
|
-
} else {
|
|
92
|
-
log('Identifiers Match', ICONS.FAIL);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (Array.isArray(retrieved.chemicalSynonyms) && retrieved.chemicalSynonyms.length === 2) {
|
|
96
|
-
log('Synonyms Match', ICONS.PASS, `(${retrieved.chemicalSynonyms.length} synonyms)`);
|
|
97
|
-
} else {
|
|
98
|
-
log('Synonyms Match', ICONS.FAIL);
|
|
99
|
-
}
|
|
100
|
-
} else {
|
|
101
|
-
log('Get Chemical By ID', ICONS.FAIL, 'Chemical not found or ID mismatch');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
print('\n' + '='.repeat(60));
|
|
105
|
-
print('TEST 3: Get Non-Existent Chemical');
|
|
106
|
-
print('='.repeat(60));
|
|
107
|
-
|
|
108
|
-
const nonExistent = await sdk.chemicals.getChemicalById('00000000-0000-0000-0000-000000000000');
|
|
109
|
-
|
|
110
|
-
if (nonExistent === null) {
|
|
111
|
-
log('Get Non-Existent', ICONS.PASS, '(Correctly returned null)');
|
|
112
|
-
} else {
|
|
113
|
-
log('Get Non-Existent', ICONS.FAIL, 'Should return null for non-existent ID');
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
print('\n' + '='.repeat(60));
|
|
117
|
-
print('TEST 4: Delete Chemical');
|
|
118
|
-
print('='.repeat(60));
|
|
119
|
-
|
|
120
|
-
const deleted = await sdk.chemicals.deleteChemical(createdChemicalId);
|
|
121
|
-
|
|
122
|
-
if (deleted && deleted.chemicalId === createdChemicalId) {
|
|
123
|
-
log('Delete Chemical', ICONS.PASS, `(Deleted ID: ${deleted.chemicalId.substring(0, 8)}...)`);
|
|
124
|
-
} else {
|
|
125
|
-
log('Delete Chemical', ICONS.FAIL, 'Delete did not return expected result');
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
print('\n' + '='.repeat(60));
|
|
129
|
-
print('TEST 5: Verify Deletion');
|
|
130
|
-
print('='.repeat(60));
|
|
131
|
-
|
|
132
|
-
const shouldBeGone = await sdk.chemicals.getChemicalById(createdChemicalId);
|
|
133
|
-
|
|
134
|
-
if (shouldBeGone === null) {
|
|
135
|
-
log('Verify Deletion', ICONS.PASS, '(Chemical no longer exists)');
|
|
136
|
-
} else {
|
|
137
|
-
log('Verify Deletion', ICONS.FAIL, 'Chemical still exists after deletion');
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
print('\n' + '='.repeat(60));
|
|
141
|
-
print(`${ICONS.PASS} Tests Passed: ${testsPassed}`);
|
|
142
|
-
if (testsFailed > 0) print(`${ICONS.FAIL} Tests Failed: ${testsFailed}`);
|
|
143
|
-
print('='.repeat(60));
|
|
144
|
-
|
|
145
|
-
} catch (error) {
|
|
146
|
-
logError('pegasus-sdk-tests', 'chemicals-tests', 'runChemicalTests', error);
|
|
147
|
-
print(`\n${ICONS.FAIL} Test Failed: ${error.message}`);
|
|
148
|
-
|
|
149
|
-
if (createdChemicalId) {
|
|
150
|
-
print('\nCleaning up test chemical...');
|
|
151
|
-
try {
|
|
152
|
-
await sdk.chemicals.deleteChemical(createdChemicalId);
|
|
153
|
-
print('Cleanup successful');
|
|
154
|
-
} catch (cleanupError) {
|
|
155
|
-
print('Cleanup failed (chemical may need manual deletion)');
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
process.exit(1);
|
|
160
|
-
} finally {
|
|
161
|
-
await sdk.disconnect();
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
runChemicalTests();
|
package/tests/search.js
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
const PegasusSDK = require('../index');
|
|
2
|
-
const { logInfo, logError } = require('@toxplanet/tphelper/logging');
|
|
3
|
-
|
|
4
|
-
const ICONS = {
|
|
5
|
-
PASS: '[OK]',
|
|
6
|
-
FAIL: '[!!]',
|
|
7
|
-
WARN: '[?]'
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
async function runTests() {
|
|
11
|
-
const sdk = new PegasusSDK();
|
|
12
|
-
let testsPassed = 0;
|
|
13
|
-
let testsFailed = 0;
|
|
14
|
-
|
|
15
|
-
const log = (test, status, details = '') => {
|
|
16
|
-
logInfo('pegasus-sdk-tests', `${test}: ${status} ${details}`);
|
|
17
|
-
if (status === ICONS.PASS) testsPassed++;
|
|
18
|
-
else if (status === ICONS.FAIL) testsFailed++;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const print = (message) => {
|
|
22
|
-
logInfo('pegasus-sdk-tests', message);
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
try {
|
|
26
|
-
await sdk.connect();
|
|
27
|
-
log('Connection', ICONS.PASS);
|
|
28
|
-
|
|
29
|
-
const health = await sdk.healthCheck();
|
|
30
|
-
const pgStatus = health.postgres.connected ? ICONS.PASS : ICONS.FAIL;
|
|
31
|
-
const osStatus = health.opensearch?.connected ? ICONS.PASS : ICONS.WARN;
|
|
32
|
-
log('Health Check', health.postgres.connected && health.opensearch?.connected ? ICONS.PASS : ICONS.WARN, `(PG:${pgStatus} OS:${osStatus})`);
|
|
33
|
-
|
|
34
|
-
if (!sdk.search?.searchChemicals) throw new Error('Search service not found');
|
|
35
|
-
log('Search Service', ICONS.PASS);
|
|
36
|
-
|
|
37
|
-
const emptyResult = await sdk.search.searchChemicals('');
|
|
38
|
-
if (emptyResult.results.length !== 0) throw new Error('Empty query should return no results');
|
|
39
|
-
log('Empty Query', ICONS.PASS);
|
|
40
|
-
|
|
41
|
-
const basicResult = await sdk.search.searchChemicals('test', { limit: 5 });
|
|
42
|
-
if (!Array.isArray(basicResult.results)) throw new Error('Invalid result structure');
|
|
43
|
-
log('Basic Search', ICONS.PASS, `(${basicResult.results.length} results)`);
|
|
44
|
-
|
|
45
|
-
const customResult = await sdk.search.searchChemicals('carbon', {
|
|
46
|
-
limit: 3,
|
|
47
|
-
casExact: 100,
|
|
48
|
-
nameExact: 50,
|
|
49
|
-
synonymExact: 75
|
|
50
|
-
});
|
|
51
|
-
log('Custom Boost', ICONS.PASS, `(${customResult.results.length} results)`);
|
|
52
|
-
|
|
53
|
-
const casResult = await sdk.search.searchChemicals('7440-06-4', { limit: 3 });
|
|
54
|
-
log('CAS Search', ICONS.PASS, `(${casResult.results.length} results)`);
|
|
55
|
-
|
|
56
|
-
const reversedCasResult = await sdk.search.searchChemicals('06/4/7440', { limit: 3 });
|
|
57
|
-
log('CAS Reversed Format', ICONS.PASS, `(${reversedCasResult.results.length} results)`);
|
|
58
|
-
|
|
59
|
-
if (basicResult.results.length > 0) {
|
|
60
|
-
const result = basicResult.results[0];
|
|
61
|
-
const requiredFields = ['id', 'name', 'cas', 'identifiers', 'synonyms', 'score'];
|
|
62
|
-
const missingFields = requiredFields.filter(field => !(field in result));
|
|
63
|
-
if (missingFields.length > 0) throw new Error(`Missing fields: ${missingFields.join(', ')}`);
|
|
64
|
-
if (!Array.isArray(result.cas) || !Array.isArray(result.identifiers) || !Array.isArray(result.synonyms)) {
|
|
65
|
-
throw new Error('Array fields invalid');
|
|
66
|
-
}
|
|
67
|
-
if (typeof result.score !== 'number') throw new Error('Score not a number');
|
|
68
|
-
log('Result Structure', ICONS.PASS);
|
|
69
|
-
} else {
|
|
70
|
-
log('Result Structure', ICONS.WARN, '(no results to validate)');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const startsWithResult = await sdk.search.searchStartsWith('test', 3);
|
|
74
|
-
const containsResult = await sdk.search.searchContains('test', 3);
|
|
75
|
-
const exactResult = await sdk.search.searchExact('test', 3);
|
|
76
|
-
if (!startsWithResult.results || !containsResult.results || !exactResult.results) {
|
|
77
|
-
throw new Error('Forwarding methods failed');
|
|
78
|
-
}
|
|
79
|
-
log('SearchService Methods', ICONS.PASS, `(starts:${startsWithResult.results.length} contains:${containsResult.results.length} exact:${exactResult.results.length})`);
|
|
80
|
-
|
|
81
|
-
const casExactResult = await sdk.search.searchByCAS('7440-06-4', 'exact');
|
|
82
|
-
const casPrefixResult = await sdk.search.searchByCAS('7440', 'prefix');
|
|
83
|
-
log('CAS Methods', ICONS.PASS, `(exact:${casExactResult.results.length} prefix:${casPrefixResult.results.length})`);
|
|
84
|
-
|
|
85
|
-
const identifierResult = await sdk.search.searchByIdentifier('CCCO', 'exact');
|
|
86
|
-
log('Identifier Method', ICONS.PASS, `(${identifierResult.results.length} results)`);
|
|
87
|
-
|
|
88
|
-
const synonymExactResult = await sdk.search.searchBySynonym('grain alcohol', 'exact');
|
|
89
|
-
const synonymPrefixResult = await sdk.search.searchBySynonym('alco', 'prefix');
|
|
90
|
-
log('Synonym Methods', ICONS.PASS, `(exact:${synonymExactResult.results.length} prefix:${synonymPrefixResult.results.length})`);
|
|
91
|
-
|
|
92
|
-
const nameSearchResult = await sdk.chemicals.searchByName('platinum', 3);
|
|
93
|
-
const synonymSearchResult = await sdk.chemicals.searchBySynonym('alcohol', 3);
|
|
94
|
-
if (!nameSearchResult.results || !synonymSearchResult.results) {
|
|
95
|
-
throw new Error('ChemicalsService methods failed');
|
|
96
|
-
}
|
|
97
|
-
log('ChemicalsService Methods', ICONS.PASS, `(name:${nameSearchResult.results.length} synonym:${synonymSearchResult.results.length})`);
|
|
98
|
-
|
|
99
|
-
const highLimitResult = await sdk.search.searchChemicals('carbon', { limit: 20 });
|
|
100
|
-
log('High Limit Search', ICONS.PASS, `(${highLimitResult.results.length} results)`);
|
|
101
|
-
|
|
102
|
-
print(`\n${'='.repeat(60)}`);
|
|
103
|
-
print('FINAL TEST: Benzene Search Results');
|
|
104
|
-
print('='.repeat(60));
|
|
105
|
-
const benzeneResults = await sdk.search.searchChemicals('benzene', { limit: 3 });
|
|
106
|
-
print(`Found ${benzeneResults.results.length} results for "benzene":\n`);
|
|
107
|
-
benzeneResults.results.forEach((result, i) => {
|
|
108
|
-
print(`${i + 1}. ${result.name}`);
|
|
109
|
-
print(` CAS: ${result.cas.join(', ') || 'N/A'}`);
|
|
110
|
-
print(` Score: ${result.score.toFixed(2)}`);
|
|
111
|
-
if (result.synonyms.length > 0) {
|
|
112
|
-
print(` Synonyms: ${result.synonyms.slice(0, 3).join(', ')}${result.synonyms.length > 3 ? '...' : ''}`);
|
|
113
|
-
}
|
|
114
|
-
print('');
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
print('='.repeat(60));
|
|
118
|
-
print(`${ICONS.PASS} Tests Passed: ${testsPassed}`);
|
|
119
|
-
if (testsFailed > 0) print(`${ICONS.FAIL} Tests Failed: ${testsFailed}`);
|
|
120
|
-
print('='.repeat(60));
|
|
121
|
-
|
|
122
|
-
} catch (error) {
|
|
123
|
-
logError('pegasus-sdk-tests', 'search-tests', 'runTests', error);
|
|
124
|
-
print(`\n${ICONS.FAIL} Test Failed: ${error.message}`);
|
|
125
|
-
process.exit(1);
|
|
126
|
-
} finally {
|
|
127
|
-
await sdk.disconnect();
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (require.main === module) {
|
|
132
|
-
runTests().catch((error) => {
|
|
133
|
-
logError('pegasus-sdk-tests', 'search-tests', 'main', error);
|
|
134
|
-
process.exit(1);
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
module.exports = runTests;
|