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 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, SQL, and SAM.
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()`, `client.documents()`, and `client.sam()`, with Redis-style dynamic route helpers for module APIs and custom endpoints.
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
- ### SAM
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 sam = client.sam();
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
- const status = await sam.status('books');
133
- const history = await sam.history('books', 5);
134
- const results = await sam.search('books', 'distributed systems', {
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
- /* Print the SAM status response body. */
139
- console.log(status.body);
140
- /* Print the SAM search history response body. */
141
- console.log(history.body);
142
- /* Print the SAM search results response body. */
143
- console.log(results.body);
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/hlquery/issues) or create new ones
173
- - Contribute to client libraries (Node.js, Go, Java, Python, PHP, Ruby, Rust, Perl, C++)
174
- - Test and report bugs
175
- - Improve documentation
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
- - 📖 [Documentation](https://docs.hlquery.com)
180
- - 🐦 [X (Twitter)](https://x.com/hlquery)
181
- - 📦 [GitHub](https://github.com/hlquery/hlquery)
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 llm() {
266
- return await this._system.llm();
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.request.execute('GET', '/cluster/health');
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.request.execute('GET', '/cluster/stats');
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.request.execute('GET', '/cluster/nodes');
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 url = new URL(path, `${this.baseUrl}/`);
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
- return await this.request.execute('POST', '/multi_search', { searches: searches });
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 llm() {
126
- return await this.request.execute('GET', '/llm');
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
- return await this.request.execute('POST', '/repair', null, params);
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
 
@@ -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.6",
4
- "description": "Node.js client library for hlquery search engine",
5
- "main": "index.js",
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 ../tests/test_node_client_conformance.js",
9
- "test:conformance": "node ../tests/test_node_client_conformance.js",
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;
@@ -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
- });