datagrok-tools 6.7.7 → 6.7.9
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/.devcontainer/PACKAGES_DEV.md +14 -13
- package/.turbo/turbo-build.log +2 -2
- package/CHANGELOG.md +16 -0
- package/CLAUDE.md +13 -1
- package/GROK_S.md +74 -7
- package/README.md +2 -1
- package/bin/commands/add.js +4 -4
- package/bin/commands/api.js +20 -12
- package/bin/commands/help.js +2 -2
- package/bin/commands/server-domains.js +24 -7
- package/bin/commands/server-observe.js +526 -0
- package/bin/commands/server.js +8 -1
- package/bin/grok.js +7 -3
- package/bin/utils/ent-helpers.js +1 -1
- package/bin/utils/migrate/walker.js +2 -1
- package/bin/utils/migrate/walker.ts +1 -1
- package/bin/utils/node-dapi.js +27 -10
- package/domain-schema.schema.json +50 -1
- package/entity-template/domain-app-imports.js +2 -2
- package/package.json +6 -3
|
@@ -66,7 +66,7 @@ Host machine
|
|
|
66
66
|
│ │ │ └── <folder>/ ← bind mount of user's repo │ │
|
|
67
67
|
│ │ ├── /var/run/docker.sock ← host Docker for stand mgmt │ │
|
|
68
68
|
│ │ ├── entrypoint.sh ← bind-mounted from tools/ │ │
|
|
69
|
-
│ │ ├── Claude Code + MCP plugins (
|
|
69
|
+
│ │ ├── Claude Code + MCP plugins (GitHub, Jira) │ │
|
|
70
70
|
│ │ ├── Playwright + Chrome for testing │ │
|
|
71
71
|
│ │ └── grok CLI for build/publish/test │ │
|
|
72
72
|
│ └────────────────────────────────────────────────────────────┘ │
|
|
@@ -197,15 +197,25 @@ Based on `node:22-bookworm-slim`. Pre-installed:
|
|
|
197
197
|
|
|
198
198
|
Runs as **root** in compose (entrypoint drops to `node` user for the main process).
|
|
199
199
|
|
|
200
|
-
## MCP plugins:
|
|
200
|
+
## MCP plugins: GitHub and Jira
|
|
201
201
|
|
|
202
202
|
The agent uses MCP plugins to search for similar issues, read context, and update
|
|
203
|
-
status.
|
|
203
|
+
status. Tickets are GitHub issues (`datagrok-ai/public`); Jira holds only read-only
|
|
204
|
+
`GROK-NNNNN` history. Set tokens in `.env` or the agent will ask interactively on first use.
|
|
204
205
|
|
|
205
|
-
###
|
|
206
|
+
### GitHub
|
|
206
207
|
|
|
207
208
|
```bash
|
|
208
209
|
# Inside tools-dev, register the MCP server:
|
|
210
|
+
claude mcp add github -s user -- \
|
|
211
|
+
npx -y @modelcontextprotocol/server-github
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Requires `GITHUB_TOKEN` in the environment (already passed from `.env`).
|
|
215
|
+
|
|
216
|
+
### Jira (optional, read-only history)
|
|
217
|
+
|
|
218
|
+
```bash
|
|
209
219
|
claude mcp add mcp-atlassian -s user -- \
|
|
210
220
|
npx -y mcp-atlassian \
|
|
211
221
|
--jira-url "$JIRA_URL" \
|
|
@@ -213,15 +223,6 @@ claude mcp add mcp-atlassian -s user -- \
|
|
|
213
223
|
--jira-token "$JIRA_TOKEN"
|
|
214
224
|
```
|
|
215
225
|
|
|
216
|
-
### GitHub
|
|
217
|
-
|
|
218
|
-
```bash
|
|
219
|
-
claude mcp add github -s user -- \
|
|
220
|
-
npx -y @modelcontextprotocol/server-github
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
Requires `GITHUB_TOKEN` in the environment (already passed from `.env`).
|
|
224
|
-
|
|
225
226
|
## Testing
|
|
226
227
|
|
|
227
228
|
### grok test (Puppeteer-based)
|
package/.turbo/turbo-build.log
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Datagrok-tools changelog
|
|
2
2
|
|
|
3
|
+
## v.next
|
|
4
|
+
|
|
5
|
+
* Upgraded Puppeteer to v25, which drops the vulnerable `extract-zip` and `basic-ftp` transitive dependencies; Node.js 22.12+ is now required
|
|
6
|
+
* `grok add app --domain`, `grok api --ui`: scaffold against `@datagrok-libraries/ems`
|
|
7
|
+
* GROK-20884: Added `grok s o errors get <signature|problem>`: each occurrence of an error with its time, user, function and session, so an error incident leads to its calls
|
|
8
|
+
* GROK-20884: Added `grok s o logs container [<name|id>]`: a docker container's log lines cut by `--since`/`--from`/`--to` and `--grep`; no name lists the containers
|
|
9
|
+
* GROK-20884: `grok s o problems get` prints one field per line with every detail in full, and `errors top --limit` returns that many rows instead of the top of every source
|
|
10
|
+
* GROK-20884: Added `grok s observe` (alias `grok s o`): problems and their alerts, problem rules, logger settings with expiring user and group entries, errors, the timeline and cloud logs (CloudWatch and the log archive)
|
|
11
|
+
* GROK-20884: Added `grok s o logs capture <login|group> [--for 30m]`: saves every API request of a user or a group until the entry expires (a `logger set` of `saveHttpRequests`; an entry that expires later keeps its time); `logs capture stop` removes an expiring entry that holds only the capture, `logs capture` lists the active ones
|
|
12
|
+
* GROK-20298: `grok s domains apply` sends the manifest JSON's `version` as the now-required `ifVersion` of a user-managed schema, accepts `--if-version current` to take the registered one, refuses such an apply without either, and forwards `friendlyName`, `description` and `storage` from the JSON for a user-managed schema (a package schema keeps its key set)
|
|
13
|
+
* GROK-20298: `domain-schema.schema.json` accepts the root `storage` key (`{kind: external, connection, schema?, catalog?}`) and the remote-name keys `table`/`column`; `grok api` types an external table's rows with `id` only, an empty expand map, and emits no `<Schema>TransactionOp`/`transaction` for an external schema
|
|
14
|
+
|
|
15
|
+
## 6.7.8 (2026-10-13)
|
|
16
|
+
|
|
17
|
+
* Fixed `grok api` (and every command other than `grok s`) exiting with code 1 and its usage text in 6.7.7: the new `--send-email` flag reached every command as `send-email: false`, which `grok api` rejected as an unknown option
|
|
18
|
+
|
|
3
19
|
## 6.7.7 (2026-10-09)
|
|
4
20
|
|
|
5
21
|
* GROK-21156: `grok s users invite [<email>] [--prefix <P>] [--send-email]`: invites a user and prints a link that logs them in (no email sent; the email may be omitted); `--prefix` starts the code with `<P>`; `--send-email` mails the invitation instead
|
package/CLAUDE.md
CHANGED
|
@@ -121,6 +121,18 @@ When a package has a `dockerfiles/` directory, `grok publish` automatically mana
|
|
|
121
121
|
|
|
122
122
|
**Practical implication:** editing any file inside `dockerfiles/<name>/` will cause `grok publish` to rebuild that container image on the next publish, without needing `--rebuild-docker`.
|
|
123
123
|
|
|
124
|
+
**Publish traps:**
|
|
125
|
+
- `grok publish` runs `npm run build` / `pnpm run build` itself (`buildPackage`); `--skip-check`
|
|
126
|
+
only skips checks, and `--rebuild` skips the local build rather than forcing one.
|
|
127
|
+
- With no local image and a transient error on the fallback lookup, CLI <= 6.5.10 published
|
|
128
|
+
`image: null` and the container went to `error`; 6.5.11+ builds instead.
|
|
129
|
+
- A republish renames a package's containers to `<name>_1`.
|
|
130
|
+
- Outside the pnpm workspace, a fresh `npm install` in a package may pull ts-loader 9.6.x /
|
|
131
|
+
webpack 5.109, which cannot resolve `datagrok-api/*` (tsc can); reuse a known-good
|
|
132
|
+
`node_modules` (ts-loader 9.5.7 / webpack 5.106) and junction `node_modules/datagrok-api` to a
|
|
133
|
+
built js-api (`cmd /c mklink /J`).
|
|
134
|
+
- Long publishes outlive a 10-minute agent tool limit; run them detached and poll a log.
|
|
135
|
+
|
|
124
136
|
### Testing Framework
|
|
125
137
|
|
|
126
138
|
Tests use Puppeteer for headless browser automation:
|
|
@@ -220,7 +232,7 @@ Based on `node:22-bookworm-slim`. Pre-installed:
|
|
|
220
232
|
| `demo` | + world, test_db, northwind |
|
|
221
233
|
| `full` | + grok_spawner, JKG, demo DBs |
|
|
222
234
|
|
|
223
|
-
See `.devcontainer/PACKAGES_DEV.md` for detailed usage docs, architecture diagram, MCP plugin setup (Jira
|
|
235
|
+
See `.devcontainer/PACKAGES_DEV.md` for detailed usage docs, architecture diagram, MCP plugin setup (GitHub/Jira), and troubleshooting.
|
|
224
236
|
|
|
225
237
|
### `grok server` / `grok s` Command
|
|
226
238
|
|
package/GROK_S.md
CHANGED
|
@@ -36,6 +36,7 @@ browser or a logged-in session.
|
|
|
36
36
|
| See what fields an entity type has | `grok s describe connections` |
|
|
37
37
|
| Hit any undocumented endpoint | `grok s raw GET /users/current` / `raw POST <path> --data '{...}'` |
|
|
38
38
|
| Check server + per-module health | `grok s healthcheck [--module <name>]` |
|
|
39
|
+
| Problems, alerts, problem rules, logger settings, errors, cloud logs | `grok s observe problems list --alert-status open` (alias `grok s o`) |
|
|
39
40
|
| Bulk operations in one round-trip | `grok s batch <entity> <verb> --json items.json` |
|
|
40
41
|
| Move entities dev to prod (bundle, or instance to instance) | `grok s pull ... --out ./bundle` / `grok s migrate ... --from dev --to prod` |
|
|
41
42
|
| Browse / query / edit domain-table rows | `grok s domains query grit.issue --filter 'status = "open"'` / `domains insert` / `domains upload` |
|
|
@@ -136,6 +137,18 @@ grok s connections test --json conn.json # test before saving
|
|
|
136
137
|
|
|
137
138
|
See `public/packages/Chembl/connections/` for connection JSON examples.
|
|
138
139
|
|
|
140
|
+
Connection tags over REST: the entity JSON carries `entityTags`
|
|
141
|
+
(`[{id, tag, owner: {id: <All users group id>}}]`, no `#type` on the owner), not `tags`.
|
|
142
|
+
`grok s connections save --json` without `entityTags` **clears** the tags (saveTags deletes rows
|
|
143
|
+
not in the list); `POST /entities/save_tags {id, entityTags}` sets them. Filter with
|
|
144
|
+
`whereTag([...])` (`?tags=x`); `whereSmart('tag = "x"')` returns nothing for connections over REST.
|
|
145
|
+
A `Web` connection's parameters must be wrapped (`{"url": {"value": "https://..."}}`); a bare
|
|
146
|
+
string makes the test route answer 400 "Invalid argument(s): value".
|
|
147
|
+
|
|
148
|
+
A connection saved over REST goes through `Project.add(uniqueName: true)` and can come back
|
|
149
|
+
renamed `<name>_1` for a package's connection — see `core/docs/PROJECTS_AND_PACKAGES.MD`
|
|
150
|
+
"Package connections on a stand".
|
|
151
|
+
|
|
139
152
|
## Group membership
|
|
140
153
|
|
|
141
154
|
`grok s groups add-members` is idempotent — it resolves each member (by login, name,
|
|
@@ -161,6 +174,12 @@ ambiguous the command prints every matching group and exits non-zero — pass a
|
|
|
161
174
|
disambiguate, or add `--user` (accepted by all four membership verbs) to restrict lookup to
|
|
162
175
|
personal groups.
|
|
163
176
|
|
|
177
|
+
> **Trap:** `grok s groups add-members <group> <users...>` without `--admin` sets `isAdmin=false`
|
|
178
|
+
> on members that are already group admins (reported as `updated`, not `noop`). Before a bulk add
|
|
179
|
+
> run `grok s groups list-members <group> --admin` and exclude those users, or re-add them with
|
|
180
|
+
> `--admin`. Pass the group UUID — `list-members Administrators` by name has returned
|
|
181
|
+
> "(no results)" while the id worked.
|
|
182
|
+
|
|
164
183
|
## User administration
|
|
165
184
|
|
|
166
185
|
```bash
|
|
@@ -411,12 +430,19 @@ grok s domains audit inventory --limit 50 # DDL and row e
|
|
|
411
430
|
grok s domains delete inventory --force # purge: data, audit, registry, grants
|
|
412
431
|
```
|
|
413
432
|
|
|
414
|
-
`schema.json` may be a full manifest or a partial apply body —
|
|
415
|
-
`propertySchemas` and `
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
433
|
+
`schema.json` may be a full manifest or a partial apply body — `tables`, `extend`,
|
|
434
|
+
`propertySchemas`, `dropTables` and `access` are sent, plus `friendlyName`, `description` and
|
|
435
|
+
`storage` when the schema is user-managed (`name` is dropped; `version` becomes `ifVersion` and
|
|
436
|
+
`incarnation` becomes `ifIncarnation` unless `--if-version` says otherwise — `current` takes
|
|
437
|
+
both from the schema registered right now, so a name deleted and re-created since the file was
|
|
438
|
+
written is a conflict, not a fresh `1`). Named tables
|
|
439
|
+
replace their definition wholesale; untouched tables stay as the registry has them. A plan
|
|
440
|
+
that drops or narrows anything is refused until `--confirm-destructive` is passed, and the
|
|
441
|
+
plan is printed with the refusal. `--if-version` fails the apply when the schema's apply
|
|
442
|
+
counter (or `ext_version` on a package schema) has moved; a user-managed schema REQUIRES the
|
|
443
|
+
token (the server answers `version-required` without it) — pass the version the edit was made
|
|
444
|
+
against, or `--if-version current` to take the one registered right now. Dropping a table
|
|
445
|
+
needs Delete on that table, dropping its columns Edit on it. Package-deployed
|
|
420
446
|
schemas cannot be applied to or deleted here — `apply` on one is the user-extension path
|
|
421
447
|
(needs `Extend`), and the manifest is owned by `grok publish`. `delete <schema>` requires
|
|
422
448
|
`--force` because it takes every row with it.
|
|
@@ -471,6 +497,44 @@ module the server does not report exits 1. Requires a valid dev key (standard `g
|
|
|
471
497
|
For an anonymous liveness probe — load balancer, k8s readiness — hit `/admin/health` directly;
|
|
472
498
|
it's on the server's unauthenticated allowlist.
|
|
473
499
|
|
|
500
|
+
## Problems, logging and errors (`grok s observe`, alias `grok s o`)
|
|
501
|
+
|
|
502
|
+
Thin commands over the server's routes; `grok s observe` with no command prints every verb and option.
|
|
503
|
+
|
|
504
|
+
```bash
|
|
505
|
+
grok s o problems list --alert-status open # the open alerts; --status, --state, --kind, --since 7d
|
|
506
|
+
grok s o problems get 'health:Jupyter@<server>' # by id or kind:key; history <id> for its records
|
|
507
|
+
grok s o problems status connection:<connection id> --data '{"status": "muted", "reason": "maintenance", "until": "2026-10-04T06:00:00Z"}'
|
|
508
|
+
grok s o problems ack 'health:Jupyter@<server>' --reason x # resolve <id> --reason ... closes the alert
|
|
509
|
+
grok s o rules get > rules.json # Settings > Alerts problem rules; rules put --json rules.json
|
|
510
|
+
grok s o rules test # evaluates the saved rules once, now
|
|
511
|
+
grok s o logger get exportSettings # the logger settings, or one path of them
|
|
512
|
+
grok s o logger set --user alice --set debugFlags='["db"]' --for 30m --reason "ticket 123" # an expiring entry, from All users
|
|
513
|
+
grok s o logger set --user alice --remove # delete that entry
|
|
514
|
+
grok s o logger history --limit 5 # log-settings-changed audit records with their diff
|
|
515
|
+
grok s o logs capture alice --for 2h --reason "ticket 123" # logger set of saveHttpRequests on an expiring entry; 30m by default
|
|
516
|
+
grok s o logs capture # the active captures; logs capture stop alice removes one
|
|
517
|
+
grok s o errors top --since 7d --by signature --limit 20 # GET /admin/metrics errors
|
|
518
|
+
grok s o errors top --since 24h --source server # ranked by distinct users: system errors (scheduler, probes) need --source
|
|
519
|
+
grok s o errors get error-incident:<key> # each occurrence of a problem's error: time, user, function, session
|
|
520
|
+
grok s o errors get <signature> --since 1d # or of a signature from errors top --by signature
|
|
521
|
+
grok s o timeline --session <id> # its events and requests of the last 15 min
|
|
522
|
+
grok s o timeline --session <id> --from 2026-10-09T10:00 # 10 min from then; --to sets the end, at most 2 h later
|
|
523
|
+
grok s o logs cloud --since 2h --filter ERROR # CloudWatch, the instance's own group; --group, --from/--to
|
|
524
|
+
grok s o logs archive list --prefix cw/ --since 1d # keys in the log archive (WORM bucket); --since filters the returned page only
|
|
525
|
+
grok s o logs archive read <key> # one archived object decoded to events
|
|
526
|
+
grok s o logs container # docker containers and their status
|
|
527
|
+
grok s o logs container scripting-python_1 --since 1h --grep 'Kernel (started|shutdown)' # one container's log lines
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
Problems need `ManageAlerts`, errors, another session's timeline and `logs cloud`/`logs archive` `ViewTelemetry`,
|
|
531
|
+
`rules test` both, logger and rules changes and `logs capture` `EditPluginsSettings`. `capture` is an expiring
|
|
532
|
+
`userGroupSettings` entry with `saveHttpRequests` on (seeded from the All Users entry, with `usage` saved); on an entry
|
|
533
|
+
that already expires later it keeps that time. `capture stop` removes only an expiring entry that holds nothing but the
|
|
534
|
+
capture; for any other entry use `logger set --user <login> --remove`. Read what it recorded with
|
|
535
|
+
`timeline --session <id>`. `logs container` reads the container's latest `--limit` lines (20000) and only then cuts `--since`/`--from`/`--to` (UTC, matched across the container's own offset) and `--grep`, so a window older than those lines comes back empty: raise `--limit`. `logs archive list --since` filters only the page the server returned (`--limit`), so narrow
|
|
536
|
+
the listing with `--prefix` first. `logs` read with the server's AWS role, or with `--connection <id>`. In durations `m` means minutes (in `grok s pull --since` it means months).
|
|
537
|
+
|
|
474
538
|
## Describing an entity type
|
|
475
539
|
|
|
476
540
|
```bash
|
|
@@ -626,9 +690,12 @@ entities shared with that account, and the target's own users never get their co
|
|
|
626
690
|
|
|
627
691
|
```bash
|
|
628
692
|
grok s pull --since 10y --type project,connection,query,script,group,layout --out ./bundle --host dev --admin
|
|
629
|
-
grok s push ./bundle --host prod --admin --on-conflict
|
|
693
|
+
grok s push ./bundle --host prod --admin --on-conflict adopt
|
|
630
694
|
```
|
|
631
695
|
|
|
696
|
+
`adopt` keeps one copy of what both instances already have, such as shared groups and spaces;
|
|
697
|
+
`skip` would leave them alone and fail everything in the bundle that points at them.
|
|
698
|
+
|
|
632
699
|
**Move a whole instance one space at a time.** Placement is exclusive on the server — one project
|
|
633
700
|
holding an entity takes it from every other — so a single bundle carrying every space has its
|
|
634
701
|
projects competing, and a stand-sized push spends its time taking rows back off each other. Scoped
|
package/README.md
CHANGED
|
@@ -106,7 +106,8 @@ For more information on configuring connections, refer to the [Connections](http
|
|
|
106
106
|
`config.yaml`): `grok report fetch <instance> <number>` downloads the report zip, `grok report read
|
|
107
107
|
<zip|json|instance number>` prints one normalized JSON object (flags `--extract-screenshot`,
|
|
108
108
|
`--extract-d42`, `--extract-client-log`), `grok report resolve <instance> <number>` marks it resolved.
|
|
109
|
-
`ticket`, `comment`, `label`, `attach` talk to JIRA with `JIRA_TOKEN
|
|
109
|
+
`ticket`, `comment`, `label`, `attach` talk to JIRA with `JIRA_TOKEN`; porting them to GitHub Issues
|
|
110
|
+
is pending. See `grok report --help`.
|
|
110
111
|
- `check` checks package content (function signatures, import statements of external modules,
|
|
111
112
|
etc.). The check is also run during package publication.
|
|
112
113
|
- `init` modifies a package template by adding config files for linters, IDE, and so on
|
package/bin/commands/add.js
CHANGED
|
@@ -16,8 +16,7 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
|
|
|
16
16
|
* answer key): u2 is not on npm, so the dependency is the workspace link, and `u2core` is the one
|
|
17
17
|
* external `@datagrok/build-config` does not know about. The css imports and the tsconfig need
|
|
18
18
|
* nothing — the shared bundler config carries the loaders and the tsconfig extends the base. */
|
|
19
|
-
const
|
|
20
|
-
const u2DgModule = '@datagrok-libraries/u2/src/dg/index.js';
|
|
19
|
+
const emsDgModule = '@datagrok-libraries/ems/src/dg';
|
|
21
20
|
const u2RspackExternal = `'datagrok-api/u2core': 'DG.U2'`;
|
|
22
21
|
const u2RspackConfig = [`const {bundler} = require('@datagrok/build-config');`, '', `module.exports = bundler({externals: {${u2RspackExternal}}});`, ''].join('\n');
|
|
23
22
|
function add(args) {
|
|
@@ -110,7 +109,7 @@ function add(args) {
|
|
|
110
109
|
* existing imports (which webpack's externals depend on). */
|
|
111
110
|
function addU2Imports() {
|
|
112
111
|
const contents = _fs.default.readFileSync(packageEntry, 'utf8');
|
|
113
|
-
if (contents.includes(
|
|
112
|
+
if (contents.includes(emsDgModule)) return;
|
|
114
113
|
const eol = contents.includes('\r\n') ? '\r\n' : '\n';
|
|
115
114
|
const imports = _fs.default.readFileSync(_path.default.join(templateDir, 'entity-template', 'domain-app-imports.js'), 'utf8').trimEnd().split(/\r?\n/);
|
|
116
115
|
const lines = contents.split(/\r?\n/);
|
|
@@ -125,7 +124,8 @@ function add(args) {
|
|
|
125
124
|
function addU2Wiring() {
|
|
126
125
|
const packageObj = JSON.parse(_fs.default.readFileSync(packagePath, 'utf8'));
|
|
127
126
|
packageObj.dependencies = packageObj.dependencies ?? {};
|
|
128
|
-
packageObj.dependencies['@datagrok-libraries/
|
|
127
|
+
packageObj.dependencies['@datagrok-libraries/ems'] ??= 'workspace:^';
|
|
128
|
+
packageObj.dependencies['@datagrok-libraries/u2'] ??= 'workspace:^';
|
|
129
129
|
_fs.default.writeFileSync(packagePath, JSON.stringify(packageObj, null, 2), 'utf8');
|
|
130
130
|
if (!_fs.default.existsSync(rspackConfigPath)) _fs.default.writeFileSync(rspackConfigPath, u2RspackConfig, 'utf8');else if (!_fs.default.readFileSync(rspackConfigPath, 'utf8').includes('datagrok-api/u2core')) color.warn(`rspack.config.js is already there — add \`externals: {${u2RspackExternal}}\` to it by hand`);
|
|
131
131
|
}
|
package/bin/commands/api.js
CHANGED
|
@@ -24,7 +24,7 @@ const dbFile = 'db.ts';
|
|
|
24
24
|
const dbUiFile = 'db-ui.ts';
|
|
25
25
|
/** Where `db-ui.ts` takes the u2 domain stack from — the same specifier Grit and Stockroom use
|
|
26
26
|
* (u2 is a relative-path dependency without a `dg` subpath export). */
|
|
27
|
-
const
|
|
27
|
+
const emsDgModule = '@datagrok-libraries/ems/src/dg';
|
|
28
28
|
|
|
29
29
|
/** Naive English plural of a (snake_case) table name — `issue` → `issues`, `box` →
|
|
30
30
|
* `boxes`, `category` → `categories`. */
|
|
@@ -229,7 +229,7 @@ function generateDomainClients(packageDir = curDir, options) {
|
|
|
229
229
|
_fs.default.writeFileSync(_path.default.join(genDir, dbFile), normEol(content).replace(/\n/g, '\r\n'), 'utf8');
|
|
230
230
|
color.log(`Successfully generated file src/generated/${dbFile}${sep}`, 'success');
|
|
231
231
|
if (ui) {
|
|
232
|
-
const uiContent = annotationForDbUiFile + `import {domains, DomainTable} from '${
|
|
232
|
+
const uiContent = annotationForDbUiFile + `import {domains, DomainTable} from '${emsDgModule}';${sep}` + wrapTokens(`import {`, dbImports, ', ', `} from './${dbFile.replace(/\.ts$/, '')}';`) + sep + sep + uiParts.join(sep);
|
|
233
233
|
_fs.default.writeFileSync(_path.default.join(genDir, dbUiFile), normEol(uiContent).replace(/\n/g, '\r\n'), 'utf8');
|
|
234
234
|
color.log(`Successfully generated file src/generated/${dbUiFile}${sep}`, 'success');
|
|
235
235
|
}
|
|
@@ -265,6 +265,10 @@ function findDependencyManifest(packageDir, schemaName) {
|
|
|
265
265
|
function generateDomainSchemaCode(manifest, manifestPath, emittedTypes, packageDir) {
|
|
266
266
|
const decls = [];
|
|
267
267
|
const systemColumnNames = new Set(domainSystemColumns.map(([name]) => name));
|
|
268
|
+
// An external table's rows carry `id` (the encoded business key) and nothing else
|
|
269
|
+
// system-side; nothing is expanded and nothing is written (external-bindings §3).
|
|
270
|
+
const external = manifest.storage?.kind === 'external';
|
|
271
|
+
const systemColumns = external ? domainSystemColumns.filter(([name]) => name === 'id') : domainSystemColumns;
|
|
268
272
|
const tableNames = Object.keys(manifest.tables);
|
|
269
273
|
// Keyed by table name; a qualified ref target ('Core.queries', 'grit.issue') lands here
|
|
270
274
|
// under its qualified key so the expand map can list its columns — it gets no accessor.
|
|
@@ -427,7 +431,7 @@ function generateDomainSchemaCode(manifest, manifestPath, emittedTypes, packageD
|
|
|
427
431
|
const columns = tableColumns[tableName];
|
|
428
432
|
const relations = tableRelations[tableName];
|
|
429
433
|
const rowLines = [`/** Row of \`${manifest.name}.${tableName}\`. */`, `export interface ${typeName}Row {`];
|
|
430
|
-
for (const [name, tsType] of
|
|
434
|
+
for (const [name, tsType] of systemColumns) rowLines.push(` ${name}: ${tsType};`);
|
|
431
435
|
for (const c of columns) rowLines.push(` ${c.name}${c.required || c.autoNumber ? '' : '?'}: ${c.tsType};`);
|
|
432
436
|
rowLines.push('}');
|
|
433
437
|
decls.push(rowLines.join(sep));
|
|
@@ -447,13 +451,13 @@ function generateDomainSchemaCode(manifest, manifestPath, emittedTypes, packageD
|
|
|
447
451
|
updateLines.push('};');
|
|
448
452
|
decls.push(updateLines.join(sep));
|
|
449
453
|
}
|
|
450
|
-
decls.push(formatColumnUnion(`${typeName}Column`, [...
|
|
454
|
+
decls.push(formatColumnUnion(`${typeName}Column`, [...systemColumns.map(([name]) => name), ...columns.map(c => c.name)]));
|
|
451
455
|
|
|
452
456
|
// Expand map: MUST stay a `type` alias — object-type literals carry the implicit
|
|
453
457
|
// index signature the `TExpand extends {[key: string]: {}}` constraint needs
|
|
454
458
|
// (an interface would fail it).
|
|
455
459
|
const expandEntries = [];
|
|
456
|
-
for (const c of columns) {
|
|
460
|
+
for (const c of external ? [] : columns) {
|
|
457
461
|
if (c.ref == null) continue;
|
|
458
462
|
const fields = tableColumns[c.ref].map(tc => `'${c.name}.${tc.name}'?: ${tc.tsType}`);
|
|
459
463
|
expandEntries.push(wrapTokens(` '${c.name}': {`, fields, '; ', '};', ' '));
|
|
@@ -463,15 +467,15 @@ function generateDomainSchemaCode(manifest, manifestPath, emittedTypes, packageD
|
|
|
463
467
|
// The link shape stays INLINE (structurally DG.DomainRelationLink): generated
|
|
464
468
|
// files must compile against the datagrok-api the package depends on, which may
|
|
465
469
|
// predate that interface.
|
|
466
|
-
for (const r of relations) expandEntries.push(` '${r.name}': {${r.name}?: {id: string; name: string}[]};`);
|
|
467
|
-
for (const childName of tableNames) {
|
|
470
|
+
for (const r of external ? [] : relations) expandEntries.push(` '${r.name}': {${r.name}?: {id: string; name: string}[]};`);
|
|
471
|
+
for (const childName of external ? [] : tableNames) {
|
|
468
472
|
const fks = tableColumns[childName].filter(c => c.ref === tableName);
|
|
469
473
|
if (fks.length === 0) continue;
|
|
470
474
|
const childType = utils.snakeToCamelCase(childName);
|
|
471
475
|
if (fks.length === 1) expandEntries.push(` 'details:${childName}': {${childName}?: ${childType}Row[]};`);else for (const fk of fks) expandEntries.push(` 'details:${childName}.${fk.name}': {${childName}?: ${childType}Row[]};`);
|
|
472
476
|
}
|
|
473
477
|
decls.push(expandEntries.length === 0 ? `export type ${typeName}Expand = {};` : [`/** Expand keys of \`${manifest.name}.${tableName}\` → fields each adds to the row ` + `(consumed by query()/builder). */`, `export type ${typeName}Expand = {`, ...expandEntries, '};'].join(sep));
|
|
474
|
-
txArms.push(` {op: 'insert'; table: '${tableName}'; ref?: string; values: DG.DomainTxValues<${typeName}Insert>; ` + `onDuplicate?: 'error'} |`, ` {op: 'update'; table: '${tableName}'; id: string; ` + `values: DG.DomainTxValues<${relations.length > 0 ? `${typeName}Update` : `Partial<${typeName}Row>`}>; ` + `expectedVersion?: number} |`, ` {op: 'delete'; table: '${tableName}'; id: string} |`);
|
|
478
|
+
if (!external) txArms.push(` {op: 'insert'; table: '${tableName}'; ref?: string; values: DG.DomainTxValues<${typeName}Insert>; ` + `onDuplicate?: 'error'} |`, ` {op: 'update'; table: '${tableName}'; id: string; ` + `values: DG.DomainTxValues<${relations.length > 0 ? `${typeName}Update` : `Partial<${typeName}Row>`}>; ` + `expectedVersion?: number} |`, ` {op: 'delete'; table: '${tableName}'; id: string} |`);
|
|
475
479
|
|
|
476
480
|
// No datetime config: the client resolves datetime columns from the domain
|
|
477
481
|
// registry itself (the Dayjs typing in <Table>Row stays true for every client).
|
|
@@ -480,10 +484,14 @@ function generateDomainSchemaCode(manifest, manifestPath, emittedTypes, packageD
|
|
|
480
484
|
clients.push(` get ${tableProp(tableName)}() {`, ` return grok.dapi.domains.table<${typeName}Row, ${typeName}Insert, ` + `${typeName}Column, ${typeName}Expand` + `${relations.length > 0 ? `, ${typeName}Update` : ''}>('${manifest.name}.${tableName}');`, ' },');
|
|
481
485
|
}
|
|
482
486
|
const schemaType = utils.snakeToCamelCase(manifest.name);
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
+
const transaction = [];
|
|
488
|
+
if (!external) {
|
|
489
|
+
const lastArm = txArms.pop();
|
|
490
|
+
txArms.push(lastArm.replace(/ \|$/, ';'));
|
|
491
|
+
decls.push([`export type ${schemaType}TransactionOp =`, ...txArms].join(sep));
|
|
492
|
+
transaction.push(` transaction<T extends ${schemaType}TransactionOp[]>(ops: [...T]):`, ` Promise<{[K in keyof T]: DG.DomainOpResultFor<T[K]>}> {`, ` return grok.dapi.domains.transaction('${manifest.name}', ops) as`, ` Promise<{[K in keyof T]: DG.DomainOpResultFor<T[K]>}>;`, ' },');
|
|
493
|
+
}
|
|
494
|
+
decls.push([`/** Typed clients for the \`${manifest.name}\` domain schema tables ` + `(lazy — no import-time side effects). */`, `export const ${utils.snakeToCamelCase(manifest.name, false)}Db = {`, ...clients, ...transaction, '};'].join(sep));
|
|
487
495
|
return decls.join(sep.repeat(2)) + sep;
|
|
488
496
|
}
|
|
489
497
|
|
package/bin/commands/help.js
CHANGED
|
@@ -96,7 +96,7 @@ grok add tests
|
|
|
96
96
|
Please note that entity names may only include letters and numbers
|
|
97
97
|
|
|
98
98
|
--domain scaffolds a working browse/CRUD app over an entity-mapped domain table
|
|
99
|
-
(\`grok.dapi.domains\`) from the
|
|
99
|
+
(\`grok.dapi.domains\`) from the EMS defaults (\`@datagrok-libraries/ems\`): a three-line
|
|
100
100
|
app function, a \`src/app.spec.json\` the designer edits, and — for a schema the package
|
|
101
101
|
does not declare yet — a one-table starter \`databases/<schema>/schema.json\`. Give it
|
|
102
102
|
one table (\`--domain tracker.issue\`), a whole schema the package declares in
|
|
@@ -140,7 +140,7 @@ Options:
|
|
|
140
140
|
--verbose Print detailed output
|
|
141
141
|
--ui Also generate \`src/generated/db-ui.ts\` — typed u2 handles
|
|
142
142
|
(\`get<Schema>Db()\` → a \`DomainTable<Row>\` per table, opened in
|
|
143
|
-
parallel behind one await) over \`@datagrok-libraries/
|
|
143
|
+
parallel behind one await) over \`@datagrok-libraries/ems\`. Once the
|
|
144
144
|
file exists, plain \`grok api\` keeps it up to date; delete it to
|
|
145
145
|
opt out again
|
|
146
146
|
`;
|
|
@@ -21,7 +21,9 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
|
|
|
21
21
|
/// `grok s domains ...` — entity-mapped domain schemas and their rows (DomainsRouter, `/domains/`).
|
|
22
22
|
/// Every verb addresses a schema as `<schema>` or a table as `<schema>.<table>`.
|
|
23
23
|
|
|
24
|
-
const APPLY_KEYS = ['tables', 'extend', 'propertySchemas', 'dropTables', 'ifVersion', 'confirmDestructive'];
|
|
24
|
+
const APPLY_KEYS = ['tables', 'extend', 'propertySchemas', 'dropTables', 'access', 'ifVersion', 'ifIncarnation', 'confirmDestructive'];
|
|
25
|
+
/** Accepted by a user-managed schema's apply only; a package schema refuses them as unknown keys. */
|
|
26
|
+
const USER_APPLY_KEYS = ['friendlyName', 'description', 'storage'];
|
|
25
27
|
const PERMISSIONS = ['View', 'Edit', 'Delete', 'Share', 'Extend'];
|
|
26
28
|
const USAGE = `Usage: grok s domains <verb> [args]
|
|
27
29
|
list [<schema>] [--filter <text>] Schemas, or the tables of one schema
|
|
@@ -210,11 +212,23 @@ async function handleDomains(dapi, verb, rest, argv, output) {
|
|
|
210
212
|
}
|
|
211
213
|
case 'apply':
|
|
212
214
|
{
|
|
213
|
-
if (!args[0] || !argv.json) return usage('apply <schema> --json manifest.json [--dry-run] [--confirm-destructive] [--if-version <v
|
|
215
|
+
if (!args[0] || !argv.json) return usage('apply <schema> --json manifest.json [--dry-run] [--confirm-destructive] [--if-version <v>|current]');
|
|
214
216
|
const a = (0, _nodeDapi.parseDomainAddress)(args[0], {
|
|
215
217
|
table: false
|
|
216
218
|
});
|
|
217
|
-
const
|
|
219
|
+
const json = readJson(argv.json);
|
|
220
|
+
// One registry read, only when the token or the metadata keys depend on who manages the schema.
|
|
221
|
+
const s = argv['if-version'] === undefined || argv['if-version'] === 'current' || USER_APPLY_KEYS.some(k => json?.[k] !== undefined) ? await domains.schema(a.schema) : null;
|
|
222
|
+
const body = applyBody(json, argv, {
|
|
223
|
+
userManaged: s?.managedBy === 'user'
|
|
224
|
+
});
|
|
225
|
+
if (body.ifVersion === 'current') {
|
|
226
|
+
// Both halves of the token as registered now: a name deleted and re-created
|
|
227
|
+
// since the file was written must conflict, not pass as a fresh `1`.
|
|
228
|
+
const m = await domains.manifest(a.schema);
|
|
229
|
+
body.ifVersion = String(m?.version ?? s.version ?? '1');
|
|
230
|
+
if (m?.incarnation !== undefined) body.ifIncarnation = String(m.incarnation);
|
|
231
|
+
} else if (body.ifVersion === undefined && s.managedBy === 'user') return usage(`apply ${a.schema} --json manifest.json --if-version <v>|current — a user-managed schema ` + 'requires the version the edit was made against (the "version" of the manifest JSON is used when present)');
|
|
218
232
|
printJson(await domains.applySchema(a.schema, body, argv['dry-run'] === true), output, r => r?.version);
|
|
219
233
|
return true;
|
|
220
234
|
}
|
|
@@ -305,12 +319,15 @@ function aggregateSpec(argv, filter, limit) {
|
|
|
305
319
|
return spec;
|
|
306
320
|
}
|
|
307
321
|
|
|
308
|
-
/** The apply payload from a schema.json (or a partial body): only the keys the server accepts
|
|
309
|
-
|
|
322
|
+
/** The apply payload from a schema.json (or a partial body): only the keys the server accepts
|
|
323
|
+
* (the schema's metadata and storage keys for a user-managed schema only), flags folded in. */
|
|
324
|
+
function applyBody(json, argv, options = {}) {
|
|
310
325
|
const body = {};
|
|
311
|
-
for (const k of APPLY_KEYS) if (json?.[k] !== undefined) body[k] = json[k];
|
|
326
|
+
for (const k of options.userManaged ? [...APPLY_KEYS, ...USER_APPLY_KEYS] : APPLY_KEYS) if (json?.[k] !== undefined) body[k] = json[k];
|
|
312
327
|
if (argv['confirm-destructive'] === true) body.confirmDestructive = true;
|
|
313
|
-
if (argv['if-version'] !== undefined) body.ifVersion = String(argv['if-version']);
|
|
328
|
+
if (argv['if-version'] !== undefined) body.ifVersion = String(argv['if-version']);else if (json?.version !== undefined) body.ifVersion = String(json.version);
|
|
329
|
+
// The manifest's `incarnation` rides with its `version` (the server keeps it optional).
|
|
330
|
+
if (json?.incarnation !== undefined) body.ifIncarnation = String(json.incarnation);
|
|
314
331
|
return body;
|
|
315
332
|
}
|
|
316
333
|
|