datagrok-tools 6.5.8 → 6.5.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Datagrok-tools changelog
2
2
 
3
+ ## 6.5.10 (2026-09-11)
4
+
5
+ * `grok s pull` — a bundle file name is capped at 200 bytes plus a digest of the full name, so it stays unique and stable across pulls. A nested view under a long space spells a longer name than a path component may hold (255 bytes on ext4 and NTFS): one TWIG snapshot reached 268 and the write failed with `ENAMETOOLONG`, taking down the whole part rather than the entity — a `--by-namespace` part that only touched the space as a dependency died with it.
6
+
7
+ ## 6.5.9 (2026-09-11)
8
+
9
+ * `grok s push/migrate` — placement no longer claims an entity the walk refused. Containment is exclusive, so asserting a project's relation to a platform entity takes it away from whatever holds it on the target: migrating a dashboard built on demo data moved `System:DemoFiles` into the migrated space, and every reference to it by name broke stand-wide until its `System` row was restored by hand. The guard covered a space's own `Files` connection but not `System:` connections, personal `Home` shares, package projects or built-in groups; it now defers to the same `untransferableReason` the walk uses.
10
+
3
11
  ## 6.5.8 (2026-09-10)
4
12
 
5
13
  * `grok s` — correctness pass from the CLI audit: every server failure now exits 1 with one line on stderr (a 200 carrying an `ApiError` — missing entity, duplicate login or group name, unknown function — used to print as data with exit 0); `--limit`/`--offset` are honoured for users, groups, connections and functions (their public routes ignore paging, so `list` and the new `count` verb go through the internal routers); `files list` works (the public files route only downloads; listing goes through the connection's file route) and prints `path, kind, size`; `tables list|get|delete` no longer throw, and `tables download|get|delete` accept the bare table name, the full `Project:Table` name or the id; `describe <entity|type>` works (registry record plus the fields of a live sample; it called a nonexistent endpoint before); positional `functions run 'F(1, 2)'` maps the values onto the function's inputs instead of silently sending `{"0": 1}` (which ran the function with nulls); `raw` takes API-relative paths (a leading `/api` is accepted), sends a body from `--json <file>` or `--data '<json>'`, and fails on non-2xx — so it, and `healthcheck`, now work against a bare-Datlas `--host` too; `shares list` accepts a name and shows the grants inherited through project links; `groups` name resolution prefers an exact match (`admin` no longer collides with `Administrators`) and `--user` works on `list-members`/`list-memberships`; `connections delete` of an unknown id fails instead of reporting success; a bad `--host` URL or alias reports the reason without the help dump. `users delete` refuses with a pointer at `users block`: the platform has no user deletion, and removing the entity record leaves the login unusable. `functions delete` covers scripts and queries.
@@ -595,11 +595,12 @@ async function pushRelations(dapi, effective, planned, rows, progress = () => {}
595
595
  let added = 0;
596
596
  for (const rel of json.relations) {
597
597
  if (linked.has(rel.entity.id)) continue;
598
- // The target stamps its own Files connection onto a space; re-attaching the source's
599
- // would leave the space with two of them.
598
+ // Placement is exclusive, so claiming an entity the walk refused takes it away from
599
+ // whatever holds it on the target: a space's own Files connection, or `System:DemoFiles`,
600
+ // which moved into a migrated space and broke every reference to it by name.
600
601
  if (!landed.has(rel.entity.id)) {
601
602
  const existing = await (0, _walker.findEntity)(dapi, rel.entity.id);
602
- if (!existing || existing.parameters?.isProject === true) continue;
603
+ if (!existing || (0, _registry.untransferableReason)(existing['#type'], existing)) continue;
603
604
  }
604
605
  wanted.push(contains(rel.entity.id) ? rel : {
605
606
  ...rel,
@@ -522,11 +522,12 @@ async function pushRelations(dapi: NodeDapi, effective: Map<string, BundleEntity
522
522
  let added = 0;
523
523
  for (const rel of json.relations) {
524
524
  if (linked.has(rel.entity.id)) continue;
525
- // The target stamps its own Files connection onto a space; re-attaching the source's
526
- // would leave the space with two of them.
525
+ // Placement is exclusive, so claiming an entity the walk refused takes it away from
526
+ // whatever holds it on the target: a space's own Files connection, or `System:DemoFiles`,
527
+ // which moved into a migrated space and broke every reference to it by name.
527
528
  if (!landed.has(rel.entity.id)) {
528
529
  const existing = await findEntity(dapi, rel.entity.id);
529
- if (!existing || existing.parameters?.isProject === true) continue;
530
+ if (!existing || untransferableReason(existing['#type'], existing)) continue;
530
531
  }
531
532
  wanted.push(contains(rel.entity.id) ? rel : {...rel, isLink: true});
532
533
  linked.add(rel.entity.id);
@@ -16,6 +16,7 @@ exports.resolveTypes = resolveTypes;
16
16
  exports.stripCredentials = stripCredentials;
17
17
  exports.typeOptionsOf = typeOptionsOf;
18
18
  exports.untransferableReason = untransferableReason;
19
+ var _crypto = require("crypto");
19
20
  /// Docs: [Entity export / import](/docs/features/grok-tool/export-import/DESIGN.md)
20
21
 
21
22
  /** Extra listing rules a `--type` alias adds on top of its entity type. */
@@ -344,6 +345,20 @@ function fileNameFor(json) {
344
345
  const cleaned = String(nqName).replace(/[:/\\?*"<>|]+/g, '.').replace(/^\.+|\.+$/g, '');
345
346
  // Windows resolves `NUL.json` (and `COM1.csv.json`) to a device, whatever the extension.
346
347
  const head = cleaned.split('.')[0];
347
- return RESERVED_RE.test(head) ? `${head}-${json.id ?? 'entity'}${cleaned.slice(head.length)}` : cleaned;
348
+ return shorten(RESERVED_RE.test(head) ? `${head}-${json.id ?? 'entity'}${cleaned.slice(head.length)}` : cleaned);
348
349
  }
349
- const RESERVED_RE = /^(NUL|CON|PRN|AUX|COM[1-9]|LPT[1-9])$/i;
350
+ const RESERVED_RE = /^(NUL|CON|PRN|AUX|COM[1-9]|LPT[1-9])$/i;
351
+
352
+ /**
353
+ * A path component caps at 255 bytes on ext4 and NTFS, and a nested view under a long space
354
+ * reaches that: one TWIG snapshot spelled 268. The write is what fails, so the whole part dies
355
+ * rather than the entity. The digest keeps a truncated name unique and stable across pulls.
356
+ */
357
+ const MAX_FILE_NAME = 200;
358
+ function shorten(name) {
359
+ if (Buffer.byteLength(name) <= MAX_FILE_NAME) return name;
360
+ const digest = (0, _crypto.createHash)('sha1').update(name).digest('hex').slice(0, 8);
361
+ let head = name;
362
+ while (Buffer.byteLength(head) > MAX_FILE_NAME - 9) head = head.slice(0, -1);
363
+ return `${head}-${digest}`;
364
+ }
@@ -1,4 +1,5 @@
1
1
  /// Docs: [Entity export / import](/docs/features/grok-tool/export-import/DESIGN.md)
2
+ import {createHash} from 'crypto';
2
3
 
3
4
  export type BytesKind = 'tables' | 'files';
4
5
 
@@ -249,7 +250,22 @@ export function fileNameFor(json: any): string {
249
250
  const cleaned = String(nqName).replace(/[:/\\?*"<>|]+/g, '.').replace(/^\.+|\.+$/g, '');
250
251
  // Windows resolves `NUL.json` (and `COM1.csv.json`) to a device, whatever the extension.
251
252
  const head = cleaned.split('.')[0];
252
- return RESERVED_RE.test(head) ? `${head}-${json.id ?? 'entity'}${cleaned.slice(head.length)}` : cleaned;
253
+ return shorten(RESERVED_RE.test(head) ? `${head}-${json.id ?? 'entity'}${cleaned.slice(head.length)}` : cleaned);
253
254
  }
254
255
 
255
256
  const RESERVED_RE = /^(NUL|CON|PRN|AUX|COM[1-9]|LPT[1-9])$/i;
257
+
258
+ /**
259
+ * A path component caps at 255 bytes on ext4 and NTFS, and a nested view under a long space
260
+ * reaches that: one TWIG snapshot spelled 268. The write is what fails, so the whole part dies
261
+ * rather than the entity. The digest keeps a truncated name unique and stable across pulls.
262
+ */
263
+ const MAX_FILE_NAME = 200;
264
+
265
+ function shorten(name: string): string {
266
+ if (Buffer.byteLength(name) <= MAX_FILE_NAME) return name;
267
+ const digest = createHash('sha1').update(name).digest('hex').slice(0, 8);
268
+ let head = name;
269
+ while (Buffer.byteLength(head) > MAX_FILE_NAME - 9) head = head.slice(0, -1);
270
+ return `${head}-${digest}`;
271
+ }
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.5.8",
7
+ "version": "6.5.10",
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": {