hlquery-node-client 1.0.2 → 1.0.4
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/.github/workflows/ci.yml +1 -1
- package/README.md +7 -24
- package/example.js +0 -5
- package/examples/basic_usage.js +9 -9
- package/examples/collections.js +15 -8
- package/examples/csv.js +6 -4
- package/examples/documents.js +33 -15
- package/examples/flush.js +7 -1
- package/examples/keys.js +5 -3
- package/examples/pdf.js +6 -4
- package/examples/search.js +48 -13
- package/examples/sql.js +30 -6
- package/lib/Client.js +98 -0
- package/lib/Documents.js +84 -6
- package/lib/Keys.js +3 -3
- package/lib/Request.js +17 -4
- package/lib/SAM.js +16 -3
- package/package.json +1 -1
- package/test/offline-smoke.js +32 -3
- package/utils/CSVParser.js +13 -0
- package/utils/Validator.js +19 -21
- /package/{LICENSE → LICENSE.md} +0 -0
package/.github/workflows/ci.yml
CHANGED
package/README.md
CHANGED
|
@@ -6,17 +6,17 @@
|
|
|
6
6
|
|
|
7
7
|
**A modular Node.js client library for hlquery, designed with a familiar and intuitive API structure.**
|
|
8
8
|
|
|
9
|
-
[](https://x.com/hlquery)
|
|
10
|
-
[](https://github.com/hlquery/hlquery
|
|
13
|
-
[](https://x.com/hlquery)
|
|
10
|
+
[](https://github.com/hlquery/node-api/actions/workflows/ci.yml)
|
|
11
|
+
[](https://github.com/hlquery/node-api)
|
|
12
|
+
[](https://github.com/hlquery/hlquery)
|
|
13
|
+
[](https://opensource.org/licenses/BSD-3-Clause)
|
|
14
14
|
|
|
15
15
|
</div>
|
|
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. 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, SQL, and SAM.
|
|
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
|
|
|
@@ -26,10 +26,6 @@ Use the Node.js client when you want hlquery calls to read like regular applicat
|
|
|
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
|
|
|
29
|
-
### Why choose it over raw HTTP?
|
|
30
|
-
|
|
31
|
-
Choose the Node.js client over raw HTTP when you want less repetitive `fetch` or `axios` boilerplate, cleaner handling for auth headers and endpoint paths, and search or indexing code that stays easier to read.
|
|
32
|
-
|
|
33
29
|
### Install
|
|
34
30
|
|
|
35
31
|
```bash
|
|
@@ -143,7 +139,7 @@ console.log(results.getBody());
|
|
|
143
139
|
```javascript
|
|
144
140
|
const rows = await client.sql('SHOW COLLECTIONS;');
|
|
145
141
|
const execResult = await client.execSql(
|
|
146
|
-
"INSERT INTO
|
|
142
|
+
"INSERT INTO books (id, title) VALUES ('book_4', 'Inserted via SQL');"
|
|
147
143
|
);
|
|
148
144
|
const books = await client.sqlSearch(
|
|
149
145
|
'books',
|
|
@@ -158,19 +154,6 @@ console.log(execResult.getBody());
|
|
|
158
154
|
console.log(books.getBody());
|
|
159
155
|
```
|
|
160
156
|
|
|
161
|
-
### Reduce Text Example
|
|
162
|
-
|
|
163
|
-
Use the raw request helper for custom module routes:
|
|
164
|
-
|
|
165
|
-
```javascript
|
|
166
|
-
const response = await client.executeRequest('GET', '/modules/<name>/<route>', null, {
|
|
167
|
-
q: 'example query'
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
/* Print the custom module route response body. */
|
|
171
|
-
console.log(response.getBody());
|
|
172
|
-
```
|
|
173
|
-
|
|
174
157
|
### Contributing
|
|
175
158
|
|
|
176
159
|
We welcome contributions from the community! All contributions must be released under the BSD 3-Clause license.
|
package/example.js
CHANGED
|
@@ -161,11 +161,6 @@ async function main() {
|
|
|
161
161
|
// Create client
|
|
162
162
|
const client = new Client(baseUrl);
|
|
163
163
|
|
|
164
|
-
// Optional: Set authentication token if provided
|
|
165
|
-
// Uncomment and set token if your server requires authentication:
|
|
166
|
-
// const authToken = 'your_token_here';
|
|
167
|
-
// client.setAuthToken(authToken, 'bearer');
|
|
168
|
-
|
|
169
164
|
if (testToken) {
|
|
170
165
|
client.setAuthToken(testToken, 'bearer');
|
|
171
166
|
console.log(`Using authentication token: ${testToken.substring(0, 8)}...\n`);
|
package/examples/basic_usage.js
CHANGED
|
@@ -7,8 +7,15 @@
|
|
|
7
7
|
const Client = require('../lib/Client');
|
|
8
8
|
|
|
9
9
|
async function main() {
|
|
10
|
+
const baseUrl = process.argv[2] || process.env.HLQ_BASE_URL || process.env.HLQUERY_BASE_URL || 'http://localhost:9200';
|
|
11
|
+
const token = process.argv[3] || process.env.HLQUERY_TOKEN || null;
|
|
12
|
+
|
|
10
13
|
// Initialize client
|
|
11
|
-
const client = new Client(
|
|
14
|
+
const client = new Client(baseUrl);
|
|
15
|
+
|
|
16
|
+
if (token) {
|
|
17
|
+
client.setAuthToken(token, 'bearer');
|
|
18
|
+
}
|
|
12
19
|
|
|
13
20
|
// Health check
|
|
14
21
|
const health = await client.health();
|
|
@@ -22,14 +29,7 @@ async function main() {
|
|
|
22
29
|
console.log(`Found ${body.collections ? body.collections.length : 0} collections`);
|
|
23
30
|
}
|
|
24
31
|
|
|
25
|
-
|
|
26
|
-
const authenticatedClient = new Client('http://localhost:9200', {
|
|
27
|
-
token: 'your_token_here',
|
|
28
|
-
auth_method: 'bearer'
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
// Or set token dynamically
|
|
32
|
-
client.setAuthToken('your_token_here', 'bearer');
|
|
32
|
+
console.log('Base URL:', baseUrl);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
main().catch(console.error);
|
package/examples/collections.js
CHANGED
|
@@ -7,16 +7,19 @@
|
|
|
7
7
|
const Client = require('../lib/Client');
|
|
8
8
|
|
|
9
9
|
async function main() {
|
|
10
|
-
const
|
|
10
|
+
const baseUrl = process.argv[2] || process.env.HLQ_BASE_URL || process.env.HLQUERY_BASE_URL || 'http://localhost:9200';
|
|
11
|
+
const token = process.argv[3] || process.env.HLQUERY_TOKEN || null;
|
|
12
|
+
const client = new Client(baseUrl);
|
|
13
|
+
const collectionName = `node_collections_example_${process.pid}`;
|
|
14
|
+
|
|
15
|
+
if (token) {
|
|
16
|
+
client.setAuthToken(token, 'bearer');
|
|
17
|
+
}
|
|
11
18
|
|
|
12
19
|
// List collections
|
|
13
20
|
const collections = await client.collections().list(0, 10);
|
|
14
21
|
console.log('Collections:', collections.getBody());
|
|
15
22
|
|
|
16
|
-
// Get collection
|
|
17
|
-
const collection = await client.collections().get('my_collection');
|
|
18
|
-
console.log('Collection details:', collection.getBody());
|
|
19
|
-
|
|
20
23
|
// Create collection
|
|
21
24
|
const schema = {
|
|
22
25
|
fields: [
|
|
@@ -25,15 +28,19 @@ async function main() {
|
|
|
25
28
|
{ name: 'embedding', type: 'float[]' }
|
|
26
29
|
]
|
|
27
30
|
};
|
|
28
|
-
const createResult = await client.collections().create(
|
|
31
|
+
const createResult = await client.collections().create(collectionName, schema);
|
|
29
32
|
console.log('Create result:', createResult.getBody());
|
|
33
|
+
|
|
34
|
+
// Get collection
|
|
35
|
+
const collection = await client.collections().get(collectionName);
|
|
36
|
+
console.log('Collection details:', collection.getBody());
|
|
30
37
|
|
|
31
38
|
// Get formatted fields
|
|
32
|
-
const fields = await client.collections().getFields(
|
|
39
|
+
const fields = await client.collections().getFields(collectionName);
|
|
33
40
|
console.log('Formatted fields:', fields.getBody());
|
|
34
41
|
|
|
35
42
|
// Delete collection
|
|
36
|
-
const deleteResult = await client.collections().delete(
|
|
43
|
+
const deleteResult = await client.collections().delete(collectionName);
|
|
37
44
|
console.log('Delete result:', deleteResult.getBody());
|
|
38
45
|
}
|
|
39
46
|
|
package/examples/csv.js
CHANGED
|
@@ -2,21 +2,23 @@
|
|
|
2
2
|
* CSV Parsing Example
|
|
3
3
|
*
|
|
4
4
|
* Usage:
|
|
5
|
-
* node examples/csv.js <collection> <csv-path> [token]
|
|
5
|
+
* node examples/csv.js <collection> <csv-path> [base-url] [token]
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const Client = require('../lib/Client');
|
|
10
10
|
|
|
11
11
|
async function main() {
|
|
12
|
-
const [, , collectionName, csvPath,
|
|
12
|
+
const [, , collectionName, csvPath, baseUrlArg, tokenArg] = process.argv;
|
|
13
13
|
|
|
14
14
|
if (!collectionName || !csvPath) {
|
|
15
|
-
console.error('Usage: node examples/csv.js <collection> <csv-path> [token]');
|
|
15
|
+
console.error('Usage: node examples/csv.js <collection> <csv-path> [base-url] [token]');
|
|
16
16
|
process.exit(1);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
const
|
|
19
|
+
const baseUrl = baseUrlArg || process.env.HLQ_BASE_URL || process.env.HLQUERY_BASE_URL || 'http://localhost:9200';
|
|
20
|
+
const token = tokenArg || process.env.HLQUERY_TOKEN || null;
|
|
21
|
+
const client = new Client(baseUrl);
|
|
20
22
|
|
|
21
23
|
if (token) {
|
|
22
24
|
client.setAuthToken(token, 'bearer');
|
package/examples/documents.js
CHANGED
|
@@ -7,38 +7,53 @@
|
|
|
7
7
|
const Client = require('../lib/Client');
|
|
8
8
|
|
|
9
9
|
async function main() {
|
|
10
|
-
const
|
|
10
|
+
const baseUrl = process.argv[2] || process.env.HLQ_BASE_URL || process.env.HLQUERY_BASE_URL || 'http://localhost:9200';
|
|
11
|
+
const token = process.argv[3] || process.env.HLQUERY_TOKEN || null;
|
|
12
|
+
const client = new Client(baseUrl);
|
|
13
|
+
const collectionName = `node_documents_example_${process.pid}`;
|
|
14
|
+
|
|
15
|
+
if (token) {
|
|
16
|
+
client.setAuthToken(token, 'bearer');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
await client.collections().delete(collectionName);
|
|
20
|
+
await client.collections().create(collectionName, {
|
|
21
|
+
fields: [
|
|
22
|
+
{ name: 'title', type: 'string' },
|
|
23
|
+
{ name: 'content', type: 'string' }
|
|
24
|
+
]
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Add document
|
|
28
|
+
const newDoc = {
|
|
29
|
+
id: 'doc_1',
|
|
30
|
+
title: 'New Document',
|
|
31
|
+
content: 'Document content'
|
|
32
|
+
};
|
|
33
|
+
const addResult = await client.documents().add(collectionName, newDoc);
|
|
34
|
+
console.log('Add result:', addResult.getBody());
|
|
11
35
|
|
|
12
36
|
// List documents
|
|
13
|
-
const docs = await client.documents().list(
|
|
37
|
+
const docs = await client.documents().list(collectionName, {
|
|
14
38
|
offset: 0,
|
|
15
39
|
limit: 10
|
|
16
40
|
});
|
|
17
41
|
console.log('Documents:', docs.getBody());
|
|
18
42
|
|
|
19
43
|
// Get document
|
|
20
|
-
const doc = await client.documents().get(
|
|
44
|
+
const doc = await client.documents().get(collectionName, 'doc_1');
|
|
21
45
|
console.log('Document:', doc.getBody());
|
|
22
46
|
|
|
23
|
-
// Add document
|
|
24
|
-
const newDoc = {
|
|
25
|
-
id: 'doc_1',
|
|
26
|
-
title: 'New Document',
|
|
27
|
-
content: 'Document content'
|
|
28
|
-
};
|
|
29
|
-
const addResult = await client.documents().add('collection', newDoc);
|
|
30
|
-
console.log('Add result:', addResult.getBody());
|
|
31
|
-
|
|
32
47
|
// Update document
|
|
33
48
|
const updatedDoc = {
|
|
34
49
|
title: 'Updated Document',
|
|
35
50
|
content: 'Updated content'
|
|
36
51
|
};
|
|
37
|
-
const updateResult = await client.documents().update(
|
|
52
|
+
const updateResult = await client.documents().update(collectionName, 'doc_1', updatedDoc);
|
|
38
53
|
console.log('Update result:', updateResult.getBody());
|
|
39
54
|
|
|
40
55
|
// Delete document
|
|
41
|
-
const deleteResult = await client.documents().delete(
|
|
56
|
+
const deleteResult = await client.documents().delete(collectionName, 'doc_1');
|
|
42
57
|
console.log('Delete result:', deleteResult.getBody());
|
|
43
58
|
|
|
44
59
|
// Bulk import
|
|
@@ -47,8 +62,11 @@ async function main() {
|
|
|
47
62
|
{ id: 'doc2', title: 'Doc 2' },
|
|
48
63
|
{ id: 'doc3', title: 'Doc 3' }
|
|
49
64
|
];
|
|
50
|
-
const importResult = await client.documents().import(
|
|
65
|
+
const importResult = await client.documents().import(collectionName, bulkDocs);
|
|
51
66
|
console.log('Import result:', importResult.getBody());
|
|
67
|
+
|
|
68
|
+
const cleanup = await client.collections().delete(collectionName);
|
|
69
|
+
console.log('Cleanup result:', cleanup.getBody());
|
|
52
70
|
}
|
|
53
71
|
|
|
54
72
|
main().catch(console.error);
|
package/examples/flush.js
CHANGED
|
@@ -12,7 +12,13 @@
|
|
|
12
12
|
const Client = require('../lib/Client');
|
|
13
13
|
|
|
14
14
|
async function main() {
|
|
15
|
-
const
|
|
15
|
+
const baseUrl = process.argv[2] || process.env.HLQ_BASE_URL || process.env.HLQUERY_BASE_URL || 'http://localhost:9200';
|
|
16
|
+
const token = process.argv[3] || process.env.HLQUERY_TOKEN || null;
|
|
17
|
+
const client = new Client(baseUrl);
|
|
18
|
+
|
|
19
|
+
if (token) {
|
|
20
|
+
client.setAuthToken(token, 'bearer');
|
|
21
|
+
}
|
|
16
22
|
|
|
17
23
|
console.log('='.repeat(70));
|
|
18
24
|
console.log('FLUSH EXAMPLE');
|
package/examples/keys.js
CHANGED
|
@@ -6,13 +6,15 @@
|
|
|
6
6
|
|
|
7
7
|
const Client = require('../index');
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
const BASE_URL = 'http://localhost:9200';
|
|
9
|
+
const ADMIN_TOKEN = process.argv[2] || process.env.HLQUERY_ADMIN_TOKEN || process.env.HLQUERY_TOKEN || null;
|
|
10
|
+
const BASE_URL = process.argv[3] || process.env.HLQ_BASE_URL || process.env.HLQUERY_BASE_URL || 'http://localhost:9200';
|
|
12
11
|
|
|
13
12
|
async function run() {
|
|
14
13
|
try {
|
|
15
14
|
const client = new Client(BASE_URL);
|
|
15
|
+
if (!ADMIN_TOKEN) {
|
|
16
|
+
throw new Error('Admin token required. Pass it as argv[2] or set HLQUERY_ADMIN_TOKEN.');
|
|
17
|
+
}
|
|
16
18
|
client.setAuthToken(ADMIN_TOKEN);
|
|
17
19
|
|
|
18
20
|
console.log('1. Creating a scoped search key for "products" collection...');
|
package/examples/pdf.js
CHANGED
|
@@ -3,21 +3,23 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Usage:
|
|
5
5
|
* npm install
|
|
6
|
-
* node examples/pdf.js <collection> <pdf-path> [token]
|
|
6
|
+
* node examples/pdf.js <collection> <pdf-path> [base-url] [token]
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const Client = require('../lib/Client');
|
|
11
11
|
|
|
12
12
|
async function main() {
|
|
13
|
-
const [, , collectionName, pdfPath,
|
|
13
|
+
const [, , collectionName, pdfPath, baseUrlArg, tokenArg] = process.argv;
|
|
14
14
|
|
|
15
15
|
if (!collectionName || !pdfPath) {
|
|
16
|
-
console.error('Usage: node examples/pdf.js <collection> <pdf-path> [token]');
|
|
16
|
+
console.error('Usage: node examples/pdf.js <collection> <pdf-path> [base-url] [token]');
|
|
17
17
|
process.exit(1);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
const
|
|
20
|
+
const baseUrl = baseUrlArg || process.env.HLQ_BASE_URL || process.env.HLQUERY_BASE_URL || 'http://localhost:9200';
|
|
21
|
+
const token = tokenArg || process.env.HLQUERY_TOKEN || null;
|
|
22
|
+
const client = new Client(baseUrl);
|
|
21
23
|
|
|
22
24
|
if (token) {
|
|
23
25
|
client.setAuthToken(token, 'bearer');
|
package/examples/search.js
CHANGED
|
@@ -7,18 +7,41 @@
|
|
|
7
7
|
const Client = require('../lib/Client');
|
|
8
8
|
|
|
9
9
|
async function main() {
|
|
10
|
-
const
|
|
10
|
+
const baseUrl = process.argv[2] || process.env.HLQ_BASE_URL || process.env.HLQUERY_BASE_URL || 'http://localhost:9200';
|
|
11
|
+
const token = process.argv[3] || process.env.HLQUERY_TOKEN || null;
|
|
12
|
+
const collectionName = process.argv[4] || `node_search_example_${process.pid}`;
|
|
13
|
+
const client = new Client(baseUrl);
|
|
11
14
|
const search = client.searchApi();
|
|
15
|
+
|
|
16
|
+
if (token) {
|
|
17
|
+
client.setAuthToken(token, 'bearer');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
await client.collections().delete(collectionName);
|
|
21
|
+
await client.collections().create(collectionName, {
|
|
22
|
+
fields: [
|
|
23
|
+
{ name: 'title', type: 'string' },
|
|
24
|
+
{ name: 'content', type: 'string' },
|
|
25
|
+
{ name: 'category', type: 'string' },
|
|
26
|
+
{ name: 'price', type: 'float' },
|
|
27
|
+
{ name: 'embedding', type: 'float[]' }
|
|
28
|
+
]
|
|
29
|
+
});
|
|
30
|
+
await client.documents().import(collectionName, [
|
|
31
|
+
{ id: 'prod_laptop_001', title: 'Laptop Computer', content: 'Portable work machine', category: 'electronics', price: 1299.99, embedding: [0.1, 0.2, 0.3, 0.4, 0.5] },
|
|
32
|
+
{ id: 'prod_keyboard_001', title: 'Wireless Keyboard', content: 'Compact Bluetooth keyboard', category: 'electronics', price: 49.99, embedding: [0.2, 0.1, 0.4, 0.3, 0.5] },
|
|
33
|
+
{ id: 'prod_notebook_001', title: 'Paper Notebook', content: 'Plain paper writing notebook', category: 'office', price: 9.99, embedding: [0.5, 0.4, 0.3, 0.2, 0.1] }
|
|
34
|
+
]);
|
|
12
35
|
|
|
13
36
|
// Simple search. The SDK uses the canonical /collections/{name}/search route.
|
|
14
|
-
const results = await search.search(
|
|
37
|
+
const results = await search.search(collectionName, {
|
|
15
38
|
q: 'search query',
|
|
16
39
|
query_by: 'title,content',
|
|
17
40
|
limit: 10
|
|
18
41
|
});
|
|
19
42
|
|
|
20
43
|
// Search with filters
|
|
21
|
-
const filteredResults = await client.search(
|
|
44
|
+
const filteredResults = await client.search(collectionName, {
|
|
22
45
|
q: 'query',
|
|
23
46
|
query_by: 'title',
|
|
24
47
|
filter_by: 'category:electronics',
|
|
@@ -28,49 +51,49 @@ async function main() {
|
|
|
28
51
|
|
|
29
52
|
// Supported query semantics
|
|
30
53
|
// Field-specific search
|
|
31
|
-
const fieldResults = await client.search(
|
|
54
|
+
const fieldResults = await client.search(collectionName, {
|
|
32
55
|
q: 'title:laptop',
|
|
33
56
|
query_by: 'title,content',
|
|
34
57
|
limit: 10
|
|
35
58
|
});
|
|
36
59
|
|
|
37
60
|
// Boolean OR query
|
|
38
|
-
const orResults = await client.search(
|
|
61
|
+
const orResults = await client.search(collectionName, {
|
|
39
62
|
q: 'title:laptop OR title:notebook',
|
|
40
63
|
query_by: 'title,content',
|
|
41
64
|
limit: 10
|
|
42
65
|
});
|
|
43
66
|
|
|
44
67
|
// Boolean NOT query
|
|
45
|
-
const notResults = await client.search(
|
|
68
|
+
const notResults = await client.search(collectionName, {
|
|
46
69
|
q: 'title:laptop NOT title:refurbished',
|
|
47
70
|
query_by: 'title,content',
|
|
48
71
|
limit: 10
|
|
49
72
|
});
|
|
50
73
|
|
|
51
74
|
// Phrase search
|
|
52
|
-
const phraseResults = await client.search(
|
|
75
|
+
const phraseResults = await client.search(collectionName, {
|
|
53
76
|
q: '"wireless keyboard"',
|
|
54
77
|
query_by: 'title',
|
|
55
78
|
limit: 10
|
|
56
79
|
});
|
|
57
80
|
|
|
58
81
|
// Wildcard search
|
|
59
|
-
const wildcardResults = await client.search(
|
|
82
|
+
const wildcardResults = await client.search(collectionName, {
|
|
60
83
|
q: 'laptop*',
|
|
61
84
|
query_by: 'title,content',
|
|
62
85
|
limit: 10
|
|
63
86
|
});
|
|
64
87
|
|
|
65
88
|
// query_by restriction
|
|
66
|
-
const restrictedResults = await client.search(
|
|
89
|
+
const restrictedResults = await client.search(collectionName, {
|
|
67
90
|
q: 'laptop',
|
|
68
91
|
query_by: 'title',
|
|
69
92
|
limit: 10
|
|
70
93
|
});
|
|
71
94
|
|
|
72
95
|
// Filter operators belong in filter_by
|
|
73
|
-
const combinedResults = await client.search(
|
|
96
|
+
const combinedResults = await client.search(collectionName, {
|
|
74
97
|
q: '*',
|
|
75
98
|
query_by: 'title,content',
|
|
76
99
|
filter_by: 'price:>100&&category:electronics',
|
|
@@ -78,7 +101,7 @@ async function main() {
|
|
|
78
101
|
});
|
|
79
102
|
|
|
80
103
|
// Vector search
|
|
81
|
-
const vectorResults = await client.vectorSearch(
|
|
104
|
+
const vectorResults = await client.vectorSearch(collectionName, {
|
|
82
105
|
body: {
|
|
83
106
|
vector: [0.1, 0.2, 0.3, 0.4, 0.5],
|
|
84
107
|
field_name: 'embedding',
|
|
@@ -91,11 +114,23 @@ async function main() {
|
|
|
91
114
|
|
|
92
115
|
// Multi-search
|
|
93
116
|
const multiResults = await search.multiSearch([
|
|
94
|
-
{ collection:
|
|
95
|
-
{ collection:
|
|
117
|
+
{ collection: collectionName, q: 'laptop', query_by: 'title' },
|
|
118
|
+
{ collection: collectionName, q: 'keyboard', query_by: 'content' }
|
|
96
119
|
]);
|
|
97
120
|
|
|
98
121
|
console.log('Search results:', results.getBody());
|
|
122
|
+
console.log('Filtered results:', filteredResults.getBody());
|
|
123
|
+
console.log('Field results:', fieldResults.getBody());
|
|
124
|
+
console.log('Boolean OR results:', orResults.getBody());
|
|
125
|
+
console.log('Boolean NOT results:', notResults.getBody());
|
|
126
|
+
console.log('Phrase results:', phraseResults.getBody());
|
|
127
|
+
console.log('Wildcard results:', wildcardResults.getBody());
|
|
128
|
+
console.log('Restricted results:', restrictedResults.getBody());
|
|
129
|
+
console.log('Combined results:', combinedResults.getBody());
|
|
130
|
+
console.log('Vector results:', vectorResults.getBody());
|
|
131
|
+
console.log('Multi-search results:', multiResults.getBody());
|
|
132
|
+
|
|
133
|
+
await client.collections().delete(collectionName);
|
|
99
134
|
}
|
|
100
135
|
|
|
101
136
|
main().catch(console.error);
|
package/examples/sql.js
CHANGED
|
@@ -7,7 +7,28 @@
|
|
|
7
7
|
const Client = require('../lib/Client');
|
|
8
8
|
|
|
9
9
|
async function main() {
|
|
10
|
-
const
|
|
10
|
+
const baseUrl = process.argv[2] || process.env.HLQ_BASE_URL || process.env.HLQUERY_BASE_URL || 'http://localhost:9200';
|
|
11
|
+
const token = process.argv[3] || process.env.HLQUERY_TOKEN || null;
|
|
12
|
+
const collectionName = `node_sql_example_${process.pid}`;
|
|
13
|
+
const client = new Client(baseUrl);
|
|
14
|
+
|
|
15
|
+
if (token) {
|
|
16
|
+
client.setAuthToken(token, 'bearer');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
await client.collections().delete(collectionName);
|
|
20
|
+
await client.collections().create(collectionName, {
|
|
21
|
+
fields: [
|
|
22
|
+
{ name: 'title', type: 'string' },
|
|
23
|
+
{ name: 'category', type: 'string' },
|
|
24
|
+
{ name: 'price', type: 'float' }
|
|
25
|
+
]
|
|
26
|
+
});
|
|
27
|
+
await client.documents().import(collectionName, [
|
|
28
|
+
{ id: 'book_1', title: 'Algebra Basics', category: 'math', price: 19.99 },
|
|
29
|
+
{ id: 'book_2', title: 'Geometry Proofs', category: 'math', price: 29.99 },
|
|
30
|
+
{ id: 'book_3', title: 'Cooking Basics', category: 'food', price: 14.99 }
|
|
31
|
+
]);
|
|
11
32
|
|
|
12
33
|
// Top-level SQL through /sql
|
|
13
34
|
const rows = await client.sql('SHOW COLLECTIONS;');
|
|
@@ -15,17 +36,20 @@ async function main() {
|
|
|
15
36
|
|
|
16
37
|
// Top-level SQL statement execution through POST /sql
|
|
17
38
|
const execResult = await client.execSql(
|
|
18
|
-
|
|
39
|
+
`INSERT INTO ${collectionName} (id, title, category, price) VALUES ('book_4', 'Inserted via SQL', 'ops', 39.99);`
|
|
19
40
|
);
|
|
20
41
|
console.log('Exec result:', execResult.getBody());
|
|
21
42
|
|
|
22
43
|
// Collection-bound SQL SELECT through /collections/{name}/documents/search
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
44
|
+
const books = await client.sqlSearch(
|
|
45
|
+
collectionName,
|
|
46
|
+
`SELECT id, title, price FROM ${collectionName} WHERE price > 10 ORDER BY price DESC LIMIT 3;`,
|
|
26
47
|
{ highlight: false }
|
|
27
48
|
);
|
|
28
|
-
console.log('
|
|
49
|
+
console.log('Books SQL results:', books.getBody());
|
|
50
|
+
|
|
51
|
+
const cleanup = await client.collections().delete(collectionName);
|
|
52
|
+
console.log('Cleanup:', cleanup.getBody());
|
|
29
53
|
}
|
|
30
54
|
|
|
31
55
|
main().catch(console.error);
|
package/lib/Client.js
CHANGED
|
@@ -393,6 +393,80 @@ class Client {
|
|
|
393
393
|
async getCollectionFields(name) {
|
|
394
394
|
return await this._collections.getFields(name);
|
|
395
395
|
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Copy a collection into a new name (schema + documents).
|
|
399
|
+
*
|
|
400
|
+
* @param {string} sourceName
|
|
401
|
+
* @param {string} targetName
|
|
402
|
+
* @param {object} options
|
|
403
|
+
* @returns {Promise<Response>}
|
|
404
|
+
*/
|
|
405
|
+
async copyCollection(sourceName, targetName, options = {}) {
|
|
406
|
+
const batchSize = Number.isInteger(options.batch_size) ? options.batch_size
|
|
407
|
+
: (Number.isInteger(options.batchSize) ? options.batchSize : 500);
|
|
408
|
+
|
|
409
|
+
const sourceResponse = await this._collections.get(sourceName);
|
|
410
|
+
if (sourceResponse.getStatusCode() !== 200) {
|
|
411
|
+
return sourceResponse;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const source = sourceResponse.getBody();
|
|
415
|
+
const schema = {};
|
|
416
|
+
|
|
417
|
+
if (source && source.fields && typeof source.fields === 'object' && !Array.isArray(source.fields) && Object.keys(source.fields).length > 0) {
|
|
418
|
+
const fieldNames = Object.keys(source.fields).sort();
|
|
419
|
+
schema.fields = fieldNames.map(name => ({ name, type: typeof source.fields[name] === 'string' ? source.fields[name] : 'string' }));
|
|
420
|
+
} else if (source && Array.isArray(source.searchable_fields) && source.searchable_fields.length > 0) {
|
|
421
|
+
schema.searchable_fields = source.searchable_fields;
|
|
422
|
+
} else {
|
|
423
|
+
schema.searchable_fields = ['title', 'content'];
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (source && Array.isArray(source.filterable_fields)) {
|
|
427
|
+
schema.filterable_fields = source.filterable_fields;
|
|
428
|
+
}
|
|
429
|
+
if (source && Array.isArray(source.sortable_fields)) {
|
|
430
|
+
schema.sortable_fields = source.sortable_fields;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (source && source.metadata && typeof source.metadata === 'object' && !Array.isArray(source.metadata)) {
|
|
434
|
+
for (const [key, value] of Object.entries(source.metadata)) {
|
|
435
|
+
if (typeof key === 'string' && key.startsWith('_')) {
|
|
436
|
+
schema[key] = value;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const createResponse = await this._collections.create(targetName, schema);
|
|
442
|
+
if (createResponse.getStatusCode() !== 201) {
|
|
443
|
+
return createResponse;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
let offset = 0;
|
|
447
|
+
|
|
448
|
+
while (true) {
|
|
449
|
+
const listResponse = await this._documents.list(sourceName, { offset, limit: batchSize });
|
|
450
|
+
if (listResponse.getStatusCode() !== 200) {
|
|
451
|
+
return listResponse;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const body = listResponse.getBody() || {};
|
|
455
|
+
const documents = Array.isArray(body.documents) ? body.documents : [];
|
|
456
|
+
if (documents.length === 0) {
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const importResponse = await this._documents.import(targetName, documents);
|
|
461
|
+
if (!importResponse.isSuccess || !importResponse.isSuccess()) {
|
|
462
|
+
return importResponse;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
offset += documents.length;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return createResponse;
|
|
469
|
+
}
|
|
396
470
|
|
|
397
471
|
// ============================================================================
|
|
398
472
|
// DOCUMENTS API
|
|
@@ -429,6 +503,30 @@ class Client {
|
|
|
429
503
|
return await this._documents.get(collectionName, documentId);
|
|
430
504
|
}
|
|
431
505
|
|
|
506
|
+
/**
|
|
507
|
+
* Copy a document to a new ID within the same collection.
|
|
508
|
+
*
|
|
509
|
+
* @param {string} collectionName
|
|
510
|
+
* @param {string} sourceId
|
|
511
|
+
* @param {string} targetId
|
|
512
|
+
* @returns {Promise<Response>}
|
|
513
|
+
*/
|
|
514
|
+
async copyDocument(collectionName, sourceId, targetId) {
|
|
515
|
+
return await this._documents.copy(collectionName, sourceId, targetId);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Fetch most-recent documents (by timestamp desc).
|
|
520
|
+
*
|
|
521
|
+
* @param {string} collectionName
|
|
522
|
+
* @param {number} limit
|
|
523
|
+
* @param {number} offset
|
|
524
|
+
* @returns {Promise<Response>}
|
|
525
|
+
*/
|
|
526
|
+
async recentDocuments(collectionName, limit = 20, offset = 0) {
|
|
527
|
+
return await this._documents.recent(collectionName, limit, offset);
|
|
528
|
+
}
|
|
529
|
+
|
|
432
530
|
/**
|
|
433
531
|
* Export documents from a collection.
|
|
434
532
|
*
|
package/lib/Documents.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
const Validator = require('../utils/Validator');
|
|
10
|
+
const Response = require('./Response');
|
|
10
11
|
const PDFParser = require('../utils/PDFParser');
|
|
11
12
|
const CSVParser = require('../utils/CSVParser');
|
|
12
13
|
|
|
@@ -167,16 +168,93 @@ class Documents {
|
|
|
167
168
|
Validator.validateCollectionName(collectionName);
|
|
168
169
|
|
|
169
170
|
// Validate each document in the array
|
|
170
|
-
if (Array.isArray(documents)) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
171
|
+
if (!Array.isArray(documents)) {
|
|
172
|
+
throw new Error('Documents import payload must be an array');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (const doc of documents) {
|
|
176
|
+
Validator.validateDocumentFields(doc);
|
|
174
177
|
}
|
|
175
178
|
|
|
176
|
-
// Server expects {documents: [...]} format
|
|
177
|
-
|
|
179
|
+
// Server expects {documents: [...]} format; fall back to per-doc inserts if bulk route is unavailable.
|
|
180
|
+
const bulkResponse = await this.request.execute('POST', `/collections/${encodeURIComponent(collectionName)}/documents/import`, {
|
|
178
181
|
documents: documents
|
|
179
182
|
});
|
|
183
|
+
|
|
184
|
+
if (bulkResponse && bulkResponse.getStatusCode && bulkResponse.getStatusCode() === 404) {
|
|
185
|
+
let inserted = 0;
|
|
186
|
+
for (const doc of documents) {
|
|
187
|
+
const response = await this.request.execute('POST', `/collections/${encodeURIComponent(collectionName)}/documents`, doc);
|
|
188
|
+
const status = response.getStatusCode();
|
|
189
|
+
if (status !== 201) {
|
|
190
|
+
return response;
|
|
191
|
+
}
|
|
192
|
+
inserted += 1;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return new Response(201, { imported: inserted, fallback: true });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return bulkResponse;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Copy one document to a new ID within the same collection.
|
|
203
|
+
*
|
|
204
|
+
* @param {string} collectionName
|
|
205
|
+
* @param {string} sourceId
|
|
206
|
+
* @param {string} targetId
|
|
207
|
+
* @returns {Promise<Response>}
|
|
208
|
+
*/
|
|
209
|
+
async copy(collectionName, sourceId, targetId) {
|
|
210
|
+
Validator.validateCollectionName(collectionName);
|
|
211
|
+
Validator.validateDocumentId(sourceId);
|
|
212
|
+
Validator.validateDocumentId(targetId);
|
|
213
|
+
|
|
214
|
+
if (sourceId === targetId) {
|
|
215
|
+
throw new Error('Source and target document ids must differ');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const targetCheck = await this.get(collectionName, targetId);
|
|
219
|
+
if (targetCheck.getStatusCode() === 200) {
|
|
220
|
+
return targetCheck;
|
|
221
|
+
}
|
|
222
|
+
if (targetCheck.getStatusCode() !== 404) {
|
|
223
|
+
return targetCheck;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const sourceResponse = await this.get(collectionName, sourceId);
|
|
227
|
+
if (sourceResponse.getStatusCode() !== 200) {
|
|
228
|
+
return sourceResponse;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const doc = { ...sourceResponse.getBody(), id: targetId };
|
|
232
|
+
delete doc.collection_id;
|
|
233
|
+
delete doc.score;
|
|
234
|
+
|
|
235
|
+
const imported = await this.import(collectionName, [doc]);
|
|
236
|
+
return imported;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Convenience helper to fetch most-recent documents (by timestamp desc).
|
|
241
|
+
*
|
|
242
|
+
* @param {string} collectionName
|
|
243
|
+
* @param {number} limit
|
|
244
|
+
* @param {number} offset
|
|
245
|
+
* @returns {Promise<Response>}
|
|
246
|
+
*/
|
|
247
|
+
async recent(collectionName, limit = 20, offset = 0) {
|
|
248
|
+
Validator.validateCollectionName(collectionName);
|
|
249
|
+
Validator.validatePagination(offset, limit);
|
|
250
|
+
|
|
251
|
+
const sql = `select * from ${collectionName} order by timestamp desc limit ${limit} offset ${offset}`;
|
|
252
|
+
return await this.request.execute(
|
|
253
|
+
'GET',
|
|
254
|
+
`/collections/${encodeURIComponent(collectionName)}/documents/search`,
|
|
255
|
+
null,
|
|
256
|
+
{ sql }
|
|
257
|
+
);
|
|
180
258
|
}
|
|
181
259
|
|
|
182
260
|
/**
|
package/lib/Keys.js
CHANGED
|
@@ -35,7 +35,7 @@ class Keys {
|
|
|
35
35
|
if (!id) {
|
|
36
36
|
throw new Error('Key ID is required');
|
|
37
37
|
}
|
|
38
|
-
return await this.request.execute('GET', `/keys/${id}`);
|
|
38
|
+
return await this.request.execute('GET', `/keys/${encodeURIComponent(id)}`);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
@@ -64,7 +64,7 @@ class Keys {
|
|
|
64
64
|
if (!id) {
|
|
65
65
|
throw new Error('Key ID is required');
|
|
66
66
|
}
|
|
67
|
-
return await this.request.execute('DELETE', `/keys/${id}`);
|
|
67
|
+
return await this.request.execute('DELETE', `/keys/${encodeURIComponent(id)}`);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/**
|
|
@@ -78,7 +78,7 @@ class Keys {
|
|
|
78
78
|
if (!id) {
|
|
79
79
|
throw new Error('Key ID is required');
|
|
80
80
|
}
|
|
81
|
-
return await this.request.execute('PUT', `/keys/${id}`, params);
|
|
81
|
+
return await this.request.execute('PUT', `/keys/${encodeURIComponent(id)}`, params);
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
package/lib/Request.js
CHANGED
|
@@ -47,20 +47,33 @@ class Request {
|
|
|
47
47
|
|
|
48
48
|
// Add query parameters
|
|
49
49
|
Object.keys(queryParams).forEach(key => {
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
const value = queryParams[key];
|
|
51
|
+
if (value === undefined || value === null) {
|
|
52
|
+
return;
|
|
52
53
|
}
|
|
54
|
+
|
|
55
|
+
if (Array.isArray(value)) {
|
|
56
|
+
url.searchParams.append(key, value.join(','));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (typeof value === 'object') {
|
|
61
|
+
url.searchParams.append(key, JSON.stringify(value));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
url.searchParams.append(key, value);
|
|
53
66
|
});
|
|
54
67
|
|
|
55
68
|
const headers = {
|
|
56
|
-
'Content-Type': 'application/json',
|
|
57
69
|
'Accept': 'application/json'
|
|
58
70
|
};
|
|
59
71
|
|
|
60
72
|
// Prepare body string if present
|
|
61
73
|
let bodyStr = null;
|
|
62
|
-
if (body !== null) {
|
|
74
|
+
if (body !== null && body !== undefined) {
|
|
63
75
|
bodyStr = typeof body === 'string' ? body : JSON.stringify(body);
|
|
76
|
+
headers['Content-Type'] = 'application/json';
|
|
64
77
|
headers['Content-Length'] = Buffer.byteLength(bodyStr);
|
|
65
78
|
}
|
|
66
79
|
|
package/lib/SAM.js
CHANGED
|
@@ -87,10 +87,14 @@ class SAM {
|
|
|
87
87
|
|
|
88
88
|
async history(collectionName = null, limit = 100, params = {}) {
|
|
89
89
|
this._validateParams(params, 'SAM history params');
|
|
90
|
+
const safeLimit = Number(limit);
|
|
91
|
+
if (!Number.isInteger(safeLimit) || safeLimit < 1) {
|
|
92
|
+
throw new Error('SAM history limit must be a positive integer');
|
|
93
|
+
}
|
|
90
94
|
|
|
91
95
|
const queryParams = {
|
|
92
96
|
...params,
|
|
93
|
-
limit:
|
|
97
|
+
limit: safeLimit
|
|
94
98
|
};
|
|
95
99
|
|
|
96
100
|
if (collectionName !== null && collectionName !== undefined && collectionName !== '') {
|
|
@@ -122,12 +126,21 @@ class SAM {
|
|
|
122
126
|
async listDocuments(collectionName, offset = 0, limit = 20, params = {}) {
|
|
123
127
|
Validator.validateCollectionName(collectionName);
|
|
124
128
|
this._validateParams(params, 'SAM document list params');
|
|
129
|
+
const safeOffset = Number(offset);
|
|
130
|
+
const safeLimit = Number(limit);
|
|
131
|
+
|
|
132
|
+
if (!Number.isInteger(safeOffset) || safeOffset < 0) {
|
|
133
|
+
throw new Error('SAM document offset must be a non-negative integer');
|
|
134
|
+
}
|
|
135
|
+
if (!Number.isInteger(safeLimit) || safeLimit < 1) {
|
|
136
|
+
throw new Error('SAM document limit must be a positive integer');
|
|
137
|
+
}
|
|
125
138
|
|
|
126
139
|
return await this.request.execute('GET', '/sam/documents', null, {
|
|
127
140
|
...params,
|
|
128
141
|
collection: collectionName,
|
|
129
|
-
offset:
|
|
130
|
-
limit:
|
|
142
|
+
offset: safeOffset,
|
|
143
|
+
limit: safeLimit
|
|
131
144
|
});
|
|
132
145
|
}
|
|
133
146
|
|
package/package.json
CHANGED
package/test/offline-smoke.js
CHANGED
|
@@ -9,6 +9,7 @@ const Request = require('../lib/Request');
|
|
|
9
9
|
const Response = require('../lib/Response');
|
|
10
10
|
const Config = require('../utils/Config');
|
|
11
11
|
const Validator = require('../utils/Validator');
|
|
12
|
+
const CSVParser = require('../utils/CSVParser');
|
|
12
13
|
|
|
13
14
|
async function main() {
|
|
14
15
|
assert.strictEqual(typeof Hlquery, 'function', 'default export should be the Client constructor');
|
|
@@ -23,7 +24,11 @@ async function main() {
|
|
|
23
24
|
Validator.validateSearchParams({ limit: 10, offset: 0 });
|
|
24
25
|
|
|
25
26
|
assert.throws(() => Validator.validateCollectionName('123bad'), /letter or underscore/);
|
|
26
|
-
assert.throws(() => Validator.
|
|
27
|
+
assert.throws(() => Validator.validateSearchParams(null), /object/);
|
|
28
|
+
assert.throws(() => Validator.validateSearchParams({ limit: NaN }), /positive integer/);
|
|
29
|
+
assert.throws(() => Validator.validateSearchParams({ offset: 1.5 }), /non-negative integer/);
|
|
30
|
+
assert.throws(() => Validator.validateDocumentFields({ title: 'bad,value' }), /comma \(\,\)/);
|
|
31
|
+
assert.throws(() => CSVParser.parseCSV('a||b', '||'), /single character/);
|
|
27
32
|
|
|
28
33
|
const response = new Response(200, { ok: true }, { 'content-type': 'application/json' });
|
|
29
34
|
assert.strictEqual(response.isSuccess(), true);
|
|
@@ -78,15 +83,23 @@ async function main() {
|
|
|
78
83
|
return;
|
|
79
84
|
}
|
|
80
85
|
|
|
81
|
-
if (req.url
|
|
86
|
+
if (req.url.startsWith('/echo-auth')) {
|
|
82
87
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
83
88
|
res.end(JSON.stringify({
|
|
84
89
|
authorization: req.headers.authorization || null,
|
|
85
|
-
apiKey: req.headers['x-api-key'] || null
|
|
90
|
+
apiKey: req.headers['x-api-key'] || null,
|
|
91
|
+
contentType: req.headers['content-type'] || null,
|
|
92
|
+
url: req.url
|
|
86
93
|
}));
|
|
87
94
|
return;
|
|
88
95
|
}
|
|
89
96
|
|
|
97
|
+
if (req.url === '/keys/key%2Fwith%20space') {
|
|
98
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
99
|
+
res.end(JSON.stringify({ id: 'key/with space' }));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
90
103
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
91
104
|
res.end(JSON.stringify({ error: 'not found' }));
|
|
92
105
|
});
|
|
@@ -111,6 +124,16 @@ async function main() {
|
|
|
111
124
|
const apiKeyEcho = await request.execute('GET', '/echo-auth');
|
|
112
125
|
assert.strictEqual(apiKeyEcho.getBody().apiKey, 'scoped-key');
|
|
113
126
|
|
|
127
|
+
const queryEcho = await request.execute('GET', '/echo-auth', undefined, {
|
|
128
|
+
fields: ['title', 'body'],
|
|
129
|
+
filter: { published: true },
|
|
130
|
+
skip: null
|
|
131
|
+
});
|
|
132
|
+
assert.strictEqual(queryEcho.getBody().contentType, null);
|
|
133
|
+
assert.match(queryEcho.getBody().url, /fields=title%2Cbody/);
|
|
134
|
+
assert.match(queryEcho.getBody().url, /filter=%7B%22published%22%3Atrue%7D/);
|
|
135
|
+
assert.doesNotMatch(queryEcho.getBody().url, /skip=/);
|
|
136
|
+
|
|
114
137
|
const client = new Client(`http://127.0.0.1:${port}`);
|
|
115
138
|
const clientHealth = await client.health();
|
|
116
139
|
assert.strictEqual(clientHealth.isSuccess(), true);
|
|
@@ -152,6 +175,12 @@ async function main() {
|
|
|
152
175
|
assert.strictEqual(typeof client.sam().listDocuments, 'function');
|
|
153
176
|
assert.strictEqual(typeof client.sam().getDocument, 'function');
|
|
154
177
|
assert.strictEqual(typeof client.sam().openDocument, 'function');
|
|
178
|
+
await assert.rejects(() => client.sam().history(null, 0), /positive integer/);
|
|
179
|
+
await assert.rejects(() => client.sam().listDocuments('books', NaN, 20), /non-negative integer/);
|
|
180
|
+
|
|
181
|
+
const keyResponse = await client.keys().get('key/with space');
|
|
182
|
+
assert.deepStrictEqual(keyResponse.getBody(), { id: 'key/with space' });
|
|
183
|
+
await assert.rejects(() => client.documents().import('books', { id: 'not-array' }), /array/);
|
|
155
184
|
} finally {
|
|
156
185
|
await new Promise((resolve) => server.close(resolve));
|
|
157
186
|
}
|
package/utils/CSVParser.js
CHANGED
|
@@ -14,6 +14,7 @@ const { ValidationException } = require('../lib/Exceptions');
|
|
|
14
14
|
class CSVParser {
|
|
15
15
|
static parseFile(filePath, options = {}) {
|
|
16
16
|
this.validateFilePath(filePath);
|
|
17
|
+
this.validateDelimiter(options.delimiter || ',');
|
|
17
18
|
|
|
18
19
|
const resolvedPath = path.resolve(filePath);
|
|
19
20
|
const stat = fs.statSync(resolvedPath);
|
|
@@ -62,6 +63,8 @@ class CSVParser {
|
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
static parseCSV(input, delimiter) {
|
|
66
|
+
this.validateDelimiter(delimiter);
|
|
67
|
+
|
|
65
68
|
const rows = [];
|
|
66
69
|
let row = [];
|
|
67
70
|
let value = '';
|
|
@@ -155,6 +158,16 @@ class CSVParser {
|
|
|
155
158
|
throw new ValidationException('CSV file path must be a non-empty string');
|
|
156
159
|
}
|
|
157
160
|
}
|
|
161
|
+
|
|
162
|
+
static validateDelimiter(delimiter) {
|
|
163
|
+
if (typeof delimiter !== 'string' || delimiter.length !== 1) {
|
|
164
|
+
throw new ValidationException('CSV delimiter must be a single character');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (delimiter === '"' || delimiter === '\r' || delimiter === '\n') {
|
|
168
|
+
throw new ValidationException('CSV delimiter cannot be a quote or newline');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
158
171
|
}
|
|
159
172
|
|
|
160
173
|
module.exports = CSVParser;
|
package/utils/Validator.js
CHANGED
|
@@ -69,35 +69,33 @@ class Validator {
|
|
|
69
69
|
* Validate pagination parameters
|
|
70
70
|
*/
|
|
71
71
|
static validatePagination(offset, limit) {
|
|
72
|
-
if (offset !== undefined && (
|
|
73
|
-
throw new ValidationException('Offset must be a non-negative
|
|
72
|
+
if (offset !== undefined && (!Number.isInteger(offset) || offset < 0)) {
|
|
73
|
+
throw new ValidationException('Offset must be a non-negative integer');
|
|
74
74
|
}
|
|
75
|
-
if (limit !== undefined && (
|
|
76
|
-
throw new ValidationException('Limit must be a positive
|
|
75
|
+
if (limit !== undefined && (!Number.isInteger(limit) || limit < 1)) {
|
|
76
|
+
throw new ValidationException('Limit must be a positive integer');
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
81
|
* Validate search parameters
|
|
82
82
|
*/
|
|
83
|
-
static validateSearchParams(params) {
|
|
84
|
-
if (params
|
|
83
|
+
static validateSearchParams(params = {}) {
|
|
84
|
+
if (params === null || typeof params !== 'object' || Array.isArray(params)) {
|
|
85
85
|
throw new ValidationException('Search parameters must be an object');
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
if (params) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
throw new ValidationException('Size must be a positive number');
|
|
100
|
-
}
|
|
88
|
+
if (params.offset !== undefined && (!Number.isInteger(params.offset) || params.offset < 0)) {
|
|
89
|
+
throw new ValidationException('Offset must be a non-negative integer');
|
|
90
|
+
}
|
|
91
|
+
if (params.limit !== undefined && (!Number.isInteger(params.limit) || params.limit < 1)) {
|
|
92
|
+
throw new ValidationException('Limit must be a positive integer');
|
|
93
|
+
}
|
|
94
|
+
if (params.from !== undefined && (!Number.isInteger(params.from) || params.from < 0)) {
|
|
95
|
+
throw new ValidationException('From must be a non-negative integer');
|
|
96
|
+
}
|
|
97
|
+
if (params.size !== undefined && (!Number.isInteger(params.size) || params.size < 1)) {
|
|
98
|
+
throw new ValidationException('Size must be a positive integer');
|
|
101
99
|
}
|
|
102
100
|
}
|
|
103
101
|
|
|
@@ -120,7 +118,7 @@ class Validator {
|
|
|
120
118
|
// Check string values for commas
|
|
121
119
|
if (typeof value === 'string' && value.includes(',')) {
|
|
122
120
|
throw new ValidationException(
|
|
123
|
-
`Field '${key}' contains invalid character: comma (
|
|
121
|
+
`Field '${key}' contains invalid character: comma (,). ` +
|
|
124
122
|
`Commas are not allowed in field values. Use underscores (_) or spaces instead, or use arrays for multiple values.`
|
|
125
123
|
);
|
|
126
124
|
}
|
|
@@ -130,7 +128,7 @@ class Validator {
|
|
|
130
128
|
for (const item of value) {
|
|
131
129
|
if (typeof item === 'string' && item.includes(',')) {
|
|
132
130
|
throw new ValidationException(
|
|
133
|
-
`Field '${key}' contains invalid character: comma (
|
|
131
|
+
`Field '${key}' contains invalid character: comma (,). ` +
|
|
134
132
|
`Array items cannot contain commas. Use underscores (_) or spaces instead.`
|
|
135
133
|
);
|
|
136
134
|
}
|
/package/{LICENSE → LICENSE.md}
RENAMED
|
File without changes
|