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/lib/Search.js CHANGED
@@ -15,6 +15,7 @@ class Search {
15
15
  constructor(request, collections) {
16
16
  this.request = request;
17
17
  this.collections = collections;
18
+ this.searchableFieldsCache = new Map();
18
19
  }
19
20
 
20
21
  /**
@@ -96,13 +97,18 @@ class Search {
96
97
  queryParams.query_by = Array.isArray(params.query_by)
97
98
  ? params.query_by.join(',')
98
99
  : params.query_by;
99
- } else if (params.q && params.q !== '') {
100
- // Auto-detect searchable fields
101
- const collection = await this.collections.get(collectionName);
102
- if (collection.getStatusCode() === 200) {
103
- const body = collection.getBody();
104
- if (body.searchable_fields && body.searchable_fields.length > 0) {
105
- queryParams.query_by = body.searchable_fields.join(',');
100
+ } else if (params.q && params.q !== '' && params.auto_detect_query_by !== false) {
101
+ const cacheKey = collectionName;
102
+ if (this.searchableFieldsCache.has(cacheKey)) {
103
+ queryParams.query_by = this.searchableFieldsCache.get(cacheKey);
104
+ } else {
105
+ const collection = await this.collections.get(collectionName);
106
+ if (collection.getStatusCode() === 200) {
107
+ const body = collection.getBody();
108
+ if (body.searchable_fields && body.searchable_fields.length > 0) {
109
+ queryParams.query_by = body.searchable_fields.join(',');
110
+ this.searchableFieldsCache.set(cacheKey, queryParams.query_by);
111
+ }
106
112
  }
107
113
  }
108
114
  }
@@ -212,10 +218,16 @@ class Search {
212
218
  * Multi-search across multiple collections
213
219
  *
214
220
  * @param {Array} searches Array of search requests
221
+ * @param {string} method GET or POST
215
222
  * @returns {Promise<Response>}
216
223
  */
217
- async multiSearch(searches) {
218
- 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 });
219
231
  }
220
232
 
