datagrok-tools 6.7.4 → 6.7.6

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.4 build /home/runner/work/public/public/tools
2
+ > datagrok-tools@6.7.6 build /home/runner/work/public/public/tools
3
3
  > node build.js
4
4
 
5
5
  Successfully compiled 51 files with Babel.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Datagrok-tools changelog
2
2
 
3
+ ## v.next
4
+
5
+ * `grok test --skip-gc` launches the browser without `--expose-gc`, so the test runner skips its garbage collection after every test; a suite of small unit tests runs several times faster (ComputeUtils driver tests: 92 s to 19 s)
6
+ * `grok setup` installs corepack when it is missing instead of asking for it by hand: Node 25 no longer bundles corepack, so every fresh checkout on current Node stopped at that step
7
+ * `grok setup` also removes the `.js`/`.d.ts` files the npm-era tsc emitted beside js-api and library sources: the workspace emits into `dist/`, and a leftover `js-api/grok.js` shadowed the `grok` command in cmd.exe
8
+ * GROK-20753: `domain-schema.schema.json` accepts the `hierarchy` table key (the table is a tree: exactly one ref column targeting itself) — without it `grok api` / `grok check` refused a manifest the server accepts
9
+ * GROK-20753: `grok api --ui` emits typed u2 handles instead of the retired `@datagrok-libraries/domain-ui` sugar: `src/generated/db-ui.ts` is now `get<Schema>Db(): Promise<<Schema>Db>` → `{schema, data: <schema>Db, tables: {<plural>: DomainTable<Row>, ...}}`, every table opened in parallel behind one await and cached per page (presence-persisted as before). `grok add app --domain <schema>.<table>` scaffolds the u2 zero-code tier the way the Stockroom package is built: a three-line `//tags: app` function over `domains.table(...).app()`, `src/app.spec.json` (the same app as a designer-editable `dg-ui/1` spec), a one-table starter `databases/<schema>/schema.json` when the package declares none, the `@datagrok-libraries/u2` workspace dependency and an `rspack.config.js` carrying the `u2core` external (everything else — the css loaders, the tsconfig — comes from `@datagrok/build-config`)
10
+ * GROK-20298: `grok s domains access <schema.table>` replaces `capabilities` — the server's `{can, fields, ...}` access shape
11
+
12
+ ## 6.7.5 (2026-09-17)
13
+
14
+ * `grok create` works outside the pnpm workspace and new packages use `@datagrok/build-config` 1.x
15
+
3
16
  ## 6.7.4 (2026-09-17)
4
17
 
5
18
  * `grok create`, `grok publish`, `grok test` and `grok run` work inside the pnpm workspace
package/CLAUDE.md CHANGED
@@ -207,7 +207,7 @@ Based on `node:22-bookworm-slim`. Pre-installed:
207
207
  #### Entrypoint (`entrypoint.sh`)
208
208
 
209
209
  1. **Repo detection:** checks if `/workspace/repo` is the public repo (has `.git` + `js-api/`) or monorepo (`public/js-api/`)
210
- 2. **Auto-clone:** if workspace is not the public repo, sparse-clones it to `$DG_PUBLIC_DIR` (default `/workspace/datagrok`) — excludes `connectors/`, `docker/`, `environments/`, `python-api/`, etc. for speed. Branch resolved as: `DG_PUBLIC_BRANCH` > `DG_VERSION` mapped to branch > `master` fallback
210
+ 2. **Auto-clone:** if workspace is not the public repo, sparse-clones it to `$DG_PUBLIC_DIR` (default `/workspace/datagrok`) — excludes `docker/`, `environments/`, `python-api/`, etc. for speed. Branch resolved as: `DG_PUBLIC_BRANCH` > `DG_VERSION` mapped to branch > `master` fallback
211
211
  3. **Workspace linking:** for non-public repos, symlinks `/workspace/repo` into the cloned repo's `packages/` dir and links `.claude`/`CLAUDE.md` at `/workspace/` for context discovery
212
212
  4. **Grok config:** auto-creates `~/.grok/config.yaml` pointing to `http://datagrok:8080/api` with key `admin` (only if config doesn't already exist)
213
213
 
package/GROK_S.md CHANGED
@@ -58,7 +58,7 @@ servers:
58
58
 
59
59
  - `grok login <server>` is the way to add a server: it registers a keypair and writes the
60
60
  entry for you. The private key stays in `~/.grok/keys/<alias>.json`.
61
- See [keypair authentication](../help/govern/access-control/keypair-authentication.md).
61
+ See [keypair authentication](https://datagrok.ai/help/govern/access-control/keypair-authentication).
62
62
  - `grok config add --alias <name> --server <url> [--key <developer-key>]` writes an entry by
63
63
  hand. The developer key is deprecated; omit it when the server is reached with a keypair.
64
64
  - Add `--default` to make it the active server.
@@ -336,7 +336,7 @@ grok s domains list grit # tables of one schema: secu
336
336
  grok s domains get grit # the manifest, as JSON (doubles as an export)
337
337
  grok s domains get grit.issue # the table's columns (--output json: its manifest section)
338
338
  grok s domains get grit.issue <row-id> # one row
339
- grok s domains capabilities grit.issue # what the current user may do on the table
339
+ grok s domains access grit.issue # can.view/insert/edit/delete/share + editable/readonly column lists
340
340
  ```
341
341
 
342
342
  ### Querying
@@ -533,7 +533,7 @@ Actions the server accepts: `create | get | delete` for `users`, `groups`, `conn
533
533
  `functions`, `queries`, `scripts` (`get | delete` for `reports`), `functions.run`
534
534
  (`{name, params}`), and `files.list | get | put | delete`. For `files.put`, add
535
535
  `"source": "<local-path>"` and the CLI base64-encodes the file into `content` before sending.
536
- `users.delete` removes the entity record only (see "List / count / get / delete").
536
+ `users.delete` is refused, as is `DELETE /entities/{id}` on a user or its personal group: users are blocked, never deleted (see "List / count / get / delete").
537
537
 
538
538
  ## Scripting pattern
539
539
 
@@ -905,6 +905,9 @@ only ever read from.
905
905
  URL, the key, and basic connectivity, and returns per-module status if the server is
906
906
  reachable. Fall back to `grok s raw GET /users/current` to isolate auth issues. A `--host`
907
907
  URL that is not the API base fails at login with the reason (`should end with /api`).
908
- - Cross-instance sync (1.28 servers): `grok s sync pairs list`, `sync setups list --pair <id>`,
909
- `sync setup get <id>`, `sync run <id>`; on 1.27 the routes do not exist and the commands
910
- answer "not found".
908
+ - Cross-instance sync (1.28 servers): `grok s sync pairs list` (`/sync/pairs`),
909
+ `sync setups list --pair <id>`, `sync setup get <id>` and `sync setup runs <id>`
910
+ (`/entity_sync_funcs`; a setup is an `EntitySyncFunc`, each run a `FuncCall` whose
911
+ `options.syncResult` holds the per-item outcome). The server has no run route: runs start
912
+ from the setup's **Run** button or its schedule. On 1.27 the routes do not exist and the
913
+ commands answer "not found".
@@ -12,8 +12,14 @@ var utils = _interopRequireWildcard(require("../utils/utils"));
12
12
  var color = _interopRequireWildcard(require("../utils/color-utils"));
13
13
  var _api = require("./api");
14
14
  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); }
