datagrok-tools 6.7.5 → 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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +9 -0
- package/CLAUDE.md +1 -1
- package/GROK_S.md +9 -6
- package/bin/commands/add.js +55 -20
- package/bin/commands/api.js +22 -46
- package/bin/commands/help.js +11 -6
- package/bin/commands/server-domains.js +9 -5
- package/bin/commands/server.js +14 -38
- package/bin/commands/setup.js +58 -12
- package/bin/commands/test.js +4 -2
- package/bin/utils/ent-helpers.js +8 -6
- package/bin/utils/node-dapi.js +4 -2
- package/bin/utils/test-utils.js +1 -0
- package/bin/utils/utils.js +2 -2
- package/domain-schema.schema.json +49 -9
- package/entity-template/domain-app-imports.js +4 -0
- package/entity-template/domain-app.js +1 -4
- package/entity-template/domain-app.spec.json +25 -0
- package/entity-template/domain-app.ts +1 -4
- package/entity-template/domain-schema.json +12 -0
- package/package.json +2 -2
package/.turbo/turbo-build.log
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
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
|
+
|
|
3
12
|
## 6.7.5 (2026-09-17)
|
|
4
13
|
|
|
5
14
|
* `grok create` works outside the pnpm workspace and new packages use `@datagrok/build-config` 1.x
|
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 `
|
|
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](
|
|
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
|
|
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`
|
|
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
|
|
909
|
-
`sync
|
|
910
|
-
|
|
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".
|
package/bin/commands/add.js
CHANGED
|
@@ -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
|
-
/**
|
|
16
|
-
|
|
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
|
|
103
|
-
*
|
|
104
|
-
function
|
|
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(
|
|
113
|
+
if (contents.includes(u2DgModule)) return;
|
|
107
114
|
const eol = contents.includes('\r\n') ? '\r\n' : '\n';
|
|
108
|
-
const
|
|
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,
|
|
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
|
|
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)
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
166
|
-
//
|
|
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;
|
package/bin/commands/api.js
CHANGED
|
@@ -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
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
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`.
|
|
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
|
|
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
|
|
194
|
-
*
|
|
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
|
|
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
|
|
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
|
|
493
|
-
*
|
|
494
|
-
*
|
|
495
|
-
*
|
|
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
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
package/bin/commands/help.js
CHANGED
|
@@ -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/
|
|
100
|
-
|
|
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
|
|
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
|
|
140
|
-
|
|
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
|
|
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
|
-
|
|
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 '
|
|
185
|
+
case 'access':
|
|
186
186
|
{
|
|
187
|
-
if (!args[0]) return usage('
|
|
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.
|
|
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 ?? {}))
|
|
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
|
|
package/bin/commands/server.js
CHANGED
|
@@ -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
|
-
//
|
|
510
|
-
//
|
|
511
|
-
//
|
|
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', `/
|
|
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', `/
|
|
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 === '
|
|
551
|
-
|
|
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
|
|
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
|
-
|
|
571
|
-
|
|
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
|
|
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
|
|
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 (
|
|
1017
|
-
grok s sync
|
|
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)
|
package/bin/commands/setup.js
CHANGED
|
@@ -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
|
|
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)
|
|
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)
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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',
|
package/bin/commands/test.js
CHANGED
|
@@ -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
|
package/bin/utils/ent-helpers.js
CHANGED
|
@@ -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
|
-
|
|
16
|
-
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
package/bin/utils/node-dapi.js
CHANGED
|
@@ -1004,8 +1004,10 @@ class NodeDomainsDataSource {
|
|
|
1004
1004
|
permission
|
|
1005
1005
|
})}`);
|
|
1006
1006
|
}
|
|
1007
|
-
|
|
1008
|
-
|
|
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). */
|
package/bin/utils/test-utils.js
CHANGED
|
@@ -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;
|
package/bin/utils/utils.js
CHANGED
|
@@ -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
|
|
223
|
-
js: new RegExp(`^//\\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.
|
|
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
|
-
"
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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": {"
|
|
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
|
}
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
|
|
2
2
|
//name: #{NAME}
|
|
3
3
|
//description: Browse and edit #{DOMAIN_TABLE} rows
|
|
4
|
-
//
|
|
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
|
-
//
|
|
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
|
}
|
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.
|
|
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.
|
|
30
|
+
"datagrok-api": "^1.27.12"
|
|
31
31
|
},
|
|
32
32
|
"bin": {
|
|
33
33
|
"datagrok-upload": "./bin/_deprecated/upload.js",
|