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.
- package/CHANGELOG.md +5 -0
- package/GROK_S.md +361 -0
- package/bin/commands/api.js +13 -3
- package/bin/commands/help.js +20 -4
- package/bin/commands/report.js +375 -35
- package/bin/commands/server.js +155 -5
- package/bin/utils/node-dapi.js +134 -11
- package/bin/utils/utils.js +35 -5
- package/package.json +3 -1
package/bin/commands/server.js
CHANGED
|
@@ -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
|
|
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:
|
|
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:
|
|
669
|
+
grok s batch files delete "System:AppData/a" "System:DemoFiles/b" --output json
|
|
520
670
|
`;
|
package/bin/utils/node-dapi.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
356
|
-
if (
|
|
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,
|
|
361
|
-
const path = filePath.slice(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
package/bin/utils/utils.js
CHANGED
|
@@ -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.
|
|
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]
|
|
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 = `#{
|
|
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 = `#{
|
|
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.
|
|
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",
|