datagrok-tools 6.5.7 → 6.5.8

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.
@@ -154,7 +154,7 @@ async function runPlaywrightTests(pkgDir, testDir, args, hostKey) {
154
154
  }
155
155
  let webUrl;
156
156
  try {
157
- webUrl = await testUtils.getWebUrl(url, token);
157
+ webUrl = process.env.DATAGROK_WEB_URL || (await testUtils.getWebUrl(url, token));
158
158
  if (webUrl.endsWith('/')) webUrl = webUrl.slice(0, -1);
159
159
  } catch {
160
160
  webUrl = url.replace(/\/api\/?$/, '');
@@ -191,6 +191,8 @@ async function runPlaywrightTests(pkgDir, testDir, args, hostKey) {
191
191
  ...process.env,
192
192
  DATAGROK_URL: webUrl,
193
193
  DATAGROK_AUTH_TOKEN: token,
194
+ DATAGROK_API_URL: url,
195
+ DATAGROK_DEV_KEY: key,
194
196
  PLAYWRIGHT_JSON_OUTPUT_NAME: reportFile
195
197
  };
196
198
  if (token2) env.DATAGROK_AUTH_TOKEN_2 = token2;
@@ -6,10 +6,23 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.createClient = createClient;
7
7
  var _nodeDapi = require("./node-dapi");
8
8
  var _testUtils = require("./test-utils");
9
- async function createClient(hostArg) {
9
+ /**
10
+ * `--admin` asks the server for an admin session, which lifts the permission filter for this run:
11
+ * without it a stand-wide pull sees only what the key's own account can, and content in other
12
+ * people's spaces is invisible. The server authorises it (`START_ADMIN_SESSION`) and signs the
13
+ * flag into the token, and a dev key mints a fresh session per invocation, so it cannot outlive
14
+ * the command or reach another session.
15
+ */
16
+ async function createClient(hostArg, admin = false) {
10
17
  const {
11
18
  url,
12
19
  key
13
20
  } = (0, _testUtils.getDevKey)(hostArg ?? '');
14
- return _nodeDapi.NodeApiClient.login(url, key);
21
+ const client = await _nodeDapi.NodeApiClient.login(url, key);
22
+ if (!admin) return client;
23
+ const token = (await client.post('/users/sessions/current/admin'))?.token;
24
+ if (!token) throw new Error(`${url} refused an admin session — the account behind this key cannot start one`);
25
+ client.token = token;
26
+ client.adminMode = true;
27
+ return client;
15
28
  }
@@ -9,8 +9,16 @@ exports.getKeys = getKeys;
9
9
  exports.printBatchOutput = printBatchOutput;
10
10
  exports.printError = printError;
11
11
  exports.printOutput = printOutput;
12
+ exports.progressReporter = progressReporter;
13
+ exports.setOutputFormat = setOutputFormat;
12
14
  /// Docs: [Grok Dapi](/docs/plans/grok-dapi/)
13
15
 
16
+ let errorFormat = 'table';
17
+
18
+ /** Set once per run so every `printError` call site reports in the requested format. */
19
+ function setOutputFormat(format) {
20
+ errorFormat = format;
21
+ }
14
22
  function printOutput(data, format) {
15
23
  if (data === null || data === undefined) {
16
24
  if (format !== 'quiet') console.log('(empty)');
@@ -118,10 +126,63 @@ function printBatchOutput(response, format) {
118
126
  error: r.error
119
127
  })), null, 2) + '\n');
120
128
  }