15
- /** The domain-ui library version a scaffolded domain app depends on. */
16
- const domainUiDependency = '^0.1.0';
15
+ /** What a u2 consumer package carries beside the imports (the Grit reference package is the
16
+ * answer key): u2 is not on npm, so the dependency is the workspace link, and `u2core` is the one
17
+ * external `@datagrok/build-config` does not know about. The css imports and the tsconfig need
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';
21
+ const u2RspackExternal = `'datagrok-api/u2core': 'DG.U2'`;
22
+ const u2RspackConfig = [`const {bundler} = require('@datagrok/build-config');`, '', `module.exports = bundler({externals: {${u2RspackExternal}}});`, ''].join('\n');
17
23
  function add(args) {
18
24
  // `--domain` is the only option any `add` entity takes (`grok add app --domain`).
19
25
  const nOptions = Object.keys(args).length - 1 - (args.domain === undefined ? 0 : 1);
@@ -28,6 +34,7 @@ function add(args) {
28
34
  const tsPath = _path.default.join(srcDir, 'package.ts');
29
35
  const detectorsPath = _path.default.join(curDir, 'detectors.js');
30
36
  const webpackConfigPath = _path.default.join(curDir, 'webpack.config.js');
37
+ const rspackConfigPath = _path.default.join(curDir, 'rspack.config.js');
31
38
  const scriptsDir = _path.default.join(curDir, 'scripts');
32
39
  const queryDir = _path.default.join(curDir, 'queries');
33
40
  const queryPath = _path.default.join(queryDir, 'queries.sql');
@@ -99,22 +106,52 @@ function add(args) {
99
106
  return [manifest.name, Object.keys(manifest.tables)];
100
107
  }
101
108
 
102
- /** Adds `import {domains} from '@datagrok-libraries/domain-ui';` to the package
103
- * entry file, after its existing imports (which webpack's externals depend on). */
104
- function addDomainUiImport() {
109
+ /** Adds the u2 import and the stylesheet imports to the package entry file, after its
110
+ * existing imports (which webpack's externals depend on). */
111
+ function addU2Imports() {
105
112
  const contents = _fs.default.readFileSync(packageEntry, 'utf8');
106
- if (contents.includes('@datagrok-libraries/domain-ui')) return;
113
+ if (contents.includes(u2DgModule)) return;
107
114
  const eol = contents.includes('\r\n') ? '\r\n' : '\n';
108
- const line = `import {domains} from '@datagrok-libraries/domain-ui';`;
115
+ const imports = _fs.default.readFileSync(_path.default.join(templateDir, 'entity-template', 'domain-app-imports.js'), 'utf8').trimEnd().split(/\r?\n/);
109
116
  const lines = contents.split(/\r?\n/);
110
117
  let last = -1;
111
118
  for (let i = 0; i < lines.length; i++) if (/^(import .*|export .* from .*);\s*$/.test(lines[i])) last = i;
112
- lines.splice(last + 1, 0, line);
119
+ lines.splice(last + 1, 0, ...imports);
113
120
  _fs.default.writeFileSync(packageEntry, lines.join(eol), 'utf8');
114
121
  }
115
122
 
123
+ /** The u2 wiring of the build: the workspace dependency in package.json and the `u2core`
124
+ * external in rspack.config.js. Each edit is skipped where the file already carries it. */
125
+ function addU2Wiring() {
126
+ const packageObj = JSON.parse(_fs.default.readFileSync(packagePath, 'utf8'));
127
+ packageObj.dependencies = packageObj.dependencies ?? {};
128
+ packageObj.dependencies['@datagrok-libraries/u2'] ??= u2Dependency;
129
+ _fs.default.writeFileSync(packagePath, JSON.stringify(packageObj, null, 2), 'utf8');
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
+ }
132
+
133
+ /** Declares a one-table starter schema in `databases/<schema>/schema.json` — the fresh-package
134
+ * case, where the schema is the app's own. */
135
+ function writeStarterManifest(schema, table) {
136
+ const target = _path.default.join(curDir, 'databases', schema, 'schema.json');
137
+ const template = _fs.default.readFileSync(_path.default.join(templateDir, 'entity-template', 'domain-schema.json'), 'utf8');
138
+ _fs.default.mkdirSync(_path.default.dirname(target), {
139
+ recursive: true
140
+ });
141
+ _fs.default.writeFileSync(target, template.replace(/#{SCHEMA}/g, schema).replace(/#{TABLE}/g, table), 'utf8');
142
+ console.log(`Declared a starter schema in databases/${schema}/schema.json — ` + `delete it if \`${schema}\` is deployed by another package`);
143
+ }
144
+
145
+ /** The same app as a `dg-ui/1` spec the designer edits, over the first table; kept once written. */
146
+ function writeAppSpec(schema, table) {
147
+ const target = _path.default.join(srcDir, 'app.spec.json');
148
+ if (_fs.default.existsSync(target)) return;
149
+ const template = _fs.default.readFileSync(_path.default.join(templateDir, 'entity-template', 'domain-app.spec.json'), 'utf8');
150
+ _fs.default.writeFileSync(target, utils.replacers['DOMAIN_TABLE'](template, `${schema}.${table}`).replace(/#{SOURCE}/g, (0, _api.tableProp)(table)), 'utf8');
151
+ }
152
+
116
153
  /** `grok add app [name] --domain <schema>[.<table>] | <schema.json path>` — a working
117
- * browse/CRUD app per table, from the `domain-ui` defaults alone. */
154
+ * browse/CRUD app per table, from the u2 defaults alone (`domains.table(...).app()`). */
118
155
  function addDomainApp(domainArg, appName) {
119
156
  if (typeof domainArg !== 'string' || domainArg === '') return color.error('`--domain` needs `<schema>` or `<schema>.<table>`, ' + 'or a path to a schema.json manifest');
120
157
  let schema;
@@ -127,7 +164,10 @@ function add(args) {
127
164
  if (!/^[A-Za-z_]\w*(\.[A-Za-z_]\w*)?$/.test(domainArg)) return color.error(`Malformed domain address: ${domainArg}. ` + 'Use `<schema>`, `<schema>.<table>`, or a path to a schema.json manifest');
128
165
  const dot = domainArg.indexOf('.');
129
166
  schema = dot === -1 ? domainArg : domainArg.slice(0, dot);
130
- if (dot !== -1) tables = [domainArg.slice(dot + 1)];else {
167
+ if (dot !== -1) {
168
+ tables = [domainArg.slice(dot + 1)];
169
+ if (!_fs.default.existsSync(_path.default.join(curDir, 'databases', schema, 'schema.json'))) writeStarterManifest(schema, tables[0]);
170
+ } else {
131
171
  // Table names of a whole schema are only known offline from its manifest.
132
172
  const manifest = readManifest(_path.default.join(curDir, 'databases', schema, 'schema.json'));
133
173
  if (manifest == null) return color.error(`No \`databases/${schema}/schema.json\` in this package: name a table ` + `(\`--domain ${schema}.<table>\`) or pass the path to the schema manifest`);
@@ -154,17 +194,12 @@ function add(args) {
154
194
  addedTables.push(`${schema}.${table}`);
155
195
  }
156
196
  if (added.length === 0) return true;
157
- addDomainUiImport();
158
- const packageObj = JSON.parse(_fs.default.readFileSync(packagePath, 'utf8'));
159
- packageObj.dependencies = packageObj.dependencies ?? {};
160
- if (packageObj.dependencies['@datagrok-libraries/domain-ui'] === undefined) {
161
- packageObj.dependencies['@datagrok-libraries/domain-ui'] = domainUiDependency;
162
- _fs.default.writeFileSync(packagePath, JSON.stringify(packageObj, null, 2), 'utf8');
163
- }
197
+ addU2Imports();
198
+ addU2Wiring();
199
+ writeAppSpec(schema, tables[0]);
164
200
 
165
- // Manifests in the package get typed clients AND the typed UI wrappers the app
166
- // code can switch to; a schema deployed by another package has none, and the
167
- // reflective app above needs none.
201
+ // Manifests in the package get typed clients AND the typed u2 handles the app code can
202
+ // switch to; the reflective app above needs neither.
168
203
  if (!(0, _api.generateDomainClients)(curDir, {
169
204
  ui: true
170
205
  })) return false;
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", {
6
6
  });
7
7
  exports.api = api;
8
8
  exports.generateDomainClients = generateDomainClients;
9
+ exports.tableProp = tableProp;
9
10
  var _fs = _interopRequireDefault(require("fs"));
10
11
  var _path = _interopRequireDefault(require("path"));
11
12
  var _ignoreWalk = _interopRequireDefault(require("ignore-walk"));
@@ -21,23 +22,20 @@ const packageFuncDirs = ['package.ts', 'package.g.ts'];
21
22
  const apiFile = 'package-api.ts';
22
23
  const dbFile = 'db.ts';
23
24
  const dbUiFile = 'db-ui.ts';
24
- /** What `db-ui.ts` imports from `@datagrok-libraries/domain-ui`. */
25
- const domainUiImports = ['domainHandler', 'domains', 'DomainAppView', 'DomainAppViewOptions', 'DomainDb', 'DomainDialogOptions', 'DomainEntityAppView', 'DomainEntityAppViewOptions', 'DomainForm', 'DomainFormOptions', 'DomainGrid', 'DomainGridOptions', 'DomainTable', 'EntityListOptions', 'EntityListWidget'];
26
- /** {@link DomainDb} members a table's camelCase property name must not shadow. */
27
- const domainDbReservedProps = ['name', 'tables', 'table', 'acquire'];
25
+ /** Where `db-ui.ts` takes the u2 domain stack from — the same specifier Grit and Stockroom use
26
+ * (u2 is a relative-path dependency without a `dg` subpath export). */
27
+ const u2DgModule = '@datagrok-libraries/u2/src/dg/index.js';
28
28
 
29
29
  /** Naive English plural of a (snake_case) table name — `issue` → `issues`, `box` →
30
- * `boxes`, `category` → `categories`. MUST match domain-ui's `pluralizeTableName`:
31
- * the runtime `DomainDb` assigns its per-table properties with the same rule, so the
32
- * generated typed interfaces bind to it. */
30
+ * `boxes`, `category` → `categories`. */
33
31
  function pluralizeTableName(name) {
34
32
  if (/(s|x|z|ch|sh)$/.test(name)) return name + 'es';
35
33
  if (/[^aeiou_]y$/.test(name)) return name.slice(0, -1) + 'ies';
36
34
  return name + 's';
37
35
  }
38
36
 
39
- /** The camelCase plural property name a table gets on the schema client and the
40
- * schema UI handle (`issue_label` → `issueLabels`). */
37
+ /** The camelCase plural property name a table gets on the schema client and under the
38
+ * schema handle's `tables` (`issue_label` → `issueLabels`). */
41
39
  function tableProp(tableName) {
42
40
  return utils.snakeToCamelCase(pluralizeTableName(tableName), false);
43
41
  }
@@ -190,8 +188,8 @@ function checkNameColision(name) {
190
188
 
191
189
  /** Generates `src/generated/db.ts` with typed clients for the package's domain schemas
192
190
  * (`databases/<schema>/schema.json` manifests), and — with [options].ui, or whenever the
193
- * file is already there — `src/generated/db-ui.ts` with the typed UI sugar over
194
- * `@datagrok-libraries/domain-ui`. No-op for packages without manifests. */
191
+ * file is already there — `src/generated/db-ui.ts` with the typed u2 handles
192
+ * (`get<Schema>Db()` → one `DomainTable<Row>` per table). No-op for packages without manifests. */
195
193
  function generateDomainClients(packageDir = curDir, options) {
196
194
  const databasesDir = _path.default.join(packageDir, 'databases');
197
195
  if (!_fs.default.existsSync(databasesDir)) return true;
@@ -231,7 +229,7 @@ function generateDomainClients(packageDir = curDir, options) {
231
229
  _fs.default.writeFileSync(_path.default.join(genDir, dbFile), normEol(content).replace(/\n/g, '\r\n'), 'utf8');
232
230
  color.log(`Successfully generated file src/generated/${dbFile}${sep}`, 'success');
233
231
  if (ui) {
234
- const uiContent = annotationForDbUiFile + `import * as DG from 'datagrok-api/dg';${sep}` + wrapTokens(`import {`, domainUiImports, ', ', `} from '@datagrok-libraries/domain-ui';`) + sep + wrapTokens(`import {`, dbImports, ', ', `} from './${dbFile.replace(/\.ts$/, '')}';`) + sep + sep + uiParts.join(sep);
232
+ const uiContent = annotationForDbUiFile + `import {domains, DomainTable} from '${u2DgModule}';${sep}` + wrapTokens(`import {`, dbImports, ', ', `} from './${dbFile.replace(/\.ts$/, '')}';`) + sep + sep + uiParts.join(sep);
235
233
  _fs.default.writeFileSync(_path.default.join(genDir, dbUiFile), normEol(uiContent).replace(/\n/g, '\r\n'), 'utf8');
236
234
  color.log(`Successfully generated file src/generated/${dbUiFile}${sep}`, 'success');
237
235
  }
@@ -473,7 +471,7 @@ function generateDomainSchemaCode(manifest, manifestPath, emittedTypes, packageD
473
471
  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[]};`);
474
472
  }
475
473
  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));
476
- txArms.push(` {op: 'insert'; table: '${tableName}'; ref?: string; values: DG.DomainTxValues<${typeName}Insert>} |`, ` {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} |`);
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} |`);
477
475
 
478
476
  // No datetime config: the client resolves datetime columns from the domain
479
477
  // registry itself (the Dayjs typing in <Table>Row stays true for every client).
@@ -489,41 +487,19 @@ function generateDomainSchemaCode(manifest, manifestPath, emittedTypes, packageD
489
487
  return decls.join(sep.repeat(2)) + sep;
490
488
  }
491
489
 
492
- /** Emits the typed UI sugar of one manifest: per-table option types and a `<table>Ui`
493
- * wrapper over the reflective `@datagrok-libraries/domain-ui` components, so app code gets
494
- * this table's columns and row type checked. Collects the names it needs from `db.ts` into
495
- * [dbImports]. Nothing here is imported by `db.ts` — data-only consumers gain no UI
496
- * dependency. */
490
+ /** Emits the typed u2 handles of one manifest: a `<Schema>Db` interface — the schema name, the
491
+ * typed data clients of `db.ts` and one `DomainTable<Row>` per table — and `get<Schema>Db()`,
492
+ * which opens every table in parallel behind ONE await and caches the promise per page.
493
+ * Collects the names it needs from `db.ts` into [dbImports]. Nothing here is imported by
494
+ * `db.ts` — data-only consumers gain no UI dependency. */
497
495
  function generateDomainUiCode(manifest, dbImports) {
498
- const decls = [];
499
- const schemaClient = `${utils.snakeToCamelCase(manifest.name, false)}Db`;
500
- dbImports.push(schemaClient);
501
- for (const tableName of Object.keys(manifest.tables)) {
502
- const type = utils.snakeToCamelCase(tableName);
503
- const address = `${manifest.name}.${tableName}`;
504
- // The generics of the client and the handle, in DomainTableClient's order. The
505
- // fifth — the update payload — exists only for a relation-bearing table, whose
506
- // update() also takes the link sets; without relations the `Partial<Row>`
507
- // default is it. Emitting four there would make `update(id, {labels: [...]})`
508
- // an excess-property error against a payload that has no `labels`.
509
- const hasRelations = Object.keys(manifest.tables[tableName].relations ?? {}).length > 0;
510
- const generics = `${type}Row, ${type}Insert, ${type}Column, ${type}Expand` + (hasRelations ? `, ${type}Update` : '');
511
- const handle = `DomainTable<${generics}>`;
512
- dbImports.push(`${type}Column`, `${type}Expand`, `${type}Insert`, `${type}Row`);
513
- if (hasRelations) dbImports.push(`${type}Update`);
514
- decls.push([`/** Query spec of \`${address}\` — columns and expand keys are compile-checked. */`, `export type ${type}QuerySpec = DG.DomainQuerySpec<${type}Column, keyof ${type}Expand & string>;`].join(sep), [`/** {@link DG.DomainQuery} parameters of \`${address}\` (schema and table are implied). */`, `export interface ${type}QueryParams extends`, ` Omit<DG.DomainQueryParams, 'schema' | 'table' | 'columns'> {`, ` columns?: ${type}Column[];`, '}'].join(sep), [`/** Options of {@link ${type}Ui.form} / {@link ${type}Ui.formDialog}. */`, `export interface ${type}FormOptions extends Omit<DomainFormOptions, 'values'> {`, ` values?: Partial<${type}Insert>;`, '}'].join(sep), [`/** Options of {@link ${type}Ui.grid}. */`, `export interface ${type}GridOptions extends Omit<DomainGridOptions, 'query' | 'defaults'> {`, ` query?: ${type}QuerySpec;`, ` defaults?: Partial<${type}Insert>;`, '}'].join(sep), [`/** Options of {@link ${type}Ui.list}. */`, `export interface ${type}ListOptions extends Omit<EntityListOptions, 'query'> {`, ` query?: ${type}QuerySpec;`, '}'].join(sep), [`/** Options of {@link ${type}Ui.listView} / {@link ${type}Ui.app}. */`, `export interface ${type}AppViewOptions extends Omit<DomainAppViewOptions, 'query'> {`, ` query?: ${type}QuerySpec;`, '}'].join(sep), ['/**', ` * Typed UI over \`${address}\`: the reflective components of`, ' * `@datagrok-libraries/domain-ui`, with this table\'s columns and row type checked at', ` * compile time. Reach it through {@link ${utils.snakeToCamelCase(tableName, false)}Ui}.`, ' */', `export class ${type}Ui {`, ` /** The table address, \`'<schema>.<table>'\`. */`, ` readonly address: string = '${address}';`, '', ` /** The typed client — the same one \`${schemaClient}.${tableProp(tableName)}\` returns. */`, ` get client(): DG.DomainTableClient<${generics}> {`, ` return ${schemaClient}.${tableProp(tableName)};`, ' }', '', ` /** The prefetched handle on \`${address}\` — THE async boundary, typed. Every`, ' * widget factory on it is synchronous, so acquire it ONCE when a page builds', ' * more than one widget; the shortcuts below acquire one of their own. */', ` table(): Promise<${handle}> {`, ` return domains.table<${generics}>(this.client);`, ' }', '', ' /** The handler registered for the table (the reflective default when none is). */', ' handler(): DG.DomainObjectHandler {', ' return domainHandler(this.address);', ' }', '', ' /** The property form of ONE row — a new one by default, an existing one with', ' * `{row}` or `{id}`; the values are this table\'s. */', ` async form(options?: ${type}FormOptions): Promise<DomainForm> {`, ' return (await this.table()).form(options);', ' }', '', ' /** {@link form} in a dialog; resolves to whether a row was saved. */', ` async formDialog(options?: ${type}FormOptions & DomainDialogOptions): Promise<boolean> {`, ' return (await this.table()).formDialog(options);', ' }', '', ' /** The browse/CRUD page: list, search, New, and a deep-linkable query. */', ` async listView(options?: ${type}AppViewOptions): Promise<DomainAppView> {`, ' return (await this.table()).listView(options);', ' }', '', ' /** THE app — {@link listView} under the name that says what it is. */', ` app(options?: ${type}AppViewOptions): Promise<DomainAppView> {`, ' return this.listView(options);', ' }', '', ' /** The row page: form, detail tabs, history. Takes the row or its id. */', ' async entityView(row: string | DG.DomainRow,', ' options?: DomainEntityAppViewOptions): Promise<DomainEntityAppView> {', ' return (await this.table()).entityView(row, options);', ' }', '', ' /** An editable grid: batch editing, one-transaction save. */', ` async grid(options?: ${type}GridOptions): Promise<DomainGrid> {`, ' return (await this.table()).grid(options);', ' }', '', ' /** A list of rows (cards / brief / grid). */', ` async list(options?: ${type}ListOptions): Promise<EntityListWidget> {`, ' return (await this.table()).list(options);', ' }', '', ' /** Opens the platform\'s create dialog, or the edit dialog for [row]. */', ' edit(row?: DG.DomainRow): Promise<boolean> {', ' return this.handler().editRow(row);', ' }', '', ' /** Opens the platform\'s row picker. */', ' pick(): Promise<DG.DomainRow | null> {', ' return this.handler().pickRow();', ' }', '', ' /** Wraps one `query()` row as a {@link DG.DomainRow} — locally, no round trip. */', ` row(values: Partial<${type}Row> | null): DG.DomainRow {`, ' return this.handler().rowFrom(values);', ' }', '', ' /** A serializable query over the table: deep links, saved filters, Open in Table View. */', ` query(params?: ${type}QueryParams): DG.DomainQuery {`, ` return new DG.DomainQuery({...params, schema: '${manifest.name}', table: '${tableName}'});`, ' }', '}'].join(sep), [`/** Typed UI over \`${address}\` (see {@link ${type}Ui}). */`, `export const ${utils.snakeToCamelCase(tableName, false)}Ui = new ${type}Ui();`].join(sep));
515
- }
516
-
517
- // The schema-level handle: every table's DomainTable under ONE await, typed
518
- // (`const db = await <schema>UiDb(); db.<plural>.form(...)`).
519
496
  const schemaType = utils.snakeToCamelCase(manifest.name);
520
- const tableProps = Object.keys(manifest.tables).filter(t => !domainDbReservedProps.includes(tableProp(t)));
521
- decls.push(['/**', ` * The typed schema handle over \`${manifest.name}\`: one prefetched {@link DomainTable}`, ' * per table, resolved together by {@link ' + utils.snakeToCamelCase(manifest.name, false) + 'UiDb} —', ' * the schema-level async boundary (see `domains.db`).', ' */', `export interface ${schemaType}UiDb extends DomainDb {`, ...tableProps.map(t => {
522
- const type = utils.snakeToCamelCase(t);
523
- const update = Object.keys(manifest.tables[t].relations ?? {}).length > 0 ? `, ${type}Update` : '';
524
- return ` readonly ${tableProp(t)}: ` + `DomainTable<${type}Row, ${type}Insert, ${type}Column, ${type}Expand${update}>;`;
525
- }), '}'].join(sep), [`/** Acquires the {@link ${schemaType}UiDb} handle — every table of`, ` * \`${manifest.name}\`, prefetched together (see \`domains.db\`). */`, `export function ${utils.snakeToCamelCase(manifest.name, false)}UiDb(): Promise<${schemaType}UiDb> {`, ` return domains.db('${manifest.name}') as Promise<${schemaType}UiDb>;`, '}'].join(sep));
526
- return decls.join(sep.repeat(2)) + sep;
497
+ const schemaClient = `${utils.snakeToCamelCase(manifest.name, false)}Db`;
498
+ const getter = `get${schemaType}Db`;
499
+ const cache = `_${utils.snakeToCamelCase(manifest.name, false)}Db`;
500
+ const tables = Object.keys(manifest.tables);
501
+ dbImports.push(schemaClient, ...tables.map(t => `${utils.snakeToCamelCase(t)}Row`));
502
+ return [['/**', ` * The typed u2 handles over \`${manifest.name}\`: one {@link DomainTable} per table, opened together by`, ` * {@link ${getter}}, beside the typed data clients (\`${schemaClient}\`) — every action, validator,`, ' * renderer and source an app declares on a table is checked against its row type.', ' */', `export interface ${schemaType}Db {`, ` readonly schema: '${manifest.name}';`, ` readonly data: typeof ${schemaClient};`, ' readonly tables: {', ...tables.map(t => ` readonly ${tableProp(t)}: DomainTable<${utils.snakeToCamelCase(t)}Row>;`), ' };', '}'].join(sep), `let ${cache}: Promise<${schemaType}Db> | undefined;`, [`/** Opens every table of \`${manifest.name}\` in parallel — the one await of an app over it; the`, ' * result is cached per page, so every caller shares the same handles (and their registries). */', `export function ${getter}(): Promise<${schemaType}Db> {`, ` return ${cache} ??= Promise.all([`, ...tables.map(t => ` domains.table<${utils.snakeToCamelCase(t)}Row>('${manifest.name}.${t}'),`), wrapTokens(' ]).then(([', tables.map(tableProp), ', ', `]): ${schemaType}Db => ({`, ' '), ` schema: '${manifest.name}',`, ` data: ${schemaClient},`, wrapTokens(' tables: {', tables.map(tableProp), ', ', '},', ' '), ' })).catch((e) => {', ` ${cache} = undefined;`, ' throw e;', ' });', '}'].join(sep)].join(sep.repeat(2)) + sep;
527
503
  }
528
504
 
529
505
  /** Joins [tokens] into `<prefix>t1<sepToken>t2...<suffix>` lines wrapped at the 120-char
@@ -26,6 +26,11 @@ const confTemplate = _jsYaml.default.load(_fs.default.readFileSync(confTemplateD
26
26
  encoding: 'utf-8'
27
27
  }));
28
28
  const dependencies = [];
29
+ function apiVersion() {
30
+ let dir = _path.default.dirname(require.resolve('datagrok-api/dg'));
31
+ while (!_fs.default.existsSync(_path.default.join(dir, 'package.json'))) dir = _path.default.dirname(dir);
32
+ return JSON.parse(_fs.default.readFileSync(_path.default.join(dir, 'package.json'), 'utf8')).version;
33
+ }
29
34
  function createDirectoryContents(name, friendlyName, config, templateDir, packageDir, ide = '', ts = true, eslint = false, test = false) {
30
35
  const filesToCreate = _fs.default.readdirSync(templateDir);
31
36
  filesToCreate.forEach(file => {
@@ -58,7 +63,7 @@ function createDirectoryContents(name, friendlyName, config, templateDir, packag
58
63
  // nothing to npm: pin published ranges and bring the toolchain in as a devDependency.
59
64
  if (!utils.isPnpmWorkspace(packageDir)) {
60
65
  const published = {
61
- 'datagrok-api': `^${require('datagrok-api/package.json').version}`,
66
+ 'datagrok-api': `^${apiVersion()}`,
62
67
  '@datagrok-libraries/test': '^1.4.0',
63
68
  'rxjs': '^6.5.5',
64
69
  'cash-dom': '^8.1.5',
@@ -72,7 +77,7 @@ function createDirectoryContents(name, friendlyName, config, templateDir, packag
72
77
  }
73
78
  }
74
79
  Object.assign(_package.devDependencies, {
75
- '@datagrok/build-config': '^0.1.0',
80
+ '@datagrok/build-config': '^1.0.0',
76
81
  'eslint': '^8.57.1',
77
82
  '@typescript-eslint/parser': '^8.39.0',
78
83
  '@typescript-eslint/eslint-plugin': '^8.39.0',
@@ -96,13 +96,15 @@ 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 \`@datagrok-libraries/domain-ui\` defaults. Give it
100
- one table (\`--domain grit.issue\`), a whole schema the package declares in
99
+ (\`grok.dapi.domains\`) from the u2 defaults (\`@datagrok-libraries/u2\`): a three-line
100
+ app function, a \`src/app.spec.json\` the designer edits, and — for a schema the package
101
+ does not declare yet — a one-table starter \`databases/<schema>/schema.json\`. Give it
102
+ one table (\`--domain tracker.issue\`), a whole schema the package declares in
101
103
  \`databases/<schema>/schema.json\` (one app per table), or the path to a schema
102
104
  manifest to copy into the package. A fresh app package is two commands:
103
105
 
104
106
  grok create MyTracker
105
- cd MyTracker && grok add app --domain grit.issue
107
+ cd MyTracker && grok add app --domain tracker.issue
106
108
 
107
109
  Supported languages for scripts:
108
110
  javascript, julia, node, octave, python, r
@@ -136,8 +138,9 @@ Options:
136
138
  [-v | --verbose] [--ui]
137
139
 
138
140
  --verbose Print detailed output
139
- --ui Also generate \`src/generated/db-ui.ts\` — typed UI wrappers over
140
- \`@datagrok-libraries/domain-ui\` for every domain table. Once the
141
+ --ui Also generate \`src/generated/db-ui.ts\` — typed u2 handles
142
+ (\`get<Schema>Db()\` → a \`DomainTable<Row>\` per table, opened in
143
+ parallel behind one await) over \`@datagrok-libraries/u2\`. Once the
141
144
  file exists, plain \`grok api\` keeps it up to date; delete it to
142
145
  opt out again
143
146
  `;
@@ -254,6 +257,7 @@ Options:
254
257
  --skip-puppeteer Skip the Puppeteer/DG.Test pass; only run Playwright (for playwright-only test directories)
255
258
  --skip-playwright Skip the Playwright pass; only run Puppeteer/DG.Test
256
259
  --skip-node Skip the Node (browserless) pass; run all tests in the browser
260
+ --skip-gc Do not collect garbage after each test (faster for suites of small unit tests)
257
261
  --node-only Run only tests annotated {node: true} headless under Node, no browser
258
262
  --link Link the package to local utils (no effect in a pnpm workspace checkout)
259
263
  --record Records the test execution process in mp4 format
@@ -352,7 +356,8 @@ Usage: grok setup [--check] [--global]
352
356
 
353
357
  Gets a public/ checkout (or a fresh worktree) ready to build, and keeps it that way after a pull:
354
358
  1. Node 20+ (22 recommended); pnpm through corepack, at the version the workspace pins
355
- 2. removes per-package node_modules left by the npm era and stray package-lock.json files
359
+ 2. removes per-package node_modules left by the npm era, stray package-lock.json files and the .js/.d.ts
360
+ files the npm-era tsc emitted beside js-api and library sources
356
361
  3. pnpm install at the workspace root
357
362
  4. reports a global grok older than the workspace one
358
363
 
@@ -38,7 +38,7 @@ const USAGE = `Usage: grok s domains <verb> [args]
38
38
  aggregate <schema.table> --json spec.json
39
39
  transaction <schema> --json ops.json
40
40
  audit <schema> | <schema.table> [<row-id>] [--limit n]
41
- capabilities <schema.table>
41
+ access <schema.table>
42
42
  grants <schema> | <schema.table>
43
43
  grant <schema> | <schema.table> <group>[,<group>...] [--access View|Edit|Delete|Share|Extend]
44
44
  revoke <schema> | <schema.table> <group>[,<group>...] [--access <permission>]
@@ -182,13 +182,13 @@ async function handleDomains(dapi, verb, rest, argv, output) {
182
182
  (0, _serverOutput.printOutput)(entries, output);
183
183
  return true;
184
184
  }
185
- case 'capabilities':
185
+ case 'access':
186
186
  {
187
- if (!args[0]) return usage('capabilities <schema.table>');
187
+ if (!args[0]) return usage('access <schema.table>');
188
188
  const a = (0, _nodeDapi.parseDomainAddress)(args[0], {
189
189
  table: true
190
190
  });
191
- (0, _serverOutput.printOutput)(flatten(await domains.capabilities(a.schema, a.table)), output);
191
+ (0, _serverOutput.printOutput)(flatten(await domains.access(a.schema, a.table)), output);
192
192
  return true;
193
193
  }
194
194
  case 'grants':
@@ -357,9 +357,13 @@ function grantRow(g) {
357
357
  groupId: g?.group?.id ?? ''
358
358
  };
359
359
  }
360
+
361
+ /** The access map as one printable row: `can.*` per right, `fields` as editable/readonly name lists. */
360
362
  function flatten(o) {
361
363
  const out = {};
362
- for (const [k, v] of Object.entries(o ?? {})) out[k] = Array.isArray(v) ? v.join(',') : v;
364
+ for (const [k, v] of Object.entries(o ?? {})) {
365
+ if (k === 'can') for (const [right, allowed] of Object.entries(v)) out[`can.${right}`] = allowed;else if (k === 'fields') for (const level of ['editable', 'readonly']) out[level] = Object.keys(v).filter(c => v[c] === level).join(',');else out[k] = Array.isArray(v) ? v.join(',') : v;
366
+ }
363
367
  return out;
364
368
  }
365
369
 
@@ -506,11 +506,9 @@ async function handleRaw(dapi, method, rest, argv, output) {
506
506
  return true;
507
507
  }
508
508
  async function handleSync(dapi, subject, rest, argv, output) {
509
- // `grok s sync` subcommands. Mirrors the API surface in
510
- // core/server/datlas/lib/src/routers/sync.dart — pairs / setups /
511
- // runs are read-only here; `run` triggers an actual push and prints
512
- // the per-item summary. Full reference in
513
- // core/docs/plans/instance-sync.md (Phase 7 / operational polish).
509
+ // Read-only mirror of core/server/datlas/lib/src/routers/sync.dart. A setup is an
510
+ // EntitySyncFunc (/entity_sync_funcs); the server has no run route, runs come from
511
+ // the UI or the setup's schedule.
514
512
  const verb = rest[0];
515
513
  // a 1.27 server has no /sync routes at all: report that as "not found", not as a failure
516
514
  const callSync = async (method, path, body) => {
@@ -533,7 +531,7 @@ async function handleSync(dapi, subject, rest, argv, output) {
533
531
  (0, _serverOutput.printError)(new Error('Usage: grok s sync setups list --pair <pair-id>'));
534
532
  return false;
535
533
  }
536
- const setups = await callSync('GET', `/sync/pairs/${encodeURIComponent(pairId)}/setups`);
534
+ const setups = await callSync('GET', `/entity_sync_funcs?pairId=${encodeURIComponent(pairId)}`);
537
535
  (0, _serverOutput.printOutput)(setups, output);
538
536
  return true;
539
537
  }
@@ -543,43 +541,21 @@ async function handleSync(dapi, subject, rest, argv, output) {
543
541
  (0, _serverOutput.printError)(new Error('Usage: grok s sync setup get <setup-id>'));
544
542
  return false;
545
543
  }
546
- const setup = await callSync('GET', `/sync/setups/${encodeURIComponent(id)}`);
544
+ const setup = await callSync('GET', `/entity_sync_funcs/${encodeURIComponent(id)}`);
547
545
  (0, _serverOutput.printOutput)(setup, output);
548
546
  return true;
549
547
  }
550
- if (subject === 'run') {
551
- // grok s sync run <setup-id>
552
- const id = rest[1] ?? verb;
548
+ if (subject === 'setup' && verb === 'runs') {
549
+ const id = rest[1];
553
550
  if (!id) {
554
- (0, _serverOutput.printError)(new Error('Usage: grok s sync run <setup-id>'));
555
- return false;
556
- }
557
- const result = await callSync('POST', `/sync/setups/${encodeURIComponent(id)}/run`, {});
558
- if (output === 'json' || output === 'csv') {
559
- (0, _serverOutput.printOutput)(result, output);
560
- return true;
561
- }
562
- if (output === 'quiet') {
563
- console.log(result?.runId ?? '');
564
- return true;
565
- }
566
- if (!result) {
567
- (0, _serverOutput.printError)(new Error(`Sync setup ${id} not found, or server has no /sync routes.`));
551
+ (0, _serverOutput.printError)(new Error('Usage: grok s sync setup runs <setup-id>'));
568
552
  return false;
569
553
  }
570
- console.log(`Run ${result?.runId} → ${result?.remoteUrl} (${result?.status})`);
571
- if (result?.counts) {
572
- const cs = Object.keys(result.counts).filter(k => (result.counts[k] ?? 0) > 0).map(k => `${result.counts[k]} ${k}`).join(', ');
573
- if (cs) console.log(` ${cs}`);
574
- }
575
- const items = Array.isArray(result?.items) ? result.items : [];
576
- if (items.length) {
577
- console.log('\nItems:');
578
- (0, _serverOutput.printOutput)(items, 'table');
579
- }
554
+ const runs = await callSync('GET', `/entity_sync_funcs/${encodeURIComponent(id)}/runs`);
555
+ (0, _serverOutput.printOutput)(runs, output);
580
556
  return true;
581
557
  }
582
- (0, _serverOutput.printError)(new Error('Usage:\n' + ' grok s sync pairs list [--status active|pending|revoked]\n' + ' grok s sync setups list --pair <pair-id>\n' + ' grok s sync setup get <setup-id>\n' + ' grok s sync run <setup-id>'));
558
+ (0, _serverOutput.printError)(new Error('Usage:\n' + ' grok s sync pairs list [--status active|pending|revoked]\n' + ' grok s sync setups list --pair <pair-id>\n' + ' grok s sync setup get <setup-id>\n' + ' grok s sync setup runs <setup-id>'));
583
559
  return false;
584
560
  }
585
561
  async function handleHealthcheck(dapi, argv, output) {
@@ -1002,7 +978,7 @@ Special commands:
1002
978
  grok s domains aggregate <schema.table> --measures 'count,sum(x) as t' [--group-by a,b] [--filter <expr>]
1003
979
  grok s domains transaction <schema> --json ops.json Ordered insert/update/delete ops, atomically
1004
980
  grok s domains audit <schema>|<schema.table> [<id>] [--limit n]
1005
- grok s domains capabilities <schema.table> What the current user may do on the table
981
+ grok s domains access <schema.table> What the current user may do on the table, per field
1006
982
  grok s domains grants <schema>|<schema.table> Direct permission grants on a schema or table
1007
983
  grok s domains grant <schema>|<schema.table> <group>[,...] [--access View|Edit|Delete|Share|Extend]
1008
984
  grok s domains revoke <schema>|<schema.table> <group>[,...] [--access <permission>]
@@ -1013,8 +989,8 @@ Special commands:
1013
989
  grok s batch manifest.json Run a workflow manifest
1014
990
  grok s sync pairs list [--status <s>] List cross-instance sync pairs (status: active|pending|revoked)
1015
991
  grok s sync setups list --pair <pair-id> List the named sync setups under a pair
1016
- grok s sync setup get <setup-id> Inspect a setup (selections, direction, last run)
1017
- grok s sync run <setup-id> Trigger a push run; prints per-item outcome
992
+ grok s sync setup get <setup-id> Inspect a setup (entries, direction, last run)
993
+ grok s sync setup runs <setup-id> A setup's past runs (per-item outcome in options.syncResult)
1018
994
 
1019
995
  Pull / push / migrate options:
1020
996
  --out <dir> Bundle directory to write (pull; merges into an existing bundle)
@@ -14,7 +14,8 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
14
14
  /**
15
15
  * `grok setup`: one command to get (or keep) a public/ checkout ready to build.
16
16
  * 1. Node 20+ (22 recommended); pnpm through corepack, at the version package.json pins.
17
- * 2. Removes per-package node_modules left by the npm era and stray package-lock.json files.
17
+ * 2. Removes per-package node_modules left by the npm era, stray package-lock.json files and the .js/.d.ts
18
+ * files the npm-era tsc emitted beside js-api and library sources.
18
19
  * 3. `pnpm install` at the workspace root.
19
20
  * 4. Reports a global `grok` older than the workspace one (`--global` updates it).
20
21
  * `--check` only reports.
@@ -41,7 +42,14 @@ async function setup(args) {
41
42
  if (pnpmVersion !== pinnedPnpm) {
42
43
  color.warn(`pnpm ${pnpmVersion || 'not found'}; the workspace pins ${pinnedPnpm}.`);
43
44
  if (!check) {
44
- if (run('corepack', ['--version']) === null) color.warn('corepack not found: `npm install -g corepack`, then rerun `grok setup`.');else {
45
+ if (run('corepack', ['--version']) === null) {
46
+ color.warn('corepack not found (Node 25 no longer bundles it): installing');
47
+ (0, _child_process.spawnSync)('npm', ['install', '-g', 'corepack'], {
48
+ stdio: 'inherit',
49
+ shell: true
50
+ });
51
+ }
52
+ if (run('corepack', ['--version']) === null) color.warn('could not install corepack: `npm install -g corepack` by hand, then rerun `grok setup`.');else {
45
53
  (0, _child_process.spawnSync)('corepack', ['enable'], {
46
54
  stdio: 'inherit',
47
55
  shell: true
@@ -58,19 +66,17 @@ async function setup(args) {
58
66
 
59
67
  // 2. leftovers from the npm era
60
68
  const projectDirs = ['js-api', 'tools', 'build-config'].map(d => _path.default.join(root, d));
61
- for (const group of ['packages', 'libraries']) for (const d of _fs.default.readdirSync(_path.default.join(root, group))) projectDirs.push(_path.default.join(root, group, d));
69
+ for (const group of ['packages', 'libraries']) for (const d of _fs.default.readdirSync(_path.default.join(root, group), {
70
+ withFileTypes: true
71
+ }).filter(e => e.isDirectory())) projectDirs.push(_path.default.join(root, group, d.name));
62
72
  const staleModules = projectDirs.filter(d => isLegacyNodeModules(_path.default.join(d, 'node_modules')));
63
73
  const staleLocks = projectDirs.map(d => _path.default.join(d, 'package-lock.json')).filter(f => _fs.default.existsSync(f));
64
- if (staleModules.length || staleLocks.length) {
65
- color.warn(`${staleModules.length} per-package node_modules from npm and ${staleLocks.length} package-lock.json file(s)` + (check ? ' would be removed' : ': removing'));
74
+ const staleEmits = projectDirs.filter(d => d === _path.default.join(root, 'js-api') || _path.default.dirname(d) === _path.default.join(root, 'libraries')).flatMap(inPlaceEmits);
75
+ if (staleModules.length || staleLocks.length || staleEmits.length) {
76
+ color.warn(`${staleModules.length} per-package node_modules from npm, ${staleLocks.length} package-lock.json file(s) and ` + `${staleEmits.length} .js/.d.ts file(s) emitted beside their .ts sources` + (check ? ' would be removed' : ': removing'));
66
77
  if (!check) {
67
- for (const d of staleModules) _fs.default.rmSync(_path.default.join(d, 'node_modules'), {
68
- recursive: true,
69
- force: true
70
- });
71
- for (const f of staleLocks) _fs.default.rmSync(f, {
72
- force: true
73
- });
78
+ const leftovers = [...staleModules.map(d => _path.default.join(d, 'node_modules')), ...staleLocks, ...staleEmits].filter(p => !tryRemove(p));
79
+ if (leftovers.length) color.warn(`could not remove ${leftovers.length} item(s) (locked by an editor, watcher or antivirus?), delete by hand:\n ${leftovers.join('\n ')}`);
74
80
  }
75
81
  } else color.info('no npm-era leftovers');
76
82
 
@@ -130,6 +136,46 @@ function isLegacyNodeModules(dir) {
130
136
  }
131
137
  return false;
132
138
  }
139
+
140
+ /**
141
+ * The npm-era tsc emitted next to the sources; the workspace emits into dist/, so a .js/.d.ts beside its .ts is stale
142
+ * (js-api/grok.js even shadows the `grok` command in cmd.exe). js-api/datagrok.js is still bundled in place and stays.
143
+ */
144
+ function inPlaceEmits(dir) {
145
+ const out = [];
146
+ const walk = (d, recurse) => {
147
+ for (const e of _fs.default.readdirSync(d, {
148
+ withFileTypes: true
149
+ })) {
150
+ const p = _path.default.join(d, e.name);
151
+ if (e.isDirectory()) {
152
+ if (recurse && e.name !== 'node_modules' && e.name !== 'dist') walk(p, true);
153
+ } else if (e.name.endsWith('.ts') && !e.name.endsWith('.d.ts')) {
154
+ const base = p.slice(0, -3);
155
+ for (const ext of ['.js', '.js.map', '.d.ts', '.d.ts.map']) if (_fs.default.existsSync(base + ext) && !(d === dir && e.name === 'datagrok.ts' && ext.startsWith('.js'))) out.push(base + ext);
156
+ }
157
+ }
158
+ };
159
+ walk(dir, false);
160
+ if (_fs.default.existsSync(_path.default.join(dir, 'src'))) walk(_path.default.join(dir, 'src'), true);
161
+ return out;
162
+ }
163
+
164
+ /** Windows reports a directory as busy/non-empty while a handle is still open on it; retry, then give up on this one only. */
165
+ function tryRemove(p) {
166
+ try {
167
+ _fs.default.rmSync(p, {
168
+ recursive: true,
169
+ force: true,
170
+ maxRetries: 5,
171
+ retryDelay: 200
172
+ });
173
+ return !_fs.default.existsSync(p);
174
+ } catch (e) {
175
+ color.warn(`${p}: ${e.code || e}`);
176
+ return false;
177
+ }
178
+ }
133
179
  function run(cmd, args) {
134
180
  const r = (0, _child_process.spawnSync)(cmd, args, {
135
181
  encoding: 'utf8',
@@ -30,7 +30,7 @@ const execFileAsync = (0, _util.promisify)(_child_process.execFile);
30
30
  // are discarded (they live in the page until a pass completes). CI runners on
31
31
  // loaded agents can exceed the 1h default — override via env.
32
32
  const testInvocationTimeout = parseInt(process.env['GROK_TEST_INVOCATION_TIMEOUT_MS'] ?? '', 10) || 3600000;
33
- const availableCommandOptions = ['host', 'package', 'csv', 'gui', 'catchUnhandled', 'platform', 'core', 'report', 'skip-build', 'skip-publish', 'path', 'record', 'verbose', 'benchmark', 'category', 'test', 'stress-test', 'link', 'tag', 'ci-cd', 'debug', 'no-retry', 'dartium', 'f', 'params', 'logfailed', 'skip-playwright', 'skip-puppeteer', 'skip-node', 'node-only'];
33
+ const availableCommandOptions = ['host', 'package', 'csv', 'gui', 'catchUnhandled', 'platform', 'core', 'report', 'skip-build', 'skip-publish', 'path', 'record', 'verbose', 'benchmark', 'category', 'test', 'stress-test', 'link', 'tag', 'ci-cd', 'debug', 'no-retry', 'dartium', 'f', 'params', 'logfailed', 'skip-playwright', 'skip-puppeteer', 'skip-node', 'node-only', 'skip-gc'];
34
34
  const curDir = process.cwd();
35
35
 
36
36
  /** Expands camelCase to space-separated lowercase: "dataManipulation" → "data manipulation" */
@@ -193,6 +193,7 @@ async function test(args) {
193
193
  if (args.debug) cmdArgs.push('--debug');
194
194
  if (args['ci-cd']) cmdArgs.push('--ci-cd');
195
195
  if (args['no-retry']) cmdArgs.push('--no-retry');
196
+ if (args['skip-gc']) cmdArgs.push('--skip-gc');
196
197
  if (!args['skip-publish']) {
197
198
  const isDevToolsOnServer = await testUtils.isPackageOnServer(args.host ?? '', 'DevTools');
198
199
  if (isDevToolsOnServer) {
@@ -403,7 +404,8 @@ async function runTesting(args, excludeNodeTests = false) {
403
404
  skipToCategory: currentSkipToCategory,
404
405
  skipToTest: currentSkipToTest,
405
406
  keepBrowserOpen: useRetry,
406
- urlParams: args.params
407
+ urlParams: args.params,
408
+ skipGc: args['skip-gc'] ?? false
407
409
  }, browserId, testInvocationTimeout, browserSession);
408
410
 
409
411
  // Store browser session for potential reuse
@@ -12,13 +12,15 @@ 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
- npm install install @datagrok-libraries/domain-ui
16
- npm run build && grok publish publish the package and open the app from the browse tree
15
+ pnpm install link @datagrok-libraries/u2 (a workspace dependency)
16
+ grok build && grok publish publish the package and open the app from the browse tree
17
17
 
18
- The app is the domain-ui defaults alone — list, search, entity page, editing,
19
- permissions and deep links. Customize through the options of the handle's factories
20
- (domains.table(...).app({actions: ...})), or register a DG.DomainObjectHandler for
21
- the table to change how its rows render.
18
+ The app is the u2 defaults alone — list, search, filters, entity page with children
19
+ and history, editing under one session, permissions and deep links. src/app.spec.json
20
+ is the same app as a designer-editable spec. To go further, run \`grok api --ui\` for
21
+ typed handles (get<Schema>Db()) and declare on a table: actions.add(...),
22
+ validators.add(column, ...), renderer, and app({app: <a DomainApp subclass>}) for
23
+ presets and shortcuts — the Grit package is the reference.
22
24
  Read more at https://datagrok.ai/help/develop/how-to/build-an-app`;
23
25
  const connection = connectionName => `
24
26
  The connection ${connectionName} has been added successfully
@@ -1004,8 +1004,10 @@ class NodeDomainsDataSource {
1004
1004
  permission
1005
1005
  })}`);
1006
1006
  }
1007
- capabilities(schema, table) {
1008
- return this.client.get(`${this.rows(schema, table)}/capabilities`);
1007
+
1008
+ /** `{can: {view, insert, edit, delete, share}, fields: {<column>: 'editable' | 'readonly'}, ...}`. */
1009
+ access(schema, table) {
1010
+ return this.client.get(`${this.rows(schema, table)}/access`);
1009
1011
  }
1010
1012
 
1011
1013
  /** JSON rows; spec = {filter, sort, columns, expand, limit, offset} (10k row cap). */
@@ -806,6 +806,7 @@ async function runBrowser(testExecutionData, browserOptions, browsersId, testInv
806
806
  devtools: browserOptions.debug
807
807
  }, defaultLaunchParameters);
808
808
  if (browserOptions.gui) params['headless'] = false;
809
+ if (browserOptions.skipGc) params.args = params.args.filter(arg => arg !== '--js-flags=--expose-gc');
809
810
  const out = await getBrowserPage(_puppeteer.default, params, browserOptions.urlParams);
810
811
  browser = out.browser;
811
812
  page = out.page;
@@ -219,8 +219,8 @@ const propertyTypes = exports.propertyTypes = ['bool', 'int', 'double', 'string'
219
219
  const headerTags = exports.headerTags = ['name', 'description', 'help-url', 'input', 'output', 'tags', 'sample', 'language', 'returns', 'test', 'sidebar', 'condition', 'top-menu', 'environment', 'require', 'editor-for', 'schedule', 'reference', 'editor', 'meta', 'connection', 'friendlyName'];
220
220
  const fileParamRegex = exports.fileParamRegex = {
221
221
  py: new RegExp(`^#\\s*(?!\\s*#)([^:]+):\\s+([^\\s\\[\\{]+) ?([^\\s\\[\\{]+)?`),
222
- ts: new RegExp(`^//\\s*(?!\\s*//)([^:]+):\\s+([^\\s\\[\\{]+) ?([^\\s\\[\\{]+)?`),
223
- js: new RegExp(`^//\\s*(?!\\s*//)([^:]+):\\s+([^\\s\\[\\{]+) ?([^\\s\\[\\{]+)?`),
222
+ ts: new RegExp(`^//\\s*(?!\\s*/)([^:]+):\\s+([^\\s\\[\\{]+) ?([^\\s\\[\\{]+)?`),
223
+ js: new RegExp(`^//\\s*(?!\\s*/)([^:]+):\\s+([^\\s\\[\\{]+) ?([^\\s\\[\\{]+)?`),
224
224
  sql: new RegExp(`^--\\s*([^:]+):\\s+([^\\s\\[\\{]+) ?([^\\s\\[\\{]+)?`)
225
225
  };
226
226
  const nameAnnRegex = exports.nameAnnRegex = /\s*(name[^:]*): ([^\n\r\[\{]+)/;
@@ -94,6 +94,11 @@
94
94
  "default": true,
95
95
  "description": "Per-table in-transaction audit trail (before/after diffs, history, restore)."
96
96
  },
97
+ "hierarchy": {
98
+ "type": "boolean",
99
+ "default": false,
100
+ "description": "The table is a tree: it must declare exactly one ref column targeting itself, which becomes its parent column. Enables the ancestor path endpoint and the 'under' subtree filter term."
101
+ },
97
102
  "extensible": {
98
103
  "type": "boolean",
99
104
  "default": false,
@@ -140,27 +145,50 @@
140
145
  },
141
146
  "constraints": {
142
147
  "type": "object",
143
- "description": "Table-level CHECK constraints keyed by constraint name. The expression may reference this table's relational columns, literals, comparison/arithmetic operators and a whitelist of keywords and immutable functions; it is normalized before it reaches the DDL.",
148
+ "description": "Table-level CHECK constraints keyed by constraint name. {\"check\": <sql>} references this table's relational columns, literals, comparison/arithmetic operators and a whitelist of keywords and immutable functions, normalized before it reaches the DDL. {\"expr\": <filter>, \"message\"?} is the platform filter grammar over this table's relational columns (literals, lists, null, a bare column name on the right of a comparison) — compiled to the same CHECK, and validated client-side by forms before the write, with the message shown on violation.",
144
149
  "propertyNames": {"$ref": "#/definitions/identifier"},
145
150
  "additionalProperties": {
146
151
  "type": "object",
147
- "required": ["check"],
148
- "additionalProperties": false,
149
- "properties": {
150
- "check": {"type": "string"}
151
- }
152
+ "oneOf": [
153
+ {
154
+ "required": ["check"],
155
+ "additionalProperties": false,
156
+ "properties": {"check": {"type": "string"}}
157
+ },
158
+ {
159
+ "required": ["expr"],
160
+ "additionalProperties": false,
161
+ "properties": {"expr": {"type": "string"}, "message": {"type": "string"}}
162
+ }
163
+ ]
152
164
  }
153
165
  },
154
166
  "grants": {
155
167
  "type": "object",
156
- "description": "Permissions granted on the table entity at deploy time, keyed by group name (a platform group such as 'All users' or 'Developers'); each lists the permissions to grant.",
168
+ "description": "Permissions granted on the table entity at deploy time, keyed by group name (a platform group such as 'All users' or 'Developers'); each lists the permissions to grant: view, edit, delete, or a custom permission this table declares.",
157
169
  "minProperties": 1,
158
170
  "additionalProperties": {
159
171
  "type": "array",
160
172
  "minItems": 1,
161
- "items": {"enum": ["view", "edit", "delete"]}
173
+ "items": {"$ref": "#/definitions/identifier"}
162
174
  }
163
175
  },
176
+ "permissions": {
177
+ "description": "Custom permissions an app gates its actions on, grantable on the table entity next to View/Edit/Delete/Share and answered as access.can.<name> and the per-row ~can_<name> column: a list of names, or name -> {description?}. Names are identifiers of at most 32 characters; view, edit, delete, share and extend are reserved.",
178
+ "oneOf": [
179
+ {"type": "array", "minItems": 1, "items": {"$ref": "#/definitions/identifier"}},
180
+ {
181
+ "type": "object",
182
+ "minProperties": 1,
183
+ "propertyNames": {"$ref": "#/definitions/identifier"},
184
+ "additionalProperties": {
185
+ "type": "object",
186
+ "additionalProperties": false,
187
+ "properties": {"description": {"type": "string"}}
188
+ }
189
+ }
190
+ ]
191
+ },
164
192
  "relations": {
165
193
  "type": "object",
166
194
  "description": "Many-to-many relations of this table keyed by relation name: each links the table to a target table through a junction table. The name shares one namespace with the columns (it is what expand and filter paths address), so it may not collide with a column of this table. Declaration order is preserved and is the order every UI surface lays the relations out in.",
@@ -297,7 +325,19 @@
297
325
  "default": {
298
326
  "description": "Default value for new rows / UI inputs."
299
327
  },
300
- "format": {"type": "string"}
328
+ "format": {"type": "string"},
329
+ "editor": {
330
+ "enum": ["textarea", "switch", "slider", "color", "tags", "markdown"],
331
+ "description": "Input editor hint for forms and dialogs (lower-case; stored as the platform InputType name on the column's property; markdown renders as a text area until the platform bridges its markdown input)."
332
+ },
333
+ "searchable": {
334
+ "type": "boolean",
335
+ "description": "The query `search` key matches this column (string only); a table declaring none searches its name column."
336
+ },
337
+ "filter": {
338
+ "type": "string",
339
+ "description": "Ref columns only: a filter-grammar expression over the target table that narrows the picker's candidates; `$name` binds to the owning row's column of that name (a dependent picker), e.g. \"country_id = $country_id\"."
340
+ }
301
341
  }
302
342
  }
303
343
  }
@@ -0,0 +1,4 @@
1
+ import {domains} from '@datagrok-libraries/u2/src/dg/index.js';
2
+
3
+ // the skins of everything the u2 domain stack renders
4
+ import '@datagrok-libraries/u2/src/dg/domain/styles.js';
@@ -1,11 +1,8 @@
1
1
 
2
2
  //name: #{NAME}
3
3
  //description: Browse and edit #{DOMAIN_TABLE} rows
4
- //meta.role: app
5
- //input: string path {meta.url: true; optional: true}
4
+ //tags: app
6
5
  //output: view result
7
6
  export async function #{NAME}() {
8
- // The declared `path` input is what makes the app URL-addressable; the page
9
- // reads the deep link itself (restoreFromUrl), so the function takes no parameter.
10
7
  return (await domains.table('#{DOMAIN_TABLE}')).app();
11
8
  }
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "dg-ui/1",
3
+ "root": {
4
+ "tag": "u2-panel",
5
+ "name": "app",
6
+ "children": [
7
+ {"tag": "u2-domain-source", "name": "#{SOURCE}", "props": {"table": "#{DOMAIN_TABLE}", "pageSize": 50}},
8
+ {"tag": "u2-splitter", "name": "layout", "props": {"direction": "horizontal", "sizes": [0.35, 0.65]}, "children": [
9
+ {"tag": "u2-panel", "name": "left", "children": [
10
+ {"tag": "u2-domain-search", "name": "search", "bind": {"source": "$.#{SOURCE}.source"}},
11
+ {"tag": "u2-domain-filters", "name": "filters", "bind": {"source": "$.#{SOURCE}.source"}},
12
+ {"tag": "u2-domain-list", "name": "list", "bind": {"source": "$.#{SOURCE}.source"}, "props": {"mode": "cards"}}
13
+ ]},
14
+ {"tag": "u2-panel", "name": "right", "children": [
15
+ {"tag": "u2-domain-form", "name": "form", "bind": {"source": "$.#{SOURCE}.source"}},
16
+ {"tag": "u2-domain-children", "name": "children", "bind": {"source": "$.#{SOURCE}.source"}, "props": {"mode": "list"}},
17
+ {"tag": "u2-domain-history", "name": "history", "bind": {"source": "$.#{SOURCE}.source"}},
18
+ {"tag": "u2-button", "name": "newButton", "props": {"text": "New"}, "on": {"click": "cmd:#{SOURCE}.newRow"}},
19
+ {"tag": "u2-button", "name": "saveButton", "props": {"text": "Save", "primary": true}, "on": {"click": "cmd:#{SOURCE}.save"}},
20
+ {"tag": "u2-button", "name": "discardButton", "props": {"text": "Discard"}, "on": {"click": "cmd:#{SOURCE}.discard"}}
21
+ ]}
22
+ ]}
23
+ ]
24
+ }
25
+ }
@@ -1,11 +1,8 @@
1
1
 
2
2
  //name: #{NAME}
3
3
  //description: Browse and edit #{DOMAIN_TABLE} rows
4
- //meta.role: app
5
- //input: string path {meta.url: true; optional: true}
4
+ //tags: app
6
5
  //output: view result
7
6
  export async function #{NAME}(): Promise<DG.ViewBase> {
8
- // The declared `path` input is what makes the app URL-addressable; the page
9
- // reads the deep link itself (restoreFromUrl), so the function takes no parameter.
10
7
  return (await domains.table('#{DOMAIN_TABLE}')).app();
11
8
  }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "#{SCHEMA}",
3
+ "version": "1.0.0",
4
+ "tables": {
5
+ "#{TABLE}": {
6
+ "columns": {
7
+ "name": {"type": "string", "required": true, "isName": true, "searchable": true},
8
+ "description": {"type": "string", "editor": "textarea"}
9
+ }
10
+ }
11
+ }
12
+ }
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "type": "git",
5
5
  "url": "https://github.com/datagrok-ai/public.git"
6
6
  },
7
- "version": "6.7.4",
7
+ "version": "6.7.6",
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
10
  "dependencies": {
@@ -27,7 +27,7 @@
27
27
  "puppeteer": "^24.15.0",
28
28
  "ts-morph": "^28.0.0",
29
29
  "@babel/generator": "^7.29.0",
30
- "datagrok-api": "^1.27.11"
30
+ "datagrok-api": "^1.27.12"
31
31
  },
32
32
  "bin": {
33
33
  "datagrok-upload": "./bin/_deprecated/upload.js",