datagrok-tools 6.1.9 → 6.1.11

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,20 @@
1
1
  # Datagrok-tools changelog
2
2
 
3
+ ## 6.1.11 (2026-04-27)
4
+
5
+ * `grok report read <path | instance number>` — normalize a report zip/json into one JSON object on stdout (envelope unwrap, `_meta.json` merge, optional `--extract-screenshot` / `--extract-d42` / `--extract-actions`)
6
+ * `grok report fetch` — single round-trip via the new `/reports/by-number/<number>/zip` endpoint, with fallback to the legacy search-then-download path on 404
7
+
8
+ ## 6.1.10 (2026-04-17)
9
+
10
+ * `grok s connections save` — create/update a connection from a JSON file (`--save-credentials` optional)
11
+ * `grok s connections test` — test connectivity by JSON body or by existing id/name
12
+ * `grok s users save` — create/update a user from a JSON file
13
+ * `grok s groups save` — create/update a group from a JSON file (`--save-relations` optional)
14
+ * `grok s shares add` — share an entity with one or more groups (`--access View|Edit`)
15
+ * `grok s shares list` — list who an entity is shared with
16
+ * Tools: Normalize package name to lowercase in grok create, preserve original as friendlyName
17
+
3
18
  ## 5.1.3 (2026-02-03)
4
19
 
5
20
  * 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
