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/Documents.js
CHANGED
|
@@ -18,6 +18,13 @@ class Documents {
|
|
|
18
18
|
constructor(request) {
|
|
19
19
|
this.request = request;
|
|
20
20
|
}
|
|
21
|
+
|
|
22
|
+
_responseFromRequestError(error) {
|
|
23
|
+
if (error && error.response) {
|
|
24
|
+
return error.response;
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
21
28
|
|
|
22
29
|
/**
|
|
23
30
|
* List documents in a collection
|
|
@@ -52,6 +59,20 @@ class Documents {
|
|
|
52
59
|
|
|
53
60
|
return await this.request.execute('GET', `/collections/${encodeURIComponent(collectionName)}/documents/${encodeURIComponent(documentId)}`);
|
|
54
61
|
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get alternate contextual phrases for a document.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} collectionName
|
|
67
|
+
* @param {string} documentId
|
|
68
|
+
* @returns {Promise<Response>}
|
|
69
|
+
*/
|
|
70
|
+
async context(collectionName, documentId) {
|
|
71
|
+
Validator.validateCollectionName(collectionName);
|
|
72
|
+
Validator.validateDocumentId(documentId);
|
|
73
|
+
|
|
74
|
+
return await this.request.execute('GET', `/collections/${encodeURIComponent(collectionName)}/documents/${encodeURIComponent(documentId)}/context`);
|
|
75
|
+
}
|
|
55
76
|
|
|
56
77
|
/**
|
|
57
78
|
* Add a document
|
|
@@ -177,14 +198,24 @@ class Documents {
|
|
|
177
198
|
}
|
|
178
199
|
|
|
179
200
|
// Server expects {documents: [...]} format; fall back to per-doc inserts if bulk route is unavailable.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
201
|
+
let bulkResponse;
|
|
202
|
+
try {
|
|
203
|
+
bulkResponse = await this.request.execute('POST', `/collections/${encodeURIComponent(collectionName)}/documents/import`, {
|
|
204
|
+
documents: documents
|
|
205
|
+
});
|
|
206
|
+
} catch (error) {
|
|
207
|
+
bulkResponse = this._responseFromRequestError(error);
|
|
208
|
+
}
|
|
183
209
|
|
|
184
210
|
if (bulkResponse && bulkResponse.getStatusCode && bulkResponse.getStatusCode() === 404) {
|
|
185
211
|
let inserted = 0;
|
|
186
212
|
for (const doc of documents) {
|
|
187
|
-
|
|
213
|
+
let response;
|
|
214
|
+
try {
|
|
215
|
+
response = await this.request.execute('POST', `/collections/${encodeURIComponent(collectionName)}/documents`, doc);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
response = this._responseFromRequestError(error);
|
|
218
|
+
}
|
|
188
219
|
const status = response.getStatusCode();
|
|
189
220
|
if (status !== 201) {
|
|
190
221
|
return response;
|
|
@@ -215,7 +246,12 @@ class Documents {
|
|
|
215
246
|
throw new Error('Source and target document ids must differ');
|
|
216
247
|
}
|
|
217
248
|
|
|
218
|
-
|
|
249
|
+
let targetCheck;
|
|
250
|
+
try {
|
|
251
|
+
targetCheck = await this.get(collectionName, targetId);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
targetCheck = this._responseFromRequestError(error);
|
|
254
|
+
}
|
|
219
255
|
if (targetCheck.getStatusCode() === 200) {
|
|
220
256
|
return targetCheck;
|
|
221
257
|
}
|
|
@@ -223,7 +259,12 @@ class Documents {
|
|
|
223
259
|
return targetCheck;
|
|
224
260
|
}
|
|
225
261
|
|
|
226
|
-
|
|
262
|
+
let sourceResponse;
|
|
263
|
+
try {
|
|
264
|
+
sourceResponse = await this.get(collectionName, sourceId);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
sourceResponse = this._responseFromRequestError(error);
|
|
267
|
+
}
|
|
227
268
|
if (sourceResponse.getStatusCode() !== 200) {
|
|
228
269
|
return sourceResponse;
|
|
229
270
|
}
|
|
@@ -302,6 +343,57 @@ class Documents {
|
|
|
302
343
|
queryParams
|
|
303
344
|
);
|
|
304
345
|
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Request query suggestions for a collection.
|
|
349
|
+
*
|
|
350
|
+
* @param {string} collectionName
|
|
351
|
+
* @param {object} params
|
|
352
|
+
* @returns {Promise<Response>}
|
|
353
|
+
*/
|
|
354
|
+
async maybe(collectionName, params = {}) {
|
|
355
|
+
Validator.validateCollectionName(collectionName);
|
|
356
|
+
Validator.validateSearchParams(params);
|
|
357
|
+
|
|
358
|
+
const method = params.body ? 'POST' : 'GET';
|
|
359
|
+
const body = params.body || (method === 'POST' ? params : null);
|
|
360
|
+
const queryParams = method === 'GET' ? params : {};
|
|
361
|
+
|
|
362
|
+
return await this.request.execute(
|
|
363
|
+
method,
|
|
364
|
+
`/collections/${encodeURIComponent(collectionName)}/documents/maybe`,
|
|
365
|
+
body,
|
|
366
|
+
queryParams
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Update documents matching a query.
|
|
372
|
+
*
|
|
373
|
+
* @param {string} collectionName
|
|
374
|
+
* @param {object} body
|
|
375
|
+
* @returns {Promise<Response>}
|
|
376
|
+
*/
|
|
377
|
+
async updateByQuery(collectionName, body = {}) {
|
|
378
|
+
Validator.validateCollectionName(collectionName);
|
|
379
|
+
Validator.validateDocumentFields(body);
|
|
380
|
+
|
|
381
|
+
return await this.request.execute('POST', `/collections/${encodeURIComponent(collectionName)}/documents/_update_by_query`, body);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Delete documents matching a query.
|
|
386
|
+
*
|
|
387
|
+
* @param {string} collectionName
|
|
388
|
+
* @param {object} body
|
|
389
|
+
* @returns {Promise<Response>}
|
|
390
|
+
*/
|
|
391
|
+
async deleteByQuery(collectionName, body = {}) {
|
|
392
|
+
Validator.validateCollectionName(collectionName);
|
|
393
|
+
Validator.validateDocumentFields(body);
|
|
394
|
+
|
|
395
|
+
return await this.request.execute('POST', `/collections/${encodeURIComponent(collectionName)}/documents/_delete_by_query`, body);
|
|
396
|
+
}
|
|
305
397
|
|
|
306
398
|
/**
|
|
307
399
|
* Delete documents by filter
|
package/lib/Exceptions.js
CHANGED
|
@@ -10,9 +10,12 @@
|
|
|
10
10
|
* Base exception class for hlquery client
|
|
11
11
|
*/
|
|
12
12
|
class HlqueryException extends Error {
|
|
13
|
-
constructor(message) {
|
|
13
|
+
constructor(message, options = {}) {
|
|
14
14
|
super(message);
|
|
15
15
|
this.name = 'HlqueryException';
|
|
16
|
+
if (options.cause) {
|
|
17
|
+
this.cause = options.cause;
|
|
18
|
+
}
|
|
16
19
|
Error.captureStackTrace(this, this.constructor);
|
|
17
20
|
}
|
|
18
21
|
}
|
|
@@ -31,11 +34,16 @@ class AuthenticationException extends HlqueryException {
|
|
|
31
34
|
* Exception thrown when a request fails
|
|
32
35
|
*/
|
|
33
36
|
class RequestException extends HlqueryException {
|
|
34
|
-
constructor(message, statusCode = 0, responseBody = null) {
|
|
35
|
-
super(message);
|
|
37
|
+
constructor(message, statusCode = 0, responseBody = null, options = {}) {
|
|
38
|
+
super(message, options);
|
|
36
39
|
this.name = 'RequestException';
|
|
37
40
|
this.statusCode = statusCode;
|
|
38
41
|
this.responseBody = responseBody;
|
|
42
|
+
this.response = options.response || null;
|
|
43
|
+
this.headers = options.headers || {};
|
|
44
|
+
this.rawBody = options.rawBody || null;
|
|
45
|
+
this.retryable = Boolean(options.retryable);
|
|
46
|
+
this.code = options.code || null;
|
|
39
47
|
}
|
|
40
48
|
}
|
|
41
49
|
|
package/lib/Modules.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hlquery Node.js Client - Runtime Modules 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 { ModuleRouteCommand, cleanRoute } = require('./Route');
|
|
10
|
+
|
|
11
|
+
function requireNonEmptyString(value, label) {
|
|
12
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
13
|
+
throw new Error(`${label} must be a non-empty string`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Runtime module discovery and dynamic route calls.
|
|
19
|
+
*/
|
|
20
|
+
class Modules {
|
|
21
|
+
constructor(request) {
|
|
22
|
+
this.request = request;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
list() {
|
|
26
|
+
return this.request.execute('GET', '/modules');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
load(name) {
|
|
30
|
+
requireNonEmptyString(name, 'Module name');
|
|
31
|
+
return this.request.execute('POST', `/loadmodule/${encodeURIComponent(name)}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
unload(name) {
|
|
35
|
+
requireNonEmptyString(name, 'Module name');
|
|
36
|
+
return this.request.execute('POST', `/unloadmodule/${encodeURIComponent(name)}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
syntax(name, queryParams = {}) {
|
|
40
|
+
return this.module(name).syntax(queryParams);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module(name) {
|
|
44
|
+
return new ModuleRouteCommand(this.request, name);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
route(name, route = '') {
|
|
48
|
+
return this.module(name).route(route);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
call(name, route = '', method = 'GET', body = null, queryParams = {}, options = {}) {
|
|
52
|
+
requireNonEmptyString(name, 'Module name');
|
|
53
|
+
requireNonEmptyString(method, 'HTTP method');
|
|
54
|
+
|
|
55
|
+
const suffix = cleanRoute(route);
|
|
56
|
+
const path = suffix
|
|
57
|
+
? `/modules/${encodeURIComponent(name)}/${suffix}`
|
|
58
|
+
: `/modules/${encodeURIComponent(name)}`;
|
|
59
|
+
|
|
60
|
+
return this.request.execute(method.toUpperCase(), path, body, queryParams, options);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = Modules;
|
package/lib/Request.js
CHANGED
|
@@ -16,11 +16,26 @@ const Response = require('./Response');
|
|
|
16
16
|
* HTTP request handler
|
|
17
17
|
*/
|
|
18
18
|
class Request {
|
|
19
|
-
constructor(baseUrl, timeout = 30000, authToken = null, authMethod = 'bearer') {
|
|
20
|
-
|
|
19
|
+
constructor(baseUrl, timeout = 30000, authToken = null, authMethod = 'bearer', options = {}) {
|
|
20
|
+
if (timeout && typeof timeout === 'object') {
|
|
21
|
+
options = timeout;
|
|
22
|
+
timeout = options.timeout || 30000;
|
|
23
|
+
authToken = options.token || null;
|
|
24
|
+
authMethod = options.auth_method || options.authMethod || 'bearer';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
this.baseUrl = baseUrl.replace(/\/+$/, '');
|
|
21
28
|
this.timeout = timeout;
|
|
22
29
|
this.authToken = authToken;
|
|
23
30
|
this.authMethod = authMethod;
|
|
31
|
+
this.agent = options.agent || null;
|
|
32
|
+
this.httpAgent = options.http_agent || options.httpAgent || null;
|
|
33
|
+
this.httpsAgent = options.https_agent || options.httpsAgent || null;
|
|
34
|
+
this.defaultHeaders = options.headers || {};
|
|
35
|
+
this.maxResponseBytes = options.max_response_bytes || options.maxResponseBytes || 0;
|
|
36
|
+
this.throwOnError = Boolean(options.throw_on_error || options.throwOnError);
|
|
37
|
+
this.queryArrayFormat = options.query_array_format || options.queryArrayFormat || 'comma';
|
|
38
|
+
this.signal = options.signal || null;
|
|
24
39
|
}
|
|
25
40
|
|
|
26
41
|
setAuthToken(token, method = 'bearer') {
|
|
@@ -42,8 +57,28 @@ class Request {
|
|
|
42
57
|
* @returns {Promise<Response>}
|
|
43
58
|
* @throws {RequestException}
|
|
44
59
|
*/
|
|
45
|
-
async execute(method, path, body = null, queryParams = {}) {
|
|
46
|
-
const
|
|
60
|
+
async execute(method, path, body = null, queryParams = {}, requestOptions = {}) {
|
|
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
|
+
}
|
|
69
|
+
const signal = requestOptions.signal || this.signal;
|
|
70
|
+
const maxResponseBytes = requestOptions.max_response_bytes || requestOptions.maxResponseBytes || this.maxResponseBytes;
|
|
71
|
+
const throwOnError = requestOptions.throw_on_error !== undefined
|
|
72
|
+
? requestOptions.throw_on_error
|
|
73
|
+
: (requestOptions.throwOnError !== undefined ? requestOptions.throwOnError : this.throwOnError);
|
|
74
|
+
const queryArrayFormat = requestOptions.query_array_format || requestOptions.queryArrayFormat || this.queryArrayFormat;
|
|
75
|
+
|
|
76
|
+
if (signal && signal.aborted) {
|
|
77
|
+
throw new RequestException('Request aborted', 0, null, {
|
|
78
|
+
code: 'ABORT_ERR',
|
|
79
|
+
retryable: false
|
|
80
|
+
});
|
|
81
|
+
}
|
|
47
82
|
|
|
48
83
|
// Add query parameters
|
|
49
84
|
Object.keys(queryParams).forEach(key => {
|
|
@@ -53,7 +88,11 @@ class Request {
|
|
|
53
88
|
}
|
|
54
89
|
|
|
55
90
|
if (Array.isArray(value)) {
|
|
56
|
-
|
|
91
|
+
if (queryArrayFormat === 'repeat' || queryArrayFormat === 'repeated') {
|
|
92
|
+
value.forEach(item => url.searchParams.append(key, item));
|
|
93
|
+
} else {
|
|
94
|
+
url.searchParams.append(key, value.join(','));
|
|
95
|
+
}
|
|
57
96
|
return;
|
|
58
97
|
}
|
|
59
98
|
|
|
@@ -66,7 +105,9 @@ class Request {
|
|
|
66
105
|
});
|
|
67
106
|
|
|
68
107
|
const headers = {
|
|
69
|
-
'Accept': 'application/json'
|
|
108
|
+
'Accept': 'application/json',
|
|
109
|
+
...this.defaultHeaders,
|
|
110
|
+
...(requestOptions.headers || {})
|
|
70
111
|
};
|
|
71
112
|
|
|
72
113
|
// Prepare body string if present
|
|
@@ -89,32 +130,112 @@ class Request {
|
|
|
89
130
|
const options = {
|
|
90
131
|
method: method,
|
|
91
132
|
headers: headers,
|
|
92
|
-
timeout: this.timeout
|
|
133
|
+
timeout: requestOptions.timeout || this.timeout
|
|
93
134
|
};
|
|
94
135
|
|
|
95
136
|
const client = url.protocol === 'https:' ? https : http;
|
|
137
|
+
const agent = this.agent || (url.protocol === 'https:' ? this.httpsAgent : this.httpAgent);
|
|
138
|
+
if (agent) {
|
|
139
|
+
options.agent = agent;
|
|
140
|
+
}
|
|
96
141
|
|
|
97
142
|
return new Promise((resolve, reject) => {
|
|
98
|
-
|
|
143
|
+
let settled = false;
|
|
144
|
+
let req = null;
|
|
145
|
+
const rejectOnce = (error) => {
|
|
146
|
+
if (settled) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
settled = true;
|
|
150
|
+
if (signal && abortHandler && typeof signal.removeEventListener === 'function') {
|
|
151
|
+
signal.removeEventListener('abort', abortHandler);
|
|
152
|
+
} else if (signal && abortHandler && typeof signal.removeListener === 'function') {
|
|
153
|
+
signal.removeListener('abort', abortHandler);
|
|
154
|
+
}
|
|
155
|
+
reject(error);
|
|
156
|
+
};
|
|
157
|
+
const resolveOnce = (response) => {
|
|
158
|
+
if (settled) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
settled = true;
|
|
162
|
+
if (signal && abortHandler && typeof signal.removeEventListener === 'function') {
|
|
163
|
+
signal.removeEventListener('abort', abortHandler);
|
|
164
|
+
} else if (signal && abortHandler && typeof signal.removeListener === 'function') {
|
|
165
|
+
signal.removeListener('abort', abortHandler);
|
|
166
|
+
}
|
|
167
|
+
resolve(response);
|
|
168
|
+
};
|
|
169
|
+
const abortHandler = () => {
|
|
170
|
+
if (req) {
|
|
171
|
+
req.destroy();
|
|
172
|
+
}
|
|
173
|
+
rejectOnce(new RequestException('Request aborted', 0, null, {
|
|
174
|
+
code: 'ABORT_ERR',
|
|
175
|
+
retryable: false
|
|
176
|
+
}));
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
if (signal && typeof signal.addEventListener === 'function') {
|
|
180
|
+
signal.addEventListener('abort', abortHandler, { once: true });
|
|
181
|
+
} else if (signal && typeof signal.once === 'function') {
|
|
182
|
+
signal.once('abort', abortHandler);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
req = client.request(url, options, (res) => {
|
|
99
186
|
const responseHeaders = {};
|
|
100
187
|
Object.keys(res.headers).forEach(key => {
|
|
101
188
|
responseHeaders[key.toLowerCase()] = res.headers[key];
|
|
102
189
|
});
|
|
103
190
|
|
|
104
|
-
|
|
191
|
+
const chunks = [];
|
|
192
|
+
let receivedBytes = 0;
|
|
105
193
|
|
|
106
194
|
res.on('data', (chunk) => {
|
|
107
|
-
|
|
195
|
+
receivedBytes += chunk.length;
|
|
196
|
+
if (maxResponseBytes > 0 && receivedBytes > maxResponseBytes) {
|
|
197
|
+
res.destroy();
|
|
198
|
+
rejectOnce(new RequestException(
|
|
199
|
+
`Response exceeded maximum size of ${maxResponseBytes} bytes`,
|
|
200
|
+
res.statusCode,
|
|
201
|
+
null,
|
|
202
|
+
{
|
|
203
|
+
headers: responseHeaders,
|
|
204
|
+
retryable: false,
|
|
205
|
+
code: 'MAX_RESPONSE_BYTES'
|
|
206
|
+
}
|
|
207
|
+
));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
chunks.push(chunk);
|
|
108
211
|
});
|
|
109
212
|
|
|
110
213
|
res.on('end', () => {
|
|
214
|
+
if (settled) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const rawBuffer = Buffer.concat(chunks);
|
|
218
|
+
const data = rawBuffer.toString('utf8');
|
|
111
219
|
let decoded;
|
|
220
|
+
let parsed = true;
|
|
112
221
|
try {
|
|
113
222
|
decoded = data ? JSON.parse(data) : null;
|
|
114
223
|
} catch (e) {
|
|
115
224
|
// If JSON parsing fails, return raw response
|
|
116
225
|
decoded = data;
|
|
226
|
+
parsed = false;
|
|
117
227
|
}
|
|
228
|
+
|
|
229
|
+
const response = new Response(res.statusCode, decoded, responseHeaders, {
|
|
230
|
+
statusMessage: res.statusMessage || '',
|
|
231
|
+
rawBody: rawBuffer,
|
|
232
|
+
parsed: parsed,
|
|
233
|
+
request: {
|
|
234
|
+
method: method,
|
|
235
|
+
url: url.toString(),
|
|
236
|
+
path: path
|
|
237
|
+
}
|
|
238
|
+
});
|
|
118
239
|
|
|
119
240
|
// Check for demo mode errors
|
|
120
241
|
if (res.statusCode === 403 && decoded && typeof decoded === 'object') {
|
|
@@ -122,30 +243,51 @@ class Request {
|
|
|
122
243
|
if (errorMsg.includes('Demo mode is enabled') ||
|
|
123
244
|
errorMsg.includes('Operation not allowed') ||
|
|
124
245
|
errorMsg.includes('Write operations are not allowed')) {
|
|
125
|
-
return
|
|
246
|
+
return rejectOnce(new DemoModeException(
|
|
126
247
|
`Demo mode is enabled on the server. Write operations are blocked. Only search and read operations are permitted. Server message: ${decoded.message || decoded.error}`
|
|
127
248
|
));
|
|
128
249
|
}
|
|
129
250
|
// Check if server rejected token because auth is disabled
|
|
130
251
|
if (errorMsg.includes('Authentication is disabled') ||
|
|
131
252
|
errorMsg.includes('Tokens are not accepted when authentication is disabled')) {
|
|
132
|
-
return
|
|
253
|
+
return rejectOnce(new AuthenticationException(
|
|
133
254
|
`Authentication is disabled on the server. Remove the token from your client configuration. Server message: ${decoded.message || decoded.error}`
|
|
134
255
|
));
|
|
135
256
|
}
|
|
136
257
|
}
|
|
258
|
+
|
|
259
|
+
if (throwOnError && response.isError()) {
|
|
260
|
+
const errorMessage = response.getError() ||
|
|
261
|
+
`${res.statusCode} ${res.statusMessage || 'HTTP error'}`;
|
|
262
|
+
return rejectOnce(new RequestException(errorMessage, res.statusCode, decoded, {
|
|
263
|
+
response: response,
|
|
264
|
+
headers: responseHeaders,
|
|
265
|
+
rawBody: rawBuffer,
|
|
266
|
+
retryable: res.statusCode >= 500
|
|
267
|
+
}));
|
|
268
|
+
}
|
|
137
269
|
|
|
138
|
-
|
|
270
|
+
resolveOnce(response);
|
|
139
271
|
});
|
|
140
272
|
});
|
|
141
273
|
|
|
142
274
|
req.on('error', (error) => {
|
|
143
|
-
|
|
275
|
+
if (settled) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
rejectOnce(new RequestException(`Request failed: ${error.message}`, 0, null, {
|
|
279
|
+
cause: error,
|
|
280
|
+
retryable: true,
|
|
281
|
+
code: error.code || null
|
|
282
|
+
}));
|
|
144
283
|
});
|
|
145
284
|
|
|
146
285
|
req.on('timeout', () => {
|
|
147
286
|
req.destroy();
|
|
148
|
-
|
|
287
|
+
rejectOnce(new RequestException('Request timeout', 0, null, {
|
|
288
|
+
retryable: true,
|
|
289
|
+
code: 'ETIMEDOUT'
|
|
290
|
+
}));
|
|
149
291
|
});
|
|
150
292
|
|
|
151
293
|
if (bodyStr !== null) {
|
package/lib/Response.js
CHANGED
|
@@ -10,10 +10,15 @@
|
|
|
10
10
|
* Response wrapper for API responses
|
|
11
11
|
*/
|
|
12
12
|
class Response {
|
|
13
|
-
constructor(statusCode, body, headers = {}) {
|
|
13
|
+
constructor(statusCode, body, headers = {}, options = {}) {
|
|
14
14
|
this.statusCode = statusCode;
|
|
15
15
|
this.body = body;
|
|
16
16
|
this.headers = headers;
|
|
17
|
+
this.statusMessage = options.statusMessage || '';
|
|
18
|
+
this.rawBody = options.rawBody || null;
|
|
19
|
+
this.request = options.request || null;
|
|
20
|
+
this.contentType = headers['content-type'] || headers['Content-Type'] || null;
|
|
21
|
+
this.parsed = options.parsed !== undefined ? options.parsed : true;
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
getStatusCode() {
|
|
@@ -27,6 +32,26 @@ class Response {
|
|
|
27
32
|
getHeaders() {
|
|
28
33
|
return this.headers;
|
|
29
34
|
}
|
|
35
|
+
|
|
36
|
+
getStatusMessage() {
|
|
37
|
+
return this.statusMessage;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getRawBody() {
|
|
41
|
+
return this.rawBody;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
getRequest() {
|
|
45
|
+
return this.request;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
getContentType() {
|
|
49
|
+
return this.contentType;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
isParsed() {
|
|
53
|
+
return this.parsed;
|
|
54
|
+
}
|
|
30
55
|
|
|
31
56
|
isSuccess() {
|
|
32
57
|
return this.statusCode >= 200 && this.statusCode < 300;
|
|
@@ -46,7 +71,8 @@ class Response {
|
|
|
46
71
|
toArray() {
|
|
47
72
|
return {
|
|
48
73
|
status: this.statusCode,
|
|
49
|
-
body: this.body
|
|
74
|
+
body: this.body,
|
|
75
|
+
headers: this.headers
|
|
50
76
|
};
|
|
51
77
|
}
|
|
52
78
|
}
|
package/lib/Route.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hlquery Node.js Client - Fluent Route 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
|
+
function requireNonEmptyString(value, label) {
|
|
10
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
11
|
+
throw new Error(`${label} must be a non-empty string`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function cleanRoute(route) {
|
|
16
|
+
if (route === null || route === undefined) {
|
|
17
|
+
return '';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (Array.isArray(route)) {
|
|
21
|
+
return route
|
|
22
|
+
.map(segment => {
|
|
23
|
+
requireNonEmptyString(String(segment), 'Route segment');
|
|
24
|
+
return encodeURIComponent(String(segment).replace(/^\/+|\/+$/g, ''));
|
|
25
|
+
})
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
.join('/');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return String(route).replace(/^\/+|\/+$/g, '');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function joinPath(basePath, route) {
|
|
34
|
+
const base = String(basePath || '').replace(/\/+$/g, '');
|
|
35
|
+
const suffix = cleanRoute(route);
|
|
36
|
+
|
|
37
|
+
if (!suffix) {
|
|
38
|
+
return base || '/';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!base || base === '/') {
|
|
42
|
+
return `/${suffix}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return `${base}/${suffix}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Small Redis-style command builder for dynamic hlquery routes.
|
|
50
|
+
*/
|
|
51
|
+
class RouteCommand {
|
|
52
|
+
constructor(request, path) {
|
|
53
|
+
this.request = request;
|
|
54
|
+
this.path = path || '/';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
route(route) {
|
|
58
|
+
return new RouteCommand(this.request, joinPath(this.path, route));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async call(method, body = null, queryParams = {}, options = {}) {
|
|
62
|
+
requireNonEmptyString(method, 'HTTP method');
|
|
63
|
+
return await this.request.execute(method.toUpperCase(), this.path, body, queryParams, options);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async get(queryParams = {}, options = {}) {
|
|
67
|
+
return await this.call('GET', null, queryParams, options);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async post(body = null, queryParams = {}, options = {}) {
|
|
71
|
+
return await this.call('POST', body, queryParams, options);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async put(body = null, queryParams = {}, options = {}) {
|
|
75
|
+
return await this.call('PUT', body, queryParams, options);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async patch(body = null, queryParams = {}, options = {}) {
|
|
79
|
+
return await this.call('PATCH', body, queryParams, options);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async delete(queryParams = {}, options = {}) {
|
|
83
|
+
return await this.call('DELETE', null, queryParams, options);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async del(queryParams = {}, options = {}) {
|
|
87
|
+
return await this.delete(queryParams, options);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Fluent command builder rooted at /modules/{name}.
|
|
93
|
+
*/
|
|
94
|
+
class ModuleRouteCommand extends RouteCommand {
|
|
95
|
+
constructor(request, name) {
|
|
96
|
+
requireNonEmptyString(name, 'Module name');
|
|
97
|
+
super(request, `/modules/${encodeURIComponent(name)}`);
|
|
98
|
+
this.name = name;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async syntax(queryParams = {}, options = {}) {
|
|
102
|
+
return await this.route('syntax').get(queryParams, options);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
RouteCommand,
|
|
108
|
+
ModuleRouteCommand,
|
|
109
|
+
cleanRoute,
|
|
110
|
+
joinPath
|
|
111
|
+
};
|