datagrok-tools 6.7.8 → 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.
@@ -1,5 +1,5 @@
1
1
 
2
- > datagrok-tools@6.7.8 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,17 @@
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
+
3
15
  ## 6.7.8 (2026-10-13)
4
16
 
5
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
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:
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
@@ -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
 
@@ -0,0 +1,526 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.OBSERVE_USAGE = void 0;
7
+ exports.fieldRows = fieldRows;
8
+ exports.handleObserve = handleObserve;
9
+ exports.parseDuration = parseDuration;
10
+ exports.parseTime = parseTime;
11
+ exports.setPath = setPath;
12
+ var fs = _interopRequireWildcard(require("fs"));
13
+ var _nodeDapi = require("../utils/node-dapi");
14
+ var _serverOutput = require("../utils/server-output");
15
+ var _walker = require("../utils/migrate/walker");
16
+ 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); }
17
+ /// `grok s observe` (alias `grok s o`): problems, problem rules, logger settings, errors, the timeline and cloud logs,
18
+ /// over `/problems`, `/admin/plugins/<key>/settings`, `/alerts/Test_rules`, `/log`, `/admin/metrics`, `/log/timeline`,
19
+ /// `/log/cloud/*`, `/log/archive/*` and `/docker/containers`.
20
+
21
+ const OBSERVE_USAGE = exports.OBSERVE_USAGE = `Usage: grok s observe <command> <verb> [args] (alias: grok s o)
22
+
23
+ problems list [--status <s>] [--state <s>] [--alert-status <s>] [--kind <k>] [--key <k>] [--since 7d] [--limit 100]
24
+ Comma lists or all; --alert-status open lists the open alerts
25
+ problems get <id|kind:key> One field per line, details by path
26
+ problems history <id|kind:key> [--limit 50]
27
+ problems status <id|kind:key> --data '{"status": "muted", "reason": "...", "until": "<ISO>"}' | --json body.json
28
+ problems ack <id|kind:key> --reason <text>
29
+ problems resolve <id|kind:key> --reason <text>
30
+ rules get The problem rules (Settings > Alerts), as JSON
31
+ rules put --json rules.json Replace them; the server checks them first
32
+ rules test Evaluate the saved rules once, now
33
+ logger get [<path>] The logger settings, or one value (exportSettings.0.types)
34
+ logger set --set <path>=<json> ... Change values; with --user <login> or --group <name|id>, that
35
+ [--for 30m | --until <ISO>] [--reason <text>] entry of userGroupSettings (a new one starts from
36
+ [--remove] All users), removed when it expires; --remove deletes it
37
+ logger history [--limit 20] The log-settings-changed audit records, with each change's diff
38
+ errors top [--since 24h | --date "this week" | --from <ISO> [--to <ISO>]] [--by message|signature]
39
+ [--package <name>] [--user <login>] [--source <s>] [--limit 10]
40
+ Ranked by distinct users, so errors nobody saw (scheduler, health
41
+ probes) come last: narrow those with --source server
42
+ errors get <signature|problem> [--since 7d] [--limit 50]
43
+ Each occurrence of an error (a signature from errors top, or the
44
+ id or kind:key of its problem): time, user, function, session
45
+ logs container [<name|id> [--since 1h | --from <ISO> [--to <ISO>]] [--grep <regex>] [--limit 20000]]
46
+ A docker container's log lines (its latest --limit, then the
47
+ window and --grep); no name, the containers and their status
48
+ timeline --session <id> [--from <ISO|-15m>] [--to <ISO>] [--limit 500]
49
+ A session's events and requests in a window: no --from, the last
50
+ 15 min; --to defaults to --from + 10 min, at most 2 h
51
+ logs cloud [--group <g>] [--since 1h | --from <ISO> [--to <ISO>]] [--filter <pattern>] [--limit 1000]
52
+ CloudWatch events; no --group, the instance's own group;
53
+ --filter is a CloudWatch filter pattern
54
+ logs archive list [--prefix <p>] [--since 7d] [--limit n]
55
+ Objects in the log archive (WORM bucket), by key; --since filters
56
+ only the page the server returned, so narrow it with --prefix
57
+ logs archive read <key> One archived object decoded to events
58
+ logs cloud and logs archive take [--connection <id>]; without it,
59
+ the instance's AWS role
60
+ logs capture [<login|group> [--for 30m] [--reason <text>]]
61
+ Save every API request of a user or a group for a while: logger set
62
+ of saveHttpRequests on their expiring entry (an entry that already
63
+ expires later keeps its time); no argument, the active captures;
64
+ read them with timeline --session <id>
65
+ logs capture stop <login|group> Remove that expiring entry; one that holds other settings is left
66
+ to logger set --remove
67
+
68
+ Durations are 30m (minutes), 24h, 7d, 2w; times are ISO 8601 (UTC without an offset) or
69
+ relative, written with = (--from=-2h).`;
70
+ const UNIT_MS = {
71
+ m: 60000,
72
+ h: 3600000,
73
+ d: 86400000,
74
+ w: 604800000
75
+ };
76
+
77
+ /** `30m`, `2h`, `7d`, `1w` in milliseconds; `m` is minutes, unlike `grok s pull --since`, where it is months. */
78
+ function parseDuration(value, flag) {
79
+ const m = /^(\d+)([mhdw])$/.exec(String(value ?? '').trim());
80
+ if (!m || Number(m[1]) <= 0) throw new Error(`${flag} expects a duration such as 30m, 2h, 7d or 1w, got '${value}'`);
81
+ return Number(m[1]) * UNIT_MS[m[2]];
82
+ }
83
+
84
+ /** ISO 8601 (UTC without an offset), or relative to [now] (`-2h`). */
85
+ function parseTime(value, flag, now = new Date()) {
86
+ let s = String(value ?? '').trim();
87
+ if (s.startsWith('-')) return new Date(now.getTime() - parseDuration(s.substring(1), flag));
88
+ if (/[T ].*[+-]\d{2}$/.test(s)) s = `${s}:00`;
89
+ const d = new Date(/^\d{4}-\d{2}-\d{2}$/.test(s) || /(Z|[+-]\d{2}:?\d{2})$/i.test(s) ? s : `${s}Z`);
90
+ if (!/^\d{4}-\d{2}-\d{2}/.test(s) || isNaN(d.getTime())) throw new Error(`${flag} expects an ISO time (2026-10-04T06:00) or a relative one (-2h), got '${value}'`);
91
+ return d;
92
+ }
93
+
94
+ /** `a.b.0.c=<json>` applied to [target]; a value that is not JSON is a string. */
95
+ function setPath(target, assignment) {
96
+ const at = assignment.indexOf('=');
97
+ if (at <= 0) throw new Error(`--set expects <path>=<json>, got '${assignment}'`);
98
+ const keys = assignment.slice(0, at).split('.');
99
+ const text = assignment.slice(at + 1);
100
+ let value;
101
+ try {
102
+ value = JSON.parse(text);
103
+ } catch {
104
+ value = text;
105
+ }
106
+ let node = target;
107
+ for (const key of keys.slice(0, -1)) node = node[key] ??= {};
108
+ node[keys[keys.length - 1]] = value;
109
+ return keys[0];
110
+ }
111
+ function readBody(argv) {
112
+ if (argv.json) return JSON.parse(fs.readFileSync(argv.json, 'utf8'));
113
+ if (argv.data !== undefined) return JSON.parse(String(argv.data));
114
+ throw new Error('Expected a body: --data \'<json>\' or --json <file>');
115
+ }
116
+ const iso = (value, flag) => value === undefined ? undefined : parseTime(value, flag).toISOString();
117
+
118
+ /** A record as `field, value` rows, nested objects by dotted path, so a table shows every value in full. */
119
+ function fieldRows(record, prefix = '') {
120
+ const rows = [];
121
+ for (const [key, value] of Object.entries(record ?? {})) if (value !== null && typeof value === 'object' && !Array.isArray(value)) rows.push(...fieldRows(value, `${prefix}${key}.`));else rows.push({
122
+ field: `${prefix}${key}`,
123
+ value
124
+ });
125
+ return rows;
126
+ }
127
+ async function handleObserve(dapi, command, verb, rest, argv, output) {
128
+ // A `:` cannot pass through a route parameter behind the proxy, so kind:key is looked up by the list's filters.
129
+ let id;
130
+ const needId = async () => {
131
+ const given = rest[0] === undefined ? '' : String(rest[0]);
132
+ const colon = given.indexOf(':');
133
+ if (!given || colon === 0 || colon === given.length - 1) throw new Error(`Usage: grok s observe ${command} ${verb} <id|kind:key>`);
134
+ if (colon < 0) {
135
+ id = given;
136
+ return;
137
+ }
138
+ const found = await dapi.problems.list({
139
+ kind: given.substring(0, colon),
140
+ key: given.substring(colon + 1),
141
+ limit: 1
142
+ });
143
+ if (!found?.length) throw new Error(`No problem '${given}'`);
144
+ id = found[0].id;
145
+ };
146
+ const problemPath = () => `/problems/${encodeURIComponent(id)}`;
147
+ // The route answers JSON as text/plain.
148
+ const settings = async key => {
149
+ const r = await dapi.raw('GET', `/admin/plugins/${key}/settings`);
150
+ return (typeof r === 'string' ? JSON.parse(r) : r)?.settings ?? {};
151
+ };
152
+ // A save within 5 s of the previous one is refused (429) until that one is applied.
153
+ const saveSettings = async (key, body) => {
154
+ for (let attempt = 0;; attempt++) {
155
+ try {
156
+ return await dapi.raw('POST', `/admin/plugins/${key}/settings`, body);
157
+ } catch (e) {
158
+ if (attempt >= 2 || e?.apiError?.errorCode !== 429) throw e;
159
+ await new Promise(resolve => setTimeout(resolve, 5000));
160
+ }
161
+ }
162
+ };
163
+ const saveLogger = body => saveSettings('logger', body);
164
+ switch (`${command} ${verb}`) {
165
+ case 'problems list':
166
+ (0, _serverOutput.printOutput)(await dapi.problems.list({
167
+ status: argv.status,
168
+ state: argv.state,
169
+ alertStatus: argv['alert-status'],
170
+ kind: argv.kind,
171
+ key: argv.key,
172
+ since: argv.since,
173
+ limit: argv.limit
174
+ }), output);
175
+ return true;
176
+ case 'problems get':
177
+ {
178
+ await needId();
179
+ const problem = await dapi.problems.find(id);
180
+ if (!problem) throw new Error(`No problem '${rest[0]}'`);
181
+ (0, _serverOutput.printOutput)(output === 'table' ? fieldRows(problem) : problem, output);
182
+ return true;
183
+ }
184
+ case 'problems history':
185
+ await needId();
186
+ (0, _serverOutput.printOutput)(await dapi.raw('GET', `${problemPath()}/history${(0, _nodeDapi.buildQuery)({
187
+ limit: argv.limit
188
+ })}`), output);
189
+ return true;
190
+ case 'problems status':
191
+ await needId();
192
+ (0, _serverOutput.printOutput)(await dapi.raw('POST', `${problemPath()}/status`, readBody(argv)), output);
193
+ return true;
194
+ case 'problems ack':
195
+ case 'problems resolve':
196
+ await needId();
197
+ (0, _serverOutput.printOutput)(await dapi.raw('POST', `${problemPath()}/${verb}`, {
198
+ reason: argv.reason
199
+ }), output);
200
+ return true;
201
+ case 'rules get':
202
+ {
203
+ const rules = (await settings('alerts'))?.problemRules;
204
+ (0, _serverOutput.printOutput)(rules ? JSON.parse(rules) : [], output === 'table' ? 'json' : output);
205
+ return true;
206
+ }
207
+ case 'rules put':
208
+ {
209
+ const rules = readBody(argv);
210
+ await saveSettings('alerts', {
211
+ problemRules: typeof rules === 'string' ? rules : JSON.stringify(rules)
212
+ });
213
+ if (output !== 'quiet') console.log('Saved the problem rules');
214
+ return true;
215
+ }
216
+ case 'rules test':
217
+ console.log(await dapi.raw('POST', '/alerts/Test_rules'));
218
+ return true;
219
+ case 'logger get':
220
+ {
221
+ let value = await settings('logger');
222
+ for (const key of rest[0] ? String(rest[0]).split('.') : []) value = value?.[key];
223
+ (0, _serverOutput.printOutput)(value, output === 'table' ? 'json' : output);
224
+ return true;
225
+ }
226
+ case 'logger set':
227
+ return loggerSet(dapi, argv, await settings('logger'), saveLogger, output);
228
+ case 'logger history':
229
+ {
230
+ const text = encodeURIComponent('eventType.name = "log-settings-changed"');
231
+ const events = (await dapi.raw('GET', `/log?text=${text}&limit=${argv.limit ?? 20}&page=1`)) ?? [];
232
+ (0, _serverOutput.printOutput)(events.map(e => {
233
+ const params = {};
234
+ for (const p of e.parameters ?? []) params[p.parameter?.name] = p.value;
235
+ const row = {
236
+ time: e.eventTime,
237
+ user: params.user,
238
+ changed: params.changed
239
+ };
240
+ return output === 'table' ? row : {
241
+ ...row,
242
+ diff: params.diff ? (0, _nodeDapi.tryParseJson)(params.diff) ?? params.diff : null
243
+ };
244
+ }), output);
245
+ return true;
246
+ }
247
+ case 'errors top':
248
+ {
249
+ if (argv.since !== undefined && (argv.from !== undefined || argv.date !== undefined)) throw new Error('--since is not used with --from or --date');
250
+ if (argv.date !== undefined && argv.from !== undefined) throw new Error('--date is not used with --from');
251
+ const metrics = await dapi.raw('GET', `/admin/metrics${(0, _nodeDapi.buildQuery)({
252
+ date_start: argv.since !== undefined ? new Date(Date.now() - parseDuration(argv.since, '--since')).toISOString() : iso(argv.from, '--from'),
253
+ date_end: iso(argv.to, '--to'),
254
+ date: argv.date,
255
+ limit: argv.limit,
256
+ errors_by: argv.by,
257
+ package: argv.package,
258
+ user: argv.user,
259
+ source: argv.source
260
+ })}`);
261
+ // The server answers the top `limit` of every source, in the global order.
262
+ const top = (metrics?.errors?.top ?? []).slice(0, Number(argv.limit ?? 10));
263
+ (0, _serverOutput.printOutput)(output === 'json' ? {
264
+ ...metrics?.errors,
265
+ top
266
+ } : top, output);
267
+ return true;
268
+ }
269
+ case 'errors get':
270
+ {
271
+ if (rest[0] === undefined) throw new Error('Usage: grok s observe errors get <signature|problem id|kind:key> [--since 7d] [--limit 50]');
272
+ const given = String(rest[0]);
273
+ if (given.includes(':')) await needId();
274
+ const problem = await dapi.problems.find(id ?? given);
275
+ const signature = problem ? problem.details?.signatureId : given;
276
+ if (!signature) throw new Error(`Problem '${given}' is not about an error: it has no signatureId`);
277
+ // An error with a stack is grouped by its stack hash, one without by its event type (ErrorIncidentProblem).
278
+ const text = `(eventType.id = "${signature}" or eventType.errorStackTraceHash = "${signature}") and ` + `eventTime > "${new Date(Date.now() - parseDuration(argv.since ?? '7d', '--since')).toISOString()}"`;
279
+ const events = (await dapi.raw('GET', `/log${(0, _nodeDapi.buildQuery)({
280
+ text,
281
+ limit: argv.limit ?? 50,
282
+ page: 1
283
+ })}`)) ?? [];
284
+ const logins = new Map();
285
+ for (const e of events) for (const p of e.parameters ?? []) if (p.parameter?.name === 'user' && p.value && !logins.has(p.value)) logins.set(p.value, (await dapi.users.find(p.value))?.login ?? p.value);
286
+ (0, _serverOutput.printOutput)(events.map(e => {
287
+ const params = {};
288
+ for (const p of e.parameters ?? []) params[p.parameter?.name] = p.value;
289
+ const row = {
290
+ time: e.eventTime,
291
+ source: params.source,
292
+ user: logins.get(params.user) ?? null,
293
+ function: params.function ?? null,
294
+ session: e.session?.id ?? null,
295
+ message: e.description
296
+ };
297
+ return output === 'table' ? row : {
298
+ ...row,
299
+ eventId: e.id,
300
+ params
301
+ };
302
+ }), output);
303
+ return true;
304
+ }
305
+ }
306
+ if (command === 'logs' && verb === 'capture') return capture(dapi, rest, argv, await settings('logger'), saveLogger, output);
307
+ if (command === 'logs') return logs(dapi, verb, rest, argv, output);
308
+ if (command === 'timeline') {
309
+ if (argv.session === undefined) throw new Error('Usage: grok s observe timeline --session <id> [--from <ISO|-15m>] [--to <ISO>] [--limit 500]');
310
+ needFrom(argv);
311
+ const window = argv.from === undefined ? {
312
+ from: iso('-15m', '--from'),
313
+ to: new Date().toISOString()
314
+ } : {
315
+ from: iso(argv.from, '--from'),
316
+ to: iso(argv.to, '--to')
317
+ };
318
+ (0, _serverOutput.printOutput)(await dapi.raw('GET', `/log/timeline${(0, _nodeDapi.buildQuery)({
319
+ session: argv.session,
320
+ ...window,
321
+ limit: argv.limit
322
+ })}`), output);
323
+ return true;
324
+ }
325
+ throw new Error(`Unknown command '${[command, verb].filter(Boolean).join(' ')}'\n${OBSERVE_USAGE}`);
326
+ }
327
+ const needFrom = argv => {
328
+ if (argv.to !== undefined && argv.from === undefined) throw new Error('--to needs --from');
329
+ };
330
+ async function logs(dapi, verb, rest, argv, output) {
331
+ const connection = argv.connection;
332
+ if (verb === 'cloud') {
333
+ if (argv.since !== undefined && argv.from !== undefined) throw new Error('--since is not used with --from');
334
+ needFrom(argv);
335
+ const group = argv.group ?? (await dapi.raw('GET', `/log/cloud/groups${(0, _nodeDapi.buildQuery)({
336
+ connection
337
+ })}`))?.[0];
338
+ if (!group) throw new Error('No log group to read: pass --group');
339
+ const window = argv.from !== undefined ? {
340
+ start: iso(argv.from, '--from'),
341
+ end: iso(argv.to, '--to')
342
+ } : {
343
+ start: new Date(Date.now() - parseDuration(argv.since ?? '1h', '--since')).toISOString()
344
+ };
345
+ (0, _serverOutput.printOutput)(await dapi.raw('GET', `/log/cloud/events${(0, _nodeDapi.buildQuery)({
346
+ connection,
347
+ group,
348
+ ...window,
349
+ filter: argv.filter,
350
+ limit: argv.limit,
351
+ format: 'json'
352
+ })}`), output);
353
+ return true;
354
+ }
355
+ if (verb === 'container') return containerLogs(dapi, rest[0], argv, output);
356
+ if (verb === 'archive' && rest[0] === 'list') {
357
+ let objects = (await dapi.raw('GET', `/log/archive/objects${(0, _nodeDapi.buildQuery)({
358
+ connection,
359
+ prefix: argv.prefix,
360
+ limit: argv.limit,
361
+ format: 'json'
362
+ })}`)) ?? [];
363
+ if (argv.since !== undefined) {
364
+ const from = Date.now() - parseDuration(argv.since, '--since');
365
+ objects = objects.filter(o => Date.parse(o.modified) >= from);
366
+ }
367
+ (0, _serverOutput.printOutput)(objects, output);
368
+ return true;
369
+ }
370
+ if (verb === 'archive' && rest[0] === 'read' && rest[1] !== undefined) {
371
+ (0, _serverOutput.printOutput)(await dapi.raw('GET', `/log/archive/events${(0, _nodeDapi.buildQuery)({
372
+ connection,
373
+ key: rest[1],
374
+ format: 'json'
375
+ })}`), output);
376
+ return true;
377
+ }
378
+ throw new Error('Usage: grok s observe logs cloud [--group <g>] ... | logs container [<name|id>] | ' + 'logs archive list [--prefix <p>] | logs archive read <key> | logs capture [<login|group>] | ' + 'logs capture stop <login|group>');
379
+ }
380
+
381
+ /// `logs container`: the server only cuts a container's log by line count, so the window and --grep apply here.
382
+ async function containerLogs(dapi, name, argv, output) {
383
+ if (name === undefined) {
384
+ const all = (await dapi.raw('GET', `/docker/containers${(0, _nodeDapi.buildQuery)({
385
+ limit: 1000
386
+ })}`)) ?? [];
387
+ (0, _serverOutput.printOutput)(all.map(c => ({
388
+ id: c.id,
389
+ name: c.name,
390
+ status: c.status
391
+ })), output);
392
+ return true;
393
+ }
394
+ if (argv.since !== undefined && argv.from !== undefined) throw new Error('--since is not used with --from');
395
+ needFrom(argv);
396
+ let id = String(name);
397
+ if (!_nodeDapi.UUID_RE.test(id)) {
398
+ const found = (await dapi.raw('GET', `/docker/containers${(0, _nodeDapi.buildQuery)({
399
+ text: id,
400
+ limit: 100
401
+ })}`)) ?? [];
402
+ const container = found.find(c => c.name === id);
403
+ if (!container) throw new Error(`No container '${id}'${found.length ? `; similar: ${found.map(c => c.name).join(', ')}` : ''}`);
404
+ id = container.id;
405
+ }
406
+ const from = argv.from !== undefined ? parseTime(argv.from, '--from').getTime() : argv.since !== undefined ? Date.now() - parseDuration(argv.since, '--since') : -Infinity;
407
+ const to = argv.to !== undefined ? parseTime(argv.to, '--to').getTime() : Infinity;
408
+ const grep = argv.grep === undefined ? null : new RegExp(String(argv.grep), 'i');
409
+ const text = await dapi.raw('GET', `/docker/containers/${id}/logs${(0, _nodeDapi.buildQuery)({
410
+ limit: argv.limit ?? 20000
411
+ })}`);
412
+ // A line without its own timestamp (a stack frame) belongs to the window of the line before it.
413
+ let inWindow = from === -Infinity && to === Infinity;
414
+ const lines = [];
415
+ for (const line of String(text ?? '').split('\n')) {
416
+ const stamp = /^(\d{4}-\d{2}-\d{2}T[\d:]{8})(?:\.\d+)?(Z|[+-]\d{2}:\d{2})/.exec(line);
417
+ if (stamp) {
418
+ const t = Date.parse(`${stamp[1]}${stamp[2]}`);
419
+ inWindow = t >= from && t <= to;
420
+ }
421
+ if (inWindow && line && (!grep || grep.test(line))) lines.push(line);
422
+ }
423
+ if (output === 'json') (0, _serverOutput.printOutput)(lines, output);else for (const line of lines) console.log(line);
424
+ return true;
425
+ }
426
+ async function loggerSet(dapi, argv, current, save, output) {
427
+ const sets = argv.set === undefined ? [] : (Array.isArray(argv.set) ? argv.set : [argv.set]).map(String);
428
+ const entry = argv.user !== undefined || argv.group !== undefined || argv.target !== undefined;
429
+ if (!sets.length && !entry) throw new Error('Usage: grok s observe logger set --set <path>=<json> ... [--user <login> | --group <name|id>]');
430
+ if (!entry && (argv.for !== undefined || argv.until !== undefined || argv.reason !== undefined || argv.remove)) throw new Error('--for, --until, --reason and --remove apply to a --user or --group entry');
431
+ const name = argv.user ?? argv.group ?? argv.name;
432
+ if (!entry) {
433
+ const body = {};
434
+ for (const s of sets) {
435
+ const key = setPath(current, s);
436
+ body[key] = current[key];
437
+ }
438
+ await save(body);
439
+ } else {
440
+ const group = argv.target ?? (argv.user !== undefined ? await dapi.groups.resolve(String(argv.user), {
441
+ personalOnly: true
442
+ }) : await dapi.groups.resolve(String(argv.group)));
443
+ const groups = {
444
+ ...current.userGroupSettings
445
+ };
446
+ const existing = groups[group.id];
447
+ if (argv.remove) {
448
+ if (!existing) throw new Error(`${name} has no logger entry`);
449
+ delete groups[group.id];
450
+ } else {
451
+ if (existing && !existing.expiresAt && (argv.for !== undefined || argv.until !== undefined)) throw new Error(`${name} has a logger entry that does not expire; change it without --for or --until`);
452
+ // A user's settings come from one entry, so a new one starts from All users instead of the defaults.
453
+ const settings = existing ? {
454
+ ...existing
455
+ } : {
456
+ ...groups[_walker.ALL_USERS]
457
+ };
458
+ if (!existing) delete settings.reason;
459
+ for (const s of sets) setPath(settings, s);
460
+ if (argv.for !== undefined) settings.expiresAt = new Date(Date.now() + parseDuration(argv.for, '--for')).toISOString();else if (argv.until !== undefined) {
461
+ settings.expiresAt = iso(argv.until, '--until');
462
+ if (Date.parse(settings.expiresAt) <= Date.now()) throw new Error(`--until ${argv.until} is in the past`);
463
+ }
464
+ if (argv.reason !== undefined) settings.reason = String(argv.reason);
465
+ groups[group.id] = settings;
466
+ }
467
+ await save({
468
+ userGroupSettings: groups
469
+ });
470
+ }
471
+ if (output !== 'quiet') console.log(!entry ? 'Saved the logger settings' : argv.remove ? `Removed the logger entry of ${name}` : `Saved the logger settings of ${name}`);
472
+ return true;
473
+ }
474
+
475
+ /// `logs capture`: logger set of saveHttpRequests, and the usage level it is saved at, on an expiring entry.
476
+ async function capture(dapi, rest, argv, current, save, output) {
477
+ const groups = current.userGroupSettings ?? {};
478
+ const stop = rest[0] === 'stop';
479
+ const name = stop ? rest[1] : rest[0];
480
+ if (name === undefined) {
481
+ if (stop) throw new Error('Usage: grok s observe logs capture stop <login|group>');
482
+ const rows = [];
483
+ for (const [id, s] of Object.entries(groups)) if (s?.saveHttpRequests === true && s.expiresAt) {
484
+ const group = await dapi.groups.find(id);
485
+ rows.push({
486
+ id,
487
+ name: group?.friendlyName ?? group?.name ?? id,
488
+ expiresAt: s.expiresAt,
489
+ reason: s.reason ?? ''
490
+ });
491
+ }
492
+ (0, _serverOutput.printOutput)(rows, output);
493
+ return true;
494
+ }
495
+ let group;
496
+ try {
497
+ group = await dapi.groups.resolve(String(name), {
498
+ personalOnly: true
499
+ });
500
+ } catch {
501
+ group = await dapi.groups.resolve(String(name));
502
+ }
503
+ const existing = groups[group.id];
504
+ if (stop) {
505
+ if (!existing?.saveHttpRequests) throw new Error(`No capture for ${name}`);
506
+ if (!existing.expiresAt) throw new Error(`The logger entry of ${name} does not expire; change it with logger set`);
507
+ const own = ['saveHttpRequests', 'postLevels', 'saveLevels', 'expiresAt', 'reason'];
508
+ const others = Object.keys(existing).filter(k => !own.includes(k) && JSON.stringify(existing[k]) !== JSON.stringify(groups[_walker.ALL_USERS]?.[k]));
509
+ if (others.length) throw new Error(`The logger entry of ${name} also holds ${others.join(', ')}; remove it with ` + `grok s o logger set --${group.personal ? 'user' : 'group'} ${name} --remove`);
510
+ return loggerSet(dapi, {
511
+ target: group,
512
+ name,
513
+ remove: true
514
+ }, current, save, output);
515
+ }
516
+ const base = existing ?? groups[_walker.ALL_USERS] ?? {};
517
+ const sets = ['saveHttpRequests=true', ...['postLevels', 'saveLevels'].filter(l => Array.isArray(base[l]) && !base[l].includes('usage')).map(l => `${l}=${JSON.stringify([...base[l], 'usage'])}`)];
518
+ const end = Math.max(Date.now() + parseDuration(argv.for ?? '30m', '--for'), Date.parse(existing?.expiresAt) || 0);
519
+ return loggerSet(dapi, {
520
+ target: group,
521
+ name,
522
+ set: sets,
523
+ reason: argv.reason,
524
+ until: new Date(end).toISOString()
525
+ }, current, save, output);
526
+ }
@@ -15,6 +15,7 @@ var _serverClient = require("../utils/server-client");
15
15
  var _serverOutput = require("../utils/server-output");