package/GROK_S.md ADDED
@@ -0,0 +1,361 @@
1
+ # Managing a Datagrok Server with `grok s`
2
+
3
+ The `grok server` command (alias `grok s`) talks to a running Datagrok instance over
4
+ its public REST API. It is the right tool for **any server-management task** that
5
+ does not require rendering the UI: creating users and groups, sharing entities,
6
+ running functions, browsing files, hitting raw API endpoints, or scripting bulk
7
+ imports.
8
+
9
+ Use this instead of UI automation (Playwright, Selenium) whenever the goal is data
10
+ management — it is faster, idempotent, scriptable, and does not depend on the
11
+ browser or a logged-in session.
12
+
13
+ ## When to use
14
+
15
+ | Task | Command |
16
+ |-------------------------------------------------------------|--------------------------------------------------------|
17
+ | Create / update a user | `grok s users save --json user.json` |
18
+ | Block / unblock a user | `grok s users block <login>` / `users unblock <login>` |
19
+ | Create / update a group | `grok s groups save --json group.json` |
20
+ | Add or remove users in a group | `grok s groups add-members <group> <user>...` |
21
+ | List members of a group | `grok s groups list-members <group>` |
22
+ | List the groups a user belongs to | `grok s groups list-memberships <user>` |
23
+ | Share a connection / query / script / project with a group | `grok s shares add <entity> <group> [--access View\|Edit]` |
24
+ | See who an entity is shared with | `grok s shares list <entity-id>` |
25
+ | Create / update a connection | `grok s connections save --json conn.json` |
26
+ | Test a connection | `grok s connections test <id-or-name>` |
27
+ | Run a registered function | `grok s functions run 'Pkg:fn(arg1,arg2)'` |
28
+ | List functions with rich filters | `grok s functions list --type script --language python --package Chem` |
29
+ | List / browse files in a file share | `grok s files list "System:AppData" -r` |
30
+ | Upload / download a table (CSV) | `grok s tables upload <name> file.csv` / `tables download <name> -O out.csv` |
31
+ | Check whether a package is deployed | `grok s packages list --filter "MyPlugin"` |
32
+ | Hit any undocumented endpoint | `grok s raw GET /api/users/current` |
33
+ | Check server + per-module health | `grok s healthcheck [--module <name>]` |
34
+ | Bulk operations in one round-trip | `grok s batch <entity> <verb> --json items.json` |
35
+
36
+ ## Configuration
37
+
38
+ Servers and credentials live in `~/.grok/config.yaml`:
39
+
40
+ ```yaml
41
+ default: local
42
+ servers:
43
+ local:
44
+ url: http://localhost:8888/api
45
+ key: admin
46
+ dev:
47
+ url: https://dev.datagrok.ai/api
48
+ key: <developer-key>
49
+ ```
50
+
51
+ - `grok config --server --alias <name> --server <url> --key <key>` writes a new entry.
52
+ - Add `--default` to make it the active server.
53
+ - Every `grok s ...` command accepts `--host <alias-or-url>` to override the default.
54
+
55
+ ## Entity operations
56
+
57
+ ### List / get / delete
58
+
59
+ ```bash
60
+ grok s users list # default: table, 50 rows
61
+ grok s users list --filter "login = 'admin'" # smart filter
62
+ grok s connections list --output json
63
+ grok s packages list --filter "name:MyPlugin" # table shows friendlyName
64
+ grok s groups get <id-or-name>
65
+ grok s connections delete <id>
66
+ ```
67
+
68
+ Options that work on every entity:
69
+
70
+ | Flag | Meaning |
71
+ |-----------------------|---------------------------------------------------|
72
+ | `--output table\|json\|csv\|quiet` | Output format; `quiet` prints ids only |
73
+ | `--filter "<expr>"` | Server-side smart filter |
74
+ | `--limit <n>` | Page size (default 50) |
75
+ | `--offset <n>` | Skip first `n` rows |
76
+ | `--host <alias\|url>` | Target a specific server from your config |
77
+
78
+ ### Save from JSON
79
+
80
+ `grok s users save` and `grok s groups save` accept the same JSON shape that the
81
+ REST API returns on `get`. The simplest valid bodies:
82
+
83
+ ```json
84
+ // user.json
85
+ { "#type": "User", "login": "alice.mendel", "firstName": "Alice", "lastName": "Mendel", "status": "active" }
86
+ ```
87
+
88
+ ```json
89
+ // group.json
90
+ { "#type": "UserGroup", "name": "Chemists", "friendlyName": "Chemists" }
91
+ ```
92
+
93
+ ```bash
94
+ grok s users save --json user.json
95
+ grok s groups save --json group.json --save-relations
96
+ ```
97
+
98
+ Include an `id` to update an existing entity; omit it to create. To introspect the
99
+ shape of an existing entity, use `grok s <entity> get <id-or-name> --output json`.
100
+
101
+ ### Connections
102
+
103
+ ```bash
104
+ grok s connections save --json conn.json --save-credentials
105
+ grok s connections test "MyUser:MyConnection" # by "author:name"
106
+ grok s connections test --json conn.json # test before saving
107
+ ```
108
+
109
+ See `public/packages/Chembl/connections/` for connection JSON examples.
110
+
111
+ ## Group membership
112
+
113
+ `grok s groups add-members` is idempotent — it resolves each member (by login, name,
114
+ or UUID), compares against the group's current children, and only writes when
115
+ something changes. Results are returned per member with statuses `added`, `updated`,
116
+ `noop`, `not-member`, or `error`.
117
+
118
+ ```bash
119
+ grok s groups add-members Chemists alice.mendel bob.curie
120
+ grok s groups add-members Chemists alice.mendel --admin # promote / add as admin
121
+ grok s groups add-members Admins analysts # nest a group inside a group
122
+ grok s groups add-members Chemists alice.mendel --user # force personal-group lookup
123
+ grok s groups remove-members Chemists alice.mendel
124
+ grok s groups list-members Chemists # all members
125
+ grok s groups list-members Chemists --admin # admin members only
126
+ grok s groups list-members Chemists --no-admin # non-admin members only
127
+ grok s groups list-memberships alice.mendel # groups a user belongs to
128
+ ```
129
+
130
+ When a name is ambiguous the command prints every matching group and exits non-zero —
131
+ pass a UUID to disambiguate, or add `--user` to restrict lookup to personal groups.
132
+
133
+ ## User administration
134
+
135
+ ```bash
136
+ grok s users block alice.mendel # accept login, UUID, or namespace:name
137
+ grok s users unblock alice.mendel
138
+ ```
139
+
140
+ Both commands resolve the argument to a full user record first, so the server receives
141
+ the entire object (matching the Python client's `grok.users.block(user)`). Blocking
142
+ flips `status` to `Blocked` and terminates active sessions; unblocking restores `Active`.
143
+
144
+ ## Sharing entities
145
+
146
+ ```bash
147
+ grok s shares add "MyUser:MyConnection" Chemists,Biologists --access Edit
148
+ grok s shares list <entity-uuid>
149
+ ```
150
+
151
+ The entity argument accepts either a UUID or an `"author:name"` pair. `--access`
152
+ defaults to `View`.
153
+
154
+ ## Running functions
155
+
156
+ ```bash
157
+ grok s functions run 'Chem:smilesToMw("CCO")' # positional args
158
+ grok s functions run 'Pkg:fn({smiles:"CCO", radius:2})' # named args
159
+ grok s functions run Pkg:fn --json params.json # big input from a file
160
+ ```
161
+
162
+ ## Listing functions with filters
163
+
164
+ `grok s functions list` composes a smart filter from flags so you don't have to hand-roll
165
+ `text=` expressions. All flags are optional and combine with `and`; `--filter` lets you
166
+ add any other smart-filter clause.
167
+
168
+ ```bash
169
+ grok s functions list --type script --language python # all python scripts
170
+ grok s functions list --type query --package Chembl # data-queries in one package
171
+ grok s functions list --package PowerPack --type package # package-bundled funcs only
172
+ grok s functions list --language r --limit 20 --output json
173
+ grok s functions list --type script --filter 'name contains "similarity"'
174
+ ```
175
+
176
+ Flag cheat sheet:
177
+
178
+ | Flag | Values | Notes |
179
+ |--------------|------------------------------------------------------------|-------------------------------------------------------------------|
180
+ | `--type` | `script`, `query` (alias for `data-query`), `function`, `package` | `function` matches both standalone and package-bundled funcs |
181
+ | `--language` | `python`, `r`, `julia`, `nodejs`, `octave`, `grok`, `javascript`, `pyodide` | Filters in the CLI (the server doesn't index Script.language); implies `--type script` when used alone |
182
+ | `--package` | package short name (e.g. `Chem`, `PowerPack`) | Maps to `package.shortName` in the smart filter |
183
+ | `--filter` | any smart-filter expression | Combined with the other flags via `and` |
184
+ | `--limit` / `--offset` | integers | Applied client-side — the public list endpoint returns the full match set |
185
+
186
+ ## Files
187
+
188
+ Remote paths are `<connector>/<file-path>`, where `<connector>` is the connection's
189
+ full name — including any namespace — e.g. `System:DemoFiles/smiles.csv`. The
190
+ separator between connector and path is the first `/`; the connector part may
191
+ contain colons (the namespace separator).
192
+
193
+ ```bash
194
+ grok s files list "System:AppData" -r # recursive
195
+ grok s files list "System:AppData/MyPlugin"
196
+ grok s files get "System:AppData/MyPlugin/config.json"
197
+ grok s files put ./smiles.csv "System:DemoFiles/smiles.csv" # upload local file
198
+ grok s files delete "System:AppData/MyPlugin/old.csv"
199
+ ```
200
+
201
+ `files put` streams the file as raw bytes (no base64), so it handles GB-scale uploads
202
+ without blowing up memory. Use `batch files.put` only when you want to bundle an
203
+ upload with other operations in a single round-trip (the batch path base64-encodes
204
+ the `source` before sending).
205
+
206
+ ## Tables
207
+
208
+ CSV-level table I/O — the shell counterpart to Python's `grok.tables.upload/download`.
209
+ Unlike `files put`, this registers a proper Datagrok table entity (returns `{ID, ...}`).
210
+
211
+ ```bash
212
+ grok s tables upload MyTable ./data.csv # CSV → table
213
+ grok s tables upload MyTable ./data.csv --output json # get ID and markup back
214
+ grok s tables download MyTable # CSV to stdout (pipe-friendly)
215
+ grok s tables download MyTable -O ./data.csv # CSV to a local file
216
+ grok s tables download <uuid> # UUID or namespace:name both work
217
+ ```
218
+
219
+ Upload streams raw bytes with `Content-Type: text/csv` so it handles large tables
220
+ without loading the whole file into a JSON envelope. `-O` / `--output-file` avoids
221
+ colliding with the format flag (`--output table|json|csv|quiet`), which still controls
222
+ how the upload result is printed.
223
+
224
+ ## Server health
225
+
226
+ ```bash
227
+ grok s healthcheck # full per-module health
228
+ grok s healthcheck --module scripting # filter to one module
229
+ grok s healthcheck --output json # machine-readable
230
+ ```
231
+
232
+ Hits `GET /public/v1/healthcheck`. Response:
233
+
234
+ ```json
235
+ {
236
+ "status": "ok",
237
+ "server": "https://public.datagrok.ai",
238
+ "version": "1.27",
239
+ "time": "2026-04-19T19:20:00.000Z",
240
+ "services": [
241
+ { "key": "scripting", "type": "Service", "name": "...", "status": "Running", "started": true, "enabled": true, "time": "..." }
242
+ ]
243
+ }
244
+ ```
245
+
246
+ `services` is the same payload as `/admin/health` (per-`GrokServiceInfo` records).
247
+ Requires a valid dev key (standard `grok s` auth). For an anonymous liveness probe —
248
+ load balancer, k8s readiness — hit `/admin/health` directly; it's on the server's
249
+ unauthenticated allowlist.
250
+
251
+ ## Raw API access
252
+
253
+ When no dedicated subcommand exists, fall through to `grok s raw`:
254
+
255
+ ```bash
256
+ grok s raw GET /api/users/current
257
+ grok s raw GET /api/packages/dev/MyPlugin
258
+ grok s raw POST /api/admin/reload-settings
259
+ ```
260
+
261
+ On **Windows Git Bash**, prefix raw paths with `MSYS_NO_PATHCONV=1` to stop the shell
262
+ from rewriting POSIX paths into Windows paths:
263
+
264
+ ```bash
265
+ MSYS_NO_PATHCONV=1 grok s raw GET /api/users/current
266
+ ```
267
+
268
+ ## Batch operations
269
+
270
+ Apply the same verb to many items in one round-trip:
271
+
272
+ ```bash
273
+ # Inline args
274
+ grok s batch files delete "System:AppData/old1.txt" "System:AppData/old2.txt"
275
+
276
+ # From a JSON array
277
+ grok s batch users save --json users.json # [{...user1}, {...user2}, ...]
278
+
279
+ # Full workflow manifest — mixed actions, optional transaction / stopOnError
280
+ grok s batch manifest.json
281
+ ```
282
+
283
+ Manifest shape:
284
+
285
+ ```json
286
+ {
287
+ "stopOnError": true,
288
+ "transaction": false,
289
+ "operations": [
290
+ { "id": "op1", "action": "users.save", "params": {"login": "alice", "firstName": "Alice"} },
291
+ { "id": "op2", "action": "groups.save", "params": {"name": "Chemists"} }
292
+ ]
293
+ }
294
+ ```
295
+
296
+ For `files.put`, add `"source": "<local-path>"` and the CLI base64-encodes the file
297
+ into `content` before sending.
298
+
299
+ ## Scripting pattern
300
+
301
+ `--output quiet` and `--output json` make `grok s` safe to compose with standard
302
+ shell tooling:
303
+
304
+ ```bash
305
+ # Pipe IDs
306
+ grok s users list --filter "status = 'active'" --output quiet \
307
+ | xargs -I{} grok s users get {}
308
+
309
+ # Filter with jq
310
+ grok s connections list --output json \
311
+ | jq '.[] | select(.dataSource=="Postgres") | .name'
312
+
313
+ # Generate JSON on the fly
314
+ for login in alice bob carol; do
315
+ printf '{"#type":"User","login":"%s","firstName":"%s","status":"active"}\n' \
316
+ "$login" "${login^}" > user.json
317
+ grok s users save --json user.json
318
+ done
319
+ ```
320
+
321
+ ## Worked example: seed users and populate groups
322
+
323
+ Create a group, bulk-create users, and drop each into the right group without touching
324
+ the UI:
325
+
326
+ ```bash
327
+ # 1. Create the group (idempotent if you include the existing id)
328
+ cat > /tmp/g.json <<'EOF'
329
+ { "#type": "UserGroup", "name": "Chemists", "friendlyName": "Chemists" }
330
+ EOF
331
+ grok s groups save --json /tmp/g.json
332
+
333
+ # 2. Create 8 users
334
+ for row in \
335
+ "alice.mendeleev:Alice:Mendeleev" \
336
+ "bob.curie:Bob:Curie" \
337
+ "carol.pauling:Carol:Pauling" ; do
338
+ IFS=: read -r login first last <<<"$row"
339
+ cat > /tmp/u.json <<EOF
340
+ { "#type": "User", "login": "$login", "firstName": "$first", "lastName": "$last", "status": "active" }
341
+ EOF
342
+ grok s users save --json /tmp/u.json --output quiet
343
+ done
344
+
345
+ # 3. Add them all to the group in one call (resolves logins to personal groups)
346
+ grok s groups add-members Chemists alice.mendeleev bob.curie carol.pauling --user
347
+
348
+ # 4. Verify
349
+ grok s groups list-members Chemists --no-admin
350
+ ```
351
+
352
+ Every subcommand above is idempotent — re-running the whole block is safe.
353
+
354
+ ## Implementation notes
355
+
356
+ - Source: `public/tools/bin/commands/server.ts`, `public/tools/bin/utils/node-dapi.ts`.
357
+ - The Node client talks directly to `/public/v1/` — no Dart interop, no browser, no
358
+ logged-in session required. Authentication uses the developer key from the config.
359
+ - If `grok s` is not working, start by running `grok s healthcheck` — it verifies the
360
+ URL, the key, and basic connectivity, and returns per-module status if the server is
361
+ reachable. Fall back to `grok s raw GET /api/users/current` to isolate auth issues.
@@ -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
+ });