datagrok-tools 6.1.9 → 6.1.11

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.
@@ -0,0 +1,670 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.buildInlineManifest = buildInlineManifest;
7
+ exports.parseFuncCall = parseFuncCall;
8
+ exports.resolveManifestSources = resolveManifestSources;
9
+ exports.server = server;
10
+ var fs = _interopRequireWildcard(require("fs"));
11
+ var _nodeDapi = require("../utils/node-dapi");
12
+ var _serverClient = require("../utils/server-client");
13
+ var _serverOutput = require("../utils/server-output");
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
+ /// Docs: [Grok Dapi](/docs/plans/grok-dapi/)
16
+
17
+ const ENTITIES = ['users', 'groups', 'functions', 'connections', 'queries', 'scripts', 'packages', 'reports', 'files', 'tables'];
18
+ const VERBS = ['list', 'get', 'delete'];
19
+ async function server(argv) {
20
+ const args = argv['_'].slice(1);
21
+ const entity = args[0];
22
+ const verb = args[1];
23
+ const rest = args.slice(2);
24
+ const output = argv.output ?? argv.o ?? 'table';
25
+ const limit = Number(argv.limit ?? argv.l ?? 50);
26
+ const offset = Number(argv.offset ?? 0);
27
+ const filter = argv.filter ?? argv.f ?? '';
28
+ const host = argv.host;
29
+ const recursive = !!(argv.r ?? argv.recursive);
30
+ if (!entity || argv.help) {
31
+ console.log(HELP_SERVER);
32
+ return true;
33
+ }
34
+ let client;
35
+ try {
36
+ client = await (0, _serverClient.createClient)(host);
37
+ } catch (err) {
38
+ (0, _serverOutput.printError)(err);
39
+ return false;
40
+ }
41
+ const dapi = new _nodeDapi.NodeDapi(client);
42
+ try {
43
+ if (entity === 'batch') return handleBatch(dapi, argv, verb, rest, output);
44
+ if (entity === 'raw') return handleRaw(dapi, verb, rest, output);
45
+ if (entity === 'describe') return handleDescribe(dapi, verb ?? rest[0], output);
46
+ if (entity === 'healthcheck') return handleHealthcheck(dapi, argv, output);
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);
49
+ if (entity === 'files' && verb === 'list') {
50
+ const path = rest[0] ?? '';
51
+ const result = await dapi.files.list(path, recursive);
52
+ (0, _serverOutput.printOutput)(result, output);
53
+ return true;
54
+ }
55
+ if (entity === 'files' && verb === 'get') {
56
+ const result = await dapi.files.get(rest[0]);
57
+ (0, _serverOutput.printOutput)(result, output);
58
+ return true;
59
+ }
60
+ if (entity === 'files' && verb === 'delete') {
61
+ await dapi.files.delete(rest[0]);
62
+ if (output !== 'quiet') console.log(`Deleted ${rest[0]}`);
63
+ return true;
64
+ }
65
+ if (entity === 'files' && verb === 'put') return handleFilesPut(dapi, rest, output);
66
+ if (entity === 'shares' && verb === 'add') return handleSharesAdd(dapi, rest, argv, output);
67
+ if (entity === 'shares' && verb === 'list') return handleSharesList(dapi, rest, output);
68
+ if (entity === 'users' && verb === 'save') return handleUserSave(dapi, argv, output);
69
+ if (entity === 'groups' && verb === 'save') return handleGroupSave(dapi, argv, output);
70
+ if (entity === 'connections' && verb === 'save') return handleConnSave(dapi, argv, output);
71
+ if (entity === 'connections' && verb === 'test') return handleConnTest(dapi, rest, argv, output);
72
+ if (entity === 'groups' && verb === 'add-members') return handleGroupAddMembers(dapi, rest, argv, output);
73
+ if (entity === 'groups' && verb === 'remove-members') return handleGroupRemoveMembers(dapi, rest, argv, output);
74
+ if (entity === 'groups' && verb === 'list-members') return handleGroupListMembers(dapi, rest, argv, output);
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);
80
+ const source = dapi[entity];
81
+ if (!source || !ENTITIES.includes(entity)) {
82
+ (0, _serverOutput.printError)(new Error(`Unknown entity type: '${entity}'. Valid: ${ENTITIES.join(', ')}`));
83
+ return false;
84
+ }
85
+ if (!verb) {
86
+ console.log(`Usage: grok s ${entity} <${VERBS.join('|')}> [args]`);
87
+ return true;
88
+ }
89
+ if (verb === 'list') {
90
+ const page = Math.floor(offset / limit);
91
+ const results = await source.filter(filter).by(limit).page(page).list();
92
+ (0, _serverOutput.printOutput)(results, output);
93
+ return true;
94
+ }
95
+ if (verb === 'get') {
96
+ if (!rest[0]) {
97
+ (0, _serverOutput.printError)(new Error('Usage: grok s <entity> get <id>'));
98
+ return false;
99
+ }
100
+ const result = await source.find(rest[0]);
101
+ (0, _serverOutput.printOutput)(result, output);
102
+ return true;
103
+ }
104
+ if (verb === 'delete') {
105
+ if (!rest[0]) {
106
+ (0, _serverOutput.printError)(new Error('Usage: grok s <entity> delete <id>'));
107
+ return false;
108
+ }
109
+ await source.delete(rest[0]);
110
+ if (output !== 'quiet') console.log(`Deleted ${entity}/${rest[0]}`);
111
+ return true;
112
+ }
113
+ (0, _serverOutput.printError)(new Error(`Unknown verb: '${verb}'. Valid: ${VERBS.join(', ')}`));
114
+ return false;
115
+ } catch (err) {
116
+ (0, _serverOutput.printError)(err);
117
+ return false;
118
+ }
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
+ }
226
+ async function handleRaw(dapi, method, rest, output) {
227
+ if (!method || !rest[0]) {
228
+ (0, _serverOutput.printError)(new Error('Usage: grok s raw <METHOD> <path>'));
229
+ return false;
230
+ }
231
+ const path = rest[0];
232
+ const result = await dapi.raw(method, path);
233
+ (0, _serverOutput.printOutput)(result, output);
234
+ return true;
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
+ }
254
+ async function handleDescribe(dapi, entityType, output) {
255
+ if (!entityType) {
256
+ (0, _serverOutput.printError)(new Error('Usage: grok s describe <entity-type>'));
257
+ return false;
258
+ }
259
+ const result = await dapi.describe(entityType);
260
+ (0, _serverOutput.printOutput)(result, output);
261
+ return true;
262
+ }
263
+ async function handleFuncRun(dapi, rest, argv, output) {
264
+ let funcName = rest[0];
265
+ if (!funcName) {
266
+ (0, _serverOutput.printError)(new Error('Usage: grok s functions run <Name:function(args...)> [--json params.json]'));
267
+ return false;
268
+ }
269
+ let params = {};
270
+ if (argv.json) {
271
+ const fs = require('fs');
272
+ try {
273
+ params = JSON.parse(fs.readFileSync(argv.json, 'utf8'));
274
+ } catch (err) {
275
+ (0, _serverOutput.printError)(new Error(`Cannot read params file: ${err.message}`));
276
+ return false;
277
+ }
278
+ } else {
279
+ const parsed = parseFuncCall(funcName);
280
+ funcName = parsed.name;
281
+ params = parsed.params;
282
+ }
283
+ const result = await dapi.functions.run(funcName, params);
284
+ (0, _serverOutput.printOutput)(result, output);
285
+ return true;
286
+ }
287
+ function readJsonFile(jsonPath) {
288
+ return JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
289
+ }
290
+ function readJsonBody(argv, entity) {
291
+ if (!argv.json) throw new Error(`Usage: grok s ${entity} save --json ${entity}.json`);
292
+ try {
293
+ return readJsonFile(argv.json);
294
+ } catch (err) {
295
+ throw new Error(`Cannot read ${entity} file '${argv.json}': ${err.message}`);
296
+ }
297
+ }
298
+ async function handleSharesAdd(dapi, rest, argv, output) {
299
+ const [entity, ...groupArgs] = rest;
300
+ if (!entity || !groupArgs.length) {
301
+ (0, _serverOutput.printError)(new Error('Usage: grok s shares add <entity-id-or-name> <group>[,<group>...] [--access View|Edit]'));
302
+ return false;
303
+ }
304
+ const groups = groupArgs.flatMap(g => g.split(',')).map(g => g.trim()).filter(Boolean).join(',');
305
+ const access = typeof argv.access === 'string' ? argv.access : 'View';
306
+ if (access !== 'View' && access !== 'Edit') {
307
+ (0, _serverOutput.printError)(new Error(`Invalid --access '${access}'. Use 'View' or 'Edit'.`));
308
+ return false;
309
+ }
310
+ const result = await dapi.shares.share(entity, groups, access);
311
+ (0, _serverOutput.printOutput)(result, output);
312
+ if (result?.status === 'failed') process.exitCode = 1;
313
+ return true;
314
+ }
315
+ async function handleSharesList(dapi, rest, output) {
316
+ const entityId = rest[0];
317
+ if (!entityId) {
318
+ (0, _serverOutput.printError)(new Error('Usage: grok s shares list <entity-id> (entity id must be a UUID)'));
319
+ return false;
320
+ }
321
+ const perms = await dapi.shares.list(entityId);
322
+ const flat = (Array.isArray(perms) ? perms : []).map(p => ({
323
+ group: p?.userGroup?.friendlyName ?? p?.userGroup?.name ?? p?.userGroup?.id ?? '',
324
+ groupId: p?.userGroup?.id ?? '',
325
+ access: p?.permission?.name ?? p?.permission?.friendlyName ?? '',
326
+ personal: p?.userGroup?.personal ?? false
327
+ }));
328
+ (0, _serverOutput.printOutput)(flat, output);
329
+ return true;
330
+ }
331
+ async function handleUserSave(dapi, argv, output) {
332
+ let body;
333
+ try {
334
+ body = readJsonBody(argv, 'users');
335
+ } catch (err) {
336
+ (0, _serverOutput.printError)(err);
337
+ return false;
338
+ }
339
+ const result = await dapi.users.save(body);
340
+ (0, _serverOutput.printOutput)(result, output);
341
+ return true;
342
+ }
343
+ async function handleGroupSave(dapi, argv, output) {
344
+ let body;
345
+ try {
346
+ body = readJsonBody(argv, 'groups');
347
+ } catch (err) {
348
+ (0, _serverOutput.printError)(err);
349
+ return false;
350
+ }
351
+ const saveRelations = argv['save-relations'] === true || argv.saveRelations === true;
352
+ const result = await dapi.groups.save(body, saveRelations);
353
+ (0, _serverOutput.printOutput)(result, output);
354
+ return true;
355
+ }
356
+ async function handleConnSave(dapi, argv, output) {
357
+ if (!argv.json) {
358
+ (0, _serverOutput.printError)(new Error('Usage: grok s connections save --json conn.json [--save-credentials]'));
359
+ return false;
360
+ }
361
+ let body;
362
+ try {
363
+ body = readJsonFile(argv.json);
364
+ } catch (err) {
365
+ (0, _serverOutput.printError)(new Error(`Cannot read connection file '${argv.json}': ${err.message}`));
366
+ return false;
367
+ }
368
+ const saveCredentials = argv['save-credentials'] === true || argv.saveCredentials === true;
369
+ const result = await dapi.connections.save(body, saveCredentials);
370
+ (0, _serverOutput.printOutput)(result, output);
371
+ return true;
372
+ }
373
+ async function handleConnTest(dapi, rest, argv, output) {
374
+ let body;
375
+ if (argv.json) {
376
+ try {
377
+ body = readJsonFile(argv.json);
378
+ } catch (err) {
379
+ (0, _serverOutput.printError)(new Error(`Cannot read connection file '${argv.json}': ${err.message}`));
380
+ return false;
381
+ }
382
+ } else if (rest[0]) {
383
+ body = await dapi.connections.find(rest[0]);
384
+ } else {
385
+ (0, _serverOutput.printError)(new Error('Usage: grok s connections test <id-or-name> | --json conn.json'));
386
+ return false;
387
+ }
388
+ await dapi.connections.test(body);
389
+ if (output !== 'quiet') console.log('ok');
390
+ return true;
391
+ }
392
+ async function handleGroupAddMembers(dapi, rest, argv, output) {
393
+ const [group, ...members] = rest;
394
+ if (!group || !members.length) {
395
+ (0, _serverOutput.printError)(new Error('Usage: grok s groups add-members <group> <member> [<member> ...] [--admin] [--user]'));
396
+ return false;
397
+ }
398
+ const isAdmin = argv.admin === true;
399
+ const personalOnly = argv.user === true;
400
+ const results = await dapi.groups.addMembers(group, members, isAdmin, personalOnly);
401
+ (0, _serverOutput.printOutput)(results, output);
402
+ const anyError = results.some(r => r.status === 'error');
403
+ if (anyError) process.exitCode = 1;
404
+ return true;
405
+ }
406
+ async function handleGroupRemoveMembers(dapi, rest, argv, output) {
407
+ const [group, ...members] = rest;
408
+ if (!group || !members.length) {
409
+ (0, _serverOutput.printError)(new Error('Usage: grok s groups remove-members <group> <member> [<member> ...] [--user]'));
410
+ return false;
411
+ }
412
+ const personalOnly = argv.user === true;
413
+ const results = await dapi.groups.removeMembers(group, members, personalOnly);
414
+ (0, _serverOutput.printOutput)(results, output);
415
+ const anyError = results.some(r => r.status === 'error');
416
+ if (anyError) process.exitCode = 1;
417
+ return true;
418
+ }
419
+ async function handleGroupListMembers(dapi, rest, argv, output) {
420
+ if (!rest[0]) {
421
+ (0, _serverOutput.printError)(new Error('Usage: grok s groups list-members <group> [--admin | --no-admin]'));
422
+ return false;
423
+ }
424
+ const admin = typeof argv.admin === 'boolean' ? argv.admin : undefined;
425
+ const result = await dapi.groups.getMembers(rest[0], admin);
426
+ (0, _serverOutput.printOutput)(result, output);
427
+ return true;
428
+ }
429
+ async function handleGroupListMemberships(dapi, rest, argv, output) {
430
+ if (!rest[0]) {
431
+ (0, _serverOutput.printError)(new Error('Usage: grok s groups list-memberships <group> [--admin | --no-admin]'));
432
+ return false;
433
+ }
434
+ const admin = typeof argv.admin === 'boolean' ? argv.admin : undefined;
435
+ const result = await dapi.groups.getMemberships(rest[0], admin);
436
+ (0, _serverOutput.printOutput)(result, output);
437
+ return true;
438
+ }
439
+ async function handleBatch(dapi, argv, verb, rest, output) {
440
+ let request;
441
+ if (verb?.endsWith('.json') && rest.length === 0) {
442
+ // grok s batch manifest.json
443
+ let raw;
444
+ try {
445
+ raw = JSON.parse(fs.readFileSync(verb, 'utf8'));
446
+ } catch (err) {
447
+ (0, _serverOutput.printError)(new Error(`Cannot read manifest file '${verb}': ${err.message}`));
448
+ return false;
449
+ }
450
+ request = resolveManifestSources(raw);
451
+ } else if (verb && ENTITIES.includes(verb)) {
452
+ const batchVerb = rest[0];
453
+ if (!batchVerb) {
454
+ (0, _serverOutput.printError)(new Error(`Usage: grok s batch ${verb} <verb> [args...]`));
455
+ return false;
456
+ }
457
+ if (argv.json) {
458
+ // grok s batch users create --json users.json
459
+ let paramsArray;
460
+ try {
461
+ const raw = JSON.parse(fs.readFileSync(argv.json, 'utf8'));
462
+ paramsArray = Array.isArray(raw) ? raw : [raw];
463
+ } catch (err) {
464
+ (0, _serverOutput.printError)(new Error(`Cannot read params file '${argv.json}': ${err.message}`));
465
+ return false;
466
+ }
467
+ request = buildInlineManifest(verb, batchVerb, paramsArray);
468
+ } else {
469
+ // grok s batch files delete path1 path2 ...
470
+ const batchArgs = rest.slice(1);
471
+ if (!batchArgs.length) {
472
+ (0, _serverOutput.printError)(new Error(`Usage: grok s batch ${verb} ${batchVerb} <arg1> [arg2 ...]`));
473
+ return false;
474
+ }
475
+ request = buildInlineManifest(verb, batchVerb, batchArgs);
476
+ }
477
+ } else {
478
+ (0, _serverOutput.printError)(new Error('Usage: grok s batch <entity> <verb> [args]\n grok s batch <entity> <verb> --json params.json\n grok s batch manifest.json'));
479
+ return false;
480
+ }
481
+ const result = await dapi.batch(request);
482
+ (0, _serverOutput.printBatchOutput)(result, output);
483
+ return result.summary.failed === 0 && result.summary.partial === 0;
484
+ }
485
+ function buildInlineManifest(entity, verb, args) {
486
+ const action = `${entity}.${verb}`;
487
+ let operations;
488
+ if (args.length > 0 && typeof args[0] === 'object') {
489
+ // Array of param objects (from --json array)
490
+ operations = args.map((p, i) => ({
491
+ id: `op${i}`,
492
+ action,
493
+ params: p
494
+ }));
495
+ } else {
496
+ // Array of string args — map to appropriate param key
497
+ const paramKey = entity === 'files' ? 'path' : 'id';
498
+ operations = args.map((arg, i) => ({
499
+ id: `op${i}`,
500
+ action,
501
+ params: {
502
+ [paramKey]: arg
503
+ }
504
+ }));
505
+ }
506
+ return {
507
+ operations
508
+ };
509
+ }
510
+ function resolveManifestSources(manifest) {
511
+ if (!manifest.operations) return manifest;
512
+ const operations = manifest.operations.map(op => {
513
+ if (op.action === 'files.put' && op.params?.source) {
514
+ const sourcePath = op.params.source;
515
+ const content = fs.readFileSync(sourcePath).toString('base64');
516
+ const {
517
+ source: _dropped,
518
+ ...rest
519
+ } = op.params;
520
+ return {
521
+ ...op,
522
+ params: {
523
+ ...rest,
524
+ content
525
+ }
526
+ };
527
+ }
528
+ return op;
529
+ });
530
+ return {
531
+ ...manifest,
532
+ operations
533
+ };
534
+ }
535
+ function parseFuncCall(expr) {
536
+ const parenIdx = expr.indexOf('(');
537
+ if (parenIdx === -1) return {
538
+ name: expr,
539
+ params: {}
540
+ };
541
+ const name = expr.slice(0, parenIdx);
542
+ const inner = expr.slice(parenIdx + 1, expr.lastIndexOf(')')).trim();
543
+ if (!inner) return {
544
+ name,
545
+ params: {}
546
+ };
547
+ if (inner.startsWith('{')) {
548
+ try {
549
+ const params = JSON.parse(inner.replace(/([{,]\s*)([a-zA-Z_]\w*)(\s*:)/g, '$1"$2"$3'));
550
+ return {
551
+ name,
552
+ params
553
+ };
554
+ } catch {
555
+ return {
556
+ name,
557
+ params: {}
558
+ };
559
+ }
560
+ }
561
+ const positional = inner.split(',').map(s => {
562
+ s = s.trim();
563
+ if (s.startsWith('"') && s.endsWith('"') || s.startsWith("'") && s.endsWith("'")) return s.slice(1, -1);
564
+ const n = Number(s);
565
+ return isNaN(n) ? s : n;
566
+ });
567
+ return {
568
+ name,
569
+ params: Object.fromEntries(positional.map((v, i) => [String(i), v]))
570
+ };
571
+ }
572
+ const HELP_SERVER = `
573
+ Usage: grok server <entity> <verb> [args] [options]
574
+ grok s <entity> <verb> [args] [options]
575
+
576
+ Manage a Datagrok server from the command line.
577
+
578
+ Entities:
579
+ users, groups, functions, connections, queries, scripts, packages, reports, files, tables
580
+
581
+ Verbs:
582
+ list List entities
583
+ get Get a single entity by ID or name
584
+ delete Delete an entity by ID
585
+
586
+ Special commands:
587
+ grok s functions run <Name:func(args)> Call a function
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
595
+ grok s raw <METHOD> <path> Hit any API endpoint
596
+ grok s describe <entity-type> Show entity JSON schema
597
+ grok s healthcheck [--module <name>] Check server + per-module health
598
+ grok s shares add <entity> <group>[,<group>...] [--access View|Edit]
599
+ Share an entity with one or more groups
600
+ grok s shares list <entity-id> List who an entity (UUID) is shared with
601
+ grok s users save --json user.json Create or update a user from a JSON file
602
+ grok s groups save --json group.json [--save-relations]
603
+ Create or update a group from a JSON file
604
+ grok s connections save --json conn.json [--save-credentials]
605
+ Create or update a connection from a JSON file
606
+ grok s connections test <id-or-name> Test connectivity of an existing connection
607
+ grok s connections test --json conn.json Test connectivity of a connection defined in JSON
608
+ grok s groups add-members <group> <m>... [--admin] Add one or more users/groups as members
609
+ grok s groups remove-members <group> <m>... Remove members (no-op if not a member)
610
+ grok s groups list-members <group> [--admin] List members (optionally filter by admin)
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)
616
+ grok s batch <entity> <verb> arg1 [arg2 ...] Batch operation (one round-trip)
617
+ grok s batch <entity> <verb> --json params.json Batch from JSON array
618
+ grok s batch manifest.json Run a workflow manifest
619
+
620
+ Options:
621
+ --host <alias|url> Server alias from config or full URL
622
+ --output <format> Output format: table (default), json, csv, quiet
623
+ --filter <text> Smart filter expression
624
+ --limit <n> Page size (default: 50)
625
+ --offset <n> Start offset (default: 0)
626
+ -r, --recursive Recursive (for files list)
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)
632
+
633
+ Batch manifest options (in manifest.json):
634
+ stopOnError Stop on first failure (default: true)
635
+ transaction Wrap DB ops in transaction (default: false)
636
+ concurrency Accepted (always treated as 1)
637
+
638
+ Examples:
639
+ grok s users list
640
+ grok s users list --output json --limit 10
641
+ grok s users save --json user.json
642
+ grok s groups save --json group.json --save-relations
643
+ grok s shares add "JohnDoe:MyConnection" Chemists,Admins --access Edit
644
+ grok s shares list <entity-uuid>
645
+ grok s connections list --filter "PostgreSQL"
646
+ grok s connections get <id>
647
+ grok s connections delete <id>
648
+ grok s connections save --json conn.json --save-credentials
649
+ grok s connections test "JohnDoe:MyConnection"
650
+ grok s connections test --json conn.json
651
+ grok s functions run 'Chem:smilesToMw("ccc")'
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"'
656
+ grok s files list "System:AppData" -r
657
+ grok s files put ./smiles.csv "System:DemoFiles/smiles.csv"
658
+ grok s raw GET /api/users/current
659
+ grok s describe connections
660
+ grok s users list --host dev
661
+ grok s users list --host "https://mygrok.com/api"
662
+ grok s groups add-members Admins alice bob --admin
663
+ grok s groups remove-members Admins alice
664
+ grok s groups list-members Admins --admin
665
+ grok s groups list-memberships alice
666
+ grok s batch files delete "System:AppData/old.txt" "System:DemoFiles/tmp.txt"
667
+ grok s batch users create --json users.json
668
+ grok s batch manifest.json
669
+ grok s batch files delete "System:AppData/a" "System:DemoFiles/b" --output json
670
+ `;
package/bin/grok.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  const argv = require('minimist')(process.argv.slice(2), {
3
- alias: {k: 'key', h: 'help', r: 'recursive', s: 'silent'},
3
+ alias: {k: 'key', h: 'help', r: 'recursive'},
4
4
  boolean: ['dartium'],
5
5
  });
6
6
  const help = require('./commands/help').help;
@@ -24,6 +24,8 @@ const commands = {
24
24
  testall: require('./commands/test-all').testAll,
25
25
  stresstest: require('./commands/stress-tests').stressTests,
26
26
  migrate: require('./commands/migrate').migrate,
27
+ server: require('./commands/server').server,
28
+ s: require('./commands/server').server,
27
29
  };
28
30
 
29
31
  const onPackageCommandNames = ['api', 'check', 'link', 'publish', 'test'];