121
- function printError(err) {
129
+ function printError(err, opts = {}) {
122
130
  const apiErr = err?.apiError;
123
- const out = apiErr ?? {
124
- error: String(err?.message ?? err)
131
+ const status = apiErr?.errorCode;
132
+ const base = apiErr?.error ?? String(err?.message ?? err);
133
+ const message = typeof status === 'number' && status >= 400 && !base.includes(String(status)) ? `${base} (HTTP ${status})` : base;
134
+ if (errorFormat === 'json') {
135
+ process.stderr.write(JSON.stringify({
136
+ ...apiErr,
137
+ error: message
138
+ }) + '\n');
139
+ return;
140
+ }
141
+ process.stderr.write(`${message}\n`);
142
+ printErrorDetails(apiErr?.body);
143
+ if (opts.verbose && apiErr?.stackTrace) process.stderr.write(apiErr.stackTrace + '\n');
144
+ }
145
+
146
+ /** Structured fields of a domain error envelope: per-row validation errors, manifest errors, a plan awaiting confirmation. */
147
+ function printErrorDetails(body) {
148
+ if (!body || typeof body !== 'object') return;
149
+ for (const r of Array.isArray(body.rows) ? body.rows : []) for (const e of Array.isArray(r?.errors) ? r.errors : []) process.stderr.write(` row ${r.index ?? ''}${e?.column ? ` ${e.column}` : ''}: ${e?.message ?? e?.code ?? ''}\n`);
150
+ for (const e of Array.isArray(body.errors) ? body.errors : []) process.stderr.write(` ${typeof e === 'string' ? e : e?.message ?? JSON.stringify(e)}\n`);
151
+ if (body.plan) process.stderr.write(JSON.stringify(body.plan, null, 2) + '\n');
152
+ }
153
+
154
+ /**
155
+ * Progress for the long migration walks. A whole-stand pull is minutes of network waiting with
156
+ * nothing to show for it, and silence is indistinguishable from a hang. Goes to stderr, so it
157
+ * shows even under `--output json` without disturbing the document, and rewrites a single line
158
+ * on a terminal.
159
+ */
160
+ function progressReporter(quiet = false) {
161
+ if (quiet) return () => {};
162
+ const tty = process.stderr.isTTY;
163
+ let last = '';
164
+ let drawn = 0;
165
+ return (stage, done, total) => {
166
+ const count = done === undefined ? '' : total === undefined ? ` ${done}` : ` ${done}/${total}`;
167
+ const line = `${stage}${count}`;
168
+ if (!tty) {
169
+ // A log cannot be rewritten in place, so it gets a line every so often rather than one per
170
+ // item — enough to show the run is alive, few enough to stay readable.
171
+ if (line !== last && (done === undefined || done === total || done % 250 === 0)) process.stderr.write(`${line}\n`);
172
+ last = line;
173
+ return;
174
+ }
175
+ // Redrawing on every item writes megabytes a minute into anything that is not really a
176
+ // terminal — a detached run produced a 7 GB file of carriage returns. The eye cannot
177
+ // follow faster than this anyway.
178
+ const finished = done !== undefined && done === total;
179
+ if (!finished && Date.now() - drawn < 100) return;
180
+ drawn = Date.now();
181
+ process.stderr.write(`\r${' '.repeat(last.length)}\r${line}`);
182
+ last = line;
183
+ if (finished) {
184
+ process.stderr.write('\n');
185
+ last = '';
186
+ }
125
187
  };
126
- process.stderr.write(JSON.stringify(out, null, 2) + '\n');
127
188
  }
@@ -127,7 +127,7 @@ function getDevKey(hostKey) {
127
127
  let key = '';
128
128
  let url = '';
129
129
  try {
130
- let url = new URL(host).href;
130
+ url = new URL(host).href;
131
131
  if (url.endsWith('/')) url = url.slice(0, -1);
132
132
  if (url in urls) key = config['servers'][urls[url]]['key'];
133
133
  } catch (error) {
@@ -77,12 +77,12 @@
77
77
  "promotion": {
78
78
  "enum": ["lazy", "eager"],
79
79
  "default": "lazy",
80
- "description": "Row mode only. lazy: entities row created on first share; eager: at insert."
80
+ "description": "Row mode only. lazy: entities row created on first share; eager: at insert, with the author granted View/Edit/Delete/Share."
81
81
  },
82
82
  "defaultRowVisibility": {
83
83
  "enum": ["table", "none"],
84
84
  "default": "table",
85
- "description": "Row/master modes: whether table-level View shows unshared rows."
85
+ "description": "Row/master modes: whether table-level View shows unshared rows. Their author always sees, edits and shares them; none hides them from everyone else."
86
86
  },
87
87
  "delegate": {
88
88
  "$ref": "#/definitions/identifier",
@@ -138,6 +138,29 @@
138
138
  "items": {"$ref": "#/definitions/identifier"},
139
139
  "description": "Property schemas contributing jsonb keys to this table."
140
140
  },
141
+ "constraints": {
142
+ "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.",
144
+ "propertyNames": {"$ref": "#/definitions/identifier"},
145
+ "additionalProperties": {
146
+ "type": "object",
147
+ "required": ["check"],
148
+ "additionalProperties": false,
149
+ "properties": {
150
+ "check": {"type": "string"}
151
+ }
152
+ }
153
+ },
154
+ "grants": {
155
+ "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.",
157
+ "minProperties": 1,
158
+ "additionalProperties": {
159
+ "type": "array",
160
+ "minItems": 1,
161
+ "items": {"enum": ["view", "edit", "delete"]}
162
+ }
163
+ },
141
164
  "relations": {
142
165
  "type": "object",
143
166
  "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.",
@@ -211,8 +234,8 @@
211
234
  "additionalProperties": false,
212
235
  "properties": {
213
236
  "type": {
214
- "enum": ["string", "int", "float", "bool", "datetime", "string_list", "ref", "user", "group", "file"],
215
- "description": "Property type; ref = FK to another table in this manifest; user/group = FK to the core users/groups tables with the matching semantic type; file = a file://<connection>/<path> string into platform file storage (semantic type File)."
237
+ "enum": ["string", "int", "float", "bool", "datetime", "string_list", "ref", "file", "json", "user", "group"],
238
+ "description": "Property type; ref = FK to another table in this manifest; user/group = aliases of ref Core.users / ref Core.groups (semantic type User / Group); file = a file://<connection>/<path> string into platform file storage (semantic type File); json = a jsonb-backed opaque object (filters, sorts, facets and aggregates refuse it)."
216
239
  },
217
240
  "required": {"type": "boolean", "default": false},
218
241
  "unique": {
@@ -220,14 +243,42 @@
220
243
  "default": false,
221
244
  "description": "Unique among live (not soft-deleted) rows."
222
245
  },
246
+ "immutable": {
247
+ "type": "boolean",
248
+ "default": false,
249
+ "description": "Write-once: the first non-null value wins; a later change is refused."
250
+ },
251
+ "autoNumber": {
252
+ "description": "Auto-numbering (int columns only; implies immutable): a blank value on insert is assigned from a monotonic counter, a supplied value is kept (the counter catches up). true = one counter per table; {scope} = one counter per master row of the named ref column of this table. Changing it on an existing column (on, off, or a scope change) is a physical change that needs a migration.",
253
+ "oneOf": [
254
+ {"enum": [true]},
255
+ {
256
+ "type": "object",
257
+ "additionalProperties": false,
258
+ "properties": {
259
+ "scope": {
260
+ "$ref": "#/definitions/identifier",
261
+ "description": "A required ref column of this table; numbering restarts per referenced row."
262
+ },
263
+ "start": {
264
+ "type": "integer",
265
+ "minimum": 1,
266
+ "default": 1,
267
+ "description": "First number assigned by a fresh counter."
268
+ }
269
+ }
270
+ }
271
+ ]
272
+ },
223
273
  "isName": {
224
274
  "type": "boolean",
225
275
  "default": false,
226
276
  "description": "Marks the primary display-name column (string columns only; at most one per table). Its value titles cards, tooltips, and entity views, and becomes the friendly name of promoted rows. Convention fallback: a string column literally named 'name'."
227
277
  },
228
278
  "ref": {
229
- "$ref": "#/definitions/identifier",
230
- "description": "Target table name (type = ref only). Cross-plugin refs are not allowed."
279
+ "type": "string",
280
+ "pattern": "^([a-z][a-z0-9_]*|[A-Za-z][A-Za-z0-9_]*\\.[a-z][a-z0-9_]*)$",
281
+ "description": "Target table (type = ref only): a table of this schema, or the qualified '<Schema>.<table>' form for any registered table — a Core table such as 'Core.users' or 'Core.queries', or a table of another plugin's schema. The reference is a hard foreign key only for same-schema targets and Core.users/Core.groups; every other target is a soft reference (no FK constraint) that may dangle after the target is hard-deleted."
231
282
  },
232
283
  "onDelete": {
233
284
  "enum": ["cascade", "restrict", "setnull"],
package/package.json CHANGED
@@ -1,6 +1,10 @@
1
1
  {
2
2
  "name": "datagrok-tools",
3
- "version": "6.5.7",
3
+ "repository": {
4
+ "type": "git",
5
+ "url": "https://github.com/datagrok-ai/public.git"
6
+ },
7
+ "version": "6.5.8",
4
8
  "description": "Utility to upload and publish packages to Datagrok",
5
9
  "homepage": "https://github.com/datagrok-ai/public/tree/master/tools#readme",
6
10
  "dependencies": {
@@ -32,6 +36,7 @@
32
36
  "update:ivp-parser": "esbuild plugins/ivp-parser.entry.mjs --bundle --format=cjs --platform=node --alias:diff-grok=../libraries/compute-utils/node_modules/diff-grok --outfile=plugins/ivp-parser.bundle.cjs",
33
37
  "debug-source-map": "node build.js --source-maps",
34
38
  "test": "vitest run --project unit",
39
+ "test:server": "vitest run --project unit bin/__tests__/node-dapi bin/__tests__/server bin/__tests__/migrate",
35
40
  "test:watch": "vitest --project unit",
36
41
  "test:integration": "vitest run --project integration",
37
42
  "test:all": "vitest run"
File without changes