hlquery-node-client 1.0.6 → 1.0.7
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/README.md +54 -24
- package/lib/Client.js +7 -78
- package/lib/Request.js +8 -1
- package/lib/Search.js +8 -2
- package/lib/System.js +14 -8
- package/lib/conformance.js +0 -15
- package/package.json +9 -20
- package/test/route-conformance.test.js +83 -0
- package/examples/sam.js +0 -39
- package/index.js +0 -44
- package/lib/SAM.js +0 -199
- package/test/offline-smoke.js +0 -455
package/README.md
CHANGED
|
@@ -16,13 +16,13 @@
|
|
|
16
16
|
|
|
17
17
|
### What is the hlquery Node.js API?
|
|
18
18
|
|
|
19
|
-
The hlquery Node.js API is the official Node.js client for [hlquery](https://github.com/hlquery/hlquery). It wraps the REST interface in a modular service-style client with helpers for collections, documents, search,
|
|
19
|
+
The hlquery Node.js API is the official Node.js client for [hlquery](https://github.com/hlquery/hlquery). It wraps the REST interface in a modular service-style client with helpers for collections, documents, search, and SQL.
|
|
20
20
|
|
|
21
21
|
It is a good fit for backend services, scripts, dashboards, and apps that want hlquery integration without repeating request code.
|
|
22
22
|
|
|
23
23
|
### Why use it?
|
|
24
24
|
|
|
25
|
-
Use the Node.js client when you want hlquery calls to read like regular application code. The client is organized around familiar modules such as `client.collections()
|
|
25
|
+
Use the Node.js client when you want hlquery calls to read like regular application code. The client is organized around familiar modules such as `client.collections()` and `client.documents()`, with Redis-style dynamic route helpers for module APIs and custom endpoints.
|
|
26
26
|
|
|
27
27
|
It also keeps the repetitive parts in one place: authentication, request parameters, endpoint paths, and parsed responses are handled consistently across the client. Common hlquery workflows are covered by default, while raw request access is still available when you need a custom route.
|
|
28
28
|
|
|
@@ -88,6 +88,16 @@ const disconnect = await client.linksDisconnect('http://node-b:9200');
|
|
|
88
88
|
const flush = await client.flush();
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
+
Maintenance actions default to `POST`, and can explicitly use either server-supported method:
|
|
92
|
+
|
|
93
|
+
```javascript
|
|
94
|
+
await client.updateCounters({ force: true });
|
|
95
|
+
await client.updateCounters({ force: true }, 'GET');
|
|
96
|
+
await client.repair({}, 'POST');
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The client also wraps readiness, startup, metrics, connection, storage, integrity, counter, user, key, module, and analytics routes through `client.system()`, `client.users()`, `client.keys()`, `client.modules()`, and `client.analytics()`.
|
|
100
|
+
|
|
91
101
|
Routes that do not have a dedicated wrapper can still be called through `executeRequest()`:
|
|
92
102
|
|
|
93
103
|
```javascript
|
|
@@ -122,25 +132,43 @@ console.log(syntax.body);
|
|
|
122
132
|
console.log(raw.body);
|
|
123
133
|
```
|
|
124
134
|
|
|
125
|
-
###
|
|
126
|
-
|
|
127
|
-
SAM is separate from vector search. It performs term and intent-style lookup, not vector similarity search.
|
|
135
|
+
### Collections, documents, and search
|
|
128
136
|
|
|
129
137
|
```javascript
|
|
130
|
-
const
|
|
138
|
+
const metadata = await client.collections().get('products');
|
|
139
|
+
const language = await client.collections().language('products');
|
|
140
|
+
|
|
141
|
+
// getFields() is a compatibility alias for collection metadata. The server has
|
|
142
|
+
// no /collections/{name}/fields route.
|
|
143
|
+
const metadataAgain = await client.collections().getFields('products');
|
|
144
|
+
|
|
145
|
+
const context = await client.documents().context('products', 'prod_1');
|
|
146
|
+
const facets = await client.documents().facetCounts('products', { facet_by: 'brand' });
|
|
147
|
+
const exported = await client.documents().export('products', { filter_by: 'active:true' });
|
|
148
|
+
const suggestions = await client.documents().maybe('products', { q: 'keybaord' });
|
|
131
149
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
limit: 10
|
|
150
|
+
await client.documents().updateByQuery('products', {
|
|
151
|
+
filter_by: 'active:false',
|
|
152
|
+
set: { archived: true }
|
|
136
153
|
});
|
|
154
|
+
await client.documents().deleteByQuery('products', { filter_by: 'expired:true' });
|
|
137
155
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
156
|
+
const searches = [{ collection: 'products', q: 'keyboard', query_by: 'title' }];
|
|
157
|
+
await client.searchApi().multiSearch(searches); // POST (default)
|
|
158
|
+
await client.searchApi().multiSearch(searches, 'GET');
|
|
159
|
+
await client.globalSearch({ collection: 'products', q: 'keyboard' });
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Synonyms, overrides, and aliases expose separate `create()` (`POST`) and `update()` (`PUT`) helpers; `upsert()` uses the client's default upsert verb. Stopwords use their collection or global create/delete routes.
|
|
163
|
+
|
|
164
|
+
### Request safety and errors
|
|
165
|
+
|
|
166
|
+
`executeRequest()` accepts a relative path on the configured hlquery origin. Absolute cross-origin URLs are rejected with `RequestException` code `CROSS_ORIGIN_REQUEST`; this prevents credentials from being forwarded to another host. HTTP error bodies retain the server's `error`, optional `message`, numeric `code`, and stable `code_text` fields.
|
|
167
|
+
|
|
168
|
+
Run the offline route-contract suite with:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
npm test
|
|
144
172
|
```
|
|
145
173
|
|
|
146
174
|
### SQL
|
|
@@ -169,17 +197,19 @@ We welcome contributions from the community! All contributions must be released
|
|
|
169
197
|
|
|
170
198
|
### How to Contribute
|
|
171
199
|
|
|
172
|
-
- Check existing [issues](https://github.com/hlquery/
|
|
173
|
-
- Contribute
|
|
174
|
-
-
|
|
175
|
-
-
|
|
200
|
+
- Check existing [Node.js API issues](https://github.com/hlquery/node-api/issues) or create new ones
|
|
201
|
+
- Contribute Node.js client changes to [hlquery/node-api](https://github.com/hlquery/node-api)
|
|
202
|
+
- Contribute shared server/API changes to [hlquery/hlquery](https://github.com/hlquery/hlquery)
|
|
203
|
+
- Test and report bugs against the Node.js client
|
|
204
|
+
- Improve Node.js-specific documentation and examples
|
|
176
205
|
|
|
177
206
|
### Community
|
|
178
207
|
|
|
179
|
-
-
|
|
180
|
-
-
|
|
181
|
-
-
|
|
208
|
+
- [Documentation](https://docs.hlquery.com)
|
|
209
|
+
- [X (Twitter)](https://x.com/hlquery)
|
|
210
|
+
- [Node.js API GitHub](https://github.com/hlquery/node-api)
|
|
211
|
+
- [hlquery GitHub](https://github.com/hlquery/hlquery)
|
|
182
212
|
|
|
183
213
|
### License
|
|
184
214
|
|
|
185
|
-
hlquery is licensed under the [BSD 3-Clause License](https://opensource.org/licenses/BSD-3-Clause).
|
|
215
|
+
The hlquery Node.js API is licensed under the [BSD 3-Clause License](https://opensource.org/licenses/BSD-3-Clause).
|
package/lib/Client.js
CHANGED
|
@@ -17,7 +17,6 @@ const Overrides = require('./Overrides');
|
|
|
17
17
|
const Synonyms = require('./Synonyms');
|
|
18
18
|
const Stopwords = require('./Stopwords');
|
|
19
19
|
const System = require('./System');
|
|
20
|
-
const SAM = require('./SAM');
|
|
21
20
|
const Modules = require('./Modules');
|
|
22
21
|
const Users = require('./Users');
|
|
23
22
|
const Analytics = require('./Analytics');
|
|
@@ -55,7 +54,6 @@ class Client {
|
|
|
55
54
|
this._synonyms = new Synonyms(this.request);
|
|
56
55
|
this._stopwords = new Stopwords(this.request);
|
|
57
56
|
this._system = new System(this.request);
|
|
58
|
-
this._sam = new SAM(this.request);
|
|
59
57
|
this._modules = new Modules(this.request);
|
|
60
58
|
this._users = new Users(this.request);
|
|
61
59
|
this._analytics = new Analytics(this.request);
|
|
@@ -262,20 +260,16 @@ class Client {
|
|
|
262
260
|
return await this._system.searchConfig();
|
|
263
261
|
}
|
|
264
262
|
|
|
265
|
-
async
|
|
266
|
-
return await this._system.
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
async updateCounters(params = {}) {
|
|
270
|
-
return await this._system.updateCounters(params);
|
|
263
|
+
async updateCounters(params = {}, method = 'POST') {
|
|
264
|
+
return await this._system.updateCounters(params, method);
|
|
271
265
|
}
|
|
272
266
|
|
|
273
267
|
async debugCounters() {
|
|
274
268
|
return await this._system.debugCounters();
|
|
275
269
|
}
|
|
276
270
|
|
|
277
|
-
async repair(params = {}) {
|
|
278
|
-
return await this._system.repair(params);
|
|
271
|
+
async repair(params = {}, method = 'POST') {
|
|
272
|
+
return await this._system.repair(params, method);
|
|
279
273
|
}
|
|
280
274
|
|
|
281
275
|
/**
|
|
@@ -305,7 +299,7 @@ class Client {
|
|
|
305
299
|
* @returns {Promise<Response>}
|
|
306
300
|
*/
|
|
307
301
|
async clusterHealth() {
|
|
308
|
-
return await this.
|
|
302
|
+
return await this._system.health();
|
|
309
303
|
}
|
|
310
304
|
|
|
311
305
|
/**
|
|
@@ -314,7 +308,7 @@ class Client {
|
|
|
314
308
|
* @returns {Promise<Response>}
|
|
315
309
|
*/
|
|
316
310
|
async clusterStats() {
|
|
317
|
-
return await this.
|
|
311
|
+
return await this._system.stats();
|
|
318
312
|
}
|
|
319
313
|
|
|
320
314
|
/**
|
|
@@ -323,7 +317,7 @@ class Client {
|
|
|
323
317
|
* @returns {Promise<Response>}
|
|
324
318
|
*/
|
|
325
319
|
async clusterNodes() {
|
|
326
|
-
return await this.
|
|
320
|
+
return await this.links();
|
|
327
321
|
}
|
|
328
322
|
|
|
329
323
|
/**
|
|
@@ -618,71 +612,6 @@ class Client {
|
|
|
618
612
|
return this._search;
|
|
619
613
|
}
|
|
620
614
|
|
|
621
|
-
/**
|
|
622
|
-
* Get SAM API instance
|
|
623
|
-
*
|
|
624
|
-
* @returns {SAM}
|
|
625
|
-
*/
|
|
626
|
-
sam() {
|
|
627
|
-
return this._sam;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
async samSearch(collectionName, query, params = {}) {
|
|
631
|
-
return await this._sam.search(collectionName, query, params);
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
async samSearchAll(query, params = {}) {
|
|
635
|
-
return await this._sam.searchAll(query, params);
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
async samRebuild(collectionName, params = {}) {
|
|
639
|
-
return await this._sam.rebuild(collectionName, params);
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
async samStatus(collectionName = null, params = {}) {
|
|
643
|
-
return await this._sam.status(collectionName, params);
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
async samDebug(collectionName = null, params = {}) {
|
|
647
|
-
return await this._sam.debug(collectionName, params);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
async samHistory(collectionName = null, limit = 100, params = {}) {
|
|
651
|
-
return await this._sam.history(collectionName, limit, params);
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
async samPause(pauseUntilMs, params = {}) {
|
|
655
|
-
return await this._sam.pause(pauseUntilMs, params);
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
async samClearPause(params = {}) {
|
|
659
|
-
return await this._sam.clearPause(params);
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
async samImprove(params = {}) {
|
|
663
|
-
return await this._sam.improve(params);
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
async samFlushActorMetadata() {
|
|
667
|
-
return await this._sam.flushActorMetadata();
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
async samDocuments(collectionName, offset = 0, limit = 20, params = {}) {
|
|
671
|
-
return await this._sam.listDocuments(collectionName, offset, limit, params);
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
async samDocument(collectionName, documentId, params = {}) {
|
|
675
|
-
return await this._sam.getDocument(collectionName, documentId, params);
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
async samOpenDocument(collectionName, documentId, interactionQuery = null, params = {}) {
|
|
679
|
-
return await this._sam.openDocument(collectionName, documentId, interactionQuery, params);
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
async samAddDocumentLabel(collectionName, documentId, labels) {
|
|
683
|
-
return await this._sam.addDocumentLabel(collectionName, documentId, labels);
|
|
684
|
-
}
|
|
685
|
-
|
|
686
615
|
/**
|
|
687
616
|
* Get aliases API instance
|
|
688
617
|
*
|
package/lib/Request.js
CHANGED
|
@@ -58,7 +58,14 @@ class Request {
|
|
|
58
58
|
* @throws {RequestException}
|
|
59
59
|
*/
|
|
60
60
|
async execute(method, path, body = null, queryParams = {}, requestOptions = {}) {
|
|
61
|
-
const
|
|
61
|
+
const baseUrl = new URL(`${this.baseUrl}/`);
|
|
62
|
+
const url = new URL(path, baseUrl);
|
|
63
|
+
if (url.origin !== baseUrl.origin) {
|
|
64
|
+
throw new RequestException('Request URL must use the configured server origin', 0, null, {
|
|
65
|
+
code: 'CROSS_ORIGIN_REQUEST',
|
|
66
|
+
retryable: false
|
|
67
|
+
});
|
|
68
|
+
}
|
|
62
69
|
const signal = requestOptions.signal || this.signal;
|
|
63
70
|
const maxResponseBytes = requestOptions.max_response_bytes || requestOptions.maxResponseBytes || this.maxResponseBytes;
|
|
64
71
|
const throwOnError = requestOptions.throw_on_error !== undefined
|
package/lib/Search.js
CHANGED
|
@@ -218,10 +218,16 @@ class Search {
|
|
|
218
218
|
* Multi-search across multiple collections
|
|
219
219
|
*
|
|
220
220
|
* @param {Array} searches Array of search requests
|
|
221
|
+
* @param {string} method GET or POST
|
|
221
222
|
* @returns {Promise<Response>}
|
|
222
223
|
*/
|
|
223
|
-
async multiSearch(searches) {
|
|
224
|
-
|
|
224
|
+
async multiSearch(searches, method = 'POST') {
|
|
225
|
+
method = String(method).toUpperCase();
|
|
226
|
+
if (method !== 'GET' && method !== 'POST') {
|
|
227
|
+
throw new Error('Multi-search method must be GET or POST');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return await this.request.execute(method, '/multi_search', { searches: searches });
|
|
225
231
|
}
|
|
226
232
|
|
|
227
233
|
/**
|
package/lib/System.js
CHANGED
|
@@ -122,20 +122,26 @@ class System {
|
|
|
122
122
|
return await this.request.execute('GET', '/search-config');
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
async
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
async updateCounters(params = {}) {
|
|
130
|
-
return await this.request.execute('POST', '/update-counters', null, params);
|
|
125
|
+
async updateCounters(params = {}, method = 'POST') {
|
|
126
|
+
method = this._getOrPostMethod(method, 'Update counters');
|
|
127
|
+
return await this.request.execute(method, '/update-counters', null, params);
|
|
131
128
|
}
|
|
132
129
|
|
|
133
130
|
async debugCounters() {
|
|
134
131
|
return await this.request.execute('GET', '/debug/counters');
|
|
135
132
|
}
|
|
136
133
|
|
|
137
|
-
async repair(params = {}) {
|
|
138
|
-
|
|
134
|
+
async repair(params = {}, method = 'POST') {
|
|
135
|
+
method = this._getOrPostMethod(method, 'Repair');
|
|
136
|
+
return await this.request.execute(method, '/repair', null, params);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
_getOrPostMethod(method, operation) {
|
|
140
|
+
method = String(method).toUpperCase();
|
|
141
|
+
if (method !== 'GET' && method !== 'POST') {
|
|
142
|
+
throw new Error(`${operation} method must be GET or POST`);
|
|
143
|
+
}
|
|
144
|
+
return method;
|
|
139
145
|
}
|
|
140
146
|
}
|
|
141
147
|
|
package/lib/conformance.js
CHANGED
|
@@ -26,9 +26,6 @@ const NODE_CLIENT_ROUTE_COVERAGE = [
|
|
|
26
26
|
{ path: '/consistency', methods: ['GET'], status: 'supported', client: 'system.consistency' },
|
|
27
27
|
{ path: '/self-check', methods: ['GET'], status: 'supported', client: 'system.selfCheck' },
|
|
28
28
|
{ path: '/admin/storage_status', methods: ['GET'], status: 'supported', client: 'system.storageStatus' },
|
|
29
|
-
{ path: '/cluster/health', methods: ['GET'], status: 'supported', client: 'client.clusterHealth' },
|
|
30
|
-
{ path: '/cluster/stats', methods: ['GET'], status: 'supported', client: 'client.clusterStats' },
|
|
31
|
-
{ path: '/cluster/nodes', methods: ['GET'], status: 'supported', client: 'client.clusterNodes' },
|
|
32
29
|
{ path: '/links', methods: ['GET'], status: 'supported', client: 'client.links' },
|
|
33
30
|
{ path: '/links/ping', methods: ['GET'], status: 'supported', client: 'client.linksPing' },
|
|
34
31
|
{ path: '/links/connect', methods: ['POST'], status: 'supported', client: 'client.linksConnect' },
|
|
@@ -53,17 +50,6 @@ const NODE_CLIENT_ROUTE_COVERAGE = [
|
|
|
53
50
|
{ path: '/multi_search', methods: ['GET', 'POST'], status: 'supported', client: 'search.multiSearch' },
|
|
54
51
|
{ path: '/search', methods: ['GET', 'POST'], status: 'supported', client: 'search.globalSearch' },
|
|
55
52
|
{ path: '/sql', methods: ['GET', 'POST'], status: 'supported', client: 'system.sql/execSql' },
|
|
56
|
-
{ path: '/sam/rebuild', methods: ['POST'], status: 'supported', client: 'sam.rebuild' },
|
|
57
|
-
{ path: '/sam/search', methods: ['GET'], status: 'supported', client: 'sam.search' },
|
|
58
|
-
{ path: '/sam/status', methods: ['GET'], status: 'supported', client: 'sam.status' },
|
|
59
|
-
{ path: '/sam/debug', methods: ['GET'], status: 'supported', client: 'sam.debug' },
|
|
60
|
-
{ path: '/sam/history', methods: ['GET'], status: 'supported', client: 'sam.history' },
|
|
61
|
-
{ path: '/sam/pause', methods: ['POST'], status: 'supported', client: 'sam.pause/clearPause' },
|
|
62
|
-
{ path: '/sam/improve', methods: ['POST'], status: 'supported', client: 'sam.improve' },
|
|
63
|
-
{ path: '/sam/flush_actor_metadata', methods: ['POST'], status: 'supported', client: 'sam.flushActorMetadata' },
|
|
64
|
-
{ path: '/sam/documents', methods: ['GET'], status: 'supported', client: 'sam.listDocuments' },
|
|
65
|
-
{ path: '/sam/documents/{collection}/{id}', methods: ['GET'], status: 'supported', client: 'sam.getDocument/openDocument' },
|
|
66
|
-
{ path: '/sam/label/add/{collection}/{id}', methods: ['POST'], status: 'supported', client: 'sam.addDocumentLabel' },
|
|
67
53
|
{ path: '/collections/{name}/documents/facet_counts', methods: ['GET', 'POST'], status: 'supported', client: 'documents.facetCounts' },
|
|
68
54
|
{ path: '/collections/{name}/documents/export', methods: ['GET', 'POST'], status: 'supported', client: 'documents.export' },
|
|
69
55
|
{ path: '/collections/{name}/synonyms', methods: ['GET'], status: 'supported', client: 'synonyms.list' },
|
|
@@ -90,7 +76,6 @@ const NODE_CLIENT_ROUTE_COVERAGE = [
|
|
|
90
76
|
{ path: '/query', methods: ['GET'], status: 'omitted', reason: 'Alias of /status; client exposes status() instead of duplicate naming.' },
|
|
91
77
|
{ path: '/search-config', methods: ['GET'], status: 'supported', client: 'system.searchConfig' },
|
|
92
78
|
{ path: '/ready', methods: ['GET'], status: 'supported', client: 'system.ready' },
|
|
93
|
-
{ path: '/llm', methods: ['GET'], status: 'supported', client: 'system.llm' },
|
|
94
79
|
{ path: '/repair', methods: ['GET', 'POST'], status: 'supported', client: 'system.repair' },
|
|
95
80
|
{ path: '/modules', methods: ['GET'], status: 'supported', client: 'modules.list' },
|
|
96
81
|
{ path: '/modules/load', methods: ['POST'], status: 'omitted', reason: 'Prefix alias; modules.load(name) uses the explicit /loadmodule/{name} form.' },
|
package/package.json
CHANGED
|
@@ -1,34 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hlquery-node-client",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Node.js client
|
|
5
|
-
"main": "
|
|
6
|
-
"type": "commonjs",
|
|
3
|
+
"version": "1.0.7",
|
|
4
|
+
"description": "Node.js client for hlquery",
|
|
5
|
+
"main": "lib/Client.js",
|
|
7
6
|
"scripts": {
|
|
8
|
-
"test": "node
|
|
9
|
-
"test:
|
|
10
|
-
"test:offline": "node test/offline-smoke.js",
|
|
11
|
-
"example": "node example.js",
|
|
12
|
-
"example:sql": "node examples/sql.js",
|
|
13
|
-
"example:pdf": "node examples/pdf.js",
|
|
14
|
-
"example:csv": "node examples/csv.js"
|
|
15
|
-
},
|
|
16
|
-
"dependencies": {
|
|
17
|
-
"pdf-parse": "^2.4.5"
|
|
7
|
+
"test": "node test/route-conformance.test.js",
|
|
8
|
+
"test:offline": "npm test"
|
|
18
9
|
},
|
|
19
10
|
"keywords": [
|
|
20
11
|
"hlquery",
|
|
21
12
|
"search",
|
|
22
|
-
"client"
|
|
23
|
-
"api"
|
|
13
|
+
"client"
|
|
24
14
|
],
|
|
25
15
|
"author": "Carlos F. Ferry <carlos.ferry@gmail.com>",
|
|
26
16
|
"license": "BSD-3-Clause",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"pdf-parse": "^2.4.5"
|
|
19
|
+
},
|
|
27
20
|
"engines": {
|
|
28
21
|
"node": ">=12.0.0"
|
|
29
|
-
},
|
|
30
|
-
"repository": {
|
|
31
|
-
"type": "git",
|
|
32
|
-
"url": "https://github.com/cferry/hlquery"
|
|
33
22
|
}
|
|
34
23
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('assert');
|
|
4
|
+
const Request = require('../lib/Request');
|
|
5
|
+
const Search = require('../lib/Search');
|
|
6
|
+
const System = require('../lib/System');
|
|
7
|
+
const { NODE_CLIENT_ROUTE_COVERAGE } = require('../lib/conformance');
|
|
8
|
+
|
|
9
|
+
class RecordingRequest {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.calls = [];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async execute(method, path, body = null, query = {}) {
|
|
15
|
+
const call = { method, path, body, query };
|
|
16
|
+
this.calls.push(call);
|
|
17
|
+
return call;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
last() {
|
|
21
|
+
return this.calls[this.calls.length - 1];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function route(path) {
|
|
26
|
+
const matches = NODE_CLIENT_ROUTE_COVERAGE.filter(entry => entry.path === path);
|
|
27
|
+
assert.strictEqual(matches.length, 1, `${path} must have exactly one conformance entry`);
|
|
28
|
+
return matches[0];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function main() {
|
|
32
|
+
const routeKeys = new Set();
|
|
33
|
+
for (const entry of NODE_CLIENT_ROUTE_COVERAGE) {
|
|
34
|
+
assert.ok(entry.path.startsWith('/'), `route must be absolute-path scoped: ${entry.path}`);
|
|
35
|
+
assert.ok(Array.isArray(entry.methods) && entry.methods.length > 0, `route must declare methods: ${entry.path}`);
|
|
36
|
+
const key = `${entry.path}:${entry.methods.join(',')}`;
|
|
37
|
+
assert.ok(!routeKeys.has(key), `duplicate conformance route: ${key}`);
|
|
38
|
+
routeKeys.add(key);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const recorder = new RecordingRequest();
|
|
42
|
+
const search = new Search(recorder, null);
|
|
43
|
+
const system = new System(recorder);
|
|
44
|
+
|
|
45
|
+
assert.deepStrictEqual(route('/multi_search').methods, ['GET', 'POST']);
|
|
46
|
+
await search.multiSearch([], 'GET');
|
|
47
|
+
assert.deepStrictEqual(recorder.last(), {
|
|
48
|
+
method: 'GET', path: '/multi_search', body: { searches: [] }, query: {}
|
|
49
|
+
});
|
|
50
|
+
await search.multiSearch([], 'POST');
|
|
51
|
+
assert.strictEqual(recorder.last().method, 'POST');
|
|
52
|
+
|
|
53
|
+
for (const [path, invoke] of [
|
|
54
|
+
['/update-counters', method => system.updateCounters({ force: 1 }, method)],
|
|
55
|
+
['/repair', method => system.repair({ collection: 'books' }, method)]
|
|
56
|
+
]) {
|
|
57
|
+
assert.deepStrictEqual(route(path).methods, ['GET', 'POST']);
|
|
58
|
+
await invoke('GET');
|
|
59
|
+
assert.strictEqual(recorder.last().method, 'GET');
|
|
60
|
+
assert.strictEqual(recorder.last().path, path);
|
|
61
|
+
await invoke('POST');
|
|
62
|
+
assert.strictEqual(recorder.last().method, 'POST');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const request = new Request('https://search.example.test', {
|
|
66
|
+
token: 'secret-token'
|
|
67
|
+
});
|
|
68
|
+
await assert.rejects(
|
|
69
|
+
request.execute('GET', 'https://attacker.example.test/collect'),
|
|
70
|
+
error => error && error.code === 'CROSS_ORIGIN_REQUEST'
|
|
71
|
+
);
|
|
72
|
+
await assert.rejects(
|
|
73
|
+
request.execute('GET', '//attacker.example.test/collect'),
|
|
74
|
+
error => error && error.code === 'CROSS_ORIGIN_REQUEST'
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
console.log(`route conformance passed (${NODE_CLIENT_ROUTE_COVERAGE.length} manifest entries)`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
main().catch(error => {
|
|
81
|
+
console.error(error);
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
});
|
package/examples/sam.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Basic SAM example for the hlquery Node.js client.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
const Client = require('../index');
|
|
6
|
-
|
|
7
|
-
async function main() {
|
|
8
|
-
const baseUrl = process.env.HLQ_BASE_URL || process.env.HLQUERY_BASE_URL || 'http://localhost:9200';
|
|
9
|
-
const token = process.argv[2] || null;
|
|
10
|
-
const collection = process.argv[3] || 'music';
|
|
11
|
-
const query = process.argv[4] || 'queen of pop';
|
|
12
|
-
|
|
13
|
-
const client = new Client(baseUrl);
|
|
14
|
-
|
|
15
|
-
if (token) {
|
|
16
|
-
client.setAuthToken(token, 'bearer');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const sam = client.sam();
|
|
20
|
-
|
|
21
|
-
const status = await sam.status(collection);
|
|
22
|
-
console.log('=== SAM STATUS ===');
|
|
23
|
-
console.log(status.getBody());
|
|
24
|
-
console.log('');
|
|
25
|
-
|
|
26
|
-
const history = await sam.history(collection, 5);
|
|
27
|
-
console.log('=== SAM HISTORY ===');
|
|
28
|
-
console.log(history.getBody());
|
|
29
|
-
console.log('');
|
|
30
|
-
|
|
31
|
-
const results = await sam.search(collection, query, { limit: 10 });
|
|
32
|
-
console.log('=== SAM SEARCH ===');
|
|
33
|
-
console.log(results.getBody());
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
main().catch((error) => {
|
|
37
|
-
console.error(error);
|
|
38
|
-
process.exit(1);
|
|
39
|
-
});
|
package/index.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* hlquery Node.js Client - Main Entry Point
|
|
3
|
-
*
|
|
4
|
-
* Copyright (C) 2021-2026, Carlos F. Ferry <carlos.ferry@gmail.com>
|
|
5
|
-
*
|
|
6
|
-
* This file is part of hlquery, released under the BSD License version 3.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
const Client = require('./lib/Client');
|
|
10
|
-
|
|
11
|
-
// Export main client class
|
|
12
|
-
module.exports = Client;
|
|
13
|
-
|
|
14
|
-
// Export all classes for advanced usage
|
|
15
|
-
module.exports.Client = Client;
|
|
16
|
-
module.exports.Request = require('./lib/Request');
|
|
17
|
-
module.exports.Response = require('./lib/Response');
|
|
18
|
-
module.exports.Collections = require('./lib/Collections');
|
|
19
|
-
module.exports.Documents = require('./lib/Documents');
|
|
20
|
-
module.exports.Search = require('./lib/Search');
|
|
21
|
-
module.exports.Keys = require('./lib/Keys');
|
|
22
|
-
module.exports.Aliases = require('./lib/Aliases');
|
|
23
|
-
module.exports.Overrides = require('./lib/Overrides');
|
|
24
|
-
module.exports.Synonyms = require('./lib/Synonyms');
|
|
25
|
-
module.exports.Stopwords = require('./lib/Stopwords');
|
|
26
|
-
module.exports.System = require('./lib/System');
|
|
27
|
-
module.exports.SAM = require('./lib/SAM');
|
|
28
|
-
module.exports.Modules = require('./lib/Modules');
|
|
29
|
-
module.exports.Users = require('./lib/Users');
|
|
30
|
-
module.exports.Analytics = require('./lib/Analytics');
|
|
31
|
-
module.exports.Route = require('./lib/Route');
|
|
32
|
-
|
|
33
|
-
// Export exceptions
|
|
34
|
-
module.exports.Exceptions = require('./lib/Exceptions');
|
|
35
|
-
|
|
36
|
-
// Export utilities
|
|
37
|
-
module.exports.Utils = {
|
|
38
|
-
Config: require('./utils/Config'),
|
|
39
|
-
Validator: require('./utils/Validator'),
|
|
40
|
-
Auth: require('./utils/Auth'),
|
|
41
|
-
Ranker: require('./utils/ranker'),
|
|
42
|
-
PDFParser: require('./utils/PDFParser'),
|
|
43
|
-
CSVParser: require('./utils/CSVParser')
|
|
44
|
-
};
|
package/lib/SAM.js
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* hlquery Node.js Client - SAM API
|
|
3
|
-
*
|
|
4
|
-
* Copyright (C) 2021-2026, Carlos F. Ferry <carlos.ferry@gmail.com>
|
|
5
|
-
*
|
|
6
|
-
* This file is part of hlquery, released under the BSD License version 3.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
const Validator = require('../utils/Validator');
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* SAM API operations
|
|
13
|
-
*/
|
|
14
|
-
class SAM {
|
|
15
|
-
constructor(request) {
|
|
16
|
-
this.request = request;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
_validateParams(params, label) {
|
|
20
|
-
if (params === null || typeof params !== 'object' || Array.isArray(params)) {
|
|
21
|
-
throw new Error(`${label} must be an object`);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async search(collectionName, query, params = {}) {
|
|
26
|
-
if (typeof query !== 'string' || query.trim() === '') {
|
|
27
|
-
throw new Error('SAM query must be a non-empty string');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
this._validateParams(params, 'SAM search params');
|
|
31
|
-
|
|
32
|
-
const queryParams = {
|
|
33
|
-
...params,
|
|
34
|
-
q: query
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
if (collectionName !== null && collectionName !== undefined && collectionName !== '') {
|
|
38
|
-
Validator.validateCollectionName(collectionName);
|
|
39
|
-
queryParams.collection = collectionName;
|
|
40
|
-
} else if (!queryParams.all && !queryParams.collections) {
|
|
41
|
-
throw new Error('Collection name is required unless all=true or collections is provided');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return await this.request.execute('GET', '/sam/search', null, queryParams);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async searchAll(query, params = {}) {
|
|
48
|
-
this._validateParams(params, 'SAM search params');
|
|
49
|
-
return await this.search(null, query, { ...params, all: true });
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async rebuild(collectionName, params = {}) {
|
|
53
|
-
Validator.validateCollectionName(collectionName);
|
|
54
|
-
this._validateParams(params, 'SAM rebuild params');
|
|
55
|
-
|
|
56
|
-
return await this.request.execute('POST', '/sam/rebuild', null, {
|
|
57
|
-
...params,
|
|
58
|
-
collection: collectionName
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async status(collectionName = null, params = {}) {
|
|
63
|
-
this._validateParams(params, 'SAM status params');
|
|
64
|
-
|
|
65
|
-
const queryParams = { ...params };
|
|
66
|
-
|
|
67
|
-
if (collectionName !== null && collectionName !== undefined && collectionName !== '') {
|
|
68
|
-
Validator.validateCollectionName(collectionName);
|
|
69
|
-
queryParams.collection = collectionName;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return await this.request.execute('GET', '/sam/status', null, queryParams);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async debug(collectionName = null, params = {}) {
|
|
76
|
-
this._validateParams(params, 'SAM debug params');
|
|
77
|
-
|
|
78
|
-
const queryParams = { ...params };
|
|
79
|
-
|
|
80
|
-
if (collectionName !== null && collectionName !== undefined && collectionName !== '') {
|
|
81
|
-
Validator.validateCollectionName(collectionName);
|
|
82
|
-
queryParams.collection = collectionName;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return await this.request.execute('GET', '/sam/debug', null, queryParams);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async history(collectionName = null, limit = 100, params = {}) {
|
|
89
|
-
this._validateParams(params, 'SAM history params');
|
|
90
|
-
const safeLimit = Number(limit);
|
|
91
|
-
if (!Number.isInteger(safeLimit) || safeLimit < 1) {
|
|
92
|
-
throw new Error('SAM history limit must be a positive integer');
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const queryParams = {
|
|
96
|
-
...params,
|
|
97
|
-
limit: safeLimit
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
if (collectionName !== null && collectionName !== undefined && collectionName !== '') {
|
|
101
|
-
Validator.validateCollectionName(collectionName);
|
|
102
|
-
queryParams.collection = collectionName;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return await this.request.execute('GET', '/sam/history', null, queryParams);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async pause(pauseUntilMs, params = {}) {
|
|
109
|
-
this._validateParams(params, 'SAM pause params');
|
|
110
|
-
|
|
111
|
-
const pause = Number(pauseUntilMs);
|
|
112
|
-
if (!Number.isFinite(pause) || pause < 0) {
|
|
113
|
-
throw new Error('SAM pause value must be a non-negative unix millisecond timestamp, or 0 to clear');
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return await this.request.execute('POST', '/sam/pause', null, {
|
|
117
|
-
...params,
|
|
118
|
-
pause
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async clearPause(params = {}) {
|
|
123
|
-
return await this.pause(0, params);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
async improve(params = {}) {
|
|
127
|
-
this._validateParams(params, 'SAM improve params');
|
|
128
|
-
return await this.request.execute('POST', '/sam/improve', null, params);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async flushActorMetadata() {
|
|
132
|
-
return await this.request.execute('POST', '/sam/flush_actor_metadata');
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
async listDocuments(collectionName, offset = 0, limit = 20, params = {}) {
|
|
136
|
-
Validator.validateCollectionName(collectionName);
|
|
137
|
-
this._validateParams(params, 'SAM document list params');
|
|
138
|
-
const safeOffset = Number(offset);
|
|
139
|
-
const safeLimit = Number(limit);
|
|
140
|
-
|
|
141
|
-
if (!Number.isInteger(safeOffset) || safeOffset < 0) {
|
|
142
|
-
throw new Error('SAM document offset must be a non-negative integer');
|
|
143
|
-
}
|
|
144
|
-
if (!Number.isInteger(safeLimit) || safeLimit < 1) {
|
|
145
|
-
throw new Error('SAM document limit must be a positive integer');
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return await this.request.execute('GET', '/sam/documents', null, {
|
|
149
|
-
...params,
|
|
150
|
-
collection: collectionName,
|
|
151
|
-
offset: safeOffset,
|
|
152
|
-
limit: safeLimit
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
async getDocument(collectionName, documentId, params = {}) {
|
|
157
|
-
Validator.validateCollectionName(collectionName);
|
|
158
|
-
Validator.validateDocumentId(documentId);
|
|
159
|
-
this._validateParams(params, 'SAM document params');
|
|
160
|
-
|
|
161
|
-
return await this.request.execute(
|
|
162
|
-
'GET',
|
|
163
|
-
`/sam/documents/${encodeURIComponent(collectionName)}/${encodeURIComponent(documentId)}`,
|
|
164
|
-
null,
|
|
165
|
-
params
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
async openDocument(collectionName, documentId, interactionQuery = null, params = {}) {
|
|
170
|
-
const queryParams = { ...params };
|
|
171
|
-
|
|
172
|
-
if (interactionQuery !== null && interactionQuery !== undefined && interactionQuery !== '') {
|
|
173
|
-
if (typeof interactionQuery !== 'string') {
|
|
174
|
-
throw new Error('SAM interaction query must be a string');
|
|
175
|
-
}
|
|
176
|
-
queryParams.interaction_query = interactionQuery;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return await this.getDocument(collectionName, documentId, queryParams);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async addDocumentLabel(collectionName, documentId, labels) {
|
|
183
|
-
Validator.validateCollectionName(collectionName);
|
|
184
|
-
Validator.validateDocumentId(documentId);
|
|
185
|
-
|
|
186
|
-
const normalizedLabels = Array.isArray(labels) ? labels : [labels];
|
|
187
|
-
if (normalizedLabels.length === 0 || normalizedLabels.some(label => typeof label !== 'string' || label.trim() === '')) {
|
|
188
|
-
throw new Error('SAM document labels must contain at least one non-empty string');
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return await this.request.execute(
|
|
192
|
-
'POST',
|
|
193
|
-
`/sam/label/add/${encodeURIComponent(collectionName)}/${encodeURIComponent(documentId)}`,
|
|
194
|
-
{ labels: normalizedLabels }
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
module.exports = SAM;
|
package/test/offline-smoke.js
DELETED
|
@@ -1,455 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const assert = require('assert');
|
|
4
|
-
const http = require('http');
|
|
5
|
-
|
|
6
|
-
const Hlquery = require('..');
|
|
7
|
-
const Client = require('../lib/Client');
|
|
8
|
-
const Collections = require('../lib/Collections');
|
|
9
|
-
const Documents = require('../lib/Documents');
|
|
10
|
-
const Aliases = require('../lib/Aliases');
|
|
11
|
-
const Request = require('../lib/Request');
|
|
12
|
-
const Response = require('../lib/Response');
|
|
13
|
-
const SAM = require('../lib/SAM');
|
|
14
|
-
const Search = require('../lib/Search');
|
|
15
|
-
const System = require('../lib/System');
|
|
16
|
-
const Modules = require('../lib/Modules');
|
|
17
|
-
const Users = require('../lib/Users');
|
|
18
|
-
const Analytics = require('../lib/Analytics');
|
|
19
|
-
const Config = require('../utils/Config');
|
|
20
|
-
const Validator = require('../utils/Validator');
|
|
21
|
-
const CSVParser = require('../utils/CSVParser');
|
|
22
|
-
|
|
23
|
-
async function main() {
|
|
24
|
-
assert.strictEqual(typeof Hlquery, 'function', 'default export should be the Client constructor');
|
|
25
|
-
assert.strictEqual(Hlquery.Client, Client, 'named Client export should match default export');
|
|
26
|
-
|
|
27
|
-
assert.strictEqual(Config.normalizeUrl('localhost:9200/'), 'http://localhost:9200');
|
|
28
|
-
assert.strictEqual(Config.normalizeUrl(new URL('http://localhost:9200///')), 'http://localhost:9200');
|
|
29
|
-
assert.strictEqual(Config.isValidUrl('http://localhost:9200'), true);
|
|
30
|
-
assert.strictEqual(Config.isValidUrl('http://'), false);
|
|
31
|
-
assert.strictEqual(Config.isValidUrl('notaurl'), false);
|
|
32
|
-
|
|
33
|
-
Validator.validateCollectionName('books');
|
|
34
|
-
Validator.validateDocumentId('doc_123');
|
|
35
|
-
Validator.validateDocumentId('doc/with space+plus');
|
|
36
|
-
Validator.validateSearchParams({ limit: 10, offset: 0 });
|
|
37
|
-
Validator.validateDocumentFields({ title: 'good,value' });
|
|
38
|
-
|
|
39
|
-
assert.throws(() => Validator.validateCollectionName('123bad'), /letter or underscore/);
|
|
40
|
-
assert.throws(() => Validator.validateSearchParams(null), /object/);
|
|
41
|
-
assert.throws(() => Validator.validateSearchParams({ limit: NaN }), /positive integer/);
|
|
42
|
-
assert.throws(() => Validator.validateSearchParams({ offset: 1.5 }), /non-negative integer/);
|
|
43
|
-
assert.throws(() => Validator.validateDocumentId('bad\nid'), /control characters/);
|
|
44
|
-
assert.throws(() => Validator.validateDocumentFields({ '': 'bad field' }), /field names/);
|
|
45
|
-
assert.throws(() => CSVParser.parseCSV('a||b', '||'), /single character/);
|
|
46
|
-
|
|
47
|
-
const response = new Response(200, { ok: true }, { 'content-type': 'application/json' }, {
|
|
48
|
-
statusMessage: 'OK',
|
|
49
|
-
rawBody: Buffer.from('{"ok":true}'),
|
|
50
|
-
request: { method: 'GET', url: 'http://localhost/health' }
|
|
51
|
-
});
|
|
52
|
-
assert.strictEqual(response.isSuccess(), true);
|
|
53
|
-
assert.deepStrictEqual(response.getBody(), { ok: true });
|
|
54
|
-
assert.strictEqual(response.getStatusMessage(), 'OK');
|
|
55
|
-
assert.strictEqual(response.getContentType(), 'application/json');
|
|
56
|
-
assert.strictEqual(response.getRawBody().toString(), '{"ok":true}');
|
|
57
|
-
|
|
58
|
-
const server = http.createServer((req, res) => {
|
|
59
|
-
if (req.url === '/health') {
|
|
60
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
61
|
-
res.end(JSON.stringify({ status: 'ok' }));
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (req.url === '/status') {
|
|
66
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
67
|
-
res.end(JSON.stringify({ status: 'ready' }));
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (req.url === '/etc') {
|
|
72
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
73
|
-
res.end(JSON.stringify({ protocol: 'http' }));
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (req.url === '/modules') {
|
|
78
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
79
|
-
res.end(JSON.stringify({ modules: ['demo'] }));
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (req.url === '/modules/demo/syntax') {
|
|
84
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
85
|
-
res.end(JSON.stringify({ name: 'demo', syntax: 'demo <q>' }));
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (req.url.startsWith('/modules/demo/search') && req.method === 'GET') {
|
|
90
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
91
|
-
res.end(JSON.stringify({ module: 'demo', route: 'search', url: req.url }));
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (req.url === '/modules/demo/index' && req.method === 'POST') {
|
|
96
|
-
let body = '';
|
|
97
|
-
req.on('data', chunk => {
|
|
98
|
-
body += chunk.toString();
|
|
99
|
-
});
|
|
100
|
-
req.on('end', () => {
|
|
101
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
102
|
-
res.end(JSON.stringify({ indexed: JSON.parse(body) }));
|
|
103
|
-
});
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (req.url === '/links') {
|
|
108
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
109
|
-
res.end(JSON.stringify({ links: [] }));
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (req.url === '/links/ping') {
|
|
114
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
115
|
-
res.end(JSON.stringify({ ok: true }));
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (req.url === '/links/connect' && req.method === 'POST') {
|
|
120
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
121
|
-
res.end(JSON.stringify({ connected: true }));
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (req.url === '/links/disconnect' && req.method === 'POST') {
|
|
126
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
127
|
-
res.end(JSON.stringify({ disconnected: true }));
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (req.url === '/slow') {
|
|
132
|
-
setTimeout(() => {
|
|
133
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
134
|
-
res.end(JSON.stringify({ done: true }));
|
|
135
|
-
}, 100);
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (req.url === '/large') {
|
|
140
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
141
|
-
res.end(JSON.stringify({ data: 'x'.repeat(128) }));
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (req.url === '/plain') {
|
|
146
|
-
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
147
|
-
res.end('plain response');
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (req.url === '/flush' && req.method === 'POST') {
|
|
152
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
153
|
-
res.end(JSON.stringify({ flushed: true }));
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (req.url.startsWith('/echo-auth')) {
|
|
158
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
159
|
-
res.end(JSON.stringify({
|
|
160
|
-
authorization: req.headers.authorization || null,
|
|
161
|
-
apiKey: req.headers['x-api-key'] || null,
|
|
162
|
-
contentType: req.headers['content-type'] || null,
|
|
163
|
-
url: req.url
|
|
164
|
-
}));
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (req.url === '/keys/key%2Fwith%20space') {
|
|
169
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
170
|
-
res.end(JSON.stringify({ id: 'key/with space' }));
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
175
|
-
res.end(JSON.stringify({ error: 'not found' }));
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
await new Promise((resolve, reject) => {
|
|
179
|
-
server.listen(0, '127.0.0.1', (err) => (err ? reject(err) : resolve()));
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
const { port } = server.address();
|
|
183
|
-
|
|
184
|
-
try {
|
|
185
|
-
const request = new Request(`http://127.0.0.1:${port}`);
|
|
186
|
-
const health = await request.execute('GET', '/health');
|
|
187
|
-
assert.strictEqual(health.getStatusCode(), 200);
|
|
188
|
-
assert.deepStrictEqual(health.getBody(), { status: 'ok' });
|
|
189
|
-
|
|
190
|
-
request.setAuthToken('secret-token', 'bearer');
|
|
191
|
-
const bearerEcho = await request.execute('GET', '/echo-auth');
|
|
192
|
-
assert.strictEqual(bearerEcho.getBody().authorization, 'Bearer secret-token');
|
|
193
|
-
|
|
194
|
-
request.setAuthToken('scoped-key', 'api-key');
|
|
195
|
-
const apiKeyEcho = await request.execute('GET', '/echo-auth');
|
|
196
|
-
assert.strictEqual(apiKeyEcho.getBody().apiKey, 'scoped-key');
|
|
197
|
-
|
|
198
|
-
const queryEcho = await request.execute('GET', '/echo-auth', undefined, {
|
|
199
|
-
fields: ['title', 'body'],
|
|
200
|
-
filter: { published: true },
|
|
201
|
-
skip: null
|
|
202
|
-
});
|
|
203
|
-
assert.strictEqual(queryEcho.getBody().contentType, null);
|
|
204
|
-
assert.match(queryEcho.getBody().url, /fields=title%2Cbody/);
|
|
205
|
-
assert.match(queryEcho.getBody().url, /filter=%7B%22published%22%3Atrue%7D/);
|
|
206
|
-
assert.doesNotMatch(queryEcho.getBody().url, /skip=/);
|
|
207
|
-
|
|
208
|
-
const repeatedQueryEcho = await request.execute('GET', '/echo-auth', undefined, {
|
|
209
|
-
fields: ['title', 'body']
|
|
210
|
-
}, {
|
|
211
|
-
query_array_format: 'repeat'
|
|
212
|
-
});
|
|
213
|
-
assert.match(repeatedQueryEcho.getBody().url, /fields=title&fields=body/);
|
|
214
|
-
|
|
215
|
-
const plain = await request.execute('GET', '/plain');
|
|
216
|
-
assert.strictEqual(plain.isParsed(), false);
|
|
217
|
-
assert.strictEqual(plain.getBody(), 'plain response');
|
|
218
|
-
assert.strictEqual(plain.getRawBody().toString(), 'plain response');
|
|
219
|
-
assert.match(plain.getRequest().url, /\/plain$/);
|
|
220
|
-
|
|
221
|
-
await assert.rejects(
|
|
222
|
-
() => request.execute('GET', '/large', null, {}, { max_response_bytes: 32 }),
|
|
223
|
-
(error) => error.code === 'MAX_RESPONSE_BYTES' && error.statusCode === 200
|
|
224
|
-
);
|
|
225
|
-
|
|
226
|
-
await assert.rejects(
|
|
227
|
-
() => request.execute('GET', '/missing', null, {}, { throw_on_error: true }),
|
|
228
|
-
(error) => error.statusCode === 404 && error.response && error.response.getError() === 'not found'
|
|
229
|
-
);
|
|
230
|
-
|
|
231
|
-
const controller = typeof AbortController === 'function' ? new AbortController() : null;
|
|
232
|
-
if (controller) {
|
|
233
|
-
const aborted = request.execute('GET', '/slow', null, {}, { signal: controller.signal });
|
|
234
|
-
controller.abort();
|
|
235
|
-
await assert.rejects(aborted, (error) => error.code === 'ABORT_ERR');
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const keepAliveAgent = new http.Agent({ keepAlive: true });
|
|
239
|
-
const agentRequest = new Request(`http://127.0.0.1:${port}`, {
|
|
240
|
-
http_agent: keepAliveAgent,
|
|
241
|
-
headers: { 'X-Test-Default': '1' }
|
|
242
|
-
});
|
|
243
|
-
const agentEcho = await agentRequest.execute('GET', '/echo-auth');
|
|
244
|
-
assert.strictEqual(agentEcho.getStatusCode(), 200);
|
|
245
|
-
keepAliveAgent.destroy();
|
|
246
|
-
|
|
247
|
-
const client = new Client(`http://127.0.0.1:${port}`);
|
|
248
|
-
const clientHealth = await client.health();
|
|
249
|
-
assert.strictEqual(clientHealth.isSuccess(), true);
|
|
250
|
-
assert.deepStrictEqual(clientHealth.getBody(), { status: 'ok' });
|
|
251
|
-
|
|
252
|
-
const clientStatus = await client.status();
|
|
253
|
-
assert.deepStrictEqual(clientStatus.getBody(), { status: 'ready' });
|
|
254
|
-
|
|
255
|
-
const clientEtc = await client.etc();
|
|
256
|
-
assert.deepStrictEqual(clientEtc.getBody(), { protocol: 'http' });
|
|
257
|
-
|
|
258
|
-
const routeEtc = await client.route('etc').get();
|
|
259
|
-
assert.deepStrictEqual(routeEtc.body, { protocol: 'http' });
|
|
260
|
-
|
|
261
|
-
const modulesList = await client.modules().list();
|
|
262
|
-
assert.deepStrictEqual(modulesList.body, { modules: ['demo'] });
|
|
263
|
-
|
|
264
|
-
const moduleSyntax = await client.modules().syntax('demo');
|
|
265
|
-
assert.deepStrictEqual(moduleSyntax.body, { name: 'demo', syntax: 'demo <q>' });
|
|
266
|
-
|
|
267
|
-
const moduleSearch = await client.module('demo').route('search').get({
|
|
268
|
-
q: 'example query'
|
|
269
|
-
});
|
|
270
|
-
assert.strictEqual(moduleSearch.body.module, 'demo');
|
|
271
|
-
assert.strictEqual(moduleSearch.body.route, 'search');
|
|
272
|
-
assert.match(moduleSearch.body.url, /q=example\+query|q=example%20query/);
|
|
273
|
-
|
|
274
|
-
const modulePost = await client.module('demo').route('index').post({
|
|
275
|
-
id: 'doc_1'
|
|
276
|
-
});
|
|
277
|
-
assert.deepStrictEqual(modulePost.body.indexed, { id: 'doc_1' });
|
|
278
|
-
|
|
279
|
-
const moduleCall = await client.modules().call('demo', 'search', 'GET', null, {
|
|
280
|
-
q: 'from call'
|
|
281
|
-
});
|
|
282
|
-
assert.match(moduleCall.body.url, /q=from\+call|q=from%20call/);
|
|
283
|
-
|
|
284
|
-
const clientLinks = await client.links();
|
|
285
|
-
assert.deepStrictEqual(clientLinks.getBody(), { links: [] });
|
|
286
|
-
|
|
287
|
-
const clientLinksPing = await client.linksPing();
|
|
288
|
-
assert.deepStrictEqual(clientLinksPing.getBody(), { ok: true });
|
|
289
|
-
|
|
290
|
-
const clientLinksConnect = await client.linksConnect('http://node-b:9200');
|
|
291
|
-
assert.deepStrictEqual(clientLinksConnect.getBody(), { connected: true });
|
|
292
|
-
|
|
293
|
-
const clientLinksDisconnect = await client.linksDisconnect('http://node-b:9200');
|
|
294
|
-
assert.deepStrictEqual(clientLinksDisconnect.getBody(), { disconnected: true });
|
|
295
|
-
|
|
296
|
-
const clientFlush = await client.flush();
|
|
297
|
-
assert.deepStrictEqual(clientFlush.getBody(), { flushed: true });
|
|
298
|
-
|
|
299
|
-
assert.strictEqual(typeof client.executeRequest, 'function');
|
|
300
|
-
assert.strictEqual(typeof client.sql, 'function');
|
|
301
|
-
assert.strictEqual(typeof client.execSql, 'function');
|
|
302
|
-
assert.strictEqual(typeof client.sqlSearch, 'function');
|
|
303
|
-
assert.strictEqual(typeof client.sam().search, 'function');
|
|
304
|
-
assert.strictEqual(typeof client.sam().searchAll, 'function');
|
|
305
|
-
assert.strictEqual(typeof client.sam().rebuild, 'function');
|
|
306
|
-
assert.strictEqual(typeof client.sam().status, 'function');
|
|
307
|
-
assert.strictEqual(typeof client.sam().debug, 'function');
|
|
308
|
-
assert.strictEqual(typeof client.sam().history, 'function');
|
|
309
|
-
assert.strictEqual(typeof client.sam().pause, 'function');
|
|
310
|
-
assert.strictEqual(typeof client.sam().clearPause, 'function');
|
|
311
|
-
assert.strictEqual(typeof client.sam().improve, 'function');
|
|
312
|
-
assert.strictEqual(typeof client.sam().flushActorMetadata, 'function');
|
|
313
|
-
assert.strictEqual(typeof client.sam().listDocuments, 'function');
|
|
314
|
-
assert.strictEqual(typeof client.sam().getDocument, 'function');
|
|
315
|
-
assert.strictEqual(typeof client.sam().openDocument, 'function');
|
|
316
|
-
assert.strictEqual(typeof client.sam().addDocumentLabel, 'function');
|
|
317
|
-
assert.strictEqual(typeof client.users().list, 'function');
|
|
318
|
-
assert.strictEqual(typeof client.analytics().click, 'function');
|
|
319
|
-
await assert.rejects(() => client.sam().history(null, 0), /positive integer/);
|
|
320
|
-
await assert.rejects(() => client.sam().listDocuments('books', NaN, 20), /non-negative integer/);
|
|
321
|
-
|
|
322
|
-
const keyResponse = await client.keys().get('key/with space');
|
|
323
|
-
assert.deepStrictEqual(keyResponse.getBody(), { id: 'key/with space' });
|
|
324
|
-
await assert.rejects(() => client.documents().import('books', { id: 'not-array' }), /array/);
|
|
325
|
-
|
|
326
|
-
const strictClient = new Client(`http://127.0.0.1:${port}`, { throw_on_error: true });
|
|
327
|
-
await assert.rejects(
|
|
328
|
-
() => strictClient.executeRequest('GET', '/missing'),
|
|
329
|
-
(error) => error.statusCode === 404 && error.response.getStatusCode() === 404
|
|
330
|
-
);
|
|
331
|
-
|
|
332
|
-
const fakeCollections = new Collections({
|
|
333
|
-
async execute(method, path, body) {
|
|
334
|
-
assert.strictEqual(body.name, 'books');
|
|
335
|
-
assert.strictEqual(body.extra, true);
|
|
336
|
-
return new Response(201, body);
|
|
337
|
-
}
|
|
338
|
-
});
|
|
339
|
-
const created = await fakeCollections.create('books', { name: 'books', extra: true });
|
|
340
|
-
assert.strictEqual(created.getBody().name, 'books');
|
|
341
|
-
await assert.rejects(() => fakeCollections.create('books', { name: 'other' }), /must match/);
|
|
342
|
-
|
|
343
|
-
const fieldsApi = new Collections({
|
|
344
|
-
async execute() {
|
|
345
|
-
return new Response(200, {
|
|
346
|
-
fields: [{ name: 'title', type: 'string' }],
|
|
347
|
-
searchable_fields: ['body'],
|
|
348
|
-
filterable_fields: ['author']
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
});
|
|
352
|
-
const fields = await fieldsApi.getFields('books');
|
|
353
|
-
assert.deepStrictEqual(fields.getBody().fields.map(field => field.name), ['title', 'body', 'author']);
|
|
354
|
-
|
|
355
|
-
const capturedRequests = [];
|
|
356
|
-
const captureRequest = {
|
|
357
|
-
async execute(method, path, body = null, queryParams = {}) {
|
|
358
|
-
capturedRequests.push({ method, path, body, queryParams });
|
|
359
|
-
return new Response(200, { ok: true });
|
|
360
|
-
}
|
|
361
|
-
};
|
|
362
|
-
const capturedCollections = new Collections(captureRequest);
|
|
363
|
-
const capturedDocuments = new Documents(captureRequest);
|
|
364
|
-
const capturedAliases = new Aliases(captureRequest);
|
|
365
|
-
const capturedSam = new SAM(captureRequest);
|
|
366
|
-
const capturedSystem = new System(captureRequest);
|
|
367
|
-
const capturedModules = new Modules(captureRequest);
|
|
368
|
-
const capturedUsers = new Users(captureRequest);
|
|
369
|
-
const capturedAnalytics = new Analytics(captureRequest);
|
|
370
|
-
|
|
371
|
-
await capturedCollections.language('books');
|
|
372
|
-
await capturedAliases.listCollection('books');
|
|
373
|
-
await capturedDocuments.context('books', 'doc/one');
|
|
374
|
-
await capturedDocuments.maybe('books', { q: 'bok' });
|
|
375
|
-
await capturedDocuments.updateByQuery('books', { filter_by: 'author:alice', update: { featured: true } });
|
|
376
|
-
await capturedDocuments.deleteByQuery('books', { filter_by: 'author:alice' });
|
|
377
|
-
await capturedSam.improve({ limit: 2, force: true });
|
|
378
|
-
await capturedSam.flushActorMetadata();
|
|
379
|
-
await capturedSam.addDocumentLabel('books', 'doc/one', ['featured']);
|
|
380
|
-
await capturedUsers.list();
|
|
381
|
-
await capturedUsers.get('alice smith');
|
|
382
|
-
await capturedUsers.create({ name: 'alice smith', flags: ['user'] });
|
|
383
|
-
await capturedUsers.update('alice smith', { description: 'Updated' });
|
|
384
|
-
await capturedUsers.delete('alice smith');
|
|
385
|
-
await capturedAnalytics.click({ collection: 'books', doc_id: 'doc/one', query: 'book', rank: 1 });
|
|
386
|
-
await capturedSystem.ready();
|
|
387
|
-
await capturedSystem.metricsHistory();
|
|
388
|
-
await capturedSystem.metricsHistoryAlias();
|
|
389
|
-
await capturedSystem.searchConfig();
|
|
390
|
-
await capturedSystem.llm();
|
|
391
|
-
await capturedSystem.updateCounters({ prefix: 'bench_' });
|
|
392
|
-
await capturedSystem.debugCounters();
|
|
393
|
-
await capturedSystem.repair({ collection: 'books', rebuild_index: true });
|
|
394
|
-
await capturedModules.load('demo');
|
|
395
|
-
await capturedModules.unload('demo');
|
|
396
|
-
|
|
397
|
-
assert.deepStrictEqual(capturedRequests, [
|
|
398
|
-
{ method: 'GET', path: '/collections/books/lang', body: null, queryParams: {} },
|
|
399
|
-
{ method: 'GET', path: '/collections/books/aliases', body: null, queryParams: {} },
|
|
400
|
-
{ method: 'GET', path: '/collections/books/documents/doc%2Fone/context', body: null, queryParams: {} },
|
|
401
|
-
{ method: 'GET', path: '/collections/books/documents/maybe', body: null, queryParams: { q: 'bok' } },
|
|
402
|
-
{ method: 'POST', path: '/collections/books/documents/_update_by_query', body: { filter_by: 'author:alice', update: { featured: true } }, queryParams: {} },
|
|
403
|
-
{ method: 'POST', path: '/collections/books/documents/_delete_by_query', body: { filter_by: 'author:alice' }, queryParams: {} },
|
|
404
|
-
{ method: 'POST', path: '/sam/improve', body: null, queryParams: { limit: 2, force: true } },
|
|
405
|
-
{ method: 'POST', path: '/sam/flush_actor_metadata', body: null, queryParams: {} },
|
|
406
|
-
{ method: 'POST', path: '/sam/label/add/books/doc%2Fone', body: { labels: ['featured'] }, queryParams: {} },
|
|
407
|
-
{ method: 'GET', path: '/users', body: null, queryParams: {} },
|
|
408
|
-
{ method: 'GET', path: '/users/alice%20smith', body: null, queryParams: {} },
|
|
409
|
-
{ method: 'POST', path: '/users', body: { name: 'alice smith', flags: ['user'] }, queryParams: {} },
|
|
410
|
-
{ method: 'PUT', path: '/users/alice%20smith', body: { description: 'Updated' }, queryParams: {} },
|
|
411
|
-
{ method: 'DELETE', path: '/users/alice%20smith', body: null, queryParams: {} },
|
|
412
|
-
{ method: 'POST', path: '/analytics/click', body: { collection: 'books', doc_id: 'doc/one', query: 'book', rank: 1 }, queryParams: {} },
|
|
413
|
-
{ method: 'GET', path: '/ready', body: null, queryParams: {} },
|
|
414
|
-
{ method: 'GET', path: '/metrics/history', body: null, queryParams: {} },
|
|
415
|
-
{ method: 'GET', path: '/metrics-history', body: null, queryParams: {} },
|
|
416
|
-
{ method: 'GET', path: '/search-config', body: null, queryParams: {} },
|
|
417
|
-
{ method: 'GET', path: '/llm', body: null, queryParams: {} },
|
|
418
|
-
{ method: 'POST', path: '/update-counters', body: null, queryParams: { prefix: 'bench_' } },
|
|
419
|
-
{ method: 'GET', path: '/debug/counters', body: null, queryParams: {} },
|
|
420
|
-
{ method: 'POST', path: '/repair', body: null, queryParams: { collection: 'books', rebuild_index: true } },
|
|
421
|
-
{ method: 'POST', path: '/loadmodule/demo', body: null, queryParams: {} },
|
|
422
|
-
{ method: 'POST', path: '/unloadmodule/demo', body: null, queryParams: {} }
|
|
423
|
-
]);
|
|
424
|
-
|
|
425
|
-
let collectionFetches = 0;
|
|
426
|
-
const searchApi = new Search({
|
|
427
|
-
async execute(method, path, body, queryParams) {
|
|
428
|
-
return new Response(200, { queryParams });
|
|
429
|
-
}
|
|
430
|
-
}, {
|
|
431
|
-
async get() {
|
|
432
|
-
collectionFetches += 1;
|
|
433
|
-
return new Response(200, { searchable_fields: ['title', 'body'] });
|
|
434
|
-
}
|
|
435
|
-
});
|
|
436
|
-
await searchApi.search('books', { q: 'one' });
|
|
437
|
-
await searchApi.search('books', { q: 'two' });
|
|
438
|
-
assert.strictEqual(collectionFetches, 1);
|
|
439
|
-
|
|
440
|
-
const noAutoDetect = await searchApi.search('books', {
|
|
441
|
-
q: 'three',
|
|
442
|
-
auto_detect_query_by: false
|
|
443
|
-
});
|
|
444
|
-
assert.strictEqual(noAutoDetect.getBody().queryParams.query_by, undefined);
|
|
445
|
-
} finally {
|
|
446
|
-
await new Promise((resolve) => server.close(resolve));
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
process.stdout.write('Node offline smoke tests passed.\n');
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
main().catch((error) => {
|
|
453
|
-
process.stderr.write(`${error.stack || error.message}\n`);
|
|
454
|
-
process.exit(1);
|
|
455
|
-
});
|