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.
@@ -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 (Jira, GitHub) │ │
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: Jira and GitHub
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. Set tokens in `.env` or the agent will ask interactively on first use.
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
- ### Jira (Atlassian)
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)
@@ -1,5 +1,5 @@
1
1
 
2
- > datagrok-tools@6.7.7 build /home/runner/work/public/public/tools
2
+ > datagrok-tools@6.7.9 build /home/runner/work/public/public/tools
3
3
  > node build.js
4
4
 
5
- Successfully compiled 51 files with Babel.
5
+ Successfully compiled 52 files with Babel.
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/GitHub), and troubleshooting.
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 — only `tables`, `extend`,
415
- `propertySchemas` and `dropTables` are sent; `name`, `version` and `description` are dropped.
416
- Named tables replace their definition wholesale; untouched tables stay as the registry has
417
- them. A plan that drops or narrows anything is refused until `--confirm-destructive` is
418
- passed, and the plan is printed with the refusal. `--if-version` fails the apply when the
419
- schema's apply counter (or `ext_version` on a package schema) has moved. Package-deployed
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 skip
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`. See `grok report --help`.
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
@@ -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 u2Dependency = 'workspace:^';
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(u2DgModule)) return;
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/u2'] ??= u2Dependency;
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
  }
@@ -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 u2DgModule = '@datagrok-libraries/u2/src/dg/index.js';
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 '${u2DgModule}';${sep}` + wrapTokens(`import {`, dbImports, ', ', `} from './${dbFile.replace(/\.ts$/, '')}';`) + sep + sep + uiParts.join(sep);
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 domainSystemColumns) rowLines.push(` ${name}: ${tsType};`);
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`, [...domainSystemColumns.map(([name]) => name), ...columns.map(c => c.name)]));
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 lastArm = txArms.pop();
484
- txArms.push(lastArm.replace(/ \|$/, ';'));
485
- decls.push([`export type ${schemaType}TransactionOp =`, ...txArms].join(sep));
486
- 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<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]>}>;`, ' },', '};'].join(sep));
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
 
@@ -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 u2 defaults (\`@datagrok-libraries/u2\`): a three-line
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/u2\`. Once the
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 body = applyBody(readJson(argv.json), argv);
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, flags folded in. */
309
- function applyBody(json, argv) {
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