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,582 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.NodeUsersDataSource = exports.NodeTablesDataSource = exports.NodeSharesDataSource = exports.NodeHttpDataSource = exports.NodeGroupsDataSource = exports.NodeFuncsDataSource = exports.NodeFilesDataSource = exports.NodeDapi = exports.NodeConnectionsDataSource = exports.NodeApiClient = void 0;
7
+ exports.ensureBodyId = ensureBodyId;
8
+ var _crypto = require("crypto");
9
+ /// Docs: [Grok Dapi](/docs/plans/grok-dapi/)
10
+
11
+ function ensureBodyId(body) {
12
+ if (body && typeof body === 'object' && !body.id) body.id = (0, _crypto.randomUUID)();
13
+ return body;
14
+ }
15
+ class NodeApiClient {
16
+ constructor(baseUrl, token) {
17
+ this.baseUrl = baseUrl;
18
+ this.token = token;
19
+ }
20
+ static async login(baseUrl, devKey) {
21
+ const res = await fetch(`${baseUrl}/users/login/dev/${devKey}`, {
22
+ method: 'POST'
23
+ });
24
+ const json = await res.json();
25
+ if (!json.token) throw new Error('Login failed. Check your developer key.');
26
+ return new NodeApiClient(baseUrl, json.token);
27
+ }
28
+ async request(method, path, body, headers) {
29
+ const url = `${this.baseUrl}${path}`;
30
+ const opts = {
31
+ method,
32
+ headers: {
33
+ 'Authorization': this.token,
34
+ 'Content-Type': 'application/json',
35
+ ...headers
36
+ }
37
+ };
38
+ if (body !== undefined) opts.body = JSON.stringify(body);
39
+ const res = await fetch(url, opts);
40
+ if (!res.ok) {
41
+ // Read as text first to avoid "Body has already been read" when JSON.parse fails
42
+ const rawText = await res.text();
43
+ let errBody;
44
+ try {
45
+ errBody = JSON.parse(rawText);
46
+ } catch {
47
+ errBody = {
48
+ error: rawText || `HTTP ${res.status}`
49
+ };
50
+ }
51
+ const err = {
52
+ error: errBody?.message ?? errBody?.error ?? `HTTP ${res.status}`,
53
+ source: errBody?.source ?? 'Server',
54
+ errorCode: errBody?.errorCode ?? res.status,
55
+ stackTrace: errBody?.stackTrace
56
+ };
57
+ throw Object.assign(new Error(err.error), {
58
+ apiError: err
59
+ });
60
+ }
61
+ if (res.status === 204 || res.headers.get('content-length') === '0') return null;
62
+ const ct = res.headers.get('content-type') ?? '';
63
+ if (ct.includes('application/json')) return res.json();
64
+ return res.text();
65
+ }
66
+ get(path) {
67
+ return this.request('GET', path);
68
+ }
69
+ post(path, body) {
70
+ return this.request('POST', path, body);
71
+ }
72
+ del(path) {
73
+ return this.request('DELETE', path);
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
+ }
114
+ }
115
+ exports.NodeApiClient = NodeApiClient;
116
+ function buildQuery(params) {
117
+ const entries = Object.entries(params).filter(([, v]) => v !== undefined && v !== null && v !== '');
118
+ if (!entries.length) return '';
119
+ return '?' + entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join('&');
120
+ }
121
+ class NodeHttpDataSource {
122
+ _filter = '';
123
+ _limit = 50;
124
+ _page = 0;
125
+ _order = '';
126
+ constructor(client, path) {
127
+ this.client = client;
128
+ this.path = path;
129
+ }
130
+ filter(w) {
131
+ this._filter = w;
132
+ return this;
133
+ }
134
+ by(n) {
135
+ this._limit = n;
136
+ return this;
137
+ }
138
+ page(n) {
139
+ this._page = n;
140
+ return this;
141
+ }
142
+ order(field, desc = false) {
143
+ this._order = desc ? `-${field}` : field;
144
+ return this;
145
+ }
146
+ async list() {
147
+ const q = buildQuery({
148
+ text: this._filter || undefined,
149
+ limit: this._limit,
150
+ page: this._page || undefined,
151
+ order: this._order || undefined
152
+ });
153
+ return this.client.get(`/public/v1/${this.path}${q}`);
154
+ }
155
+ async find(id) {
156
+ return this.client.get(`/public/v1/${this.path}/${encodeURIComponent(id.replace(':', '.'))}`);
157
+ }
158
+ async count() {
159
+ const q = buildQuery({
160
+ text: this._filter || undefined
161
+ });
162
+ const res = await this.client.get(`/public/v1/${this.path}/count${q}`);
163
+ return typeof res === 'number' ? res : res?.count ?? 0;
164
+ }
165
+ async delete(idOrEntity) {
166
+ const id = typeof idOrEntity === 'string' ? idOrEntity : idOrEntity?.id ?? '';
167
+ await this.client.del(`/public/v1/${this.path}/${encodeURIComponent(id)}`);
168
+ }
169
+ }
170
+ exports.NodeHttpDataSource = NodeHttpDataSource;
171
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
172
+ class NodeGroupsDataSource extends NodeHttpDataSource {
173
+ constructor(client) {
174
+ super(client, 'groups');
175
+ }
176
+ async save(group, saveRelations = false) {
177
+ const q = buildQuery({
178
+ saveRelations: saveRelations ? 'true' : undefined
179
+ });
180
+ return this.client.post(`/public/v1/groups${q}`, ensureBodyId(group));
181
+ }
182
+ async lookup(name) {
183
+ const q = buildQuery({
184
+ query: name
185
+ });
186
+ return this.client.get(`/public/v1/groups/lookup${q}`);
187
+ }
188
+ async resolve(idOrName, opts = {}) {
189
+ if (UUID_RE.test(idOrName)) return this.find(idOrName);
190
+ const matches = await this.lookup(idOrName);
191
+ let candidates = matches;
192
+ if (opts.personalOnly) candidates = matches.filter(g => g?.personal === true);
193
+ if (!candidates.length) {
194
+ const suffix = opts.personalOnly ? ' (personal)' : '';
195
+ throw new Error(`No group matching '${idOrName}'${suffix}`);
196
+ }
197
+ if (candidates.length > 1) {
198
+ const list = candidates.map(g => ` ${g.id} ${g.friendlyName ?? g.name ?? ''}`).join('\n');
199
+ throw new Error(`Multiple groups match '${idOrName}':\n${list}\nUse the ID to disambiguate.`);
200
+ }
201
+ return candidates[0];
202
+ }
203
+ async addMembers(group, members, isAdmin = false, personalOnly = false) {
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);
208
+ const children = Array.isArray(parent.children) ? parent.children : [];
209
+ const results = [];
210
+ let mutated = false;
211
+ for (const m of members) {
212
+ let child;
213
+ try {
214
+ child = await this.resolve(m, {
215
+ personalOnly
216
+ });
217
+ } catch (err) {
218
+ results.push({
219
+ member: m,
220
+ status: 'error',
221
+ error: err?.message ?? String(err)
222
+ });
223
+ continue;
224
+ }
225
+ const existing = children.find(r => r?.child?.id === child.id);
226
+ if (existing) {
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) {
230
+ results.push({
231
+ member: m,
232
+ status: 'noop'
233
+ });
234
+ } else {
235
+ existing.isAdmin = isAdmin;
236
+ mutated = true;
237
+ results.push({
238
+ member: m,
239
+ status: 'updated'
240
+ });
241
+ }
242
+ } else {
243
+ // Each GroupRelation row needs a non-null id; the server rejects the save otherwise.
244
+ children.push({
245
+ id: (0, _crypto.randomUUID)(),
246
+ parent: {
247
+ id: parent.id
248
+ },
249
+ child: {
250
+ id: child.id
251
+ },
252
+ isAdmin
253
+ });
254
+ mutated = true;
255
+ results.push({
256
+ member: m,
257
+ status: 'added'
258
+ });
259
+ }
260
+ }
261
+ if (mutated) {
262
+ parent.children = children;
263
+ await this.save(parent, true);
264
+ }
265
+ return results;
266
+ }
267
+ async removeMembers(group, members, personalOnly = false) {
268
+ const resolved = await this.resolve(group);
269
+ const parent = await this.find(resolved.id);
270
+ const results = [];
271
+ const children = Array.isArray(parent.children) ? parent.children : [];
272
+ let mutated = false;
273
+ for (const m of members) {
274
+ let child;
275
+ try {
276
+ child = await this.resolve(m, {
277
+ personalOnly
278
+ });
279
+ } catch (err) {
280
+ results.push({
281
+ member: m,
282
+ status: 'error',
283
+ error: err?.message ?? String(err)
284
+ });
285
+ continue;
286
+ }
287
+ const idx = children.findIndex(r => r?.child?.id === child.id);
288
+ if (idx === -1) {
289
+ results.push({
290
+ member: m,
291
+ status: 'not-member'
292
+ });
293
+ } else {
294
+ children.splice(idx, 1);
295
+ mutated = true;
296
+ results.push({
297
+ member: m,
298
+ status: 'removed'
299
+ });
300
+ }
301
+ }
302
+ if (mutated) {
303
+ parent.children = children;
304
+ await this.save(parent, true);
305
+ }
306
+ return results;
307
+ }
308
+ async getMembers(group, admin) {
309
+ const parent = await this.resolve(group);
310
+ const q = buildQuery({
311
+ admin: admin === undefined ? undefined : String(admin)
312
+ });
313
+ return this.client.get(`/public/v1/groups/${encodeURIComponent(parent.id)}/members${q}`);
314
+ }
315
+ async getMemberships(group, admin) {
316
+ const parent = await this.resolve(group);
317
+ const q = buildQuery({
318
+ admin: admin === undefined ? undefined : String(admin)
319
+ });
320
+ return this.client.get(`/public/v1/groups/${encodeURIComponent(parent.id)}/memberships${q}`);
321
+ }
322
+ }
323
+ exports.NodeGroupsDataSource = NodeGroupsDataSource;
324
+ class NodeSharesDataSource {
325
+ constructor(client) {
326
+ this.client = client;
327
+ }
328
+ async share(entity, groups, access = 'View') {
329
+ const name = encodeURIComponent(entity.replace(':', '.'));
330
+ const q = buildQuery({
331
+ groups,
332
+ access
333
+ });
334
+ return this.client.post(`/public/v1/entities/${name}/shares${q}`);
335
+ }
336
+ async list(entityId) {
337
+ const q = buildQuery({
338
+ entityId
339
+ });
340
+ return this.client.get(`/privileges/permissions${q}`);
341
+ }
342
+ }
343
+ exports.NodeSharesDataSource = NodeSharesDataSource;
344
+ class NodeUsersDataSource extends NodeHttpDataSource {
345
+ constructor(client) {
346
+ super(client, 'users');
347
+ }
348
+ async save(user) {
349
+ return this.client.post('/public/v1/users', ensureBodyId(user));
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
+ }
357
+ }
358
+ exports.NodeUsersDataSource = NodeUsersDataSource;
359
+ class NodeConnectionsDataSource extends NodeHttpDataSource {
360
+ constructor(client) {
361
+ super(client, 'connections');
362
+ }
363
+ async save(conn, saveCredentials = false) {
364
+ const q = buildQuery({
365
+ saveCredentials: saveCredentials ? 'true' : undefined
366
+ });
367
+ return this.client.post(`/public/v1/connections${q}`, conn);
368
+ }
369
+ async test(conn) {
370
+ const result = await this.client.post(`/public/v1/connections/test`, conn);
371
+ const text = typeof result === 'string' ? result.replace(/^"|"$/g, '') : String(result ?? '');
372
+ if (text !== 'ok') throw new Error(text || 'Connection test failed');
373
+ }
374
+ }
375
+ exports.NodeConnectionsDataSource = NodeConnectionsDataSource;
376
+ class NodeFuncsDataSource extends NodeHttpDataSource {
377
+ async run(name, params) {
378
+ const normalizedName = name.replace(':', '.');
379
+ const result = await this.client.post(`/public/v1/functions/${encodeURIComponent(normalizedName)}/call`, params ?? {});
380
+ // Datagrok returns HTTP 200 with an ApiError body when the function doesn't exist
381
+ const parsed = typeof result === 'string' ? tryParseJson(result) : result;
382
+ if (parsed?.['#type'] === 'ApiError') {
383
+ const err = {
384
+ error: parsed.message ?? 'Function call failed',
385
+ errorCode: parsed.errorCode,
386
+ stackTrace: parsed.stackTrace
387
+ };
388
+ throw Object.assign(new Error(err.error), {
389
+ apiError: err
390
+ });
391
+ }
392
+ return result;
393
+ }
394
+ }
395
+ exports.NodeFuncsDataSource = NodeFuncsDataSource;
396
+ function tryParseJson(s) {
397
+ try {
398
+ return JSON.parse(s);
399
+ } catch {
400
+ return null;
401
+ }
402
+ }
403
+ class NodeFilesDataSource {
404
+ constructor(client) {
405
+ this.client = client;
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
+ */
418
+ splitPath(filePath) {
419
+ const slashIdx = filePath.indexOf('/');
420
+ if (slashIdx === -1) return {
421
+ connector: filePath.replace(/:/g, '.'),
422
+ path: ''
423
+ };
424
+ const connector = filePath.slice(0, slashIdx).replace(/:/g, '.');
425
+ const path = filePath.slice(slashIdx + 1);
426
+ return {
427
+ connector,
428
+ path
429
+ };
430
+ }
431
+ async list(filePath, recursive = false) {
432
+ const {
433
+ connector,
434
+ path
435
+ } = this.splitPath(filePath);
436
+ const q = buildQuery({
437
+ recursive: recursive ? 'true' : undefined
438
+ });
439
+ const seg = path ? `${connector}/${path}` : connector;
440
+ return this.client.get(`/public/v1/files/${seg}${q}`);
441
+ }
442
+ async get(filePath) {
443
+ const {
444
+ connector,
445
+ path
446
+ } = this.splitPath(filePath);
447
+ const seg = path ? `${connector}/${path}` : connector;
448
+ return this.client.get(`/public/v1/files/${seg}`);
449
+ }
450
+ async delete(filePath) {
451
+ const {
452
+ connector,
453
+ path
454
+ } = this.splitPath(filePath);
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
+ };
478
+ }
479
+ }
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;
514
+ class NodeDapi {
515
+ constructor(client) {
516
+ this.client = client;
517
+ }
518
+ get users() {
519
+ return new NodeUsersDataSource(this.client);
520
+ }
521
+ get groups() {
522
+ return new NodeGroupsDataSource(this.client);
523
+ }
524
+ get functions() {
525
+ return new NodeFuncsDataSource(this.client, 'functions');
526
+ }
527
+ get connections() {
528
+ return new NodeConnectionsDataSource(this.client);
529
+ }
530
+ get queries() {
531
+ return new NodeHttpDataSource(this.client, 'queries');
532
+ }
533
+ get scripts() {
534
+ return new NodeHttpDataSource(this.client, 'scripts');
535
+ }
536
+ get packages() {
537
+ return new NodeHttpDataSource(this.client, 'packages');
538
+ }
539
+ get reports() {
540
+ return new NodeHttpDataSource(this.client, 'reports');
541
+ }
542
+ get files() {
543
+ return new NodeFilesDataSource(this.client);
544
+ }
545
+ get shares() {
546
+ return new NodeSharesDataSource(this.client);
547
+ }
548
+ get tables() {
549
+ return new NodeTablesDataSource(this.client);
550
+ }
551
+ async raw(method, path, body) {
552
+ // Raw paths are relative to server root (e.g. /api/users/current).
553
+ // Strip the trailing /api from baseUrl to avoid double prefix.
554
+ const serverRoot = this.client.baseUrl.replace(/\/api\/?$/, '');
555
+ const url = `${serverRoot}${path}`;
556
+ const opts = {
557
+ method: method.toUpperCase(),
558
+ headers: {
559
+ 'Authorization': this.client.token,
560
+ 'Content-Type': 'application/json'
561
+ }
562
+ };
563
+ if (body !== undefined) opts.body = JSON.stringify(body);
564
+ const res = await fetch(url, opts);
565
+ const ct = res.headers.get('content-type') ?? '';
566
+ if (ct.includes('application/json')) return res.json();
567
+ return res.text();
568
+ }
569
+ async batch(request) {
570
+ return this.client.post('/public/v1/batch', request);
571
+ }
572
+ async describe(entityType) {
573
+ try {
574
+ return await this.client.get(`/public/v1/entity-types/${encodeURIComponent(entityType)}`);
575
+ } catch {
576
+ return await this.client.get(`/entities/types${buildQuery({
577
+ name: entityType
578
+ })}`);
579
+ }
580
+ }
581
+ }
582
+ exports.NodeDapi = NodeDapi;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.createClient = createClient;
7
+ var _nodeDapi = require("./node-dapi");
8
+ var _testUtils = require("./test-utils");
9
+ async function createClient(hostArg) {
10
+ const {
11
+ url,
12
+ key
13
+ } = (0, _testUtils.getDevKey)(hostArg ?? '');
14
+ return _nodeDapi.NodeApiClient.login(url, key);
15
+ }