hlquery-node-client 1.0.5 → 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()`, so collection management, document indexing, search, SQL, and SAM workflows stay easy to find.
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
 
@@ -49,12 +49,14 @@ const client = new Client(process.env.HLQ_BASE_URL || process.env.HLQUERY_BASE_U
49
49
  });
50
50
 
51
51
  const health = await client.system().health();
52
+
52
53
  /* Print the HTTP status code from the health response. */
53
54
  console.log('status:', health.getStatusCode());
54
55
 
55
56
  const collections = await client.collections().list(0, 10);
57
+
56
58
  /* Print the collection list response body. */
57
- console.log(collections.getBody());
59
+ console.log(collections.body);
58
60
  ```
59
61
 
60
62
  ### Auth
@@ -86,6 +88,16 @@ const disconnect = await client.linksDisconnect('http://node-b:9200');
86
88
  const flush = await client.flush();
87
89
  ```
88
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
+
89
101
  Routes that do not have a dedicated wrapper can still be called through `executeRequest()`:
90
102
 
91
103
  ```javascript
@@ -93,45 +105,70 @@ const response = await client.executeRequest('GET', '/modules/<name>/<route>', n
93
105
  q: 'example query'
94
106
  });
95
107
 
96
- /* Print the server status response body. */
97
- console.log(status.getBody());
98
- /* Print the health response body. */
99
- console.log(health.getBody());
100
- /* Print the runtime configuration response body. */
101
- console.log(etc.getBody());
102
- /* Print the configured cluster links response body. */
103
- console.log(links.getBody());
104
- /* Print the link ping response body. */
105
- console.log(ping.getBody());
106
- /* Print the link connect response body. */
107
- console.log(connect.getBody());
108
- /* Print the link disconnect response body. */
109
- console.log(disconnect.getBody());
110
- /* Print the flush response body. */
111
- console.log(flush.getBody());
112
- /* Print the custom route response body. */
113
- console.log(response.getBody());
108
+ console.log(response.body);
114
109
  ```
115
110
 
116
- ### SAM
111
+ For module routes and custom endpoints, the client also supports a Redis-style fluent API:
112
+
113
+ ```javascript
114
+ const result = await client.module('<name>').route('<route>').get({
115
+ q: 'example query'
116
+ });
117
+
118
+ console.log(result.body);
119
+
120
+ const indexed = await client.module('<name>').route('index').post({
121
+ id: 'doc_1',
122
+ title: 'Example'
123
+ });
124
+
125
+ const modules = await client.modules().list();
126
+ const syntax = await client.modules().syntax('<name>');
127
+ const raw = await client.route('etc').get();
128
+
129
+ console.log(indexed.body);
130
+ console.log(modules.body);
131
+ console.log(syntax.body);
132
+ console.log(raw.body);
133
+ ```
117
134
 
118
- SAM is separate from vector search. It performs term and intent-style lookup, not vector similarity search.
135
+ ### Collections, documents, and search
119
136
 
120
137
  ```javascript
121
- const sam = client.sam();
138
+ const metadata = await client.collections().get('products');
139
+ const language = await client.collections().language('products');
122
140
 
