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,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
|
+
}
|