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.
- package/CHANGELOG.md +15 -0
- package/CLAUDE.md +68 -0
- package/GROK_S.md +361 -0
- package/bin/__tests__/build.test.js +116 -0
- package/bin/__tests__/build.test.ts +101 -0
- package/bin/__tests__/node-dapi.connections.test.js +120 -0
- package/bin/__tests__/node-dapi.connections.test.ts +84 -0
- package/bin/__tests__/node-dapi.groups.test.js +467 -0
- package/bin/__tests__/node-dapi.groups.test.ts +298 -0
- package/bin/__tests__/node-dapi.integration.test.js +406 -0
- package/bin/__tests__/node-dapi.integration.test.ts +447 -0
- package/bin/__tests__/node-dapi.shares.test.js +107 -0
- package/bin/__tests__/node-dapi.shares.test.ts +70 -0
- package/bin/__tests__/node-dapi.users.test.js +86 -0
- package/bin/__tests__/node-dapi.users.test.ts +58 -0
- package/bin/__tests__/server-output.test.js +171 -0
- package/bin/__tests__/server-output.test.ts +133 -0
- package/bin/__tests__/server.test.js +277 -0
- package/bin/__tests__/server.test.ts +197 -0
- package/bin/commands/api.js +13 -3
- package/bin/commands/build.js +1 -1
- package/bin/commands/create.js +8 -5
- package/bin/commands/help.js +80 -4
- package/bin/commands/report.js +231 -36
- package/bin/commands/server.js +670 -0
- package/bin/grok.js +3 -1
- package/bin/utils/node-dapi.js +582 -0
- package/bin/utils/server-client.js +15 -0
- package/bin/utils/server-output.js +127 -0
- package/bin/utils/utils.js +35 -5
- package/package-template/package.json +1 -1
- package/package.json +10 -3
- package/vitest.config.ts +25 -0
|
@@ -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'
|
|
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'];
|