221
233
  /**
package/lib/System.js CHANGED
@@ -30,6 +30,10 @@ class System {
30
30
  return await this.request.execute('GET', '/boot-status');
31
31
  }
32
32
 
33
+ async ready() {
34
+ return await this.request.execute('GET', '/ready');
35
+ }
36
+
33
37
  async info() {
34
38
  return await this.request.execute('GET', '/');
35
39
  }
@@ -46,6 +50,14 @@ class System {
46
50
  return await this.request.execute('GET', '/metrics.json');
47
51
  }
48
52
 
53
+ async metricsHistory() {
54
+ return await this.request.execute('GET', '/metrics/history');
55
+ }
56
+
57
+ async metricsHistoryAlias() {
58
+ return await this.request.execute('GET', '/metrics-history');
59
+ }
60
+
49
61
  async connections() {
50
62
  return await this.request.execute('GET', '/connections');
51
63
  }
@@ -105,6 +117,32 @@ class System {
105
117
  async storageStatus() {
106
118
  return await this.request.execute('GET', '/admin/storage_status');
107
119
  }
120
+
121
+ async searchConfig() {
122
+ return await this.request.execute('GET', '/search-config');
123
+ }
124
+
125
+ async updateCounters(params = {}, method = 'POST') {
126
+ method = this._getOrPostMethod(method, 'Update counters');
127
+ return await this.request.execute(method, '/update-counters', null, params);
128
+ }
129
+
130
+ async debugCounters() {
131
+ return await this.request.execute('GET', '/debug/counters');
132
+ }
133
+
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;
145
+ }
108
146
  }
109
147
 
110
148
  module.exports = System;
package/lib/Users.js ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * hlquery Node.js Client - Users 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
+ class Users {
10
+ constructor(request) {
11
+ this.request = request;
12
+ }
13
+
14
+ _validateName(name) {
15
+ if (typeof name !== 'string' || name.trim() === '') {
16
+ throw new Error('User name is required');
17
+ }
18
+ }
19
+
20
+ async list() {
21
+ return await this.request.execute('GET', '/users');
22
+ }
23
+
24
+ async get(name) {
25
+ this._validateName(name);
26
+ return await this.request.execute('GET', `/users/${encodeURIComponent(name)}`);
27
+ }
28
+
29
+ async create(params) {
30
+ if (!params || typeof params !== 'object' || Array.isArray(params)) {
31
+ throw new Error('User parameters must be an object');
32
+ }
33
+ this._validateName(params.name);
34
+ return await this.request.execute('POST', '/users', params);
35
+ }
36
+
37
+ async update(name, params) {
38
+ this._validateName(name);
39
+ if (!params || typeof params !== 'object' || Array.isArray(params)) {
40
+ throw new Error('User parameters must be an object');
41
+ }
42
+ return await this.request.execute('PUT', `/users/${encodeURIComponent(name)}`, params);
43
+ }
44
+
45
+ async delete(name) {
46
+ this._validateName(name);
47
+ return await this.request.execute('DELETE', `/users/${encodeURIComponent(name)}`);
48
+ }
49
+ }
50
+
51
+ module.exports = Users;
@@ -14,6 +14,8 @@ const NODE_CLIENT_ROUTE_COVERAGE = [
14
14
  { path: '/stats', methods: ['GET'], status: 'supported', client: 'system.stats' },
15
15
  { path: '/metrics', methods: ['GET'], status: 'supported', client: 'system.metrics' },
16
16
  { path: '/metrics.json', methods: ['GET'], status: 'supported', client: 'system.metricsJson' },
17
+ { path: '/metrics/history', methods: ['GET'], status: 'supported', client: 'system.metricsHistory' },
18
+ { path: '/metrics-history', methods: ['GET'], status: 'supported', client: 'system.metricsHistoryAlias' },
17
19
  { path: '/connections', methods: ['GET'], status: 'supported', client: 'system.connections' },
18
20
  { path: '/rocksdb', methods: ['GET'], status: 'supported', client: 'system.rocksdb' },
19
21
  { path: '/_rocksdb', methods: ['GET'], status: 'supported', client: 'system.rocksdbInternal' },
@@ -24,9 +26,6 @@ const NODE_CLIENT_ROUTE_COVERAGE = [
24
26
  { path: '/consistency', methods: ['GET'], status: 'supported', client: 'system.consistency' },
25
27
  { path: '/self-check', methods: ['GET'], status: 'supported', client: 'system.selfCheck' },
26
28
  { path: '/admin/storage_status', methods: ['GET'], status: 'supported', client: 'system.storageStatus' },
27
- { path: '/cluster/health', methods: ['GET'], status: 'supported', client: 'client.clusterHealth' },
28
- { path: '/cluster/stats', methods: ['GET'], status: 'supported', client: 'client.clusterStats' },
29
- { path: '/cluster/nodes', methods: ['GET'], status: 'supported', client: 'client.clusterNodes' },
30
29
  { path: '/links', methods: ['GET'], status: 'supported', client: 'client.links' },
31
30
  { path: '/links/ping', methods: ['GET'], status: 'supported', client: 'client.linksPing' },
32
31
  { path: '/links/connect', methods: ['POST'], status: 'supported', client: 'client.linksConnect' },
@@ -35,24 +34,22 @@ const NODE_CLIENT_ROUTE_COVERAGE = [
35
34
  { path: '/collections/distributed', methods: ['GET'], status: 'supported', client: 'client.listCollectionsDistributed' },
36
35
  { path: '/collections', methods: ['GET', 'POST'], status: 'supported', client: 'collections.list/create' },
37
36
  { path: '/collections/{name}', methods: ['GET', 'DELETE'], status: 'supported', client: 'collections.get/delete' },
37
+ { path: '/collections/{name}/lang', methods: ['GET'], status: 'supported', client: 'collections.language' },
38
+ { path: '/collections/{name}/aliases', methods: ['GET'], status: 'supported', client: 'aliases.listCollection' },
38
39
  { path: '/collections/{name}/update', methods: ['POST'], status: 'supported', client: 'collections.update' },
39
40
  { path: '/collections/{name}/documents', methods: ['GET', 'POST', 'DELETE'], status: 'supported', client: 'documents.list/add/deleteByFilter' },
40
41
  { path: '/collections/{name}/documents/import', methods: ['POST'], status: 'supported', client: 'documents.import' },
41
42
  { path: '/collections/{name}/documents/{id}', methods: ['GET', 'PUT', 'DELETE'], status: 'supported', client: 'documents.get/update/delete' },
43
+ { path: '/collections/{name}/documents/{id}/context', methods: ['GET'], status: 'supported', client: 'documents.context' },
44
+ { path: '/collections/{name}/documents/maybe', methods: ['GET', 'POST'], status: 'supported', client: 'documents.maybe' },
45
+ { path: '/collections/{name}/documents/_update_by_query', methods: ['POST'], status: 'supported', client: 'documents.updateByQuery' },
46
+ { path: '/collections/{name}/documents/_delete_by_query', methods: ['POST'], status: 'supported', client: 'documents.deleteByQuery' },
42
47
  { path: '/collections/{name}/search', methods: ['GET', 'POST'], status: 'supported', client: 'search.search' },
43
48
  { path: '/collections/{name}/documents/search', methods: ['GET', 'POST'], status: 'supported', client: 'search.searchLegacy' },
44
49
  { path: '/collections/{name}/vector_search', methods: ['GET', 'POST'], status: 'supported', client: 'search.vectorSearch' },
45
50
  { path: '/multi_search', methods: ['GET', 'POST'], status: 'supported', client: 'search.multiSearch' },
46
51
  { path: '/search', methods: ['GET', 'POST'], status: 'supported', client: 'search.globalSearch' },
47
52
  { path: '/sql', methods: ['GET', 'POST'], status: 'supported', client: 'system.sql/execSql' },
48
- { path: '/sam/rebuild', methods: ['POST'], status: 'supported', client: 'sam.rebuild' },
49
- { path: '/sam/search', methods: ['GET'], status: 'supported', client: 'sam.search' },
50
- { path: '/sam/status', methods: ['GET'], status: 'supported', client: 'sam.status' },
51
- { path: '/sam/debug', methods: ['GET'], status: 'supported', client: 'sam.debug' },
52
- { path: '/sam/history', methods: ['GET'], status: 'supported', client: 'sam.history' },
53
- { path: '/sam/pause', methods: ['POST'], status: 'supported', client: 'sam.pause/clearPause' },
54
- { path: '/sam/documents', methods: ['GET'], status: 'supported', client: 'sam.listDocuments' },
55
- { path: '/sam/documents/{collection}/{id}', methods: ['GET'], status: 'supported', client: 'sam.getDocument/openDocument' },
56
53
  { path: '/collections/{name}/documents/facet_counts', methods: ['GET', 'POST'], status: 'supported', client: 'documents.facetCounts' },
57
54
  { path: '/collections/{name}/documents/export', methods: ['GET', 'POST'], status: 'supported', client: 'documents.export' },
58
55
  { path: '/collections/{name}/synonyms', methods: ['GET'], status: 'supported', client: 'synonyms.list' },
@@ -71,15 +68,24 @@ const NODE_CLIENT_ROUTE_COVERAGE = [
71
68
  { path: '/aliases/{name}', methods: ['GET', 'POST', 'PUT', 'DELETE'], status: 'supported', client: 'aliases.get/create/update/delete' },
72
69
  { path: '/keys', methods: ['GET', 'POST'], status: 'supported', client: 'keys.list/create' },
73
70
  { path: '/keys/{id}', methods: ['GET', 'PUT', 'DELETE'], status: 'supported', client: 'keys.get/update/delete' },
74
- { path: '/update-counters', methods: ['GET', 'POST'], status: 'omitted', reason: 'Internal maintenance endpoint; intentionally left off the public Node client.' },
71
+ { path: '/users', methods: ['GET', 'POST'], status: 'supported', client: 'users.list/create' },
72
+ { path: '/users/{name}', methods: ['GET', 'PUT', 'DELETE'], status: 'supported', client: 'users.get/update/delete' },
73
+ { path: '/analytics/click', methods: ['POST'], status: 'supported', client: 'analytics.click' },
74
+ { path: '/update-counters', methods: ['GET', 'POST'], status: 'supported', client: 'system.updateCounters' },
75
+ { path: '/debug/counters', methods: ['GET'], status: 'supported', client: 'system.debugCounters' },
75
76
  { path: '/query', methods: ['GET'], status: 'omitted', reason: 'Alias of /status; client exposes status() instead of duplicate naming.' },
76
- { path: '/search-config', methods: ['GET'], status: 'omitted', reason: 'Diagnostic configuration endpoint; stable client exposes raw executeRequest() for this route.' },
77
- { path: '/ready', methods: ['GET'], status: 'omitted', reason: 'Readiness alias; client exposes startup() and bootStatus() instead of duplicate naming.' },
78
- { path: '/llm', methods: ['GET', 'POST'], status: 'omitted', reason: 'LLM runtime endpoint is module/config dependent and not part of the stable SDK surface yet.' },
79
- { path: '/repair', methods: ['POST'], status: 'omitted', reason: 'Administrative repair endpoint; intentionally left off the public Node client.' },
80
- { path: '/modules', methods: ['GET'], status: 'omitted', reason: 'Module APIs are intentionally omitted until the module surface is documented and stabilized for SDKs.' },
81
- { path: '/modules/{name}', methods: ['ANY'], status: 'omitted', reason: 'Module-specific routes are dynamic and intentionally not wrapped in the stable Node client.' },
82
- { path: '/modules/{name}/syntax', methods: ['GET'], status: 'omitted', reason: 'Module-specific routes are dynamic and intentionally not wrapped in the stable Node client.' }
77
+ { path: '/search-config', methods: ['GET'], status: 'supported', client: 'system.searchConfig' },
78
+ { path: '/ready', methods: ['GET'], status: 'supported', client: 'system.ready' },
79
+ { path: '/repair', methods: ['GET', 'POST'], status: 'supported', client: 'system.repair' },
80
+ { path: '/modules', methods: ['GET'], status: 'supported', client: 'modules.list' },
81
+ { path: '/modules/load', methods: ['POST'], status: 'omitted', reason: 'Prefix alias; modules.load(name) uses the explicit /loadmodule/{name} form.' },
82
+ { path: '/modules/unload', methods: ['POST'], status: 'omitted', reason: 'Prefix alias; modules.unload(name) uses the explicit /unloadmodule/{name} form.' },
83
+ { path: '/loadmodule', methods: ['POST'], status: 'omitted', reason: 'Body-based alias; modules.load(name) uses the explicit /loadmodule/{name} form.' },
84
+ { path: '/loadmodule/{name}', methods: ['POST'], status: 'supported', client: 'modules.load' },
85
+ { path: '/unloadmodule', methods: ['POST'], status: 'omitted', reason: 'Body-based alias; modules.unload(name) uses the explicit /unloadmodule/{name} form.' },
86
+ { path: '/unloadmodule/{name}', methods: ['POST'], status: 'supported', client: 'modules.unload' },
87
+ { path: '/modules/{name}', methods: ['ANY'], status: 'supported', client: 'modules.call' },
88
+ { path: '/modules/{name}/syntax', methods: ['GET'], status: 'supported', client: 'modules.syntax' }
83
89
  ];
84
90
 
85
91
  module.exports = {
package/package.json CHANGED
@@ -1,34 +1,23 @@
1
1
  {
2
2
  "name": "hlquery-node-client",
3
- "version": "1.0.5",
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/utils/Config.js CHANGED
@@ -23,6 +23,14 @@ class Config {
23
23
  timeout: options.timeout || 30000,
24
24
  token: options.token || null,
25
25
  auth_method: options.auth_method || 'bearer',
26
+ agent: options.agent || null,
27
+ http_agent: options.http_agent || null,
28
+ https_agent: options.https_agent || null,
29
+ headers: options.headers || {},
30
+ max_response_bytes: options.max_response_bytes || 0,
31
+ throw_on_error: options.throw_on_error || false,
32
+ query_array_format: options.query_array_format || 'comma',
33
+ signal: options.signal || null,
26
34
  ...options
27
35
  };
28
36
  }
@@ -34,11 +42,17 @@ class Config {
34
42
  if (!url) {
35
43
  return Config.getDefaultBaseUrl();
36
44
  }
45
+ if (url instanceof URL) {
46
+ url = url.toString();
47
+ }
48
+ if (typeof url !== 'string') {
49
+ url = String(url);
50
+ }
37
51
  url = url.trim();
38
52
  if (!url.startsWith('http://') && !url.startsWith('https://')) {
39
53
  url = 'http://' + url;
40
54
  }
41
- return url.replace(/\/$/, '');
55
+ return url.replace(/\/+$/, '');
42
56
  }
43
57
 
44
58
  /**
@@ -47,7 +61,8 @@ class Config {
47
61
  static isValidUrl(url) {
48
62
  try {
49
63
  const urlObj = new URL(url);
50
- return urlObj.protocol === 'http:' || urlObj.protocol === 'https:';
64
+ return (urlObj.protocol === 'http:' || urlObj.protocol === 'https:') &&
65
+ Boolean(urlObj.hostname);
51
66
  } catch (e) {
52
67
  return false;
53
68
  }
@@ -56,12 +56,12 @@ class Validator {
56
56
  throw new ValidationException('Document ID must be between 1 and 64 characters');
57
57
  }
58
58
 
59
- // Document IDs should be URL-safe: alphanumeric, underscores, and hyphens only
60
- if (!/^[a-zA-Z0-9_-]+$/.test(idStr)) {
61
- throw new ValidationException(
62
- 'Document ID contains invalid characters. ' +
63
- 'Use only letters, numbers, underscores, and hyphens'
64
- );
59
+ if (idStr.trim() === '') {
60
+ throw new ValidationException('Document ID must be a non-empty string or number');
61
+ }
62
+
63
+ if (/[\x00-\x1F\x7F]/.test(idStr)) {
64
+ throw new ValidationException('Document ID contains control characters');
65
65
  }
66
66
  }
67
67
 
@@ -100,8 +100,7 @@ class Validator {
100
100
  }
101
101
 
102
102
  /**
103
- * Validate document field values for invalid characters
104
- * Commas are not allowed in string field values as they're reserved for internal parsing
103
+ * Validate document field values.
105
104
  *
106
105
  * @param {object} document - Document object to validate
107
106
  * @throws {ValidationException} If document contains invalid characters
@@ -111,28 +110,12 @@ class Validator {
111
110
  return; // Skip validation for non-objects or arrays (will be validated per-item)
112
111
  }
113
112
 
114
- for (const [key, value] of Object.entries(document)) {
115
- // Skip the 'id' field as it has its own validation
116
- if (key === 'id') continue;
117
-
118
- // Check string values for commas
119
- if (typeof value === 'string' && value.includes(',')) {
120
- throw new ValidationException(
121
- `Field '${key}' contains invalid character: comma (,). ` +
122
- `Commas are not allowed in field values. Use underscores (_) or spaces instead, or use arrays for multiple values.`
123
- );
113
+ for (const key of Object.keys(document)) {
114
+ if (key === '') {
115
+ throw new ValidationException('Document field names must be non-empty strings');
124
116
  }
125
-
126
- // Check array values - ensure they don't contain strings with commas
127
- if (Array.isArray(value)) {
128
- for (const item of value) {
129
- if (typeof item === 'string' && item.includes(',')) {
130
- throw new ValidationException(
131
- `Field '${key}' contains invalid character: comma (,). ` +
132
- `Array items cannot contain commas. Use underscores (_) or spaces instead.`
133
- );
134
- }
135
- }
117
+ if (/[\x00-\x1F\x7F]/.test(key)) {
118
+ throw new ValidationException(`Field '${key}' contains control characters`);
136
119
  }
137
120
  }
138
121
  }
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,40 +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
-
29
- // Export exceptions
30
- module.exports.Exceptions = require('./lib/Exceptions');
31
-
32
- // Export utilities
33
- module.exports.Utils = {
34
- Config: require('./utils/Config'),
35
- Validator: require('./utils/Validator'),
36
- Auth: require('./utils/Auth'),
37
- Ranker: require('./utils/ranker'),
38
- PDFParser: require('./utils/PDFParser'),
39
- CSVParser: require('./utils/CSVParser')
40
- };