datagrok-tools 6.7.8 → 6.7.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- FROM node:22-bookworm-slim
1
+ FROM node:22.23.4-bookworm-slim
2
2
 
3
3
  # System deps + Chrome stable
4
4
  RUN apt-get update && apt-get install -y --no-install-recommends \
@@ -1,5 +1,5 @@
1
1
 
2
- > datagrok-tools@6.7.8 build /home/runner/work/public/public/tools
2
+ > datagrok-tools@6.7.10 build /home/runner/work/public/public/tools
3
3
  > node build.js
4
4
 
5
- Successfully compiled 51 files with Babel.
5
+ Successfully compiled 55 files with Babel.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # Datagrok-tools changelog
2
2
 
3
+ ## 6.7.10 (2026-11-06)
4
+
5
+ * Added `grok src`: git recipes across the repos of the datagrok umbrella (`clone`, `add`, `sparse`, `switch`, `status`, `pull`, `at`, `bump`, `wt add`, `doctor`)
6
+ * Kept positionals, `--url` and `--dir` verbatim (`1.10` no longer becomes 1.1), and an explicit boolean such as `--force` now reaches every command instead of being dropped
7
+ * Added `grok s o groups list|get|add|set|rm|put|reset`, `endpoints list|rm`, `logs match '<filter>'` (with `--space`) and `logs activations [stop]`: log groups and endpoints of Settings > Logger
8
+ * `grok publish`: A local container build that fails for the host platform on Apple Silicon is retried for linux/amd64, so a package whose base image has no arm64 variant (the Chembl ChEMBL database) builds
9
+ * `grok test`: A package built on u2 or ems runs with u2 strict — a u2 misuse throws a `U2UsageError` naming the fix, and one the page caught and logged fails the run when it is the package's own (a stack frame in its bundle, or none to tell; another package's prints as a warning); `U2_STRICT=report` lists them instead without failing
10
+ * `grok check`: A package built on u2 or ems fails on a problem of u2's consumer check whatever its version (a 0.x package only printed it), so a CSS rule over a new `.u2-*` class or `[data-u2…]` attribute fails the check
11
+ * Added `grok s o problems severity <id|kind:key> <info|warning|critical|none>`: pins the severity a problem's raises use (`none` unpins it)
12
+ * Added `grok connect-folder`: connects a local folder as a connection, served from a Docker container; the agent image tag is pulled on every connect, so a moved tag replaces a stale local agent
13
+ * `grok connect-folder`: Runs only the Docker Hub `datagrok/proxy_storage` agent image whatever image the server names; `--image` runs another, such as a copy in a private registry
14
+ * Added `grok s ... --log [<spec>]` on every `grok s` command (`grok s o` included): its requests share a `datlas-log-id`, every server instance keeps what they log at the spec (`verbose`, `standard`, levels, debug flags or `all`) in memory only, and at the end the command gathers it and prints it to stderr
15
+ * Added `grok s functions run ... --debug`: a debug call (`?debug=true`) whose own log, a query's stages as the query debugger shows them, is kept and printed the same way
16
+ * Added `grok s o logs request <logId>`: gathers what the requests of one `--log` command logged
17
+ * `grok s status`: server build, uptime, DB pool and HTTP connections from `/info/server`; for admins also the requests in flight (`?extended=true`)
18
+ * `grok api`: Fixed double plurals in table accessors (`categorieses` → `categories`)
19
+ * Build: Fixed `package-api.ts` of a decorator package lagging one build behind `package.g.ts`
20
+ * Added a `restore` arm per soft-delete table to the `<Schema>TransactionOp` that `grok api` generates, so the typed `transaction()` accepts the restore ops the server runs
21
+ * Made `grok api` type a required column with a `default` as optional on `<Table>Insert`: the server fills it when an insert omits it
22
+ * Fixed `grok api` emitting a choices type that does not compile, or holds a different value, when a choice contains `'` or `\`
23
+
24
+ ## 6.7.9 (2026-10-16)
25
+
26
+ * Upgraded Puppeteer to v25, which drops the vulnerable `extract-zip` and `basic-ftp` transitive dependencies; Node.js 22.12+ is now required
27
+ * `grok add app --domain`, `grok api --ui`: scaffold against `@datagrok-libraries/ems`
28
+ * 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
29
+ * 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
30
+ * 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
31
+ * 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)
32
+ * 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
33
+ * 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)
34
+ * 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
35
+
3
36
  ## 6.7.8 (2026-10-13)
4
37
 
5
38
  * 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
package/CLAUDE.md CHANGED
@@ -33,6 +33,7 @@ The CLI uses a modular command pattern. Each command is a separate module that:
33
33
  - `claude.ts` - Launch a Dockerized dev environment with Datagrok + Claude Code
34
34
  - `migrate.ts` - Update legacy packages
35
35
  - `init.ts` - Apply configuration to existing packages
36
+ - `src.ts` - Git recipes across the repos of the datagrok umbrella (`grok src`)
36
37
 
37
38
  The commands `api`, `check`, `link`, `publish`, and `test` support the `--all` flag to run recursively across all packages in the current directory.
38
39
 
@@ -121,6 +122,18 @@ When a package has a `dockerfiles/` directory, `grok publish` automatically mana
121
122
 
122
123
  **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
124
 
125
+ **Publish traps:**
126
+ - `grok publish` runs `npm run build` / `pnpm run build` itself (`buildPackage`); `--skip-check`
127
+ only skips checks, and `--rebuild` skips the local build rather than forcing one.
128
+ - With no local image and a transient error on the fallback lookup, CLI <= 6.5.10 published
129
+ `image: null` and the container went to `error`; 6.5.11+ builds instead.
130
+ - A republish renames a package's containers to `<name>_1`.
131
+ - Outside the pnpm workspace, a fresh `npm install` in a package may pull ts-loader 9.6.x /
132
+ webpack 5.109, which cannot resolve `datagrok-api/*` (tsc can); reuse a known-good
133
+ `node_modules` (ts-loader 9.5.7 / webpack 5.106) and junction `node_modules/datagrok-api` to a
134
+ built js-api (`cmd /c mklink /J`).
135
+ - Long publishes outlive a 10-minute agent tool limit; run them detached and poll a log.
136
+
124
137
  ### Testing Framework
125
138
 
126
139
  Tests use Puppeteer for headless browser automation:
package/GROK_S.md CHANGED
@@ -36,6 +36,8 @@ 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
+ | Server build, uptime, DB pool, requests in flight | `grok s status` |
40
+ | Problems, alerts, problem rules, logger settings, errors, cloud logs | `grok s observe problems list --alert-status open` (alias `grok s o`) |
39
41
  | Bulk operations in one round-trip | `grok s batch <entity> <verb> --json items.json` |
40
42
  | Move entities dev to prod (bundle, or instance to instance) | `grok s pull ... --out ./bundle` / `grok s migrate ... --from dev --to prod` |
41
43
  | Browse / query / edit domain-table rows | `grok s domains query grit.issue --filter 'status = "open"'` / `domains insert` / `domains upload` |
@@ -136,6 +138,18 @@ grok s connections test --json conn.json # test before saving
136
138
 
137
139
  See `public/packages/Chembl/connections/` for connection JSON examples.
138
140
 
141
+ Connection tags over REST: the entity JSON carries `entityTags`
142
+ (`[{id, tag, owner: {id: <All users group id>}}]`, no `#type` on the owner), not `tags`.
143
+ `grok s connections save --json` without `entityTags` **clears** the tags (saveTags deletes rows
144
+ not in the list); `POST /entities/save_tags {id, entityTags}` sets them. Filter with
145
+ `whereTag([...])` (`?tags=x`); `whereSmart('tag = "x"')` returns nothing for connections over REST.
146
+ A `Web` connection's parameters must be wrapped (`{"url": {"value": "https://..."}}`); a bare
147
+ string makes the test route answer 400 "Invalid argument(s): value".
148
+
149
+ A connection saved over REST goes through `Project.add(uniqueName: true)` and can come back
150
+ renamed `<name>_1` for a package's connection — see `core/docs/PROJECTS_AND_PACKAGES.MD`
151
+ "Package connections on a stand".
152
+
139
153
  ## Group membership
140
154
 
141
155
  `grok s groups add-members` is idempotent — it resolves each member (by login, name,
@@ -161,6 +175,12 @@ ambiguous the command prints every matching group and exits non-zero — pass a
161
175
  disambiguate, or add `--user` (accepted by all four membership verbs) to restrict lookup to
162
176
  personal groups.
163
177
 
178
+ > **Trap:** `grok s groups add-members <group> <users...>` without `--admin` sets `isAdmin=false`
179
+ > on members that are already group admins (reported as `updated`, not `noop`). Before a bulk add
180
+ > run `grok s groups list-members <group> --admin` and exclude those users, or re-add them with
181
+ > `--admin`. Pass the group UUID — `list-members Administrators` by name has returned
182
+ > "(no results)" while the id worked.
183
+
164
184
  ## User administration
165
185
 
166
186
  ```bash
@@ -198,6 +218,7 @@ grants inherited through the entity's project links (`inherited: true`).
198
218
  grok s functions run 'Chem:smilesToMw("CCO")' # positional args
199
219
  grok s functions run 'Pkg:fn({smiles:"CCO", radius:2})' # named args
200
220
  grok s functions run Pkg:fn --json params.json # big input from a file
221
+ grok s functions run 'MyPkg:MyQuery()' --debug # a debug call: the query's own log on stderr
201
222
  ```
202
223
 
203
224
  The server binds arguments by parameter name, so positional values are mapped onto the
@@ -411,12 +432,19 @@ grok s domains audit inventory --limit 50 # DDL and row e
411
432
  grok s domains delete inventory --force # purge: data, audit, registry, grants
412
433
  ```
413
434
 
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
435
+ `schema.json` may be a full manifest or a partial apply body — `tables`, `extend`,
436
+ `propertySchemas`, `dropTables` and `access` are sent, plus `friendlyName`, `description` and
437
+ `storage` when the schema is user-managed (`name` is dropped; `version` becomes `ifVersion` and
438
+ `incarnation` becomes `ifIncarnation` unless `--if-version` says otherwise — `current` takes
439
+ both from the schema registered right now, so a name deleted and re-created since the file was
440
+ written is a conflict, not a fresh `1`). Named tables
441
+ replace their definition wholesale; untouched tables stay as the registry has them. A plan
442
+ that drops or narrows anything is refused until `--confirm-destructive` is passed, and the
443
+ plan is printed with the refusal. `--if-version` fails the apply when the schema's apply
444
+ counter (or `ext_version` on a package schema) has moved; a user-managed schema REQUIRES the
445
+ token (the server answers `version-required` without it) — pass the version the edit was made
446
+ against, or `--if-version current` to take the one registered right now. Dropping a table
447
+ needs Delete on that table, dropping its columns Edit on it. Package-deployed
420
448
  schemas cannot be applied to or deleted here — `apply` on one is the user-extension path
421
449
  (needs `Extend`), and the manifest is owned by `grok publish`. `delete <schema>` requires
422
450
  `--force` because it takes every row with it.
@@ -471,6 +499,110 @@ module the server does not report exits 1. Requires a valid dev key (standard `g
471
499
  For an anonymous liveness probe — load balancer, k8s readiness — hit `/admin/health` directly;
472
500
  it's on the server's unauthenticated allowlist.
473
501
 
502
+ ### Server status
503
+
504
+ ```bash
505
+ grok s status # build, uptime, DB pool, HTTP connections
506
+ grok s status --output json # {server, extended}
507
+ grok s status --output quiet # just the version
508
+ ```
509
+
510
+ Reads `GET /info/server` and `GET /info/server?extended=true`: `Version`, `Branch`, `Commit`,
511
+ build `Date`, `Uptime`, `Requests` served, `Used Connections` / `Pool Connections` of the DB pool,
512
+ `Isolate`, `Service network`, `webRoot`, and the `Http Server` connection counts. For an admin the
513
+ extended call replaces the active-connection count with the requests in flight per endpoint
514
+ (`{"GET /api/...": n}`), printed as a table; for anyone else the two responses are the same.
515
+
516
+ ## Problems, logging and errors (`grok s observe`, alias `grok s o`)
517
+
518
+ Thin commands over the server's routes; `grok s observe` with no command prints every verb and option.
519
+
520
+ ```bash
521
+ grok s o problems list --alert-status open # the open alerts; --status, --state, --kind, --since 7d
522
+ grok s o problems get 'health:Jupyter@<server>' # by id or kind:key; history <id> for its records
523
+ grok s o problems status connection:<connection id> --data '{"status": "muted", "reason": "maintenance", "until": "2026-10-04T06:00:00Z"}'
524
+ grok s o problems ack 'health:Jupyter@<server>' --reason x # resolve <id> --reason ... closes the alert
525
+ grok s o problems severity dashboard:<project id> critical --reason 'board meeting' # pin; none unpins
526
+ grok s o rules get > rules.json # Settings > Alerts problem rules; rules put --json rules.json
527
+ grok s o rules test # evaluates the saved rules once, now
528
+ grok s o logger get exportSettings # the logger settings, or one path of them
529
+ grok s o logger set --user alice --set debugFlags='["db"]' --for 30m --reason "ticket 123" # an expiring entry, from All users
530
+ grok s o logger set --user alice --remove # delete that entry
531
+ grok s o logger history --limit 5 # log-settings-changed audit records with their diff
532
+ grok s o logs capture alice --for 2h --reason "ticket 123" # logger set of saveHttpRequests on an expiring entry; 30m by default
533
+ grok s o logs capture # the active captures; logs capture stop alice removes one
534
+ grok s o errors top --since 7d --by signature --limit 20 # GET /admin/metrics errors
535
+ grok s o errors top --since 24h --source server # ranked by distinct users: system errors (scheduler, probes) need --source
536
+ grok s o errors get error-incident:<key> # each occurrence of a problem's error: time, user, function, session
537
+ grok s o errors get <signature> --since 1d # or of a signature from errors top --by signature
538
+ grok s o timeline --session <id> # its events and requests of the last 15 min
539
+ grok s o timeline --session <id> --from 2026-10-09T10:00 # 10 min from then; --to sets the end, at most 2 h later
540
+ grok s o logs cloud --since 2h --filter ERROR # CloudWatch, the instance's own group; --group, --from/--to
541
+ grok s o logs archive list --prefix cw/ --since 1d # keys in the log archive (WORM bucket); --since filters the returned page only
542
+ grok s o logs archive read <key> # one archived object decoded to events
543
+ grok s o logs container # docker containers and their status
544
+ grok s o logs container scripting-python_1 --since 1h --grep 'Kernel (started|shutdown)' # one container's log lines
545
+ grok s o logs match 'level in (error, warning) and not text ~ ResizeObserver' --since 2h # what a log filter matches
546
+ grok s o logs match --level error --package Chembl --grep 'time(d )?out' # flags add conditions joined by and; --grep is a regex
547
+ grok s o logs match --level audit --space Chem # audit of entities in the Chem space and its subspaces (case-sensitive)
548
+ grok s o logs match 'space startsWith Chem: or space endsWith :Assays' # space: the entity's space path; = ~ =~ startsWith endsWith
549
+ grok s o groups # log groups (Settings > Logger): name, kind, levels, filter, where to
550
+ grok s o groups add --name Chembl --levels error,warning --filter 'package = Chembl' --to db,grafana:otlp
551
+ grok s o groups set Chembl --when 'level = error and package = Chembl' --scope session --for 15m --before 60s --flags db
552
+ grok s o groups set Chembl --enabled false # only the flags given change; groups rm Chembl deletes it
553
+ grok s o groups put --json groups.json # replace every log group (the GROK_PARAMETERS settings.logger.logGroups shape)
554
+ grok s o groups reset # back to the default groups; the audit group can only be switched off, not removed
555
+ grok s o endpoints # where groups can send records; endpoints rm grafana
556
+ grok s o logs activations # triggered groups on now; logs activations stop <group id> <key>
557
+ ```
558
+
559
+ Problems need `ManageAlerts`, errors, another session's timeline and `logs cloud`/`logs archive` `ViewTelemetry`,
560
+ `rules test` both, logger and rules changes, `groups`/`endpoints` changes, `logs activations stop` and `logs capture`
561
+ `EditPluginsSettings`; `logs match` and `logs activations` need `ViewTelemetry`, and changing the audit group or an
562
+ endpoint it sends to also needs `EditAuditLogging`. `logs match` takes the whole filter as one quoted argument; `--grep`
563
+ is a regex and `--space` matches the space and its subspaces, both case-sensitive, as `=~` is. `groups add|set|rm`
564
+ need a server that returns its default log groups; on an older one, replace them all with `groups put`. `capture` is an expiring
565
+ `userGroupSettings` entry with `saveHttpRequests` on (seeded from the All Users entry, with `usage` saved); on an entry
566
+ that already expires later it keeps that time. `capture stop` removes only an expiring entry that holds nothing but the
567
+ capture; for any other entry use `logger set --user <login> --remove`. Read what it recorded with
568
+ `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
569
+ 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).
570
+
571
+ ### Logging one command (`--log`)
572
+
573
+ `logger set --user` changes what a user logs for a while; `--log` makes the server keep what the
574
+ requests of one command log, and prints it at the end. It works on every `grok s` command, `grok s o`
575
+ included:
576
+
577
+ ```bash
578
+ grok s functions run UsageAnalysis:Groups --log # verbose: every level
579
+ grok s functions run UsageAnalysis:Groups --log verbose,db # and every SQL statement (the db debug flag)
580
+ grok s users list --log debug,storage,credentials # levels and flags; flags alone add debug
581
+ grok s functions run 'MyPkg:MyQuery()' --debug # the query's own debug log, plus errors and warnings
582
+ grok s users list --log --output json 2>log.jsonl # one JSON object per record
583
+ ```
584
+
585
+ Every request of the command carries one `datlas-log-id` (a new UUID per command) and the spec in
586
+ `datlas-log`. A command is often several requests, and each can land on a different isolate and
587
+ server instance: each keeps what its request logged at the spec in memory, in a buffer for that id.
588
+ Those records never reach the database, the console or a sync destination, and what the server logs
589
+ the usual way does not change. When the command ends, `GET /log/requests/<logId>` gathers the
590
+ buffers from every isolate of every instance over the message queue, hands them over and forgets
591
+ them; the CLI prints them to stderr, so stdout keeps the result. The gather also adds what the
592
+ server saved the usual way for the command's session at the levels of the spec (errors, warnings, audit),
593
+ skipping copies of records it already has, so an instance that died before it could answer still
594
+ shows its saved errors. A buffer nobody gathers is dropped after an hour. No permission is needed; another user's id takes `ViewTelemetry`.
595
+ `grok s o logs request <logId>` gathers one id by hand.
596
+
597
+ The spec is comma-separated: `verbose`, `standard`, the levels (`error`, `warning`, `info`, `debug`,
598
+ `audit`, `usage`) and the debug flags of the logger settings (`db`, `socket`, `storage`, `credentials`,
599
+ `docker`, `auth`, ... or `all`).
600
+
601
+ `functions run --debug` sends `?debug=true`: the call runs as a debug call (not cached), the way the
602
+ query debugger in the UI runs it, and its own log (connection, the query's stages) is kept with the
603
+ rest; without `--log` the spec is `error,warning`. Only what runs in Datlas is kept: a script that
604
+ runs on a queue worker adds nothing here.
605
+
474
606
  ## Describing an entity type
475
607
 
476
608
  ```bash
package/README.md CHANGED
@@ -151,3 +151,17 @@ For more information on configuring connections, refer to the [Connections](http
151
151
  | debug | Sets a debug breakpoint before each test execution, allowing you to debug the test. Only works in `gui` mode |
152
152
 
153
153
  - `link` command is used for public plugins development to link `datagrok-api` and libraries.
154
+ - `connect-folder` makes a local folder a Datagrok connection. A folder agent in Docker dials out to the server (no
155
+ inbound port) with a key of its own that can only serve this folder; your keypair (`grok login <server>` first)
156
+ never leaves the machine. Connecting the same folder again resumes the same connection. Requires Docker and the
157
+ "Connect local folders" permission.
158
+
159
+ ```shell
160
+ grok connect-folder <folder> [--host <alias>] [--read-only] [--foreground] [--image <ref>] [--force]
161
+ grok connect-folder list [--host <alias>] [--output json]
162
+ grok connect-folder start|stop|logs <connection> [-f]
163
+ grok connect-folder rm <connection>
164
+ ```
165
+
166
+ `<connection>` is the connection name, a connection id prefix (8+ characters), or the folder. `stop` keeps the
167
+ folder disconnected after a reboot; `rm` removes the container, the key and the connection.
@@ -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,20 +24,30 @@ 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
- /** Naive English plural of a (snake_case) table name — `issue` → `issues`, `box` →
30
- * `boxes`, `category` → `categories`. */
29
+ /** The platform's `StringUtils.toSingular` (ddt utils.dart): regular forms only; a noun already
30
+ * singular in `-is`/`-us`/`-ss` passes through. */
31
+ function singularizeTableName(name) {
32
+ if (name.length <= 2 || !name.endsWith('s') || /(ss|is|us)$/.test(name)) return name;
33
+ if (name.endsWith('ies') && name.length > 3) return name.slice(0, -3) + 'y';
34
+ return /(xes|zzes|ches|shes|sses|tuses)$/.test(name) ? name.slice(0, -2) : name.slice(0, -1);
35
+ }
36
+
37
+ /** The platform's `StringUtils.toPlural` (ddt utils.dart) — `issue` → `issues`, `box` →
38
+ * `boxes`, `category` → `categories`, `analysis` → `analyses`. */
31
39
  function pluralizeTableName(name) {
40
+ if (name.endsWith('is')) return name.slice(0, -2) + 'es';
32
41
  if (/(s|x|z|ch|sh)$/.test(name)) return name + 'es';
33
- if (/[^aeiou_]y$/.test(name)) return name.slice(0, -1) + 'ies';
42
+ if (/[^aeiou]y$/.test(name)) return name.slice(0, -1) + 'ies';
34
43
  return name + 's';
35
44
  }
