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 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
+ });