123
- const status = await sam.status('books');
124
- const history = await sam.history('books', 5);
125
- const results = await sam.search('books', 'distributed systems', {
126
- limit: 10
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' });
149
+
150
+ await client.documents().updateByQuery('products', {
151
+ filter_by: 'active:false',
152
+ set: { archived: true }
127
153
  });
154
+ await client.documents().deleteByQuery('products', { filter_by: 'expired:true' });
128
155
 
129
- /* Print the SAM status response body. */
130
- console.log(status.getBody());
131
- /* Print the SAM search history response body. */
132
- console.log(history.getBody());
133
- /* Print the SAM search results response body. */
134
- console.log(results.getBody());
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
135
172
  ```
136
173
 
137
174
  ### SQL
@@ -147,11 +184,11 @@ const books = await client.sqlSearch(
147
184
  );
148
185
 
149
186
  /* Print the SQL query response body. */
150
- console.log(rows.getBody());
187
+ console.log(rows.body);
151
188
  /* Print the SQL execution response body. */
152
- console.log(execResult.getBody());
189
+ console.log(execResult.body);
153
190
  /* Print the collection SQL search response body. */
154
- console.log(books.getBody());
191
+ console.log(books.body);
155
192
  ```
156
193
 
157
194
  ### Contributing
@@ -160,17 +197,19 @@ We welcome contributions from the community! All contributions must be released
160
197
 
161
198
  ### How to Contribute
162
199
 
163
- - Check existing [issues](https://github.com/hlquery/hlquery/issues) or create new ones
164
- - Contribute to client libraries (Node.js, Go, Java, Python, PHP, Ruby, Rust, Perl, C++)
165
- - Test and report bugs
166
- - 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
167
205
 
168
206
  ### Community
169
207
 
170
- - 📖 [Documentation](https://docs.hlquery.com)
171
- - 🐦 [X (Twitter)](https://x.com/hlquery)
172
- - 📦 [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)
173
212
 
174
213
  ### License
175
214
 
176
- 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/Aliases.js CHANGED
@@ -20,6 +20,11 @@ class Aliases {
20
20
  return await this.request.execute('GET', '/aliases');
21
21
  }
22
22
 
23
+ async listCollection(collectionName) {
24
+ Validator.validateCollectionName(collectionName);
25
+ return await this.request.execute('GET', `/collections/${encodeURIComponent(collectionName)}/aliases`);
26
+ }
27
+
23
28
  async get(name) {
24
29
  Validator.validateCollectionName(name);
25
30
  return await this.request.execute('GET', `/aliases/${encodeURIComponent(name)}`);
@@ -0,0 +1,30 @@
1
+ /**
2
+ * hlquery Node.js Client - Analytics 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
+ class Analytics {
12
+ constructor(request) {
13
+ this.request = request;
14
+ }
15
+
16
+ async click(payload) {
17
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
18
+ throw new Error('Analytics click payload must be an object');
19
+ }
20
+
21
+ Validator.validateCollectionName(payload.collection);
22
+
23
+ const documentId = payload.doc_id !== undefined ? payload.doc_id : payload.document_id;
24
+ Validator.validateDocumentId(documentId);
25
+
26
+ return await this.request.execute('POST', '/analytics/click', payload);
27
+ }
28
+ }
29
+
30
+ module.exports = Analytics;
package/lib/Client.js CHANGED
@@ -17,7 +17,10 @@ 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');
20
+ const Modules = require('./Modules');
21
+ const Users = require('./Users');
22
+ const Analytics = require('./Analytics');
23
+ const { RouteCommand, ModuleRouteCommand } = require('./Route');
21
24
  const Config = require('../utils/Config');
22
25
 
23
26
  /**
@@ -38,7 +41,8 @@ class Client {
38
41
  baseUrl,
39
42
  opts.timeout,
40
43
  opts.token || null,
41
- opts.auth_method || 'bearer'
44
+ opts.auth_method || 'bearer',
45
+ opts
42
46
  );
43
47
 
44
48
  this._collections = new Collections(this.request);
@@ -50,7 +54,9 @@ class Client {
50
54
  this._synonyms = new Synonyms(this.request);
51
55
  this._stopwords = new Stopwords(this.request);
52
56
  this._system = new System(this.request);
53
- this._sam = new SAM(this.request);
57
+ this._modules = new Modules(this.request);
58
+ this._users = new Users(this.request);
59
+ this._analytics = new Analytics(this.request);
54
60
  }
55
61
 
56
62
  /**
@@ -143,6 +149,10 @@ class Client {
143
149
  return await this._system.bootStatus();
144
150
  }
145
151
 
152
+ async ready() {
153
+ return await this._system.ready();
154
+ }
155
+
146
156
  /**
147
157
  * Get metrics.
148
158
  *
@@ -161,6 +171,10 @@ class Client {
161
171
  return await this._system.metricsJson();
162
172
  }
163
173
 
174
+ async metricsHistory() {
175
+ return await this._system.metricsHistory();
176
+ }
177
+
164
178
  /**
165
179
  * Get live connections.
166
180
  *
@@ -242,6 +256,22 @@ class Client {
242
256
  return await this._system.storageStatus();
243
257
  }
244
258
 
259
+ async searchConfig() {
260
+ return await this._system.searchConfig();
261
+ }
262
+
263
+ async updateCounters(params = {}, method = 'POST') {
264
+ return await this._system.updateCounters(params, method);
265
+ }
266
+
267
+ async debugCounters() {
268
+ return await this._system.debugCounters();
269
+ }
270
+
271
+ async repair(params = {}, method = 'POST') {
272
+ return await this._system.repair(params, method);
273
+ }
274
+
245
275
  /**
246
276
  * Execute a top-level SQL query through GET /sql.
247
277
  *
@@ -269,7 +299,7 @@ class Client {
269
299
  * @returns {Promise<Response>}
270
300
  */
271
301
  async clusterHealth() {
272
- return await this.request.execute('GET', '/cluster/health');
302
+ return await this._system.health();
273
303
  }
274
304
 
275
305
  /**
@@ -278,7 +308,7 @@ class Client {
278
308
  * @returns {Promise<Response>}
279
309
  */
280
310
  async clusterStats() {
281
- return await this.request.execute('GET', '/cluster/stats');
311
+ return await this._system.stats();
282
312
  }
283
313
 
284
314
  /**
@@ -287,7 +317,7 @@ class Client {
287
317
  * @returns {Promise<Response>}
288
318
  */
289
319
  async clusterNodes() {
290
- return await this.request.execute('GET', '/cluster/nodes');
320
+ return await this.links();
291
321
  }
292
322
 
293
323
  /**
@@ -383,6 +413,10 @@ class Client {
383
413
  async getCollection(name) {
384
414
  return await this._collections.get(name);
385
415
  }
416
+
417
+ async getCollectionLanguage(name) {
418
+ return await this._collections.language(name);
419
+ }
386
420
 
387
421
  /**
388
422
  * Get collection fields (formatted)
@@ -503,6 +537,10 @@ class Client {
503
537
  return await this._documents.get(collectionName, documentId);
504
538
  }
505
539
 
540
+ async getDocumentContext(collectionName, documentId) {
541
+ return await this._documents.context(collectionName, documentId);
542
+ }
543
+
506
544
  /**
507
545
  * Copy a document to a new ID within the same collection.
508
546
  *
@@ -548,6 +586,18 @@ class Client {
548
586
  async facetCounts(collectionName, params = {}) {
549
587
  return await this._documents.facetCounts(collectionName, params);
550
588
  }
589
+
590
+ async maybeDocuments(collectionName, params = {}) {
591
+ return await this._documents.maybe(collectionName, params);
592
+ }
593
+
594
+ async updateDocumentsByQuery(collectionName, body = {}) {
595
+ return await this._documents.updateByQuery(collectionName, body);
596
+ }
597
+
598
+ async deleteDocumentsByQuery(collectionName, body = {}) {
599
+ return await this._documents.deleteByQuery(collectionName, body);
600
+ }
551
601
 
552
602
  // ============================================================================
553
603
  // SEARCH API
@@ -562,59 +612,6 @@ class Client {
562
612
  return this._search;
563
613
  }
564
614
 
565
- /**
566
- * Get SAM API instance
567
- *
568
- * @returns {SAM}
569
- */
570
- sam() {
571
- return this._sam;
572
- }
573
-
574
- async samSearch(collectionName, query, params = {}) {
575
- return await this._sam.search(collectionName, query, params);
576
- }
577
-
578
- async samSearchAll(query, params = {}) {
579
- return await this._sam.searchAll(query, params);
580
- }
581
-
582
- async samRebuild(collectionName, params = {}) {
583
- return await this._sam.rebuild(collectionName, params);
584
- }
585
-
586
- async samStatus(collectionName = null, params = {}) {
587
- return await this._sam.status(collectionName, params);
588
- }
589
-
590
- async samDebug(collectionName = null, params = {}) {
591
- return await this._sam.debug(collectionName, params);
592
- }
593
-
594
- async samHistory(collectionName = null, limit = 100, params = {}) {
595
- return await this._sam.history(collectionName, limit, params);
596
- }
597
-
598
- async samPause(pauseUntilMs, params = {}) {
599
- return await this._sam.pause(pauseUntilMs, params);
600
- }
601
-
602
- async samClearPause(params = {}) {
603
- return await this._sam.clearPause(params);
604
- }
605
-
606
- async samDocuments(collectionName, offset = 0, limit = 20, params = {}) {
607
- return await this._sam.listDocuments(collectionName, offset, limit, params);
608
- }
609
-
610
- async samDocument(collectionName, documentId, params = {}) {
611
- return await this._sam.getDocument(collectionName, documentId, params);
612
- }
613
-
614
- async samOpenDocument(collectionName, documentId, interactionQuery = null, params = {}) {
615
- return await this._sam.openDocument(collectionName, documentId, interactionQuery, params);
616
- }
617
-
618
615
  /**
619
616
  * Get aliases API instance
620
617
  *
@@ -668,6 +665,57 @@ class Client {
668
665
  keys() {
669
666
  return this._keys;
670
667
  }
668
+
669
+ /**
670
+ * Get runtime modules API instance
671
+ *
672
+ * @returns {Modules}
673
+ */
674
+ modules() {
675
+ return this._modules;
676
+ }
677
+
678
+ /**
679
+ * Get users API instance.
680
+ *
681
+ * @returns {Users}
682
+ */
683
+ users() {
684
+ return this._users;
685
+ }
686
+
687
+ /**
688
+ * Get analytics API instance.
689
+ *
690
+ * @returns {Analytics}
691
+ */
692
+ analytics() {
693
+ return this._analytics;
694
+ }
695
+
696
+ async analyticsClick(payload) {
697
+ return await this._analytics.click(payload);
698
+ }
699
+
700
+ /**
701
+ * Build a Redis-style dynamic module command.
702
+ *
703
+ * @param {string} name
704
+ * @returns {ModuleRouteCommand}
705
+ */
706
+ module(name) {
707
+ return new ModuleRouteCommand(this.request, name);
708
+ }
709
+
710
+ /**
711
+ * Build a Redis-style top-level route command.
712
+ *
713
+ * @param {string|Array<string>} route
714
+ * @returns {RouteCommand}
715
+ */
716
+ route(route) {
717
+ return new RouteCommand(this.request, '/').route(route);
718
+ }
671
719
 
672
720
  /**
673
721
  * Search documents
@@ -722,8 +770,8 @@ class Client {
722
770
  * @param {object} queryParams Query parameters
723
771
  * @returns {Promise<Response>}
724
772
  */
725
- async executeRequest(method, path, body = null, queryParams = {}) {
726
- return await this.request.execute(method, path, body, queryParams);
773
+ async executeRequest(method, path, body = null, queryParams = {}, options = {}) {
774
+ return await this.request.execute(method, path, body, queryParams, options);
727
775
  }
728
776
 
729
777
  // ============================================================================
@@ -42,6 +42,17 @@ class Collections {
42
42
  Validator.validateCollectionName(name);
43
43
  return await this.request.execute('GET', `/collections/${encodeURIComponent(name)}`);
44
44
  }
45
+
46
+ /**
47
+ * Get detected collection language information.
48
+ *
49
+ * @param {string} name Collection name
50
+ * @returns {Promise<Response>}
51
+ */
52
+ async language(name) {
53
+ Validator.validateCollectionName(name);
54
+ return await this.request.execute('GET', `/collections/${encodeURIComponent(name)}/lang`);
55
+ }
45
56
 
46
57
  /**
47
58
  * Create a new collection
@@ -52,10 +63,14 @@ class Collections {
52
63
  */
53
64
  async create(name, schema) {
54
65
  Validator.validateCollectionName(name);
66
+ if (schema && schema.name !== undefined && schema.name !== name) {
67
+ throw new Error('Schema name must match the collection name argument');
68
+ }
69
+
55
70
  // Merge schema with name at top level (server expects fields/searchable_fields at root)
56
71
  const requestBody = {
57
- name: name,
58
- ...schema
72
+ ...schema,
73
+ name: name
59
74
  };
60
75
  return await this.request.execute('POST', '/collections', requestBody);
61
76
  }
@@ -99,6 +114,24 @@ class Collections {
99
114
  const body = response.getBody();
100
115
  const allFields = [];
101
116
  const fieldTypes = {};
117
+
118
+ if (Array.isArray(body.fields)) {
119
+ for (const field of body.fields) {
120
+ const fieldName = typeof field === 'string' ? field : field.name;
121
+ if (!fieldName) {
122
+ continue;
123
+ }
124
+ if (!allFields.includes(fieldName)) {
125
+ allFields.push(fieldName);
126
+ }
127
+ if (!fieldTypes[fieldName]) {
128
+ fieldTypes[fieldName] = [];
129
+ }
130
+ if (field.type && !fieldTypes[fieldName].includes(field.type)) {
131
+ fieldTypes[fieldName].push(field.type);
132
+ }
133
+ }
134
+ }
102
135
 
103
136
  // Collect searchable fields
104
137
  if (body.searchable_fields) {
@@ -142,7 +175,7 @@ class Collections {
142
175
  // Format fields
143
176
  const fields = allFields.map(field => ({
144
177
  name: field,
145
- type: fieldTypes[field].join(', ')
178
+ type: fieldTypes[field].join(', ') || 'unknown'
146
179
  }));
147
180
 
148
181
  return new Response(200, {