datagrok-tools 6.1.10 → 6.1.12

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.
@@ -14,7 +14,7 @@ var _serverOutput = require("../utils/server-output");
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
15
  /// Docs: [Grok Dapi](/docs/plans/grok-dapi/)
16
16
 
17
- const ENTITIES = ['users', 'groups', 'functions', 'connections', 'queries', 'scripts', 'packages', 'reports', 'files'];
17
+ const ENTITIES = ['users', 'groups', 'functions', 'connections', 'queries', 'scripts', 'packages', 'reports', 'files', 'tables'];
18
18
  const VERBS = ['list', 'get', 'delete'];
19
19
  async function server(argv) {
20
20
  const args = argv['_'].slice(1);
@@ -43,7 +43,9 @@ async function server(argv) {
43
43
  if (entity === 'batch') return handleBatch(dapi, argv, verb, rest, output);
44
44
  if (entity === 'raw') return handleRaw(dapi, verb, rest, output);
45
45
  if (entity === 'describe') return handleDescribe(dapi, verb ?? rest[0], output);
46
+ if (entity === 'healthcheck') return handleHealthcheck(dapi, argv, output);
46
47
  if (entity === 'functions' && verb === 'run') return handleFuncRun(dapi, rest, argv, output);
48
+ if (entity === 'functions' && verb === 'list') return handleFunctionsList(dapi, argv, limit, offset, filter, output);
47
49
  if (entity === 'files' && verb === 'list') {
48
50
  const path = rest[0] ?? '';
49
51
  const result = await dapi.files.list(path, recursive);
@@ -60,6 +62,7 @@ async function server(argv) {
60
62
  if (output !== 'quiet') console.log(`Deleted ${rest[0]}`);
61
63
  return true;
62
64
  }
65
+ if (entity === 'files' && verb === 'put') return handleFilesPut(dapi, rest, output);
63
66
  if (entity === 'shares' && verb === 'add') return handleSharesAdd(dapi, rest, argv, output);
64
67
  if (entity === 'shares' && verb === 'list') return handleSharesList(dapi, rest, output);
65
68
  if (entity === 'users' && verb === 'save') return handleUserSave(dapi, argv, output);
@@ -70,6 +73,10 @@ async function server(argv) {
70
73
  if (entity === 'groups' && verb === 'remove-members') return handleGroupRemoveMembers(dapi, rest, argv, output);
71
74
  if (entity === 'groups' && verb === 'list-members') return handleGroupListMembers(dapi, rest, argv, output);
72
75
  if (entity === 'groups' && verb === 'list-memberships') return handleGroupListMemberships(dapi, rest, argv, output);
76
+ if (entity === 'users' && verb === 'block') return handleUserBlock(dapi, rest, output);
77
+ if (entity === 'users' && verb === 'unblock') return handleUserUnblock(dapi, rest, output);
78
+ if (entity === 'tables' && verb === 'download') return handleTablesDownload(dapi, rest, argv, output);
79
+ if (entity === 'tables' && verb === 'upload') return handleTablesUpload(dapi, rest, output);
73
80
  const source = dapi[entity];
74
81
  if (!source || !ENTITIES.includes(entity)) {
75
82
  (0, _serverOutput.printError)(new Error(`Unknown entity type: '${entity}'. Valid: ${ENTITIES.join(', ')}`));
@@ -110,6 +117,112 @@ async function server(argv) {
110
117
  return false;
111
118
  }
112
119
  }
120
+ async function handleFilesPut(dapi, rest, output) {
121
+ const [localPath, remotePath] = rest;
122
+ if (!localPath || !remotePath) {
123
+ (0, _serverOutput.printError)(new Error('Usage: grok s files put <local-path> <remote-path>\n e.g. grok s files put ./smiles.csv "System:DemoFiles/smiles.csv"'));
124
+ return false;
125
+ }
126
+ if (!fs.existsSync(localPath)) {
127
+ (0, _serverOutput.printError)(new Error(`Local file not found: ${localPath}`));
128
+ return false;
129
+ }
130
+ const result = await dapi.files.put(localPath, remotePath);
131
+ if (output === 'quiet') return true;
132
+ if (output === 'json') (0, _serverOutput.printOutput)(result, output);else console.log(`Uploaded ${result.size} bytes to ${result.path}`);
133
+ return true;
134
+ }
135
+ async function handleUserBlock(dapi, rest, output) {
136
+ if (!rest[0]) {
137
+ (0, _serverOutput.printError)(new Error('Usage: grok s users block <id-or-login>'));
138
+ return false;
139
+ }
140
+ const user = await dapi.users.find(rest[0]);
141
+ await dapi.users.block(user);
142
+ if (output !== 'quiet') console.log(`Blocked ${user?.login ?? rest[0]}`);
143
+ return true;
144
+ }
145
+ async function handleUserUnblock(dapi, rest, output) {
146
+ if (!rest[0]) {
147
+ (0, _serverOutput.printError)(new Error('Usage: grok s users unblock <id-or-login>'));
148
+ return false;
149
+ }
150
+ const user = await dapi.users.find(rest[0]);
151
+ await dapi.users.unblock(user);
152
+ if (output !== 'quiet') console.log(`Unblocked ${user?.login ?? rest[0]}`);
153
+ return true;
154
+ }
155
+ async function handleTablesDownload(dapi, rest, argv, output) {
156
+ const name = rest[0];
157
+ if (!name) {
158
+ (0, _serverOutput.printError)(new Error('Usage: grok s tables download <name-or-id> [-O <file>]'));
159
+ return false;
160
+ }
161
+ const csv = await dapi.tables.download(name);
162
+ const outFile = argv['output-file'] ?? argv.O;
163
+ if (outFile) {
164
+ fs.writeFileSync(outFile, csv);
165
+ if (output !== 'quiet') console.log(`Wrote ${csv.length} bytes to ${outFile}`);
166
+ } else process.stdout.write(csv);
167
+ return true;
168
+ }
169
+ async function handleTablesUpload(dapi, rest, output) {
170
+ const [name, localPath] = rest;
171
+ if (!name || !localPath) {
172
+ (0, _serverOutput.printError)(new Error('Usage: grok s tables upload <name> <file.csv>'));
173
+ return false;
174
+ }
175
+ if (!fs.existsSync(localPath)) {
176
+ (0, _serverOutput.printError)(new Error(`Local file not found: ${localPath}`));
177
+ return false;
178
+ }
179
+ const result = await dapi.tables.upload(name, localPath);
180
+ if (output === 'quiet') console.log(result?.ID ?? result?.id ?? '');else (0, _serverOutput.printOutput)(result, output);
181
+ return true;
182
+ }
183
+
184
+ /**
185
+ * Normalize common type aliases to the server's `source` field. The server uses
186
+ * four discriminators: `function` (standalone), `function-package` (bundled in a
187
+ * plugin package), `script`, `data-query`. `--type function` broadens to both
188
+ * standalone and package funcs since that matches user intent.
189
+ */
190
+ function funcTypeClause(t) {
191
+ const v = t.toLowerCase();
192
+ if (v === 'query' || v === 'data-query' || v === 'dataquery') return 'source="data-query"';
193
+ if (v === 'script') return 'source="script"';
194
+ if (v === 'package' || v === 'package-function' || v === 'packagefunc') return 'source="function-package"';
195
+ if (v === 'function' || v === 'func') return '(source="function" or source="function-package")';
196
+ return `source="${v}"`;
197
+ }
198
+ async function handleFunctionsList(dapi, argv, limit, offset, userFilter, output) {
199
+ const typeArg = argv.type;
200
+ const language = argv.language;
201
+ const pkg = argv.package;
202
+
203
+ // --language only makes sense for scripts; if the user left --type off, imply 'script'.
204
+ const effectiveType = typeArg ?? (language ? 'script' : undefined);
205
+ const clauses = [];
206
+ if (effectiveType) clauses.push(funcTypeClause(effectiveType));
207
+ if (pkg) clauses.push(`package.shortName="${pkg}"`);
208
+ if (userFilter) clauses.push(`(${userFilter})`);
209
+ const composed = clauses.join(' and ');
210
+
211
+ // Pull a generous batch; the server's public functions endpoint ignores limit/page
212
+ // today, so pagination is done client-side below.
213
+ let results = await dapi.functions.filter(composed).by(10000).list();
214
+
215
+ // `language` lives on the Script subclass and isn't queryable via smart filter —
216
+ // post-filter in Node. Same for any other subclass-only attribute we add later.
217
+ if (language) {
218
+ const want = language.toLowerCase();
219
+ results = results.filter(f => (f?.language ?? '').toLowerCase() === want);
220
+ }
221
+ if (offset > 0) results = results.slice(offset);
222
+ if (limit > 0) results = results.slice(0, limit);
223
+ (0, _serverOutput.printOutput)(results, output);
224
+ return true;
225
+ }
113
226
  async function handleRaw(dapi, method, rest, output) {
114
227
  if (!method || !rest[0]) {
115
228
  (0, _serverOutput.printError)(new Error('Usage: grok s raw <METHOD> <path>'));
@@ -120,6 +233,24 @@ async function handleRaw(dapi, method, rest, output) {
120
233
  (0, _serverOutput.printOutput)(result, output);
121
234
  return true;
122
235
  }
236
+ async function handleHealthcheck(dapi, argv, output) {
237
+ const module = argv.module;
238
+ const path = module ? `/api/public/v1/healthcheck?module=${encodeURIComponent(module)}` : '/api/public/v1/healthcheck';
239
+ const result = await dapi.raw('GET', path);
240
+ if (output === 'json') {
241
+ (0, _serverOutput.printOutput)(result, output);
242
+ return true;
243
+ }
244
+ if (output !== 'quiet') {
245
+ console.log(`status: ${result?.status ?? ''}`);
246
+ console.log(`server: ${result?.server ?? ''}`);
247
+ console.log(`version: ${result?.version ?? ''}`);
248
+ console.log(`time: ${result?.time ?? ''}`);
249
+ console.log('');
250
+ }
251
+ (0, _serverOutput.printOutput)(result?.services ?? [], output);
252
+ return true;
253
+ }
123
254
  async function handleDescribe(dapi, entityType, output) {
124
255
  if (!entityType) {
125
256
  (0, _serverOutput.printError)(new Error('Usage: grok s describe <entity-type>'));
@@ -445,7 +576,7 @@ Usage: grok server <entity> <verb> [args] [options]
445
576
  Manage a Datagrok server from the command line.
446
577
 
447
578
  Entities:
448
- users, groups, functions, connections, queries, scripts, packages, reports, files
579
+ users, groups, functions, connections, queries, scripts, packages, reports, files, tables
449
580
 
450
581
  Verbs:
451
582
  list List entities
@@ -454,9 +585,16 @@ Verbs:
454
585
 
455
586
  Special commands:
456
587
  grok s functions run <Name:func(args)> Call a function
457
- grok s files list [path] [-r] List files (recursive with -r)
588
+ grok s functions list [--type <t>] [--language <l>] [--package <p>] [--filter <expr>]
589
+ Type: script|query|function|package
590
+ Language applies to scripts (python, r, julia, nodejs, octave, grok)
591
+ grok s files list <path> [-r] List files (recursive with -r)
592
+ grok s files get <path> Download a file (returns bytes)
593
+ grok s files delete <path> Delete a file
594
+ grok s files put <local> <remote> Upload a local file
458
595
  grok s raw <METHOD> <path> Hit any API endpoint
459
596
  grok s describe <entity-type> Show entity JSON schema
597
+ grok s healthcheck [--module <name>] Check server + per-module health
460
598
  grok s shares add <entity> <group>[,<group>...] [--access View|Edit]
461
599
  Share an entity with one or more groups
462
600
  grok s shares list <entity-id> List who an entity (UUID) is shared with
@@ -471,6 +609,10 @@ Special commands:
471
609
  grok s groups remove-members <group> <m>... Remove members (no-op if not a member)
472
610
  grok s groups list-members <group> [--admin] List members (optionally filter by admin)
473
611
  grok s groups list-memberships <group> [--admin] List parent groups
612
+ grok s users block <id-or-login> Block a user from the platform
613
+ grok s users unblock <id-or-login> Unblock a previously blocked user
614
+ grok s tables upload <name> <file.csv> Upload a CSV as a Datagrok table
615
+ grok s tables download <name-or-id> [-O <file>] Download a table as CSV (stdout by default)
474
616
  grok s batch <entity> <verb> arg1 [arg2 ...] Batch operation (one round-trip)
475
617
  grok s batch <entity> <verb> --json params.json Batch from JSON array
476
618
  grok s batch manifest.json Run a workflow manifest
@@ -483,6 +625,10 @@ Options:
483
625
  --offset <n> Start offset (default: 0)
484
626
  -r, --recursive Recursive (for files list)
485
627
  --json <file> Read function parameters or batch params from JSON file
628
+ -O, --output-file Write table download to a file instead of stdout
629
+ --type <t> Function discriminator: script | query | function | package
630
+ --language <lang> Script language: python, r, julia, nodejs, octave, grok
631
+ --package <name> Restrict to functions belonging to a package (by short name)
486
632
 
487
633
  Batch manifest options (in manifest.json):
488
634
  stopOnError Stop on first failure (default: true)
@@ -504,7 +650,11 @@ Examples:
504
650
  grok s connections test --json conn.json
505
651
  grok s functions run 'Chem:smilesToMw("ccc")'
506
652
  grok s functions run Chem:test --json params.json
653
+ grok s functions list --type script --language python --limit 20
654
+ grok s functions list --package Chem --type query
655
+ grok s functions list --package PowerPack --type package --filter 'name contains "grid"'
507
656
  grok s files list "System:AppData" -r
657
+ grok s files put ./smiles.csv "System:DemoFiles/smiles.csv"
508
658
  grok s raw GET /api/users/current
509
659
  grok s describe connections
510
660
  grok s users list --host dev
@@ -513,8 +663,8 @@ Examples:
513
663
  grok s groups remove-members Admins alice
514
664
  grok s groups list-members Admins --admin
515
665
  grok s groups list-memberships alice
516
- grok s batch files delete "System:AppData/old.txt" "System:AppData/tmp.txt"
666
+ grok s batch files delete "System:AppData/old.txt" "System:DemoFiles/tmp.txt"
517
667
  grok s batch users create --json users.json
518
668
  grok s batch manifest.json
519
- grok s batch files delete "System:AppData/a" "System:AppData/b" --output json
669
+ grok s batch files delete "System:AppData/a" "System:DemoFiles/b" --output json
520
670
  `;
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.NodeUsersDataSource = exports.NodeSharesDataSource = exports.NodeHttpDataSource = exports.NodeGroupsDataSource = exports.NodeFuncsDataSource = exports.NodeFilesDataSource = exports.NodeDapi = exports.NodeConnectionsDataSource = exports.NodeApiClient = void 0;
6
+ exports.NodeUsersDataSource = exports.NodeTablesDataSource = exports.NodeSharesDataSource = exports.NodeHttpDataSource = exports.NodeGroupsDataSource = exports.NodeFuncsDataSource = exports.NodeFilesDataSource = exports.NodeDapi = exports.NodeConnectionsDataSource = exports.NodeApiClient = void 0;
7
7
  exports.ensureBodyId = ensureBodyId;
8
8
  var _crypto = require("crypto");
9
9
  /// Docs: [Grok Dapi](/docs/plans/grok-dapi/)
@@ -72,6 +72,45 @@ class NodeApiClient {
72
72
  del(path) {
73
73
  return this.request('DELETE', path);
74
74
  }
75
+
76
+ /**
77
+ * POST raw bytes — used for file/table uploads where the body must be the content
78
+ * itself, not JSON. Defaults to `application/octet-stream`; pass `text/csv` (or
79
+ * similar) when the server demands a specific content type.
80
+ */
81
+ async putBytes(path, bytes, contentType = 'application/octet-stream') {
82
+ const res = await fetch(`${this.baseUrl}${path}`, {
83
+ method: 'POST',
84
+ headers: {
85
+ 'Authorization': this.token,
86
+ 'Content-Type': contentType
87
+ },
88
+ body: bytes
89
+ });
90
+ if (!res.ok) {
91
+ const rawText = await res.text();
92
+ let errBody;
93
+ try {
94
+ errBody = JSON.parse(rawText);
95
+ } catch {
96
+ errBody = {
97
+ error: rawText || `HTTP ${res.status}`
98
+ };
99
+ }
100
+ const err = {
101
+ error: errBody?.message ?? errBody?.error ?? `HTTP ${res.status}`,
102
+ source: errBody?.source ?? 'Server',
103
+ errorCode: errBody?.errorCode ?? res.status,
104
+ stackTrace: errBody?.stackTrace
105
+ };
106
+ throw Object.assign(new Error(err.error), {
107
+ apiError: err
108
+ });
109
+ }
110
+ const ct = res.headers.get('content-type') ?? '';
111
+ if (ct.includes('application/json')) return res.json();
112
+ return res.text();
113
+ }
75
114
  }
76
115
  exports.NodeApiClient = NodeApiClient;
77
116
  function buildQuery(params) {
@@ -162,7 +201,10 @@ class NodeGroupsDataSource extends NodeHttpDataSource {
162
201
  return candidates[0];
163
202
  }
164
203
  async addMembers(group, members, isAdmin = false, personalOnly = false) {
165
- const parent = await this.resolve(group);
204
+ // Always fetch via find() so parent.children comes back expanded; lookup() returns a
205
+ // pruned projection and replacing that empty list on save would drop existing members.
206
+ const resolved = await this.resolve(group);
207
+ const parent = await this.find(resolved.id);
166
208
  const children = Array.isArray(parent.children) ? parent.children : [];
167
209
  const results = [];
168
210
  let mutated = false;
@@ -182,7 +224,9 @@ class NodeGroupsDataSource extends NodeHttpDataSource {
182
224
  }
183
225
  const existing = children.find(r => r?.child?.id === child.id);
184
226
  if (existing) {
185
- if (existing.isAdmin === isAdmin) {
227
+ // Server returns isAdmin as null/undefined for non-admin relations; normalize
228
+ // the comparison so re-runs report `noop` instead of `updated`.
229
+ if ((existing.isAdmin ?? false) === isAdmin) {
186
230
  results.push({
187
231
  member: m,
188
232
  status: 'noop'
@@ -196,7 +240,9 @@ class NodeGroupsDataSource extends NodeHttpDataSource {
196
240
  });
197
241
  }
198
242
  } else {
243
+ // Each GroupRelation row needs a non-null id; the server rejects the save otherwise.
199
244
  children.push({
245
+ id: (0, _crypto.randomUUID)(),
200
246
  parent: {
201
247
  id: parent.id
202
248
  },
@@ -219,7 +265,8 @@ class NodeGroupsDataSource extends NodeHttpDataSource {
219
265
  return results;
220
266
  }
221
267
  async removeMembers(group, members, personalOnly = false) {
222
- const parent = await this.resolve(group);
268
+ const resolved = await this.resolve(group);
269
+ const parent = await this.find(resolved.id);
223
270
  const results = [];
224
271
  const children = Array.isArray(parent.children) ? parent.children : [];
225
272
  let mutated = false;
@@ -301,6 +348,12 @@ class NodeUsersDataSource extends NodeHttpDataSource {
301
348
  async save(user) {
302
349
  return this.client.post('/public/v1/users', ensureBodyId(user));
303
350
  }
351
+ async block(user) {
352
+ await this.client.post('/public/v1/users/block', user);
353
+ }
354
+ async unblock(user) {
355
+ await this.client.post('/public/v1/users/unblock', user);
356
+ }
304
357
  }
305
358
  exports.NodeUsersDataSource = NodeUsersDataSource;
306
359
  class NodeConnectionsDataSource extends NodeHttpDataSource {
@@ -351,14 +404,25 @@ class NodeFilesDataSource {
351
404
  constructor(client) {
352
405
  this.client = client;
353
406
  }
407
+
408
+ /**
409
+ * Split a user-facing file path into {connector, path}.
410
+ *
411
+ * Input format: `<connector>/<file-path>` where `<connector>` is the connection's
412
+ * full name — including namespace — e.g. `System:DemoFiles/smiles_1M.csv`.
413
+ * The connector can contain colons (the namespace separator); the file path
414
+ * starts after the first `/`. Colons in the connector segment are converted
415
+ * to `.` so it forms a single URL path segment (the Dart server reverses
416
+ * this with `replaceAll('.', ':')`).
417
+ */
354
418
  splitPath(filePath) {
355
- const colonIdx = filePath.indexOf(':');
356
- if (colonIdx === -1) return {
357
- connector: filePath.replace(':', '.'),
419
+ const slashIdx = filePath.indexOf('/');
420
+ if (slashIdx === -1) return {
421
+ connector: filePath.replace(/:/g, '.'),
358
422
  path: ''
359
423
  };
360
- const connector = filePath.slice(0, colonIdx).replace(':', '.');
361
- const path = filePath.slice(colonIdx + 1).replace(/^\//, '');
424
+ const connector = filePath.slice(0, slashIdx).replace(/:/g, '.');
425
+ const path = filePath.slice(slashIdx + 1);
362
426
  return {
363
427
  connector,
364
428
  path
@@ -380,17 +444,73 @@ class NodeFilesDataSource {
380
444
  connector,
381
445
  path
382
446
  } = this.splitPath(filePath);
383
- return this.client.get(`/public/v1/files/${connector}/${path}`);
447
+ const seg = path ? `${connector}/${path}` : connector;
448
+ return this.client.get(`/public/v1/files/${seg}`);
384
449
  }
385
450
  async delete(filePath) {
386
451
  const {
387
452
  connector,
388
453
  path
389
454
  } = this.splitPath(filePath);
390
- await this.client.del(`/public/v1/files/${connector}/${path}`);
455
+ const seg = path ? `${connector}/${path}` : connector;
456
+ await this.client.del(`/public/v1/files/${seg}`);
457
+ }
458
+
459
+ /**
460
+ * Upload a local file to a Datagrok file share.
461
+ * Streams raw bytes to POST `/public/v1/files/<connector>/<path>` — no base64,
462
+ * no JSON wrapping — so it handles large files without blowing up memory.
463
+ */
464
+ async put(localPath, remotePath) {
465
+ const fs = require('fs');
466
+ const {
467
+ connector,
468
+ path
469
+ } = this.splitPath(remotePath);
470
+ if (!path) throw new Error(`Remote path must include a file name after the connector: got '${remotePath}'`);
471
+ const bytes = fs.readFileSync(localPath);
472
+ const res = await this.client.putBytes(`/public/v1/files/${connector}/${path}`, bytes);
473
+ return {
474
+ path: remotePath,
475
+ size: bytes.length,
476
+ response: res
477
+ };
391
478
  }
392
479
  }
393
480
  exports.NodeFilesDataSource = NodeFilesDataSource;
481
+ class NodeTablesDataSource {
482
+ constructor(client) {
483
+ this.client = client;
484
+ }
485
+
486
+ /** GET /public/v1/tables/{name} — returns CSV text. Name accepts UUID or `namespace:name`. */
487
+ async download(name) {
488
+ const seg = encodeURIComponent(name.replace(/:/g, '.'));
489
+ const result = await this.client.get(`/public/v1/tables/${seg}`);
490
+ // Datagrok returns HTTP 200 + ApiError JSON when the table isn't found.
491
+ const parsed = typeof result === 'string' ? tryParseJson(result) : result;
492
+ if (parsed && typeof parsed === 'object' && parsed['#type'] === 'ApiError') {
493
+ const err = {
494
+ error: parsed.message ?? 'Table download failed',
495
+ errorCode: parsed.errorCode,
496
+ stackTrace: parsed.stackTrace
497
+ };
498
+ throw Object.assign(new Error(err.error), {
499
+ apiError: err
500
+ });
501
+ }
502
+ return result;
503
+ }
504
+
505
+ /** POST /public/v1/tables/{name} with raw CSV bytes. Returns `{ID, Grok name, Markup, URL}`. */
506
+ async upload(name, localPath) {
507
+ const fs = require('fs');
508
+ const bytes = fs.readFileSync(localPath);
509
+ const seg = encodeURIComponent(name.replace(/:/g, '.'));
510
+ return this.client.putBytes(`/public/v1/tables/${seg}`, bytes, 'text/csv');
511
+ }
512
+ }
513
+ exports.NodeTablesDataSource = NodeTablesDataSource;
394
514
  class NodeDapi {
395
515
  constructor(client) {
396
516
  this.client = client;
@@ -425,6 +545,9 @@ class NodeDapi {
425
545
  get shares() {
426
546
  return new NodeSharesDataSource(this.client);
427
547
  }
548
+ get tables() {
549
+ return new NodeTablesDataSource(this.client);
550
+ }
428
551
  async raw(method, path, body) {
429
552
  // Raw paths are relative to server root (e.g. /api/users/current).
430
553
  // Strip the trailing /api from baseUrl to avoid double prefix.
@@ -4,7 +4,9 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
4
4
  Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
- exports.cacheValues = exports.absUrlRegex = exports.TemplateBuilder = void 0;
7
+ exports.absUrlRegex = exports.TemplateBuilder = void 0;
8
+ exports.buildJsDoc = buildJsDoc;
9
+ exports.cacheValues = void 0;
8
10
  exports.camelCaseToKebab = camelCaseToKebab;
9
11
  exports.checkScriptLocation = checkScriptLocation;
10
12
  exports.commentMap = void 0;
@@ -58,6 +60,26 @@ function descriptionToComment(s) {
58
60
  if (s.length === 0) return '';
59
61
  return '/**\n' + s + '\n*/\n';
60
62
  }
63
+ const JSDOC_META_KEYS = ['choices', 'suggestions', 'semType'];
64
+ function buildJsDoc(opts) {
65
+ const {
66
+ description,
67
+ inputs
68
+ } = opts;
69
+ const lines = [];
70
+ if (description) lines.push(` * ${description}`);
71
+ for (const {
72
+ name,
73
+ type,
74
+ options = {}
75
+ } of inputs) {
76
+ const metaLines = JSDOC_META_KEYS.filter(k => options[k]).map(k => ` * ${k}: ${options[k]}`);
77
+ if (!options.description && !metaLines.length) continue;
78
+ const desc = options.description ? ` - ${options.description}` : '';
79
+ lines.push(` * @param {${type}} ${name}${desc}`, ...metaLines);
80
+ }
81
+ return lines.length ? `/**\n${lines.join('\n')}\n */\n` : '';
82
+ }
61
83
  function spaceToCamelCase(s, firstUpper = true) {
62
84
  s = s.replace(/\s+./g, x => x[x.length - 1].toUpperCase());
63
85
  return (firstUpper ? s[0].toUpperCase() : s[0].toLowerCase()) + s.slice(1);
@@ -107,6 +129,7 @@ const replacers = exports.replacers = {
107
129
  PACKAGE_DETECTORS_NAME: (s, name) => s.replace(/#{PACKAGE_DETECTORS_NAME}/g, kebabToCamelCase(name)),
108
130
  PACKAGE_NAMESPACE: (s, name) => s.replace(/#{PACKAGE_NAMESPACE}/g, name),
109
131
  FUNC_DESCRIPTION: (s, desc) => s.replace(/#{FUNC_DESCRIPTION}/g, descriptionToComment(desc)),
132
+ FUNC_JSDOC: (s, opts) => s.replace(/#{FUNC_JSDOC}/g, buildJsDoc(opts)),
110
133
  FUNC_NAME: (s, name) => s.replace(/#{FUNC_NAME}/g, friendlyNameToName(name)),
111
134
  FUNC_NAME_LOWERCASE: (s, name) => s.replace(/#{FUNC_NAME_LOWERCASE}/g, friendlyNameToName(name, false)),
112
135
  PARAMS_OBJECT: (s, params) => s.replace(/#{PARAMS_OBJECT}/g, params.length ? `{ ${params.map(p => p.name).join(', ')} }` : `{}`),
@@ -219,7 +242,7 @@ function getScriptOutputType(script, comment = '#') {
219
242
  }
220
243
  ;
221
244
  function getScriptInputs(script, comment = '#') {
222
- const regex = new RegExp(`${comment}\\s*input:\\s?([a-z_]+)(?:<[^>]*>)?\\s+(\\w+)(?:[^{\\n]*{[^}\\n]*})?`, 'g');
245
+ const regex = new RegExp(`${comment}\\s*input:\\s?([a-z_]+)(?:<[^>]*>)?\\s+(\\w+)(?:[^{\\n]*\\{([^}\\n]*)\\})?`, 'g');
223
246
  const testOptional = inputAnnotation => /isOptional\s*:\s*true/.test(inputAnnotation) || /optional\s*:\s*true/.test(inputAnnotation);
224
247
  const inputAnnotations = [...script.matchAll(regex)];
225
248
  let firstTsValidOptionalIdx = inputAnnotations.length - 1;
@@ -235,12 +258,19 @@ function getScriptInputs(script, comment = '#') {
235
258
  const nullable = /nullable\s*:\s*true/.test(match[0]);
236
259
  const type = dgToTsTypeMap[match[1]] || 'any';
237
260
  const name = match[2];
261
+ const options = {};
262
+ for (const part of (match[3] ?? '').split(';')) {
263
+ const i = part.indexOf(':');
264
+ if (i < 0) continue;
265
+ options[part.slice(0, i).trim()] = part.slice(i + 1).trim();
266
+ }
238
267
  inputs.push({
239
268
  type,
240
269
  name,
241
270
  isOptional,
242
271
  undefinable,
243
- nullable
272
+ nullable,
273
+ options
244
274
  });
245
275
  }
246
276
  return inputs;
@@ -254,10 +284,10 @@ function getScriptDescription(script, comment = '#') {
254
284
  }
255
285
  ;
256
286
  const dgImports = exports.dgImports = `import * as grok from 'datagrok-api/grok';\nimport * as DG from 'datagrok-api/dg';\n`;
257
- const scriptWrapperTemplate = exports.scriptWrapperTemplate = `#{FUNC_DESCRIPTION}export async function #{FUNC_NAME_LOWERCASE}(#{TYPED_PARAMS}): Promise<#{OUTPUT_TYPE}> {
287
+ const scriptWrapperTemplate = exports.scriptWrapperTemplate = `#{FUNC_JSDOC}export async function #{FUNC_NAME_LOWERCASE}(#{TYPED_PARAMS}): Promise<#{OUTPUT_TYPE}> {
258
288
  return await grok.functions.call('#{PACKAGE_NAMESPACE}:#{FUNC_NAME}', #{PARAMS_OBJECT});
259
289
  }`;
260
- const queryWrapperTemplate = exports.queryWrapperTemplate = `#{FUNC_DESCRIPTION}export async function #{FUNC_NAME_LOWERCASE}(#{TYPED_PARAMS}): Promise<#{OUTPUT_TYPE}> {
290
+ const queryWrapperTemplate = exports.queryWrapperTemplate = `#{FUNC_JSDOC}export async function #{FUNC_NAME_LOWERCASE}(#{TYPED_PARAMS}): Promise<#{OUTPUT_TYPE}> {
261
291
  return await grok.data.query('#{PACKAGE_NAMESPACE}:#{FUNC_NAME}', #{PARAMS_OBJECT});
262
292
  }`;
263
293
  const namespaceTemplate = exports.namespaceTemplate = `export namespace #{PACKAGE_NAMESPACE} {\n#{NAME}\n}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datagrok-tools",
3
- "version": "6.1.10",
3
+ "version": "6.1.12",
4
4
  "description": "Utility to upload and publish packages to Datagrok",
5
5
  "homepage": "https://github.com/datagrok-ai/public/tree/master/tools#readme",
6
6
  "dependencies": {
@@ -9,6 +9,7 @@
9
9
  "@babel/traverse": "^7.23.7",
10
10
  "@typescript-eslint/typescript-estree": "^8.31.1",
11
11
  "@typescript-eslint/visitor-keys": "^8.31.1",
12
+ "adm-zip": "^0.5.17",
12
13
  "archiver": "^4.0.2",
13
14
  "archiver-promise": "^1.0.0",
14
15
  "datagrok-api": "^1.26.0",
@@ -70,6 +71,7 @@
70
71
  "@babel/preset-env": "^7.23.8",
71
72
  "@babel/preset-typescript": "7.15.0",
72
73
  "@datagrok-misc/eslint-plugin-config": "^1.0.0",
74
+ "@types/adm-zip": "^0.5.8",
73
75
  "@types/ignore-walk": "^4.0.3",
74
76
  "@types/inquirer": "^8.2.10",
75
77
  "@types/js-yaml": "^4.0.9",