16
16
  var _serverMigrate = require("./server-migrate");
17
17
  var _serverDomains = require("./server-domains");
18
+ var _serverObserve = require("./server-observe");
18
19
  var _registry = require("../utils/migrate/registry");
19
20
  var _walker = require("../utils/migrate/walker");
20
21
  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); }
@@ -27,7 +28,7 @@ const ENTITY_TYPES = {
27
28
  reports: 'UserReport'
28
29
  };
29
30
  const ENTITIES = ['users', 'groups', 'functions', 'connections', 'queries', 'scripts', 'packages', 'reports', 'files', 'tables'];
30
- const COMMANDS = ['shares', 'domains', 'raw', 'batch', 'describe', 'healthcheck', 'sync', 'pull', 'push', 'migrate', 'diff', 'bundle', 'token'];
31
+ const COMMANDS = ['shares', 'domains', 'raw', 'batch', 'describe', 'healthcheck', 'sync', 'pull', 'push', 'migrate', 'diff', 'bundle', 'token', 'observe', 'o'];
31
32
  const VERBS = ['list', 'count', 'get', 'delete'];
32
33
  async function server(argv) {
33
34
  const args = argv['_'].slice(1);
@@ -45,6 +46,10 @@ async function server(argv) {
45
46
  console.log(HELP_SERVER);
46
47
  return true;
47
48
  }
49
+ if ((entity === 'observe' || entity === 'o') && !verb) {
50
+ console.log(_serverObserve.OBSERVE_USAGE);
51
+ return true;
52
+ }
48
53
  let client;
49
54
  try {
50
55
  client = await (0, _serverClient.createClient)(host, !!argv.admin);
@@ -60,6 +65,7 @@ async function server(argv) {
60
65
  // escaping to grok.js as an unhandled rejection (stack trace + exit 255)
61
66
  if (['pull', 'push', 'migrate', 'diff', 'bundle'].includes(entity)) return await (0, _serverMigrate.handleMigrate)(dapi, entity, [verb, ...rest].filter(Boolean), argv, output);
62
67
  if (entity === 'domains') return await (0, _serverDomains.handleDomains)(dapi, verb, rest, argv, output);
68
+ if (entity === 'observe' || entity === 'o') return await (0, _serverObserve.handleObserve)(dapi, verb, rest[0], rest.slice(1), argv, output);
63
69
  if (entity === 'batch') return await handleBatch(dapi, argv, verb, rest, output);
64
70
  // Shell scripts that used to curl /users/login/dev get a token the same way
65
71
  // every other command does, whatever credential the config holds.
@@ -927,6 +933,7 @@ Manage a Datagrok server from the command line.
927
933
  Entities:
928
934
  users, groups, functions, connections, queries, scripts, packages, reports, files, tables
929
935
  (plus domains, shares, batch, raw, describe, healthcheck, sync, pull/push/migrate/diff/bundle below)
936
+ observe (alias o): problems, problem rules, logger settings, errors, timeline, cloud logs; grok s observe prints its verbs
930
937
 
931
938
  Verbs:
932
939
  list List entities (--filter, --limit, --offset)
@@ -12,7 +12,7 @@ const domainApp = (appNames, tables) => `
12
12
  The application${appNames.length > 1 ? 's' : ''} ${appNames.join(', ')} ` + `(over ${tables.join(', ')}) ${appNames.length > 1 ? 'have' : 'has'} been added successfully
13
13
 
14
14
  Next steps:
15
- pnpm install link @datagrok-libraries/u2 (a workspace dependency)
15
+ pnpm install link @datagrok-libraries/ems (a workspace dependency)
16
16
  grok build && grok publish publish the package and open the app from the browse tree
17
17
 
18
18
  The app is the u2 defaults alone — list, search, filters, entity page with children
@@ -3,6 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ exports.ALL_USERS = void 0;
6
7
  exports.collectExternals = collectExternals;
7
8
  exports.compileFilter = compileFilter;
8
9
  exports.expand = expand;
@@ -28,7 +29,7 @@ const noProgress = () => {};
28
29
 
29
30
  /** What the bundle points at but does not carry: resolvable on the source, or dead there too. */
30
31
 
31
- const ALL_USERS = 'a4b45840-9a50-11e6-9cc9-8546b8bf62e6';
32
+ const ALL_USERS = exports.ALL_USERS = 'a4b45840-9a50-11e6-9cc9-8546b8bf62e6';
32
33
 
33
34
  /** One round trip per entity, tens of thousands per stand — modest, since the server is shared. */
34
35
  const WALK_CONCURRENCY = 12;
@@ -30,7 +30,7 @@ export interface External {id: string; type: string; nqName: string}
30
30
  /** What the bundle points at but does not carry: resolvable on the source, or dead there too. */
31
31
  export interface Outside {externals: External[]; dangling: string[]}
32
32
 
33
- const ALL_USERS = 'a4b45840-9a50-11e6-9cc9-8546b8bf62e6';
33
+ export const ALL_USERS = 'a4b45840-9a50-11e6-9cc9-8546b8bf62e6';
34
34
 
35
35
  /** One round trip per entity, tens of thousands per stand — modest, since the server is shared. */
36
36
  const WALK_CONCURRENCY = 12;
@@ -3,13 +3,14 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.NodeUsersDataSource = exports.NodeTablesDataSource = exports.NodeSharesDataSource = exports.NodePackagesDataSource = exports.NodeHttpDataSource = exports.NodeGroupsDataSource = exports.NodeFuncsDataSource = exports.NodeFilesDataSource = exports.NodeDomainsDataSource = exports.NodeDapi = exports.NodeConnectionsDataSource = exports.NodeApiClient = exports.InternalDataSource = void 0;
6
+ exports.UUID_RE = exports.NodeUsersDataSource = exports.NodeTablesDataSource = exports.NodeSharesDataSource = exports.NodePackagesDataSource = exports.NodeHttpDataSource = exports.NodeGroupsDataSource = exports.NodeFuncsDataSource = exports.NodeFilesDataSource = exports.NodeDomainsDataSource = exports.NodeDapi = exports.NodeConnectionsDataSource = exports.NodeApiClient = exports.InternalDataSource = void 0;
7
7
  exports.apiPath = apiPath;
8
8
  exports.buildQuery = buildQuery;
9
9
  exports.ensureBodyId = ensureBodyId;
10
10
  exports.mapPositionalParams = mapPositionalParams;
11
11
  exports.parseDomainAddress = parseDomainAddress;
12
12
  exports.throwIfApiError = throwIfApiError;
13
+ exports.tryParseJson = tryParseJson;
13
14
  var _crypto = require("crypto");
14
15
  var _keypair = require("./keypair");
15
16
  /// Docs: [Grok Dapi](/docs/plans/grok-dapi/)
@@ -142,7 +143,7 @@ class NodeApiClient {
142
143
  return null;
143
144
  }
144
145
  const ct = res.headers.get('content-type') ?? '';
145
- return throwIfApiError(ct.includes('application/json') ? await res.json() : await res.text());
146
+ return throwIfApiError(ct.includes('application/json') ? await res.json() : await res.text(), res.headers);
146
147
  }
147
148
  get(path) {
148
149
  return this.request('GET', path);
@@ -170,7 +171,7 @@ class NodeApiClient {
170
171
  }, false, BYTES_TIMEOUT_MS);
171
172
  if (!res.ok) await throwHttpError(res);
172
173
  const ct = res.headers.get('content-type') ?? '';
173
- return throwIfApiError(ct.includes('application/json') ? await res.json() : await res.text());
174
+ return throwIfApiError(ct.includes('application/json') ? await res.json() : await res.text(), res.headers);
174
175
  }
175
176
 
176
177
  /** POST a JSON body and read the response as raw bytes (d42 query results). */
@@ -236,13 +237,15 @@ async function throwHttpError(res) {
236
237
  * a JSON string when the handler returned text. Every response passes through here so a
237
238
  * missing entity, a rejected save or an unknown function throws instead of printing as data.
238
239
  */
239
- function throwIfApiError(payload) {
240
+ /** A user error answers HTTP 200 with the ApiError in the body and its status in the `api-error-code` header. */
241
+ function throwIfApiError(payload, headers) {
240
242
  const parsed = typeof payload === 'string' ? tryParseJson(payload) : payload;
241
243
  if (parsed?.['#type'] === 'ApiError') {
244
+ const code = headers?.get('api-error-code');
242
245
  const err = {
243
246
  error: parsed.message ?? 'Request failed',
244
247
  source: 'Server',
245
- errorCode: parsed.errorCode,
248
+ errorCode: parsed.errorCode ?? (code ? Number(code) : undefined),
246
249
  stackTrace: parsed.stackTrace,
247
250
  body: parsed
248
251
  };
@@ -388,7 +391,7 @@ class InternalDataSource {
388
391
  }
389
392
  }
390
393
  exports.InternalDataSource = InternalDataSource;
391
- const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
394
+ const UUID_RE = exports.UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
392
395
  class NodeGroupsDataSource extends NodeHttpDataSource {
393
396
  constructor(client) {
394
397
  super(client, 'groups', '/groups');
@@ -936,6 +939,10 @@ function parseDomainAddress(s, opts = {}) {
936
939
  if (opts.table === false && address.table) throw new Error(`'${str}' names a table; this command needs a schema`);
937
940
  return address;
938
941
  }
942
+ /** Every literal a `/domains/{schema}/{table}/{id}` route registers at the id position, the dot
943
+ * segments a proxy folds away, and the u2 draft sentinel — the Dart `DomainRowKey.reserved` list. */
944
+ const RESERVED_ROW_KEYS = ['access', 'aggregate', 'audit', 'batch', 'delete', 'facets', 'filters', 'query', 'update', 'version', 'watch', 'new', '.', '..'];
945
+
939
946
  /**
940
947
  * Client for entity-mapped domain tables (`/domains/...`, DomainsRouter). Rows are plain
941
948
  * JSON objects; the server validates, permission-checks and audits every write. Errors
@@ -950,6 +957,13 @@ class NodeDomainsDataSource {
950
957
  return `/domains/${encodeURIComponent(schema)}/${encodeURIComponent(table)}`;
951
958
  }
952
959
 
960
+ /** The row-id segment as the Dart `DomainRowKey` spells it (grok_shared domain_manifest_rules.dart):
961
+ * escaped, then percent-encoded once more so an external key's own `%2C` survives the proxy. */
962
+ row(schema, table, id) {
963
+ const escaped = RESERVED_ROW_KEYS.includes(id) || id.startsWith('~') ? `~${id}` : id;
964
+ return `${this.rows(schema, table)}/${encodeURIComponent(escaped)}`;
965
+ }
966
+
953
967
  /** Registered schemas with their tables (`GET /domains/schemas`); [text] is a smart filter. */
954
968
  schemas(text) {
955
969
  return this.client.get(`/domains/schemas${buildQuery({
@@ -1001,7 +1015,7 @@ class NodeDomainsDataSource {
1001
1015
  })}`);
1002
1016
  }
1003
1017
  rowAudit(schema, table, id) {
1004
- return this.client.get(`${this.rows(schema, table)}/${encodeURIComponent(id)}/audit`);
1018
+ return this.client.get(`${this.row(schema, table, id)}/audit`);
1005
1019
  }
1006
1020
  grants(entityId) {
1007
1021
  return this.client.get(`/domains/grants/${encodeURIComponent(entityId)}`);
@@ -1053,7 +1067,7 @@ class NodeDomainsDataSource {
1053
1067
  /** One row, or null when it does not exist or is not visible (the server answers 404). */
1054
1068
  async getRow(schema, table, id) {
1055
1069
  try {
1056
- return await this.client.get(`${this.rows(schema, table)}/${encodeURIComponent(id)}`);
1070
+ return await this.client.get(this.row(schema, table, id));
1057
1071
  } catch (err) {
1058
1072
  if (err?.apiError?.errorCode === 404) return null;
1059
1073
  throw err;
@@ -1073,10 +1087,10 @@ class NodeDomainsDataSource {
1073
1087
  values
1074
1088
  };
1075
1089
  if (version !== undefined) body.version = version;
1076
- return this.client.request('PATCH', `${this.rows(schema, table)}/${encodeURIComponent(id)}`, body);
1090
+ return this.client.request('PATCH', this.row(schema, table, id), body);
1077
1091
  }
1078
1092
  deleteRow(schema, table, id) {
1079
- return this.client.del(`${this.rows(schema, table)}/${encodeURIComponent(id)}`);
1093
+ return this.client.del(this.row(schema, table, id));
1080
1094
  }
1081
1095
 
1082
1096
  /** Soft-deletes up to [limit] (≤1000) matching rows in one transaction; `{deleted, hasMore}`. */
@@ -1138,6 +1152,9 @@ class NodeDapi {
1138
1152
  get reports() {
1139
1153
  return this.internal('/reports');
1140
1154
  }
1155
+ get problems() {
1156
+ return this.internal('/problems');
1157
+ }
1141
1158
  get files() {
1142
1159
  return new NodeFilesDataSource(this.client);
1143
1160
  }
@@ -16,6 +16,36 @@
16
16
  "type": "string",
17
17
  "description": "Manifest version (semver recommended). Recorded in the registry on deploy."
18
18
  },
19
+ "storage": {
20
+ "type": "object",
21
+ "description": "Where the rows live. Absent = domain: the platform creates and owns the tables. external: the tables already exist in the database of the named platform connection and are read through it (no row security, soft delete, audit, relations, constraints, property schemas or extensions; read-only unless 'writable'); every table then declares its remote primary key as businessKey, and 'table'/'column' carry the remote names when they differ from the declared ones.",
22
+ "required": ["kind"],
23
+ "additionalProperties": false,
24
+ "properties": {
25
+ "kind": {"enum": ["domain", "external"]},
26
+ "connection": {
27
+ "type": "string",
28
+ "minLength": 1,
29
+ "description": "external: the qualified name of the platform connection to read through ('Package:Connection'); required."
30
+ },
31
+ "schema": {
32
+ "$ref": "#/definitions/remoteIdentifier",
33
+ "description": "external: the schema inside the remote database (e.g. 'public', 'dbo'); required."
34
+ },
35
+ "catalog": {
36
+ "$ref": "#/definitions/remoteIdentifier",
37
+ "description": "external: the catalog/database, for targets that have one."
38
+ },
39
+ "writable": {
40
+ "type": "boolean",
41
+ "default": false,
42
+ "description": "external: rows may be inserted, patched and deleted through the connection (one remote transaction per request, keyed by businessKey; on ClickHouse, which commits every statement, a request is one multi-row INSERT and patch/delete/upsert are refused). The publisher must hold Query, AddRows, ChangeValues and RemoveRows on the connection; a table opts out with its own 'writable': false."
43
+ }
44
+ },
45
+ "if": {"properties": {"kind": {"const": "external"}}},
46
+ "then": {"required": ["connection", "schema"]},
47
+ "else": {"properties": {"connection": false, "schema": false, "catalog": false, "writable": false}}
48
+ },
19
49
  "extensible": {
20
50
  "type": "object",
21
51
  "description": "Opt-in for user extensions of this schema. Users holding the Extend permission on the schema entity may then add their own tables; per-table column extension is opted in with the table's own 'extensible' flag. User objects are retained across plugin upgrades and never appear in generated typed clients.",
@@ -64,11 +94,26 @@
64
94
  {"not": {"type": "string", "pattern": "^x_"}}
65
95
  ]
66
96
  },
97
+ "remoteIdentifier": {
98
+ "description": "A name inside the remote database, verbatim (spaces allowed, the connector quotes it); it may not contain brackets, quotes, backslashes or dots.",
99
+ "type": "string",
100
+ "minLength": 1,
101
+ "maxLength": 256,
102
+ "pattern": "^[^\\[\\]\"'`\\\\.]+$"
103
+ },
67
104
  "table": {
68
105
  "type": "object",
69
106
  "required": ["columns"],
70
107
  "additionalProperties": false,
71
108
  "properties": {
109
+ "table": {
110
+ "$ref": "#/definitions/remoteIdentifier",
111
+ "description": "External storage only: the remote table's name when it differs from the declared one (e.g. 'Order Details')."
112
+ },
113
+ "writable": {
114
+ "type": "boolean",
115
+ "description": "Writable external storage only: false keeps this table read-only while the rest of the binding accepts writes."
116
+ },
72
117
  "securityMode": {
73
118
  "enum": ["table", "master", "row"],
74
119
  "default": "table",
@@ -263,7 +308,11 @@
263
308
  "properties": {
264
309
  "type": {
265
310
  "enum": ["string", "int", "float", "bool", "datetime", "string_list", "ref", "file", "json", "user", "group"],
266
- "description": "Property type; ref = FK to another table in this manifest; user/group = aliases of ref Core.users / ref Core.groups (semantic type User / Group); file = a file://<connection>/<path> string into platform file storage (semantic type File); json = a jsonb-backed opaque object (filters, sorts, facets and aggregates refuse it)."
311
+ "description": "Property type; ref = FK to another table in this manifest; user/group = aliases of ref Core.users / ref Core.groups (semantic type User / Group); file = a file://<connection>/<path> string into platform file storage (semantic type File); json = a jsonb-backed opaque object (filters, sorts, facets and aggregates refuse it). An external storage admits string, int, float, bool, datetime and same-manifest ref only."
312
+ },
313
+ "column": {
314
+ "$ref": "#/definitions/remoteIdentifier",
315
+ "description": "External storage only: the remote column's name when it differs from the declared one (e.g. 'OrderID')."
267
316
  },
268
317
  "required": {"type": "boolean", "default": false},
269
318
  "unique": {
@@ -1,4 +1,4 @@
1
- import {domains} from '@datagrok-libraries/u2/src/dg/index.js';
1
+ import {domains} from '@datagrok-libraries/ems/src/dg';
2
2
 
3
3
  // the skins of everything the u2 domain stack renders
4
- import '@datagrok-libraries/u2/src/dg/domain/styles.js';
4
+ import '@datagrok-libraries/ems/src/dg/styles.js';
package/package.json CHANGED
@@ -4,9 +4,12 @@
4
4
  "type": "git",
5
5
  "url": "https://github.com/datagrok-ai/public.git"
6
6
  },
7
- "version": "6.7.8",
7
+ "version": "6.7.9",
8
8
  "description": "Utility to upload and publish packages to Datagrok",
9
9
  "homepage": "https://github.com/datagrok-ai/public/tree/master/tools#readme",
10
+ "engines": {
11
+ "node": ">=22.12.0"
12
+ },
10
13
  "dependencies": {
11
14
  "@babel/parser": "^7.29.7",
12
15
  "@babel/runtime": "^7.29.7",
@@ -24,7 +27,7 @@
24
27
  "minimist": "^1.2.8",
25
28
  "node-fetch": "^2.7.0",
26
29
  "papaparse": "^5.5.3",
27
- "puppeteer": "^24.15.0",
30
+ "puppeteer": "^25.14.0",
28
31
  "ts-morph": "^28.0.0",
29
32
  "@babel/generator": "^7.29.0",
30
33
  "datagrok-api": "^1.27.12"