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 +84 -45
- package/lib/Aliases.js +5 -0
- package/lib/Analytics.js +30 -0
- package/lib/Client.js +109 -61
- package/lib/Collections.js +36 -3
- package/lib/Documents.js +98 -6
- package/lib/Exceptions.js +11 -3
- package/lib/Modules.js +64 -0
- package/lib/Request.js +157 -15
- package/lib/Response.js +28 -2
- package/lib/Route.js +111 -0
- package/lib/Search.js +21 -9
- package/lib/System.js +38 -0
- package/lib/Users.js +51 -0
- package/lib/conformance.js +25 -19
- package/package.json +9 -20
- package/test/route-conformance.test.js +83 -0
- package/utils/Config.js +17 -2
- package/utils/Validator.js +12 -29
- package/examples/sam.js +0 -39
- package/index.js +0 -40
- package/lib/SAM.js +0 -174
- package/test/offline-smoke.js +0 -194
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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;
|
package/lib/conformance.js
CHANGED
|
@@ -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: '/
|
|
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: '
|
|
77
|
-
{ path: '/ready', methods: ['GET'], status: '
|
|
78
|
-
{ path: '/
|
|
79
|
-
{ path: '/
|
|
80
|
-
{ path: '/modules', methods: ['
|
|
81
|
-
{ path: '/modules/
|
|
82
|
-
{ path: '/
|
|
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.
|
|
4
|
-
"description": "Node.js client
|
|
5
|
-
"main": "
|
|
6
|
-
"type": "commonjs",
|
|
3
|
+
"version": "1.0.7",
|
|
4
|
+
"description": "Node.js client for hlquery",
|
|
5
|
+
"main": "lib/Client.js",
|
|
7
6
|
"scripts": {
|
|
8
|
-
"test": "node
|
|
9
|
-
"test:
|
|
10
|
-
"test:offline": "node test/offline-smoke.js",
|
|
11
|
-
"example": "node example.js",
|
|
12
|
-
"example:sql": "node examples/sql.js",
|
|
13
|
-
"example:pdf": "node examples/pdf.js",
|
|
14
|
-
"example:csv": "node examples/csv.js"
|
|
15
|
-
},
|
|
16
|
-
"dependencies": {
|
|
17
|
-
"pdf-parse": "^2.4.5"
|
|
7
|
+
"test": "node test/route-conformance.test.js",
|
|
8
|
+
"test:offline": "npm test"
|
|
18
9
|
},
|
|
19
10
|
"keywords": [
|
|
20
11
|
"hlquery",
|
|
21
12
|
"search",
|
|
22
|
-
"client"
|
|
23
|
-
"api"
|
|
13
|
+
"client"
|
|
24
14
|
],
|
|
25
15
|
"author": "Carlos F. Ferry <carlos.ferry@gmail.com>",
|
|
26
16
|
"license": "BSD-3-Clause",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"pdf-parse": "^2.4.5"
|
|
19
|
+
},
|
|
27
20
|
"engines": {
|
|
28
21
|
"node": ">=12.0.0"
|
|
29
|
-
},
|
|
30
|
-
"repository": {
|
|
31
|
-
"type": "git",
|
|
32
|
-
"url": "https://github.com/cferry/hlquery"
|
|
33
22
|
}
|
|
34
23
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('assert');
|
|
4
|
+
const Request = require('../lib/Request');
|
|
5
|
+
const Search = require('../lib/Search');
|
|
6
|
+
const System = require('../lib/System');
|
|
7
|
+
const { NODE_CLIENT_ROUTE_COVERAGE } = require('../lib/conformance');
|
|
8
|
+
|
|
9
|
+
class RecordingRequest {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.calls = [];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async execute(method, path, body = null, query = {}) {
|
|
15
|
+
const call = { method, path, body, query };
|
|
16
|
+
this.calls.push(call);
|
|
17
|
+
return call;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
last() {
|
|
21
|
+
return this.calls[this.calls.length - 1];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function route(path) {
|
|
26
|
+
const matches = NODE_CLIENT_ROUTE_COVERAGE.filter(entry => entry.path === path);
|
|
27
|
+
assert.strictEqual(matches.length, 1, `${path} must have exactly one conformance entry`);
|
|
28
|
+
return matches[0];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function main() {
|
|
32
|
+
const routeKeys = new Set();
|
|
33
|
+
for (const entry of NODE_CLIENT_ROUTE_COVERAGE) {
|
|
34
|
+
assert.ok(entry.path.startsWith('/'), `route must be absolute-path scoped: ${entry.path}`);
|
|
35
|
+
assert.ok(Array.isArray(entry.methods) && entry.methods.length > 0, `route must declare methods: ${entry.path}`);
|
|
36
|
+
const key = `${entry.path}:${entry.methods.join(',')}`;
|
|
37
|
+
assert.ok(!routeKeys.has(key), `duplicate conformance route: ${key}`);
|
|
38
|
+
routeKeys.add(key);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const recorder = new RecordingRequest();
|
|
42
|
+
const search = new Search(recorder, null);
|
|
43
|
+
const system = new System(recorder);
|
|
44
|
+
|
|
45
|
+
assert.deepStrictEqual(route('/multi_search').methods, ['GET', 'POST']);
|
|
46
|
+
await search.multiSearch([], 'GET');
|
|
47
|
+
assert.deepStrictEqual(recorder.last(), {
|
|
48
|
+
method: 'GET', path: '/multi_search', body: { searches: [] }, query: {}
|
|
49
|
+
});
|
|
50
|
+
await search.multiSearch([], 'POST');
|
|
51
|
+
assert.strictEqual(recorder.last().method, 'POST');
|
|
52
|
+
|
|
53
|
+
for (const [path, invoke] of [
|
|
54
|
+
['/update-counters', method => system.updateCounters({ force: 1 }, method)],
|
|
55
|
+
['/repair', method => system.repair({ collection: 'books' }, method)]
|
|
56
|
+
]) {
|
|
57
|
+
assert.deepStrictEqual(route(path).methods, ['GET', 'POST']);
|
|
58
|
+
await invoke('GET');
|
|
59
|
+
assert.strictEqual(recorder.last().method, 'GET');
|
|
60
|
+
assert.strictEqual(recorder.last().path, path);
|
|
61
|
+
await invoke('POST');
|
|
62
|
+
assert.strictEqual(recorder.last().method, 'POST');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const request = new Request('https://search.example.test', {
|
|
66
|
+
token: 'secret-token'
|
|
67
|
+
});
|
|
68
|
+
await assert.rejects(
|
|
69
|
+
request.execute('GET', 'https://attacker.example.test/collect'),
|
|
70
|
+
error => error && error.code === 'CROSS_ORIGIN_REQUEST'
|
|
71
|
+
);
|
|
72
|
+
await assert.rejects(
|
|
73
|
+
request.execute('GET', '//attacker.example.test/collect'),
|
|
74
|
+
error => error && error.code === 'CROSS_ORIGIN_REQUEST'
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
console.log(`route conformance passed (${NODE_CLIENT_ROUTE_COVERAGE.length} manifest entries)`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
main().catch(error => {
|
|
81
|
+
console.error(error);
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
});
|
package/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
|
}
|
package/utils/Validator.js
CHANGED
|
@@ -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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
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
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
};
|