datagrok-tools 6.1.8 → 6.1.10
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 +10 -0
- package/CLAUDE.md +68 -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/build.js +1 -1
- package/bin/commands/create.js +8 -5
- package/bin/commands/help.js +61 -1
- package/bin/commands/publish.js +12 -1
- package/bin/commands/server.js +520 -0
- package/bin/grok.js +3 -1
- package/bin/utils/node-dapi.js +459 -0
- package/bin/utils/server-client.js +15 -0
- package/bin/utils/server-output.js +127 -0
- package/package-template/package.json +1 -1
- package/package.json +8 -3
- package/vitest.config.ts +25 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.NodeUsersDataSource = 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
|
+
exports.NodeApiClient = NodeApiClient;
|
|
77
|
+
function buildQuery(params) {
|
|
78
|
+
const entries = Object.entries(params).filter(([, v]) => v !== undefined && v !== null && v !== '');
|
|
79
|
+
if (!entries.length) return '';
|
|
80
|
+
return '?' + entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join('&');
|
|
81
|
+
}
|
|
82
|
+
class NodeHttpDataSource {
|
|
83
|
+
_filter = '';
|
|
84
|
+
_limit = 50;
|
|
85
|
+
_page = 0;
|
|
86
|
+
_order = '';
|
|
87
|
+
constructor(client, path) {
|
|
88
|
+
this.client = client;
|
|
89
|
+
this.path = path;
|
|
90
|
+
}
|
|
91
|
+
filter(w) {
|
|
92
|
+
this._filter = w;
|
|
93
|
+
return this;
|
|
94
|
+
}
|
|
95
|
+
by(n) {
|
|
96
|
+
this._limit = n;
|
|
97
|
+
return this;
|
|
98
|
+
}
|
|
99
|
+
page(n) {
|
|
100
|
+
this._page = n;
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
order(field, desc = false) {
|
|
104
|
+
this._order = desc ? `-${field}` : field;
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
async list() {
|
|
108
|
+
const q = buildQuery({
|
|
109
|
+
text: this._filter || undefined,
|
|
110
|
+
limit: this._limit,
|
|
111
|
+
page: this._page || undefined,
|
|
112
|
+
order: this._order || undefined
|
|
113
|
+
});
|
|
114
|
+
return this.client.get(`/public/v1/${this.path}${q}`);
|
|
115
|
+
}
|
|
116
|
+
async find(id) {
|
|
117
|
+
return this.client.get(`/public/v1/${this.path}/${encodeURIComponent(id.replace(':', '.'))}`);
|
|
118
|
+
}
|
|
119
|
+
async count() {
|
|
120
|
+
const q = buildQuery({
|
|
121
|
+
text: this._filter || undefined
|
|
122
|
+
});
|
|
123
|
+
const res = await this.client.get(`/public/v1/${this.path}/count${q}`);
|
|
124
|
+
return typeof res === 'number' ? res : res?.count ?? 0;
|
|
125
|
+
}
|
|
126
|
+
async delete(idOrEntity) {
|
|
127
|
+
const id = typeof idOrEntity === 'string' ? idOrEntity : idOrEntity?.id ?? '';
|
|
128
|
+
await this.client.del(`/public/v1/${this.path}/${encodeURIComponent(id)}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
exports.NodeHttpDataSource = NodeHttpDataSource;
|
|
132
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
133
|
+
class NodeGroupsDataSource extends NodeHttpDataSource {
|
|
134
|
+
constructor(client) {
|
|
135
|
+
super(client, 'groups');
|
|
136
|
+
}
|
|
137
|
+
async save(group, saveRelations = false) {
|
|
138
|
+
const q = buildQuery({
|
|
139
|
+
saveRelations: saveRelations ? 'true' : undefined
|
|
140
|
+
});
|
|
141
|
+
return this.client.post(`/public/v1/groups${q}`, ensureBodyId(group));
|
|
142
|
+
}
|
|
143
|
+
async lookup(name) {
|
|
144
|
+
const q = buildQuery({
|
|
145
|
+
query: name
|
|
146
|
+
});
|
|
147
|
+
return this.client.get(`/public/v1/groups/lookup${q}`);
|
|
148
|
+
}
|
|
149
|
+
async resolve(idOrName, opts = {}) {
|
|
150
|
+
if (UUID_RE.test(idOrName)) return this.find(idOrName);
|
|
151
|
+
const matches = await this.lookup(idOrName);
|
|
152
|
+
let candidates = matches;
|
|
153
|
+
if (opts.personalOnly) candidates = matches.filter(g => g?.personal === true);
|
|
154
|
+
if (!candidates.length) {
|
|
155
|
+
const suffix = opts.personalOnly ? ' (personal)' : '';
|
|
156
|
+
throw new Error(`No group matching '${idOrName}'${suffix}`);
|
|
157
|
+
}
|
|
158
|
+
if (candidates.length > 1) {
|
|
159
|
+
const list = candidates.map(g => ` ${g.id} ${g.friendlyName ?? g.name ?? ''}`).join('\n');
|
|
160
|
+
throw new Error(`Multiple groups match '${idOrName}':\n${list}\nUse the ID to disambiguate.`);
|
|
161
|
+
}
|
|
162
|
+
return candidates[0];
|
|
163
|
+
}
|
|
164
|
+
async addMembers(group, members, isAdmin = false, personalOnly = false) {
|
|
165
|
+
const parent = await this.resolve(group);
|
|
166
|
+
const children = Array.isArray(parent.children) ? parent.children : [];
|
|
167
|
+
const results = [];
|
|
168
|
+
let mutated = false;
|
|
169
|
+
for (const m of members) {
|
|
170
|
+
let child;
|
|
171
|
+
try {
|
|
172
|
+
child = await this.resolve(m, {
|
|
173
|
+
personalOnly
|
|
174
|
+
});
|
|
175
|
+
} catch (err) {
|
|
176
|
+
results.push({
|
|
177
|
+
member: m,
|
|
178
|
+
status: 'error',
|
|
179
|
+
error: err?.message ?? String(err)
|
|
180
|
+
});
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const existing = children.find(r => r?.child?.id === child.id);
|
|
184
|
+
if (existing) {
|
|
185
|
+
if (existing.isAdmin === isAdmin) {
|
|
186
|
+
results.push({
|
|
187
|
+
member: m,
|
|
188
|
+
status: 'noop'
|
|
189
|
+
});
|
|
190
|
+
} else {
|
|
191
|
+
existing.isAdmin = isAdmin;
|
|
192
|
+
mutated = true;
|
|
193
|
+
results.push({
|
|
194
|
+
member: m,
|
|
195
|
+
status: 'updated'
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
children.push({
|
|
200
|
+
parent: {
|
|
201
|
+
id: parent.id
|
|
202
|
+
},
|
|
203
|
+
child: {
|
|
204
|
+
id: child.id
|
|
205
|
+
},
|
|
206
|
+
isAdmin
|
|
207
|
+
});
|
|
208
|
+
mutated = true;
|
|
209
|
+
results.push({
|
|
210
|
+
member: m,
|
|
211
|
+
status: 'added'
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (mutated) {
|
|
216
|
+
parent.children = children;
|
|
217
|
+
await this.save(parent, true);
|
|
218
|
+
}
|
|
219
|
+
return results;
|
|
220
|
+
}
|
|
221
|
+
async removeMembers(group, members, personalOnly = false) {
|
|
222
|
+
const parent = await this.resolve(group);
|
|
223
|
+
const results = [];
|
|
224
|
+
const children = Array.isArray(parent.children) ? parent.children : [];
|
|
225
|
+
let mutated = false;
|
|
226
|
+
for (const m of members) {
|
|
227
|
+
let child;
|
|
228
|
+
try {
|
|
229
|
+
child = await this.resolve(m, {
|
|
230
|
+
personalOnly
|
|
231
|
+
});
|
|
232
|
+
} catch (err) {
|
|
233
|
+
results.push({
|
|
234
|
+
member: m,
|
|
235
|
+
status: 'error',
|
|
236
|
+
error: err?.message ?? String(err)
|
|
237
|
+
});
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const idx = children.findIndex(r => r?.child?.id === child.id);
|
|
241
|
+
if (idx === -1) {
|
|
242
|
+
results.push({
|
|
243
|
+
member: m,
|
|
244
|
+
status: 'not-member'
|
|
245
|
+
});
|
|
246
|
+
} else {
|
|
247
|
+
children.splice(idx, 1);
|
|
248
|
+
mutated = true;
|
|
249
|
+
results.push({
|
|
250
|
+
member: m,
|
|
251
|
+
status: 'removed'
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (mutated) {
|
|
256
|
+
parent.children = children;
|
|
257
|
+
await this.save(parent, true);
|
|
258
|
+
}
|
|
259
|
+
return results;
|
|
260
|
+
}
|
|
261
|
+
async getMembers(group, admin) {
|
|
262
|
+
const parent = await this.resolve(group);
|
|
263
|
+
const q = buildQuery({
|
|
264
|
+
admin: admin === undefined ? undefined : String(admin)
|
|
265
|
+
});
|
|
266
|
+
return this.client.get(`/public/v1/groups/${encodeURIComponent(parent.id)}/members${q}`);
|
|
267
|
+
}
|
|
268
|
+
async getMemberships(group, admin) {
|
|
269
|
+
const parent = await this.resolve(group);
|
|
270
|
+
const q = buildQuery({
|
|
271
|
+
admin: admin === undefined ? undefined : String(admin)
|
|
272
|
+
});
|
|
273
|
+
return this.client.get(`/public/v1/groups/${encodeURIComponent(parent.id)}/memberships${q}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
exports.NodeGroupsDataSource = NodeGroupsDataSource;
|
|
277
|
+
class NodeSharesDataSource {
|
|
278
|
+
constructor(client) {
|
|
279
|
+
this.client = client;
|
|
280
|
+
}
|
|
281
|
+
async share(entity, groups, access = 'View') {
|
|
282
|
+
const name = encodeURIComponent(entity.replace(':', '.'));
|
|
283
|
+
const q = buildQuery({
|
|
284
|
+
groups,
|
|
285
|
+
access
|
|
286
|
+
});
|
|
287
|
+
return this.client.post(`/public/v1/entities/${name}/shares${q}`);
|
|
288
|
+
}
|
|
289
|
+
async list(entityId) {
|
|
290
|
+
const q = buildQuery({
|
|
291
|
+
entityId
|
|
292
|
+
});
|
|
293
|
+
return this.client.get(`/privileges/permissions${q}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
exports.NodeSharesDataSource = NodeSharesDataSource;
|
|
297
|
+
class NodeUsersDataSource extends NodeHttpDataSource {
|
|
298
|
+
constructor(client) {
|
|
299
|
+
super(client, 'users');
|
|
300
|
+
}
|
|
301
|
+
async save(user) {
|
|
302
|
+
return this.client.post('/public/v1/users', ensureBodyId(user));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
exports.NodeUsersDataSource = NodeUsersDataSource;
|
|
306
|
+
class NodeConnectionsDataSource extends NodeHttpDataSource {
|
|
307
|
+
constructor(client) {
|
|
308
|
+
super(client, 'connections');
|
|
309
|
+
}
|
|
310
|
+
async save(conn, saveCredentials = false) {
|
|
311
|
+
const q = buildQuery({
|
|
312
|
+
saveCredentials: saveCredentials ? 'true' : undefined
|
|
313
|
+
});
|
|
314
|
+
return this.client.post(`/public/v1/connections${q}`, conn);
|
|
315
|
+
}
|
|
316
|
+
async test(conn) {
|
|
317
|
+
const result = await this.client.post(`/public/v1/connections/test`, conn);
|
|
318
|
+
const text = typeof result === 'string' ? result.replace(/^"|"$/g, '') : String(result ?? '');
|
|
319
|
+
if (text !== 'ok') throw new Error(text || 'Connection test failed');
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
exports.NodeConnectionsDataSource = NodeConnectionsDataSource;
|
|
323
|
+
class NodeFuncsDataSource extends NodeHttpDataSource {
|
|
324
|
+
async run(name, params) {
|
|
325
|
+
const normalizedName = name.replace(':', '.');
|
|
326
|
+
const result = await this.client.post(`/public/v1/functions/${encodeURIComponent(normalizedName)}/call`, params ?? {});
|
|
327
|
+
// Datagrok returns HTTP 200 with an ApiError body when the function doesn't exist
|
|
328
|
+
const parsed = typeof result === 'string' ? tryParseJson(result) : result;
|
|
329
|
+
if (parsed?.['#type'] === 'ApiError') {
|
|
330
|
+
const err = {
|
|
331
|
+
error: parsed.message ?? 'Function call failed',
|
|
332
|
+
errorCode: parsed.errorCode,
|
|
333
|
+
stackTrace: parsed.stackTrace
|
|
334
|
+
};
|
|
335
|
+
throw Object.assign(new Error(err.error), {
|
|
336
|
+
apiError: err
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
exports.NodeFuncsDataSource = NodeFuncsDataSource;
|
|
343
|
+
function tryParseJson(s) {
|
|
344
|
+
try {
|
|
345
|
+
return JSON.parse(s);
|
|
346
|
+
} catch {
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
class NodeFilesDataSource {
|
|
351
|
+
constructor(client) {
|
|
352
|
+
this.client = client;
|
|
353
|
+
}
|
|
354
|
+
splitPath(filePath) {
|
|
355
|
+
const colonIdx = filePath.indexOf(':');
|
|
356
|
+
if (colonIdx === -1) return {
|
|
357
|
+
connector: filePath.replace(':', '.'),
|
|
358
|
+
path: ''
|
|
359
|
+
};
|
|
360
|
+
const connector = filePath.slice(0, colonIdx).replace(':', '.');
|
|
361
|
+
const path = filePath.slice(colonIdx + 1).replace(/^\//, '');
|
|
362
|
+
return {
|
|
363
|
+
connector,
|
|
364
|
+
path
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
async list(filePath, recursive = false) {
|
|
368
|
+
const {
|
|
369
|
+
connector,
|
|
370
|
+
path
|
|
371
|
+
} = this.splitPath(filePath);
|
|
372
|
+
const q = buildQuery({
|
|
373
|
+
recursive: recursive ? 'true' : undefined
|
|
374
|
+
});
|
|
375
|
+
const seg = path ? `${connector}/${path}` : connector;
|
|
376
|
+
return this.client.get(`/public/v1/files/${seg}${q}`);
|
|
377
|
+
}
|
|
378
|
+
async get(filePath) {
|
|
379
|
+
const {
|
|
380
|
+
connector,
|
|
381
|
+
path
|
|
382
|
+
} = this.splitPath(filePath);
|
|
383
|
+
return this.client.get(`/public/v1/files/${connector}/${path}`);
|
|
384
|
+
}
|
|
385
|
+
async delete(filePath) {
|
|
386
|
+
const {
|
|
387
|
+
connector,
|
|
388
|
+
path
|
|
389
|
+
} = this.splitPath(filePath);
|
|
390
|
+
await this.client.del(`/public/v1/files/${connector}/${path}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
exports.NodeFilesDataSource = NodeFilesDataSource;
|
|
394
|
+
class NodeDapi {
|
|
395
|
+
constructor(client) {
|
|
396
|
+
this.client = client;
|
|
397
|
+
}
|
|
398
|
+
get users() {
|
|
399
|
+
return new NodeUsersDataSource(this.client);
|
|
400
|
+
}
|
|
401
|
+
get groups() {
|
|
402
|
+
return new NodeGroupsDataSource(this.client);
|
|
403
|
+
}
|
|
404
|
+
get functions() {
|
|
405
|
+
return new NodeFuncsDataSource(this.client, 'functions');
|
|
406
|
+
}
|
|
407
|
+
get connections() {
|
|
408
|
+
return new NodeConnectionsDataSource(this.client);
|
|
409
|
+
}
|
|
410
|
+
get queries() {
|
|
411
|
+
return new NodeHttpDataSource(this.client, 'queries');
|
|
412
|
+
}
|
|
413
|
+
get scripts() {
|
|
414
|
+
return new NodeHttpDataSource(this.client, 'scripts');
|
|
415
|
+
}
|
|
416
|
+
get packages() {
|
|
417
|
+
return new NodeHttpDataSource(this.client, 'packages');
|
|
418
|
+
}
|
|
419
|
+
get reports() {
|
|
420
|
+
return new NodeHttpDataSource(this.client, 'reports');
|
|
421
|
+
}
|
|
422
|
+
get files() {
|
|
423
|
+
return new NodeFilesDataSource(this.client);
|
|
424
|
+
}
|
|
425
|
+
get shares() {
|
|
426
|
+
return new NodeSharesDataSource(this.client);
|
|
427
|
+
}
|
|
428
|
+
async raw(method, path, body) {
|
|
429
|
+
// Raw paths are relative to server root (e.g. /api/users/current).
|
|
430
|
+
// Strip the trailing /api from baseUrl to avoid double prefix.
|
|
431
|
+
const serverRoot = this.client.baseUrl.replace(/\/api\/?$/, '');
|
|
432
|
+
const url = `${serverRoot}${path}`;
|
|
433
|
+
const opts = {
|
|
434
|
+
method: method.toUpperCase(),
|
|
435
|
+
headers: {
|
|
436
|
+
'Authorization': this.client.token,
|
|
437
|
+
'Content-Type': 'application/json'
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
if (body !== undefined) opts.body = JSON.stringify(body);
|
|
441
|
+
const res = await fetch(url, opts);
|
|
442
|
+
const ct = res.headers.get('content-type') ?? '';
|
|
443
|
+
if (ct.includes('application/json')) return res.json();
|
|
444
|
+
return res.text();
|
|
445
|
+
}
|
|
446
|
+
async batch(request) {
|
|
447
|
+
return this.client.post('/public/v1/batch', request);
|
|
448
|
+
}
|
|
449
|
+
async describe(entityType) {
|
|
450
|
+
try {
|
|
451
|
+
return await this.client.get(`/public/v1/entity-types/${encodeURIComponent(entityType)}`);
|
|
452
|
+
} catch {
|
|
453
|
+
return await this.client.get(`/entities/types${buildQuery({
|
|
454
|
+
name: entityType
|
|
455
|
+
})}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
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
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.cellStr = cellStr;
|
|
7
|
+
exports.csvCell = csvCell;
|
|
8
|
+
exports.getKeys = getKeys;
|
|
9
|
+
exports.printBatchOutput = printBatchOutput;
|
|
10
|
+
exports.printError = printError;
|
|
11
|
+
exports.printOutput = printOutput;
|
|
12
|
+
/// Docs: [Grok Dapi](/docs/plans/grok-dapi/)
|
|
13
|
+
|
|
14
|
+
function printOutput(data, format) {
|
|
15
|
+
if (data === null || data === undefined) {
|
|
16
|
+
if (format !== 'quiet') console.log('(empty)');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const rows = Array.isArray(data) ? data : [data];
|
|
20
|
+
switch (format) {
|
|
21
|
+
case 'json':
|
|
22
|
+
console.log(JSON.stringify(data, null, 2));
|
|
23
|
+
break;
|
|
24
|
+
case 'csv':
|
|
25
|
+
printCsv(rows);
|
|
26
|
+
break;
|
|
27
|
+
case 'quiet':
|
|
28
|
+
for (const row of rows) {
|
|
29
|
+
if (typeof row === 'object' && row !== null) console.log(row.id ?? row.name ?? JSON.stringify(row));else console.log(row);
|
|
30
|
+
}
|
|
31
|
+
break;
|
|
32
|
+
default:
|
|
33
|
+
printTable(rows);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function cellStr(v) {
|
|
37
|
+
if (v === null || v === undefined) return '';
|
|
38
|
+
if (typeof v === 'object') {
|
|
39
|
+
if (v.name) return v.name;
|
|
40
|
+
if (v.id) return v.id;
|
|
41
|
+
return JSON.stringify(v).slice(0, 40);
|
|
42
|
+
}
|
|
43
|
+
return String(v);
|
|
44
|
+
}
|
|
45
|
+
function printTable(rows) {
|
|
46
|
+
if (!rows.length) {
|
|
47
|
+
console.log('(no results)');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (typeof rows[0] !== 'object' || rows[0] === null) {
|
|
51
|
+
for (const r of rows) console.log(r);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const keys = getKeys(rows);
|
|
55
|
+
const widths = {};
|
|
56
|
+
for (const k of keys) widths[k] = k.length;
|
|
57
|
+
for (const row of rows) for (const k of keys) widths[k] = Math.max(widths[k], cellStr(row[k]).length);
|
|
58
|
+
const header = keys.map(k => k.padEnd(widths[k])).join(' ');
|
|
59
|
+
const sep = keys.map(k => '-'.repeat(widths[k])).join(' ');
|
|
60
|
+
console.log(header);
|
|
61
|
+
console.log(sep);
|
|
62
|
+
for (const row of rows) console.log(keys.map(k => cellStr(row[k]).padEnd(widths[k])).join(' '));
|
|
63
|
+
}
|
|
64
|
+
function printCsv(rows) {
|
|
65
|
+
if (!rows.length) return;
|
|
66
|
+
if (typeof rows[0] !== 'object' || rows[0] === null) {
|
|
67
|
+
for (const r of rows) console.log(csvCell(String(r)));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const keys = getKeys(rows);
|
|
71
|
+
console.log(keys.map(csvCell).join(','));
|
|
72
|
+
for (const row of rows) console.log(keys.map(k => csvCell(cellStr(row[k]))).join(','));
|
|
73
|
+
}
|
|
74
|
+
function csvCell(s) {
|
|
75
|
+
return s.includes(',') || s.includes('"') || s.includes('\n') ? `"${s.replace(/"/g, '""')}"` : s;
|
|
76
|
+
}
|
|
77
|
+
function getKeys(rows) {
|
|
78
|
+
const seen = new Set();
|
|
79
|
+
const keys = [];
|
|
80
|
+
for (const row of rows) for (const k of Object.keys(row)) if (!seen.has(k)) {
|
|
81
|
+
seen.add(k);
|
|
82
|
+
keys.push(k);
|
|
83
|
+
}
|
|
84
|
+
return keys.slice(0, 12);
|
|
85
|
+
}
|
|
86
|
+
function printBatchOutput(response, format) {
|
|
87
|
+
if (format === 'json') {
|
|
88
|
+
console.log(JSON.stringify(response, null, 2));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (format === 'quiet') {
|
|
92
|
+
const s = response.summary;
|
|
93
|
+
console.log(`${s.total} total: ${s.succeeded} succeeded, ${s.failed} failed, ${s.partial} partial, ${s.skipped} skipped`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// table and csv: print summary then per-op rows
|
|
98
|
+
const s = response.summary;
|
|
99
|
+
console.log(`Summary: ${s.total} total ${s.succeeded} succeeded ${s.failed} failed ${s.partial} partial ${s.skipped} skipped`);
|
|
100
|
+
if (!response.results.length) return;
|
|
101
|
+
console.log('');
|
|
102
|
+
const rows = response.results.map(r => {
|
|
103
|
+
const brief = r.status === 'error' ? r.error?.error ?? 'error' : r.status === 'skipped' ? r.reason ?? 'skipped' : r.status === 'partial' ? `${r.summary?.succeeded}/${r.summary?.total} succeeded` : '';
|
|
104
|
+
return {
|
|
105
|
+
id: r.id ?? '',
|
|
106
|
+
action: r.action,
|
|
107
|
+
status: r.status,
|
|
108
|
+
detail: brief
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
if (format === 'csv') printCsv(rows);else printTable(rows);
|
|
112
|
+
|
|
113
|
+
// Write per-op errors to stderr
|
|
114
|
+
const errors = response.results.filter(r => r.status === 'error');
|
|
115
|
+
if (errors.length) process.stderr.write(JSON.stringify(errors.map(r => ({
|
|
116
|
+
id: r.id,
|
|
117
|
+
action: r.action,
|
|
118
|
+
error: r.error
|
|
119
|
+
})), null, 2) + '\n');
|
|
120
|
+
}
|
|
121
|
+
function printError(err) {
|
|
122
|
+
const apiErr = err?.apiError;
|
|
123
|
+
const out = apiErr ?? {
|
|
124
|
+
error: String(err?.message ?? err)
|
|
125
|
+
};
|
|
126
|
+
process.stderr.write(JSON.stringify(out, null, 2) + '\n');
|
|
127
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "datagrok-tools",
|
|
3
|
-
"version": "6.1.
|
|
3
|
+
"version": "6.1.10",
|
|
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": {
|
|
@@ -32,7 +32,11 @@
|
|
|
32
32
|
"prepublishOnly": "babel bin --extensions .ts -d bin",
|
|
33
33
|
"babel": "babel bin --extensions .ts -d bin",
|
|
34
34
|
"build": "babel bin --extensions .ts -d bin",
|
|
35
|
-
"debug-source-map": "babel bin --extensions .ts -d bin --source-maps true"
|
|
35
|
+
"debug-source-map": "babel bin --extensions .ts -d bin --source-maps true",
|
|
36
|
+
"test": "vitest run --project unit",
|
|
37
|
+
"test:watch": "vitest --project unit",
|
|
38
|
+
"test:integration": "vitest run --project integration",
|
|
39
|
+
"test:all": "vitest run"
|
|
36
40
|
},
|
|
37
41
|
"bin": {
|
|
38
42
|
"datagrok-upload": "./bin/_deprecated/upload.js",
|
|
@@ -69,13 +73,14 @@
|
|
|
69
73
|
"@types/ignore-walk": "^4.0.3",
|
|
70
74
|
"@types/inquirer": "^8.2.10",
|
|
71
75
|
"@types/js-yaml": "^4.0.9",
|
|
72
|
-
"@types/node": "^
|
|
76
|
+
"@types/node": "^18.0.0",
|
|
73
77
|
"@types/papaparse": "^5.3.15",
|
|
74
78
|
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
|
75
79
|
"@typescript-eslint/parser": "^5.62.0",
|
|
76
80
|
"eslint": "^8.56.0",
|
|
77
81
|
"eslint-config-google": "^0.14.0",
|
|
78
82
|
"typescript": "^5.3.3",
|
|
83
|
+
"vitest": "^3.2.4",
|
|
79
84
|
"webpack": "^5.89.0",
|
|
80
85
|
"webpack-cli": "^5.1.4"
|
|
81
86
|
}
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {defineConfig} from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
projects: [
|
|
6
|
+
{
|
|
7
|
+
test: {
|
|
8
|
+
name: 'unit',
|
|
9
|
+
environment: 'node',
|
|
10
|
+
include: ['bin/**/*.test.ts'],
|
|
11
|
+
exclude: ['bin/**/*.integration.test.ts'],
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
test: {
|
|
16
|
+
name: 'integration',
|
|
17
|
+
environment: 'node',
|
|
18
|
+
include: ['bin/**/*.integration.test.ts'],
|
|
19
|
+
testTimeout: 30_000,
|
|
20
|
+
hookTimeout: 30_000,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
});
|