36
45
 
37
46
  /** The camelCase plural property name a table gets on the schema client and under the
38
- * schema handle's `tables` (`issue_label` → `issueLabels`). */
47
+ * schema handle's `tables` (`issue_label` → `issueLabels`, `categories` → `categories`): the
48
+ * plural of the singular, as the platform derives a table's plural name. */
39
49
  function tableProp(tableName) {
40
- return utils.snakeToCamelCase(pluralizeTableName(tableName), false);
50
+ return utils.snakeToCamelCase(pluralizeTableName(singularizeTableName(tableName)), false);
41
51
  }
42
52
  const domainSchemaPath = _path.default.join(_path.default.dirname(_path.default.dirname(__dirname)), 'domain-schema.schema.json');
43
53
  /** The platform's own schema, whose tables a qualified ref may target (`Core.users`, ...). */
@@ -229,7 +239,7 @@ function generateDomainClients(packageDir = curDir, options) {
229
239
  _fs.default.writeFileSync(_path.default.join(genDir, dbFile), normEol(content).replace(/\n/g, '\r\n'), 'utf8');
230
240
  color.log(`Successfully generated file src/generated/${dbFile}${sep}`, 'success');
231
241
  if (ui) {
232
- const uiContent = annotationForDbUiFile + `import {domains, DomainTable} from '${u2DgModule}';${sep}` + wrapTokens(`import {`, dbImports, ', ', `} from './${dbFile.replace(/\.ts$/, '')}';`) + sep + sep + uiParts.join(sep);
242
+ const uiContent = annotationForDbUiFile + `import {domains, DomainTable} from '${emsDgModule}';${sep}` + wrapTokens(`import {`, dbImports, ', ', `} from './${dbFile.replace(/\.ts$/, '')}';`) + sep + sep + uiParts.join(sep);
233
243
  _fs.default.writeFileSync(_path.default.join(genDir, dbUiFile), normEol(uiContent).replace(/\n/g, '\r\n'), 'utf8');
234
244
  color.log(`Successfully generated file src/generated/${dbUiFile}${sep}`, 'success');
235
245
  }
@@ -265,6 +275,10 @@ function findDependencyManifest(packageDir, schemaName) {
265
275
  function generateDomainSchemaCode(manifest, manifestPath, emittedTypes, packageDir) {
266
276
  const decls = [];
267
277
  const systemColumnNames = new Set(domainSystemColumns.map(([name]) => name));
278
+ // An external table's rows carry `id` (the encoded business key) and nothing else
279
+ // system-side; nothing is expanded and nothing is written (external-bindings §3).
280
+ const external = manifest.storage?.kind === 'external';
281
+ const systemColumns = external ? domainSystemColumns.filter(([name]) => name === 'id') : domainSystemColumns;
268
282
  const tableNames = Object.keys(manifest.tables);
269
283
  // Keyed by table name; a qualified ref target ('Core.queries', 'grit.issue') lands here
270
284
  // under its qualified key so the expand map can list its columns — it gets no accessor.
@@ -273,7 +287,9 @@ function generateDomainSchemaCode(manifest, manifestPath, emittedTypes, packageD
273
287
  // different sets fall back to a `<alias><PascalTable>` name (deterministic).
274
288
  const choicesAliases = {};
275
289
  const choicesAlias = (tableName, columnName, values) => {
276
- const valueSet = values.map(v => `'${v}'`).join(' | ');
290
+ // JSON.stringify escapes every special character; inside single quotes only ' needs one more
291
+ const literals = values.map(v => `'${JSON.stringify(v).slice(1, -1).replace(/'/g, `\\'`)}'`);
292
+ const valueSet = literals.join(' | ');
277
293
  let alias = utils.snakeToCamelCase(tableName) + utils.snakeToCamelCase(columnName);
278
294
  if (choicesAliases[alias] !== undefined && choicesAliases[alias] !== valueSet) alias += utils.snakeToCamelCase(tableName);
279
295
  if (choicesAliases[alias] !== undefined && choicesAliases[alias] !== valueSet) {
@@ -282,7 +298,7 @@ function generateDomainSchemaCode(manifest, manifestPath, emittedTypes, packageD
282
298
  }
283
299
  if (choicesAliases[alias] === undefined) {
284
300
  choicesAliases[alias] = valueSet;
285
- decls.push(wrapTokens(`export type ${alias} = `, values.map(v => `'${v}'`), ' | ', ';'));
301
+ decls.push(wrapTokens(`export type ${alias} = `, literals, ' | ', ';'));
286
302
  }
287
303
  return alias;
288
304
  };
@@ -360,6 +376,7 @@ function generateDomainSchemaCode(manifest, manifestPath, emittedTypes, packageD
360
376
  insertType: column.type === 'datetime' ? 'Dayjs | string' : tsType,
361
377
  required: column.required === true,
362
378
  autoNumber: column.autoNumber != null,
379
+ hasDefault: column.default != null,
363
380
  ref: ref
364
381
  });
365
382
  return true;
@@ -376,6 +393,7 @@ function generateDomainSchemaCode(manifest, manifestPath, emittedTypes, packageD
376
393
  }
377
394
  return columns;
378
395
  }
396
+ const accessorTables = new Map();
379
397
  for (const tableName of tableNames) {
380
398
  const typeName = utils.snakeToCamelCase(tableName);
381
399
  if (emittedTypes.has(typeName)) {
@@ -383,6 +401,12 @@ function generateDomainSchemaCode(manifest, manifestPath, emittedTypes, packageD
383
401
  return null;
384
402
  }
385
403
  emittedTypes.add(typeName);
404
+ const accessor = tableProp(tableName);
405
+ if (accessorTables.has(accessor)) {
406
+ color.error(`${manifestPath}: tables '${accessorTables.get(accessor)}' and '${tableName}' ` + `both get the accessor '${accessor}' (rename one of them)`);
407
+ return null;
408
+ }
409
+ accessorTables.set(accessor, tableName);
386
410
  const columns = collectColumns(tableName, manifest, false);
387
411
  if (columns == null) return null;
388
412
  tableColumns[tableName] = columns;
@@ -427,12 +451,12 @@ function generateDomainSchemaCode(manifest, manifestPath, emittedTypes, packageD
427
451
  const columns = tableColumns[tableName];
428
452
  const relations = tableRelations[tableName];
429
453
  const rowLines = [`/** Row of \`${manifest.name}.${tableName}\`. */`, `export interface ${typeName}Row {`];
430
- for (const [name, tsType] of domainSystemColumns) rowLines.push(` ${name}: ${tsType};`);
454
+ for (const [name, tsType] of systemColumns) rowLines.push(` ${name}: ${tsType};`);
431
455
  for (const c of columns) rowLines.push(` ${c.name}${c.required || c.autoNumber ? '' : '?'}: ${c.tsType};`);
432
456
  rowLines.push('}');
433
457
  decls.push(rowLines.join(sep));
434
458
  const insertLines = [`/** Insert payload for \`${manifest.name}.${tableName}\`. */`, `export interface ${typeName}Insert {`];
435
- for (const c of columns) insertLines.push(` ${c.name}${c.required ? '' : '?'}: ${c.insertType};`);
459
+ for (const c of columns) insertLines.push(` ${c.name}${c.required && !c.hasDefault ? '' : '?'}: ${c.insertType};`);
436
460
  for (const r of relations) insertLines.push(` /** Link set: \`${r.target}\` row ids. */`, ` ${r.name}?: string[];`);
437
461
  if (table.idempotency === true) insertLines.push(' idempotencyKey?: string;');
438
462
  insertLines.push('}');
@@ -447,13 +471,13 @@ function generateDomainSchemaCode(manifest, manifestPath, emittedTypes, packageD
447
471
  updateLines.push('};');
448
472
  decls.push(updateLines.join(sep));
449
473
  }
450
- decls.push(formatColumnUnion(`${typeName}Column`, [...domainSystemColumns.map(([name]) => name), ...columns.map(c => c.name)]));
474
+ decls.push(formatColumnUnion(`${typeName}Column`, [...systemColumns.map(([name]) => name), ...columns.map(c => c.name)]));
451
475
 
452
476
  // Expand map: MUST stay a `type` alias — object-type literals carry the implicit
453
477
  // index signature the `TExpand extends {[key: string]: {}}` constraint needs
454
478
  // (an interface would fail it).
455
479
  const expandEntries = [];
456
- for (const c of columns) {
480
+ for (const c of external ? [] : columns) {
457
481
  if (c.ref == null) continue;
458
482
  const fields = tableColumns[c.ref].map(tc => `'${c.name}.${tc.name}'?: ${tc.tsType}`);
459
483
  expandEntries.push(wrapTokens(` '${c.name}': {`, fields, '; ', '};', ' '));
@@ -463,15 +487,16 @@ function generateDomainSchemaCode(manifest, manifestPath, emittedTypes, packageD
463
487
  // The link shape stays INLINE (structurally DG.DomainRelationLink): generated
464
488
  // files must compile against the datagrok-api the package depends on, which may
465
489
  // 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) {
490
+ for (const r of external ? [] : relations) expandEntries.push(` '${r.name}': {${r.name}?: {id: string; name: string}[]};`);
491
+ for (const childName of external ? [] : tableNames) {
468
492
  const fks = tableColumns[childName].filter(c => c.ref === tableName);
469
493
  if (fks.length === 0) continue;
470
494
  const childType = utils.snakeToCamelCase(childName);
471
495
  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
496
  }
473
497
  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} |`);
498
+ 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} |`);
499
+ if (!external && table.softDelete !== false) txArms.push(` {op: 'restore'; table: '${tableName}'; id: string} |`);
475
500
 
476
501
  // No datetime config: the client resolves datetime columns from the domain
477
502
  // registry itself (the Dayjs typing in <Table>Row stays true for every client).
@@ -480,10 +505,14 @@ function generateDomainSchemaCode(manifest, manifestPath, emittedTypes, packageD
480
505
  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
506
  }
482
507
  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));
508
+ const transaction = [];
509
+ if (!external) {
510
+ const lastArm = txArms.pop();
511
+ txArms.push(lastArm.replace(/ \|$/, ';'));
512
+ decls.push([`export type ${schemaType}TransactionOp =`, ...txArms].join(sep));
513
+ 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]>}>;`, ' },');
514
+ }
515
+ 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
516
  return decls.join(sep.repeat(2)) + sep;
488
517
  }
489
518
 
@@ -13,6 +13,7 @@ exports.checkImportStatements = checkImportStatements;
13
13
  exports.checkNpmIgnore = checkNpmIgnore;
14
14
  exports.checkPackageFile = checkPackageFile;
15
15
  exports.checkSourceMap = checkSourceMap;
16
+ exports.checkU2Usage = checkU2Usage;
16
17
  exports.extractExternals = extractExternals;
17
18
  var _fs = _interopRequireDefault(require("fs"));
18
19
  var _path = _interopRequireDefault(require("path"));
@@ -20,6 +21,7 @@ var _ignoreWalk = _interopRequireDefault(require("ignore-walk"));
20
21
  var utils = _interopRequireWildcard(require("../utils/utils"));
21
22
  var color = _interopRequireWildcard(require("../utils/color-utils"));
22
23
  var _const = require("../utils/const");
24
+ var _child_process = require("child_process");
23
25
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
24
26
  const warns = ['Latest package version', 'Datagrok API version should contain', 'is served by the platform'];
25
27
  const forbiddenNames = ['function', 'class', 'export'];
@@ -91,6 +93,8 @@ function runChecks(packagePath, soft = false, noExit = false) {
91
93
  if (externals) errors.push(...checkImportStatements(packagePath, jsTsFiles, externals));
92
94
  }
93
95
  errors.push(...checkDatagrokApiImports(packagePath, jsTsFiles));
96
+ const u2Errors = checkU2Usage(packagePath, json);
97
+ errors.push(...u2Errors);
94
98
  warnings.push(...checkHeavyImports(packagePath, jsTsFiles, _path.default.basename(packagePath)));
95
99
  if (!soft) errors.push(...checkSourceMap(packagePath));
96
100
  errors.push(...checkNpmIgnore(packagePath));
@@ -111,7 +115,8 @@ function runChecks(packagePath, soft = false, noExit = false) {
111
115
  if (errors.length) {
112
116
  console.log(`Checking package ${_path.default.basename(packagePath)}...`);
113
117
  showError(errors);
114
- if (soft || json.version.startsWith('0') || errors.every(w => warns.some(ww => w.includes(ww)))) return true;
118
+ // the u2 consumer ratchet holds pre-1.0 packages too
119
+ if (soft || u2Errors.length === 0 && (json.version.startsWith('0') || errors.every(w => warns.some(ww => w.includes(ww))))) return true;
115
120
  if (noExit) return false;else process.exit(1);
116
121
  }
117
122
  color.log(`Checking package ${_path.default.basename(packagePath)}...\t\t\t\u2713 OK`);
@@ -213,6 +218,43 @@ function checkDatagrokApiImports(packagePath, files) {
213
218
  }
214
219
  return errors;
215
220
  }
221
+
222
+ /** u2's consumer check for a package built on u2 or ems: the u2 lint rules and the shrink-only counts of
223
+ * `consumers-baseline.json`. Prints the ratchet line; growth and enforced rules are errors. */
224
+ function checkU2Usage(packagePath, json) {
225
+ const deps = {
226
+ ...json.dependencies,
227
+ ...json.devDependencies
228
+ };
229
+ if (!('@datagrok-libraries/u2' in deps) && !('@datagrok-libraries/ems' in deps)) return [];
230
+ let script;
231
+ try {
232
+ script = _path.default.join(_path.default.dirname(require.resolve('@datagrok-libraries/u2/package.json', {
233
+ paths: [packagePath]
234
+ })), 'scripts', 'check-consumer.mjs');
235
+ } catch {
236
+ return [];
237
+ }
238
+ if (!_fs.default.existsSync(script)) return [];
239
+ const run = (0, _child_process.spawnSync)(process.execPath, [script, packagePath, '--json'], {
240
+ encoding: 'utf-8',
241
+ maxBuffer: 64 * 1024 * 1024,
242
+ timeout: 300000
243
+ });
244
+ let report;
245
+ try {
246
+ report = JSON.parse(run.stdout);
247
+ } catch {
248
+ return [`u2 consumer check could not run: ${(run.stderr || run.stdout || String(run.error)).trim()}`];
249
+ }
250
+ const errors = [];
251
+ for (const tree of report.trees) {
252
+ for (const line of tree.summary) console.log(line);
253
+ for (const note of tree.notes) color.log(` ${note}`, 'warn');
254
+ errors.push(...tree.problems);
255
+ }
256
+ return errors;
257
+ }
216
258
  function checkHeavyImports(packagePath, files, packageName = '') {
217
259
  const warnings = [];
218
260
  const isTestPackage = /tests?$/i.test(packageName);