datagrok-tools 6.1.8 → 6.1.10
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/CHANGELOG.md +10 -0
- package/CLAUDE.md +68 -0
- package/bin/__tests__/build.test.js +116 -0
- package/bin/__tests__/build.test.ts +101 -0
- package/bin/__tests__/node-dapi.connections.test.js +120 -0
- package/bin/__tests__/node-dapi.connections.test.ts +84 -0
- package/bin/__tests__/node-dapi.groups.test.js +467 -0
- package/bin/__tests__/node-dapi.groups.test.ts +298 -0
- package/bin/__tests__/node-dapi.integration.test.js +406 -0
- package/bin/__tests__/node-dapi.integration.test.ts +447 -0
- package/bin/__tests__/node-dapi.shares.test.js +107 -0
- package/bin/__tests__/node-dapi.shares.test.ts +70 -0
- package/bin/__tests__/node-dapi.users.test.js +86 -0
- package/bin/__tests__/node-dapi.users.test.ts +58 -0
- package/bin/__tests__/server-output.test.js +171 -0
- package/bin/__tests__/server-output.test.ts +133 -0
- package/bin/__tests__/server.test.js +277 -0
- package/bin/__tests__/server.test.ts +197 -0
- package/bin/commands/build.js +1 -1
- package/bin/commands/create.js +8 -5
- package/bin/commands/help.js +61 -1
- package/bin/commands/publish.js +12 -1
- package/bin/commands/server.js +520 -0
- package/bin/grok.js +3 -1
- package/bin/utils/node-dapi.js +459 -0
- package/bin/utils/server-client.js +15 -0
- package/bin/utils/server-output.js +127 -0
- package/package-template/package.json +1 -1
- package/package.json +8 -3
- package/vitest.config.ts +25 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Datagrok-tools changelog
|
|
2
2
|
|
|
3
|
+
## 6.1.10 (2026-04-17)
|
|
4
|
+
|
|
5
|
+
* `grok s connections save` — create/update a connection from a JSON file (`--save-credentials` optional)
|
|
6
|
+
* `grok s connections test` — test connectivity by JSON body or by existing id/name
|
|
7
|
+
* `grok s users save` — create/update a user from a JSON file
|
|
8
|
+
* `grok s groups save` — create/update a group from a JSON file (`--save-relations` optional)
|
|
9
|
+
* `grok s shares add` — share an entity with one or more groups (`--access View|Edit`)
|
|
10
|
+
* `grok s shares list` — list who an entity is shared with
|
|
11
|
+
* Tools: Normalize package name to lowercase in grok create, preserve original as friendlyName
|
|
12
|
+
|
|
3
13
|
## 5.1.3 (2026-02-03)
|
|
4
14
|
|
|
5
15
|
* GROK-19407:
|
package/CLAUDE.md
CHANGED
|
@@ -28,6 +28,7 @@ The CLI uses a modular command pattern. Each command is a separate module that:
|
|
|
28
28
|
- `test-all.ts` - Run tests across multiple packages
|
|
29
29
|
- `stress-tests.ts` - Run stress tests (must be run from ApiTests package)
|
|
30
30
|
- `api.ts` - Auto-generate TypeScript wrappers for scripts/queries
|
|
31
|
+
- `server.ts` - Manage and inspect a running Datagrok server (`grok server` / `grok s`)
|
|
31
32
|
- `link.ts` - Link libraries for plugin development
|
|
32
33
|
- `claude.ts` - Launch a Dockerized dev environment with Datagrok + Claude Code
|
|
33
34
|
- `migrate.ts` - Update legacy packages
|
|
@@ -220,6 +221,73 @@ Based on `node:22-bookworm-slim`. Pre-installed:
|
|
|
220
221
|
|
|
221
222
|
See `.devcontainer/PACKAGES_DEV.md` for detailed usage docs, architecture diagram, MCP plugin setup (Jira/GitHub), and troubleshooting.
|
|
222
223
|
|
|
224
|
+
### `grok server` / `grok s` Command
|
|
225
|
+
|
|
226
|
+
Use `grok s` to inspect and debug a running Datagrok server from the CLI — list entities,
|
|
227
|
+
call functions, browse files, or hit any API endpoint. Reads config from `~/.grok/config.yaml`
|
|
228
|
+
(same as `grok publish`). Use `--host <alias|url>` to target a specific server.
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
# Create / update entities (from JSON file — supports both create and update)
|
|
232
|
+
grok s users save --json user.json
|
|
233
|
+
grok s groups save --json group.json --save-relations
|
|
234
|
+
|
|
235
|
+
# Share entities
|
|
236
|
+
grok s shares add "JohnDoe:MyConnection" Chemists,Admins --access Edit
|
|
237
|
+
grok s shares list <entity-uuid>
|
|
238
|
+
|
|
239
|
+
# List / inspect entities
|
|
240
|
+
grok s users list
|
|
241
|
+
grok s packages list --filter "MyPlugin" # check if a plugin is published
|
|
242
|
+
grok s connections list --output json
|
|
243
|
+
grok s functions list --filter "Chem" # find registered functions
|
|
244
|
+
grok s connections get <id>
|
|
245
|
+
grok s connections delete <id>
|
|
246
|
+
grok s connections save --json conn.json --save-credentials # create or update
|
|
247
|
+
grok s connections test "JohnDoe:MyConnection" # test by id or name
|
|
248
|
+
grok s connections test --json conn.json # test a connection defined in JSON
|
|
249
|
+
|
|
250
|
+
# Call a server function
|
|
251
|
+
grok s functions run 'Chem:smilesToMw("ccc")'
|
|
252
|
+
grok s functions run 'Pkg:fn({a:5,b:22})'
|
|
253
|
+
|
|
254
|
+
# Browse file storage
|
|
255
|
+
grok s files list "System:AppData" -r # list files recursively
|
|
256
|
+
grok s files list "System:AppData/MyPlugin"
|
|
257
|
+
|
|
258
|
+
# Manage group membership
|
|
259
|
+
grok s groups add-members Admins alice bob # add two users (non-admin)
|
|
260
|
+
grok s groups add-members Admins alice --admin # add as admin (flips if already member)
|
|
261
|
+
grok s groups add-members Admins alice --user # force personal-group lookup
|
|
262
|
+
grok s groups remove-members Admins alice bob # remove members
|
|
263
|
+
grok s groups list-members Admins # all members
|
|
264
|
+
grok s groups list-members Admins --admin # admin members only
|
|
265
|
+
grok s groups list-members Admins --no-admin # non-admin members only
|
|
266
|
+
grok s groups list-memberships alice # groups alice belongs to
|
|
267
|
+
|
|
268
|
+
# Hit any API endpoint directly
|
|
269
|
+
grok s raw GET /api/users/current
|
|
270
|
+
grok s raw GET /api/packages/dev/MyPlugin
|
|
271
|
+
|
|
272
|
+
# Describe entity JSON schema
|
|
273
|
+
grok s describe connections
|
|
274
|
+
|
|
275
|
+
# Target a specific server
|
|
276
|
+
grok s users list --host dev
|
|
277
|
+
grok s users list --host "https://my.datagrok.ai/api"
|
|
278
|
+
|
|
279
|
+
# Output formats: table (default), json, csv, quiet (IDs only)
|
|
280
|
+
grok s packages list --output json
|
|
281
|
+
grok s users list --output quiet | xargs ... # pipe IDs
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Windows Git Bash:** prefix raw paths with `MSYS_NO_PATHCONV=1` to prevent POSIX→Windows
|
|
285
|
+
path conversion: `MSYS_NO_PATHCONV=1 grok s raw GET /api/users/current`
|
|
286
|
+
|
|
287
|
+
**Implementation:** `bin/commands/server.ts`, `bin/utils/node-dapi.ts` (Node.js REST client),
|
|
288
|
+
`bin/utils/server-output.ts` (formatters). The Node.js dapi bypasses the Dart interop layer
|
|
289
|
+
and calls `/public/v1/` endpoints directly — same approach as the Python CLI.
|
|
290
|
+
|
|
223
291
|
## Key Patterns and Conventions
|
|
224
292
|
|
|
225
293
|
### Naming Conventions
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _vitest = require("vitest");
|
|
4
|
+
var _build = require("../commands/build");
|
|
5
|
+
(0, _vitest.describe)('getNestedValue', () => {
|
|
6
|
+
(0, _vitest.it)('returns value for a simple key', () => {
|
|
7
|
+
(0, _vitest.expect)((0, _build.getNestedValue)({
|
|
8
|
+
name: 'Chem'
|
|
9
|
+
}, 'name')).toBe('Chem');
|
|
10
|
+
});
|
|
11
|
+
(0, _vitest.it)('returns value for a nested path', () => {
|
|
12
|
+
(0, _vitest.expect)((0, _build.getNestedValue)({
|
|
13
|
+
a: {
|
|
14
|
+
b: {
|
|
15
|
+
c: 42
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}, 'a.b.c')).toBe(42);
|
|
19
|
+
});
|
|
20
|
+
(0, _vitest.it)('returns undefined for a missing key', () => {
|
|
21
|
+
(0, _vitest.expect)((0, _build.getNestedValue)({
|
|
22
|
+
name: 'Chem'
|
|
23
|
+
}, 'version')).toBeUndefined();
|
|
24
|
+
});
|
|
25
|
+
(0, _vitest.it)('returns undefined when a mid-path segment is null', () => {
|
|
26
|
+
(0, _vitest.expect)((0, _build.getNestedValue)({
|
|
27
|
+
a: null
|
|
28
|
+
}, 'a.b')).toBeUndefined();
|
|
29
|
+
});
|
|
30
|
+
(0, _vitest.it)('returns undefined when a mid-path segment is missing', () => {
|
|
31
|
+
(0, _vitest.expect)((0, _build.getNestedValue)({
|
|
32
|
+
a: {}
|
|
33
|
+
}, 'a.b.c')).toBeUndefined();
|
|
34
|
+
});
|
|
35
|
+
(0, _vitest.it)('returns undefined for an empty path (splits to empty string key)', () => {
|
|
36
|
+
(0, _vitest.expect)((0, _build.getNestedValue)({
|
|
37
|
+
x: 1
|
|
38
|
+
}, '')).toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
const pkg = overrides => ({
|
|
42
|
+
dir: '/tmp/pkg',
|
|
43
|
+
name: overrides.name ?? 'test-pkg',
|
|
44
|
+
friendlyName: overrides.friendlyName ?? overrides.name ?? 'Test Pkg',
|
|
45
|
+
version: overrides.version ?? '1.0.0',
|
|
46
|
+
packageJson: overrides
|
|
47
|
+
});
|
|
48
|
+
(0, _vitest.describe)('applyFilter', () => {
|
|
49
|
+
const packages = [pkg({
|
|
50
|
+
name: 'Chem',
|
|
51
|
+
version: '1.5.0',
|
|
52
|
+
category: 'Cheminformatics'
|
|
53
|
+
}), pkg({
|
|
54
|
+
name: 'Bio',
|
|
55
|
+
version: '2.0.0',
|
|
56
|
+
category: 'Bioinformatics'
|
|
57
|
+
}), pkg({
|
|
58
|
+
name: 'PowerGrid',
|
|
59
|
+
version: '1.5.0',
|
|
60
|
+
category: 'Viewers'
|
|
61
|
+
})];
|
|
62
|
+
(0, _vitest.it)('returns all packages when filter matches all', () => {
|
|
63
|
+
(0, _vitest.expect)((0, _build.applyFilter)(packages, 'name:.')).toHaveLength(3);
|
|
64
|
+
});
|
|
65
|
+
(0, _vitest.it)('filters by exact name match', () => {
|
|
66
|
+
const result = (0, _build.applyFilter)(packages, 'name:^Chem$');
|
|
67
|
+
(0, _vitest.expect)(result).toHaveLength(1);
|
|
68
|
+
(0, _vitest.expect)(result[0].name).toBe('Chem');
|
|
69
|
+
});
|
|
70
|
+
(0, _vitest.it)('filters by partial name (regex substring)', () => {
|
|
71
|
+
const result = (0, _build.applyFilter)(packages, 'name:Bio');
|
|
72
|
+
(0, _vitest.expect)(result).toHaveLength(1);
|
|
73
|
+
(0, _vitest.expect)(result[0].name).toBe('Bio');
|
|
74
|
+
});
|
|
75
|
+
(0, _vitest.it)('returns empty array when nothing matches', () => {
|
|
76
|
+
(0, _vitest.expect)((0, _build.applyFilter)(packages, 'name:NOMATCH')).toHaveLength(0);
|
|
77
|
+
});
|
|
78
|
+
(0, _vitest.it)('filters by version', () => {
|
|
79
|
+
const result = (0, _build.applyFilter)(packages, 'version:^1\\.5');
|
|
80
|
+
(0, _vitest.expect)(result).toHaveLength(2);
|
|
81
|
+
(0, _vitest.expect)(result.map(p => p.name)).toEqual(_vitest.expect.arrayContaining(['Chem', 'PowerGrid']));
|
|
82
|
+
});
|
|
83
|
+
(0, _vitest.it)('applies && conjunction (both conditions must match)', () => {
|
|
84
|
+
const result = (0, _build.applyFilter)(packages, 'name:Chem && version:1\\.5');
|
|
85
|
+
(0, _vitest.expect)(result).toHaveLength(1);
|
|
86
|
+
(0, _vitest.expect)(result[0].name).toBe('Chem');
|
|
87
|
+
});
|
|
88
|
+
(0, _vitest.it)('returns empty when one part of && conjunction fails', () => {
|
|
89
|
+
(0, _vitest.expect)((0, _build.applyFilter)(packages, 'name:Chem && version:^2')).toHaveLength(0);
|
|
90
|
+
});
|
|
91
|
+
(0, _vitest.it)('filters by nested field', () => {
|
|
92
|
+
const withNested = [pkg({
|
|
93
|
+
name: 'A',
|
|
94
|
+
datagrok: {
|
|
95
|
+
apiVersion: '1.0'
|
|
96
|
+
}
|
|
97
|
+
}), pkg({
|
|
98
|
+
name: 'B',
|
|
99
|
+
datagrok: {
|
|
100
|
+
apiVersion: '2.0'
|
|
101
|
+
}
|
|
102
|
+
})];
|
|
103
|
+
const result = (0, _build.applyFilter)(withNested, 'datagrok.apiVersion:^1');
|
|
104
|
+
(0, _vitest.expect)(result).toHaveLength(1);
|
|
105
|
+
(0, _vitest.expect)(result[0].name).toBe('A');
|
|
106
|
+
});
|
|
107
|
+
(0, _vitest.it)('returns empty when field does not exist', () => {
|
|
108
|
+
(0, _vitest.expect)((0, _build.applyFilter)(packages, 'nonexistent:anything')).toHaveLength(0);
|
|
109
|
+
});
|
|
110
|
+
(0, _vitest.it)('treats filter with no colon as field name with match-all pattern', () => {
|
|
111
|
+
// No colon → field = whole string, pattern = /./ (matches any value)
|
|
112
|
+
// The function returns packages where the field exists and is non-empty
|
|
113
|
+
const result = (0, _build.applyFilter)(packages, 'name');
|
|
114
|
+
(0, _vitest.expect)(result).toHaveLength(3);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {describe, it, expect} from 'vitest';
|
|
2
|
+
import {getNestedValue, applyFilter} from '../commands/build';
|
|
3
|
+
|
|
4
|
+
describe('getNestedValue', () => {
|
|
5
|
+
it('returns value for a simple key', () => {
|
|
6
|
+
expect(getNestedValue({name: 'Chem'}, 'name')).toBe('Chem');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('returns value for a nested path', () => {
|
|
10
|
+
expect(getNestedValue({a: {b: {c: 42}}}, 'a.b.c')).toBe(42);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('returns undefined for a missing key', () => {
|
|
14
|
+
expect(getNestedValue({name: 'Chem'}, 'version')).toBeUndefined();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('returns undefined when a mid-path segment is null', () => {
|
|
18
|
+
expect(getNestedValue({a: null}, 'a.b')).toBeUndefined();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('returns undefined when a mid-path segment is missing', () => {
|
|
22
|
+
expect(getNestedValue({a: {}}, 'a.b.c')).toBeUndefined();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('returns undefined for an empty path (splits to empty string key)', () => {
|
|
26
|
+
expect(getNestedValue({x: 1}, '')).toBeUndefined();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const pkg = (overrides: Record<string, any>) => ({
|
|
31
|
+
dir: '/tmp/pkg',
|
|
32
|
+
name: overrides.name ?? 'test-pkg',
|
|
33
|
+
friendlyName: overrides.friendlyName ?? overrides.name ?? 'Test Pkg',
|
|
34
|
+
version: overrides.version ?? '1.0.0',
|
|
35
|
+
packageJson: overrides,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('applyFilter', () => {
|
|
39
|
+
const packages = [
|
|
40
|
+
pkg({name: 'Chem', version: '1.5.0', category: 'Cheminformatics'}),
|
|
41
|
+
pkg({name: 'Bio', version: '2.0.0', category: 'Bioinformatics'}),
|
|
42
|
+
pkg({name: 'PowerGrid', version: '1.5.0', category: 'Viewers'}),
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
it('returns all packages when filter matches all', () => {
|
|
46
|
+
expect(applyFilter(packages, 'name:.')).toHaveLength(3);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('filters by exact name match', () => {
|
|
50
|
+
const result = applyFilter(packages, 'name:^Chem$');
|
|
51
|
+
expect(result).toHaveLength(1);
|
|
52
|
+
expect(result[0].name).toBe('Chem');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('filters by partial name (regex substring)', () => {
|
|
56
|
+
const result = applyFilter(packages, 'name:Bio');
|
|
57
|
+
expect(result).toHaveLength(1);
|
|
58
|
+
expect(result[0].name).toBe('Bio');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('returns empty array when nothing matches', () => {
|
|
62
|
+
expect(applyFilter(packages, 'name:NOMATCH')).toHaveLength(0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('filters by version', () => {
|
|
66
|
+
const result = applyFilter(packages, 'version:^1\\.5');
|
|
67
|
+
expect(result).toHaveLength(2);
|
|
68
|
+
expect(result.map((p) => p.name)).toEqual(expect.arrayContaining(['Chem', 'PowerGrid']));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('applies && conjunction (both conditions must match)', () => {
|
|
72
|
+
const result = applyFilter(packages, 'name:Chem && version:1\\.5');
|
|
73
|
+
expect(result).toHaveLength(1);
|
|
74
|
+
expect(result[0].name).toBe('Chem');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('returns empty when one part of && conjunction fails', () => {
|
|
78
|
+
expect(applyFilter(packages, 'name:Chem && version:^2')).toHaveLength(0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('filters by nested field', () => {
|
|
82
|
+
const withNested = [
|
|
83
|
+
pkg({name: 'A', datagrok: {apiVersion: '1.0'}}),
|
|
84
|
+
pkg({name: 'B', datagrok: {apiVersion: '2.0'}}),
|
|
85
|
+
];
|
|
86
|
+
const result = applyFilter(withNested, 'datagrok.apiVersion:^1');
|
|
87
|
+
expect(result).toHaveLength(1);
|
|
88
|
+
expect(result[0].name).toBe('A');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('returns empty when field does not exist', () => {
|
|
92
|
+
expect(applyFilter(packages, 'nonexistent:anything')).toHaveLength(0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('treats filter with no colon as field name with match-all pattern', () => {
|
|
96
|
+
// No colon → field = whole string, pattern = /./ (matches any value)
|
|
97
|
+
// The function returns packages where the field exists and is non-empty
|
|
98
|
+
const result = applyFilter(packages, 'name');
|
|
99
|
+
expect(result).toHaveLength(3);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _vitest = require("vitest");
|
|
4
|
+
var _nodeDapi = require("../utils/node-dapi");
|
|
5
|
+
function makeMock(responder) {
|
|
6
|
+
const calls = [];
|
|
7
|
+
const client = {
|
|
8
|
+
async request(method, path, body) {
|
|
9
|
+
calls.push({
|
|
10
|
+
method,
|
|
11
|
+
path,
|
|
12
|
+
body
|
|
13
|
+
});
|
|
14
|
+
return responder(method, path, body);
|
|
15
|
+
},
|
|
16
|
+
get(path) {
|
|
17
|
+
return this.request('GET', path);
|
|
18
|
+
},
|
|
19
|
+
post(path, body) {
|
|
20
|
+
return this.request('POST', path, body);
|
|
21
|
+
},
|
|
22
|
+
del(path) {
|
|
23
|
+
return this.request('DELETE', path);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
client,
|
|
28
|
+
calls
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const CONN = {
|
|
32
|
+
name: 'TestPg',
|
|
33
|
+
dataSource: 'PostgreSQL',
|
|
34
|
+
server: 'localhost',
|
|
35
|
+
port: 5432,
|
|
36
|
+
db: 'datagrok'
|
|
37
|
+
};
|
|
38
|
+
(0, _vitest.describe)('NodeConnectionsDataSource.save', () => {
|
|
39
|
+
(0, _vitest.it)('POSTs to /public/v1/connections without saveCredentials by default', async () => {
|
|
40
|
+
const {
|
|
41
|
+
client,
|
|
42
|
+
calls
|
|
43
|
+
} = makeMock((method, path, body) => {
|
|
44
|
+
if (method === 'POST' && path === '/public/v1/connections') return {
|
|
45
|
+
...body,
|
|
46
|
+
id: 'new-id'
|
|
47
|
+
};
|
|
48
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
49
|
+
});
|
|
50
|
+
const ds = new _nodeDapi.NodeConnectionsDataSource(client);
|
|
51
|
+
const saved = await ds.save(CONN);
|
|
52
|
+
(0, _vitest.expect)(saved.id).toBe('new-id');
|
|
53
|
+
(0, _vitest.expect)(calls[0].path).toBe('/public/v1/connections');
|
|
54
|
+
(0, _vitest.expect)(calls[0].body).toEqual(CONN);
|
|
55
|
+
});
|
|
56
|
+
(0, _vitest.it)('appends saveCredentials=true when requested', async () => {
|
|
57
|
+
const {
|
|
58
|
+
client,
|
|
59
|
+
calls
|
|
60
|
+
} = makeMock((method, path, body) => {
|
|
61
|
+
if (method === 'POST' && path === '/public/v1/connections?saveCredentials=true') return {
|
|
62
|
+
...body,
|
|
63
|
+
id: 'new-id'
|
|
64
|
+
};
|
|
65
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
66
|
+
});
|
|
67
|
+
const ds = new _nodeDapi.NodeConnectionsDataSource(client);
|
|
68
|
+
await ds.save(CONN, true);
|
|
69
|
+
(0, _vitest.expect)(calls[0].path).toBe('/public/v1/connections?saveCredentials=true');
|
|
70
|
+
});
|
|
71
|
+
(0, _vitest.it)('omits saveCredentials query param when false', async () => {
|
|
72
|
+
const {
|
|
73
|
+
client,
|
|
74
|
+
calls
|
|
75
|
+
} = makeMock((method, path) => {
|
|
76
|
+
if (method === 'POST' && path === '/public/v1/connections') return {
|
|
77
|
+
id: 'new-id'
|
|
78
|
+
};
|
|
79
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
80
|
+
});
|
|
81
|
+
const ds = new _nodeDapi.NodeConnectionsDataSource(client);
|
|
82
|
+
await ds.save(CONN, false);
|
|
83
|
+
(0, _vitest.expect)(calls[0].path).toBe('/public/v1/connections');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
(0, _vitest.describe)('NodeConnectionsDataSource.test', () => {
|
|
87
|
+
(0, _vitest.it)('POSTs the connection body to /public/v1/connections/test and resolves on "ok"', async () => {
|
|
88
|
+
const {
|
|
89
|
+
client,
|
|
90
|
+
calls
|
|
91
|
+
} = makeMock((method, path) => {
|
|
92
|
+
if (method === 'POST' && path === '/public/v1/connections/test') return 'ok';
|
|
93
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
94
|
+
});
|
|
95
|
+
const ds = new _nodeDapi.NodeConnectionsDataSource(client);
|
|
96
|
+
await ds.test(CONN);
|
|
97
|
+
(0, _vitest.expect)(calls[0].body).toEqual(CONN);
|
|
98
|
+
});
|
|
99
|
+
(0, _vitest.it)('strips surrounding quotes on a JSON-encoded "ok"', async () => {
|
|
100
|
+
const {
|
|
101
|
+
client
|
|
102
|
+
} = makeMock(() => '"ok"');
|
|
103
|
+
const ds = new _nodeDapi.NodeConnectionsDataSource(client);
|
|
104
|
+
await (0, _vitest.expect)(ds.test(CONN)).resolves.toBeUndefined();
|
|
105
|
+
});
|
|
106
|
+
(0, _vitest.it)('throws the server error text when the response is not ok', async () => {
|
|
107
|
+
const {
|
|
108
|
+
client
|
|
109
|
+
} = makeMock(() => 'connection refused: tcp://localhost:5432');
|
|
110
|
+
const ds = new _nodeDapi.NodeConnectionsDataSource(client);
|
|
111
|
+
await (0, _vitest.expect)(ds.test(CONN)).rejects.toThrow(/connection refused/);
|
|
112
|
+
});
|
|
113
|
+
(0, _vitest.it)('throws a generic message on empty response', async () => {
|
|
114
|
+
const {
|
|
115
|
+
client
|
|
116
|
+
} = makeMock(() => '');
|
|
117
|
+
const ds = new _nodeDapi.NodeConnectionsDataSource(client);
|
|
118
|
+
await (0, _vitest.expect)(ds.test(CONN)).rejects.toThrow(/failed/i);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {describe, it, expect} from 'vitest';
|
|
2
|
+
import {NodeConnectionsDataSource} from '../utils/node-dapi';
|
|
3
|
+
|
|
4
|
+
interface Call {method: string; path: string; body?: any}
|
|
5
|
+
|
|
6
|
+
function makeMock(responder: (method: string, path: string, body?: any) => any) {
|
|
7
|
+
const calls: Call[] = [];
|
|
8
|
+
const client: any = {
|
|
9
|
+
async request(method: string, path: string, body?: any) {
|
|
10
|
+
calls.push({method, path, body});
|
|
11
|
+
return responder(method, path, body);
|
|
12
|
+
},
|
|
13
|
+
get(path: string) { return this.request('GET', path); },
|
|
14
|
+
post(path: string, body?: any) { return this.request('POST', path, body); },
|
|
15
|
+
del(path: string) { return this.request('DELETE', path); },
|
|
16
|
+
};
|
|
17
|
+
return {client, calls};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const CONN = {name: 'TestPg', dataSource: 'PostgreSQL', server: 'localhost', port: 5432, db: 'datagrok'};
|
|
21
|
+
|
|
22
|
+
describe('NodeConnectionsDataSource.save', () => {
|
|
23
|
+
it('POSTs to /public/v1/connections without saveCredentials by default', async () => {
|
|
24
|
+
const {client, calls} = makeMock((method, path, body) => {
|
|
25
|
+
if (method === 'POST' && path === '/public/v1/connections') return {...body, id: 'new-id'};
|
|
26
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
27
|
+
});
|
|
28
|
+
const ds = new NodeConnectionsDataSource(client);
|
|
29
|
+
const saved = await ds.save(CONN);
|
|
30
|
+
expect(saved.id).toBe('new-id');
|
|
31
|
+
expect(calls[0].path).toBe('/public/v1/connections');
|
|
32
|
+
expect(calls[0].body).toEqual(CONN);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('appends saveCredentials=true when requested', async () => {
|
|
36
|
+
const {client, calls} = makeMock((method, path, body) => {
|
|
37
|
+
if (method === 'POST' && path === '/public/v1/connections?saveCredentials=true') return {...body, id: 'new-id'};
|
|
38
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
39
|
+
});
|
|
40
|
+
const ds = new NodeConnectionsDataSource(client);
|
|
41
|
+
await ds.save(CONN, true);
|
|
42
|
+
expect(calls[0].path).toBe('/public/v1/connections?saveCredentials=true');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('omits saveCredentials query param when false', async () => {
|
|
46
|
+
const {client, calls} = makeMock((method, path) => {
|
|
47
|
+
if (method === 'POST' && path === '/public/v1/connections') return {id: 'new-id'};
|
|
48
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
49
|
+
});
|
|
50
|
+
const ds = new NodeConnectionsDataSource(client);
|
|
51
|
+
await ds.save(CONN, false);
|
|
52
|
+
expect(calls[0].path).toBe('/public/v1/connections');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('NodeConnectionsDataSource.test', () => {
|
|
57
|
+
it('POSTs the connection body to /public/v1/connections/test and resolves on "ok"', async () => {
|
|
58
|
+
const {client, calls} = makeMock((method, path) => {
|
|
59
|
+
if (method === 'POST' && path === '/public/v1/connections/test') return 'ok';
|
|
60
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
61
|
+
});
|
|
62
|
+
const ds = new NodeConnectionsDataSource(client);
|
|
63
|
+
await ds.test(CONN);
|
|
64
|
+
expect(calls[0].body).toEqual(CONN);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('strips surrounding quotes on a JSON-encoded "ok"', async () => {
|
|
68
|
+
const {client} = makeMock(() => '"ok"');
|
|
69
|
+
const ds = new NodeConnectionsDataSource(client);
|
|
70
|
+
await expect(ds.test(CONN)).resolves.toBeUndefined();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('throws the server error text when the response is not ok', async () => {
|
|
74
|
+
const {client} = makeMock(() => 'connection refused: tcp://localhost:5432');
|
|
75
|
+
const ds = new NodeConnectionsDataSource(client);
|
|
76
|
+
await expect(ds.test(CONN)).rejects.toThrow(/connection refused/);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('throws a generic message on empty response', async () => {
|
|
80
|
+
const {client} = makeMock(() => '');
|
|
81
|
+
const ds = new NodeConnectionsDataSource(client);
|
|
82
|
+
await expect(ds.test(CONN)).rejects.toThrow(/failed/i);
|
|
83
|
+
});
|
|
84
|
+
});
|