datagrok-tools 6.1.9 → 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/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,277 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _vitest = require("vitest");
|
|
4
|
+
var os = _interopRequireWildcard(require("os"));
|
|
5
|
+
var fs = _interopRequireWildcard(require("fs"));
|
|
6
|
+
var path = _interopRequireWildcard(require("path"));
|
|
7
|
+
var _server = require("../commands/server");
|
|
8
|
+
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); }
|
|
9
|
+
(0, _vitest.describe)('parseFuncCall', () => {
|
|
10
|
+
(0, _vitest.it)('parses a single string argument', () => {
|
|
11
|
+
(0, _vitest.expect)((0, _server.parseFuncCall)('Chem:smilesToMw("ccc")')).toEqual({
|
|
12
|
+
name: 'Chem:smilesToMw',
|
|
13
|
+
params: {
|
|
14
|
+
'0': 'ccc'
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
(0, _vitest.it)('parses a single-quoted string argument', () => {
|
|
19
|
+
(0, _vitest.expect)((0, _server.parseFuncCall)("Pkg:fn('hello')")).toEqual({
|
|
20
|
+
name: 'Pkg:fn',
|
|
21
|
+
params: {
|
|
22
|
+
'0': 'hello'
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
(0, _vitest.it)('parses a numeric argument', () => {
|
|
27
|
+
(0, _vitest.expect)((0, _server.parseFuncCall)('Pkg:fn(42)')).toEqual({
|
|
28
|
+
name: 'Pkg:fn',
|
|
29
|
+
params: {
|
|
30
|
+
'0': 42
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
(0, _vitest.it)('parses multiple positional arguments', () => {
|
|
35
|
+
(0, _vitest.expect)((0, _server.parseFuncCall)('Pkg:fn(1, "hello", 3.14)')).toEqual({
|
|
36
|
+
name: 'Pkg:fn',
|
|
37
|
+
params: {
|
|
38
|
+
'0': 1,
|
|
39
|
+
'1': 'hello',
|
|
40
|
+
'2': 3.14
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
(0, _vitest.it)('parses an object argument with quoted keys', () => {
|
|
45
|
+
(0, _vitest.expect)((0, _server.parseFuncCall)('Pkg:fn({"a":5,"b":22})')).toEqual({
|
|
46
|
+
name: 'Pkg:fn',
|
|
47
|
+
params: {
|
|
48
|
+
a: 5,
|
|
49
|
+
b: 22
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
(0, _vitest.it)('parses an object argument with unquoted keys', () => {
|
|
54
|
+
(0, _vitest.expect)((0, _server.parseFuncCall)('Pkg:fn({a:5,b:22})')).toEqual({
|
|
55
|
+
name: 'Pkg:fn',
|
|
56
|
+
params: {
|
|
57
|
+
a: 5,
|
|
58
|
+
b: 22
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
(0, _vitest.it)('parses an object argument with a boolean value', () => {
|
|
63
|
+
(0, _vitest.expect)((0, _server.parseFuncCall)('Pkg:fn({unquoted:true})')).toEqual({
|
|
64
|
+
name: 'Pkg:fn',
|
|
65
|
+
params: {
|
|
66
|
+
unquoted: true
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
(0, _vitest.it)('returns empty params for a no-argument call', () => {
|
|
71
|
+
(0, _vitest.expect)((0, _server.parseFuncCall)('Pkg:fn()')).toEqual({
|
|
72
|
+
name: 'Pkg:fn',
|
|
73
|
+
params: {}
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
(0, _vitest.it)('returns empty params when there are no parentheses', () => {
|
|
77
|
+
(0, _vitest.expect)((0, _server.parseFuncCall)('Pkg:fn')).toEqual({
|
|
78
|
+
name: 'Pkg:fn',
|
|
79
|
+
params: {}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
(0, _vitest.it)('handles a package-less function name', () => {
|
|
83
|
+
(0, _vitest.expect)((0, _server.parseFuncCall)('myFunc("arg")')).toEqual({
|
|
84
|
+
name: 'myFunc',
|
|
85
|
+
params: {
|
|
86
|
+
'0': 'arg'
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
(0, _vitest.describe)('buildInlineManifest', () => {
|
|
92
|
+
(0, _vitest.it)('sets action to entity.verb', () => {
|
|
93
|
+
const result = (0, _server.buildInlineManifest)('users', 'delete', ['abc']);
|
|
94
|
+
(0, _vitest.expect)(result.operations[0].action).toBe('users.delete');
|
|
95
|
+
});
|
|
96
|
+
(0, _vitest.it)('maps string args to {id} param for non-file entities', () => {
|
|
97
|
+
const result = (0, _server.buildInlineManifest)('users', 'delete', ['id1', 'id2']);
|
|
98
|
+
(0, _vitest.expect)(result.operations).toEqual([{
|
|
99
|
+
id: 'op0',
|
|
100
|
+
action: 'users.delete',
|
|
101
|
+
params: {
|
|
102
|
+
id: 'id1'
|
|
103
|
+
}
|
|
104
|
+
}, {
|
|
105
|
+
id: 'op1',
|
|
106
|
+
action: 'users.delete',
|
|
107
|
+
params: {
|
|
108
|
+
id: 'id2'
|
|
109
|
+
}
|
|
110
|
+
}]);
|
|
111
|
+
});
|
|
112
|
+
(0, _vitest.it)('maps string args to {path} param for the files entity', () => {
|
|
113
|
+
const result = (0, _server.buildInlineManifest)('files', 'delete', ['System:AppData/a.txt', 'System:AppData/b.txt']);
|
|
114
|
+
(0, _vitest.expect)(result.operations).toEqual([{
|
|
115
|
+
id: 'op0',
|
|
116
|
+
action: 'files.delete',
|
|
117
|
+
params: {
|
|
118
|
+
path: 'System:AppData/a.txt'
|
|
119
|
+
}
|
|
120
|
+
}, {
|
|
121
|
+
id: 'op1',
|
|
122
|
+
action: 'files.delete',
|
|
123
|
+
params: {
|
|
124
|
+
path: 'System:AppData/b.txt'
|
|
125
|
+
}
|
|
126
|
+
}]);
|
|
127
|
+
});
|
|
128
|
+
(0, _vitest.it)('passes object array args through as params directly', () => {
|
|
129
|
+
const objs = [{
|
|
130
|
+
name: 'Alice',
|
|
131
|
+
email: 'a@x.com'
|
|
132
|
+
}, {
|
|
133
|
+
name: 'Bob',
|
|
134
|
+
email: 'b@x.com'
|
|
135
|
+
}];
|
|
136
|
+
const result = (0, _server.buildInlineManifest)('users', 'create', objs);
|
|
137
|
+
(0, _vitest.expect)(result.operations).toEqual([{
|
|
138
|
+
id: 'op0',
|
|
139
|
+
action: 'users.create',
|
|
140
|
+
params: {
|
|
141
|
+
name: 'Alice',
|
|
142
|
+
email: 'a@x.com'
|
|
143
|
+
}
|
|
144
|
+
}, {
|
|
145
|
+
id: 'op1',
|
|
146
|
+
action: 'users.create',
|
|
147
|
+
params: {
|
|
148
|
+
name: 'Bob',
|
|
149
|
+
email: 'b@x.com'
|
|
150
|
+
}
|
|
151
|
+
}]);
|
|
152
|
+
});
|
|
153
|
+
(0, _vitest.it)('assigns sequential op ids starting at op0', () => {
|
|
154
|
+
const result = (0, _server.buildInlineManifest)('groups', 'delete', ['x', 'y', 'z']);
|
|
155
|
+
(0, _vitest.expect)(result.operations.map(o => o.id)).toEqual(['op0', 'op1', 'op2']);
|
|
156
|
+
});
|
|
157
|
+
(0, _vitest.it)('returns a single operation for a single arg', () => {
|
|
158
|
+
const result = (0, _server.buildInlineManifest)('connections', 'delete', ['conn-id']);
|
|
159
|
+
(0, _vitest.expect)(result.operations).toHaveLength(1);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
(0, _vitest.describe)('resolveManifestSources', () => {
|
|
163
|
+
let tmpFile;
|
|
164
|
+
(0, _vitest.beforeEach)(() => {
|
|
165
|
+
tmpFile = path.join(os.tmpdir(), `grok-batch-test-${Date.now()}.bin`);
|
|
166
|
+
});
|
|
167
|
+
(0, _vitest.afterEach)(() => {
|
|
168
|
+
if (fs.existsSync(tmpFile)) fs.unlinkSync(tmpFile);
|
|
169
|
+
});
|
|
170
|
+
(0, _vitest.it)('passes through a manifest with no files.put operations unchanged', () => {
|
|
171
|
+
const manifest = {
|
|
172
|
+
operations: [{
|
|
173
|
+
id: 'op0',
|
|
174
|
+
action: 'users.delete',
|
|
175
|
+
params: {
|
|
176
|
+
id: 'abc'
|
|
177
|
+
}
|
|
178
|
+
}]
|
|
179
|
+
};
|
|
180
|
+
(0, _vitest.expect)((0, _server.resolveManifestSources)(manifest)).toEqual(manifest);
|
|
181
|
+
});
|
|
182
|
+
(0, _vitest.it)('passes through a files.put operation that has no source field', () => {
|
|
183
|
+
const manifest = {
|
|
184
|
+
operations: [{
|
|
185
|
+
id: 'op0',
|
|
186
|
+
action: 'files.put',
|
|
187
|
+
params: {
|
|
188
|
+
path: 'System:AppData/f.txt',
|
|
189
|
+
content: 'aGk='
|
|
190
|
+
}
|
|
191
|
+
}]
|
|
192
|
+
};
|
|
193
|
+
(0, _vitest.expect)((0, _server.resolveManifestSources)(manifest)).toEqual(manifest);
|
|
194
|
+
});
|
|
195
|
+
(0, _vitest.it)('reads the source file, base64-encodes its contents, and removes the source key', () => {
|
|
196
|
+
const data = 'hello world';
|
|
197
|
+
fs.writeFileSync(tmpFile, data);
|
|
198
|
+
const manifest = {
|
|
199
|
+
operations: [{
|
|
200
|
+
id: 'op0',
|
|
201
|
+
action: 'files.put',
|
|
202
|
+
params: {
|
|
203
|
+
path: 'System:AppData/f.txt',
|
|
204
|
+
source: tmpFile
|
|
205
|
+
}
|
|
206
|
+
}]
|
|
207
|
+
};
|
|
208
|
+
const result = (0, _server.resolveManifestSources)(manifest);
|
|
209
|
+
const params = result.operations[0].params;
|
|
210
|
+
(0, _vitest.expect)(params.content).toBe(Buffer.from(data).toString('base64'));
|
|
211
|
+
(0, _vitest.expect)(params.source).toBeUndefined();
|
|
212
|
+
});
|
|
213
|
+
(0, _vitest.it)('preserves other params alongside the injected content', () => {
|
|
214
|
+
fs.writeFileSync(tmpFile, 'data');
|
|
215
|
+
const manifest = {
|
|
216
|
+
operations: [{
|
|
217
|
+
id: 'op0',
|
|
218
|
+
action: 'files.put',
|
|
219
|
+
params: {
|
|
220
|
+
path: 'System:AppData/f.txt',
|
|
221
|
+
source: tmpFile,
|
|
222
|
+
extra: 'val'
|
|
223
|
+
}
|
|
224
|
+
}]
|
|
225
|
+
};
|
|
226
|
+
const result = (0, _server.resolveManifestSources)(manifest);
|
|
227
|
+
const params = result.operations[0].params;
|
|
228
|
+
(0, _vitest.expect)(params.path).toBe('System:AppData/f.txt');
|
|
229
|
+
(0, _vitest.expect)(params.extra).toBe('val');
|
|
230
|
+
});
|
|
231
|
+
(0, _vitest.it)('leaves non-files.put operations untouched in a mixed manifest', () => {
|
|
232
|
+
fs.writeFileSync(tmpFile, 'x');
|
|
233
|
+
const manifest = {
|
|
234
|
+
operations: [{
|
|
235
|
+
id: 'op0',
|
|
236
|
+
action: 'users.delete',
|
|
237
|
+
params: {
|
|
238
|
+
id: 'u1'
|
|
239
|
+
}
|
|
240
|
+
}, {
|
|
241
|
+
id: 'op1',
|
|
242
|
+
action: 'files.put',
|
|
243
|
+
params: {
|
|
244
|
+
path: 'System:AppData/f.txt',
|
|
245
|
+
source: tmpFile
|
|
246
|
+
}
|
|
247
|
+
}]
|
|
248
|
+
};
|
|
249
|
+
const result = (0, _server.resolveManifestSources)(manifest);
|
|
250
|
+
(0, _vitest.expect)(result.operations[0]).toEqual({
|
|
251
|
+
id: 'op0',
|
|
252
|
+
action: 'users.delete',
|
|
253
|
+
params: {
|
|
254
|
+
id: 'u1'
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
(0, _vitest.expect)(result.operations[1].params.source).toBeUndefined();
|
|
258
|
+
(0, _vitest.expect)(result.operations[1].params.content).toBeDefined();
|
|
259
|
+
});
|
|
260
|
+
(0, _vitest.it)('correctly encodes binary file contents', () => {
|
|
261
|
+
const bytes = Buffer.from([0x00, 0xff, 0x10, 0xab]);
|
|
262
|
+
fs.writeFileSync(tmpFile, bytes);
|
|
263
|
+
const manifest = {
|
|
264
|
+
operations: [{
|
|
265
|
+
id: 'op0',
|
|
266
|
+
action: 'files.put',
|
|
267
|
+
params: {
|
|
268
|
+
path: 'System:AppData/f.bin',
|
|
269
|
+
source: tmpFile
|
|
270
|
+
}
|
|
271
|
+
}]
|
|
272
|
+
};
|
|
273
|
+
const result = (0, _server.resolveManifestSources)(manifest);
|
|
274
|
+
const decoded = Buffer.from(result.operations[0].params.content, 'base64');
|
|
275
|
+
(0, _vitest.expect)(decoded).toEqual(bytes);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import {describe, it, expect, beforeEach, afterEach} from 'vitest';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import {parseFuncCall, buildInlineManifest, resolveManifestSources} from '../commands/server';
|
|
6
|
+
|
|
7
|
+
describe('parseFuncCall', () => {
|
|
8
|
+
it('parses a single string argument', () => {
|
|
9
|
+
expect(parseFuncCall('Chem:smilesToMw("ccc")')).toEqual({
|
|
10
|
+
name: 'Chem:smilesToMw',
|
|
11
|
+
params: {'0': 'ccc'},
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('parses a single-quoted string argument', () => {
|
|
16
|
+
expect(parseFuncCall("Pkg:fn('hello')")).toEqual({
|
|
17
|
+
name: 'Pkg:fn',
|
|
18
|
+
params: {'0': 'hello'},
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('parses a numeric argument', () => {
|
|
23
|
+
expect(parseFuncCall('Pkg:fn(42)')).toEqual({
|
|
24
|
+
name: 'Pkg:fn',
|
|
25
|
+
params: {'0': 42},
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('parses multiple positional arguments', () => {
|
|
30
|
+
expect(parseFuncCall('Pkg:fn(1, "hello", 3.14)')).toEqual({
|
|
31
|
+
name: 'Pkg:fn',
|
|
32
|
+
params: {'0': 1, '1': 'hello', '2': 3.14},
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('parses an object argument with quoted keys', () => {
|
|
37
|
+
expect(parseFuncCall('Pkg:fn({"a":5,"b":22})')).toEqual({
|
|
38
|
+
name: 'Pkg:fn',
|
|
39
|
+
params: {a: 5, b: 22},
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('parses an object argument with unquoted keys', () => {
|
|
44
|
+
expect(parseFuncCall('Pkg:fn({a:5,b:22})')).toEqual({
|
|
45
|
+
name: 'Pkg:fn',
|
|
46
|
+
params: {a: 5, b: 22},
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('parses an object argument with a boolean value', () => {
|
|
51
|
+
expect(parseFuncCall('Pkg:fn({unquoted:true})')).toEqual({
|
|
52
|
+
name: 'Pkg:fn',
|
|
53
|
+
params: {unquoted: true},
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('returns empty params for a no-argument call', () => {
|
|
58
|
+
expect(parseFuncCall('Pkg:fn()')).toEqual({
|
|
59
|
+
name: 'Pkg:fn',
|
|
60
|
+
params: {},
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('returns empty params when there are no parentheses', () => {
|
|
65
|
+
expect(parseFuncCall('Pkg:fn')).toEqual({
|
|
66
|
+
name: 'Pkg:fn',
|
|
67
|
+
params: {},
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('handles a package-less function name', () => {
|
|
72
|
+
expect(parseFuncCall('myFunc("arg")')).toEqual({
|
|
73
|
+
name: 'myFunc',
|
|
74
|
+
params: {'0': 'arg'},
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('buildInlineManifest', () => {
|
|
80
|
+
it('sets action to entity.verb', () => {
|
|
81
|
+
const result = buildInlineManifest('users', 'delete', ['abc']);
|
|
82
|
+
expect(result.operations[0].action).toBe('users.delete');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('maps string args to {id} param for non-file entities', () => {
|
|
86
|
+
const result = buildInlineManifest('users', 'delete', ['id1', 'id2']);
|
|
87
|
+
expect(result.operations).toEqual([
|
|
88
|
+
{id: 'op0', action: 'users.delete', params: {id: 'id1'}},
|
|
89
|
+
{id: 'op1', action: 'users.delete', params: {id: 'id2'}},
|
|
90
|
+
]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('maps string args to {path} param for the files entity', () => {
|
|
94
|
+
const result = buildInlineManifest('files', 'delete', ['System:AppData/a.txt', 'System:AppData/b.txt']);
|
|
95
|
+
expect(result.operations).toEqual([
|
|
96
|
+
{id: 'op0', action: 'files.delete', params: {path: 'System:AppData/a.txt'}},
|
|
97
|
+
{id: 'op1', action: 'files.delete', params: {path: 'System:AppData/b.txt'}},
|
|
98
|
+
]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('passes object array args through as params directly', () => {
|
|
102
|
+
const objs = [{name: 'Alice', email: 'a@x.com'}, {name: 'Bob', email: 'b@x.com'}];
|
|
103
|
+
const result = buildInlineManifest('users', 'create', objs);
|
|
104
|
+
expect(result.operations).toEqual([
|
|
105
|
+
{id: 'op0', action: 'users.create', params: {name: 'Alice', email: 'a@x.com'}},
|
|
106
|
+
{id: 'op1', action: 'users.create', params: {name: 'Bob', email: 'b@x.com'}},
|
|
107
|
+
]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('assigns sequential op ids starting at op0', () => {
|
|
111
|
+
const result = buildInlineManifest('groups', 'delete', ['x', 'y', 'z']);
|
|
112
|
+
expect(result.operations.map((o) => o.id)).toEqual(['op0', 'op1', 'op2']);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('returns a single operation for a single arg', () => {
|
|
116
|
+
const result = buildInlineManifest('connections', 'delete', ['conn-id']);
|
|
117
|
+
expect(result.operations).toHaveLength(1);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('resolveManifestSources', () => {
|
|
122
|
+
let tmpFile: string;
|
|
123
|
+
|
|
124
|
+
beforeEach(() => {
|
|
125
|
+
tmpFile = path.join(os.tmpdir(), `grok-batch-test-${Date.now()}.bin`);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
afterEach(() => {
|
|
129
|
+
if (fs.existsSync(tmpFile)) fs.unlinkSync(tmpFile);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('passes through a manifest with no files.put operations unchanged', () => {
|
|
133
|
+
const manifest = {
|
|
134
|
+
operations: [{id: 'op0', action: 'users.delete', params: {id: 'abc'}}],
|
|
135
|
+
};
|
|
136
|
+
expect(resolveManifestSources(manifest)).toEqual(manifest);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('passes through a files.put operation that has no source field', () => {
|
|
140
|
+
const manifest = {
|
|
141
|
+
operations: [{id: 'op0', action: 'files.put', params: {path: 'System:AppData/f.txt', content: 'aGk='}}],
|
|
142
|
+
};
|
|
143
|
+
expect(resolveManifestSources(manifest)).toEqual(manifest);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('reads the source file, base64-encodes its contents, and removes the source key', () => {
|
|
147
|
+
const data = 'hello world';
|
|
148
|
+
fs.writeFileSync(tmpFile, data);
|
|
149
|
+
|
|
150
|
+
const manifest = {
|
|
151
|
+
operations: [{id: 'op0', action: 'files.put', params: {path: 'System:AppData/f.txt', source: tmpFile}}],
|
|
152
|
+
};
|
|
153
|
+
const result = resolveManifestSources(manifest);
|
|
154
|
+
const params = result.operations[0].params as any;
|
|
155
|
+
expect(params.content).toBe(Buffer.from(data).toString('base64'));
|
|
156
|
+
expect(params.source).toBeUndefined();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('preserves other params alongside the injected content', () => {
|
|
160
|
+
fs.writeFileSync(tmpFile, 'data');
|
|
161
|
+
|
|
162
|
+
const manifest = {
|
|
163
|
+
operations: [{id: 'op0', action: 'files.put', params: {path: 'System:AppData/f.txt', source: tmpFile, extra: 'val'}}],
|
|
164
|
+
};
|
|
165
|
+
const result = resolveManifestSources(manifest);
|
|
166
|
+
const params = result.operations[0].params as any;
|
|
167
|
+
expect(params.path).toBe('System:AppData/f.txt');
|
|
168
|
+
expect(params.extra).toBe('val');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('leaves non-files.put operations untouched in a mixed manifest', () => {
|
|
172
|
+
fs.writeFileSync(tmpFile, 'x');
|
|
173
|
+
|
|
174
|
+
const manifest = {
|
|
175
|
+
operations: [
|
|
176
|
+
{id: 'op0', action: 'users.delete', params: {id: 'u1'}},
|
|
177
|
+
{id: 'op1', action: 'files.put', params: {path: 'System:AppData/f.txt', source: tmpFile}},
|
|
178
|
+
],
|
|
179
|
+
};
|
|
180
|
+
const result = resolveManifestSources(manifest);
|
|
181
|
+
expect(result.operations[0]).toEqual({id: 'op0', action: 'users.delete', params: {id: 'u1'}});
|
|
182
|
+
expect((result.operations[1].params as any).source).toBeUndefined();
|
|
183
|
+
expect((result.operations[1].params as any).content).toBeDefined();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('correctly encodes binary file contents', () => {
|
|
187
|
+
const bytes = Buffer.from([0x00, 0xff, 0x10, 0xab]);
|
|
188
|
+
fs.writeFileSync(tmpFile, bytes);
|
|
189
|
+
|
|
190
|
+
const manifest = {
|
|
191
|
+
operations: [{id: 'op0', action: 'files.put', params: {path: 'System:AppData/f.bin', source: tmpFile}}],
|
|
192
|
+
};
|
|
193
|
+
const result = resolveManifestSources(manifest);
|
|
194
|
+
const decoded = Buffer.from((result.operations[0].params as any).content, 'base64');
|
|
195
|
+
expect(decoded).toEqual(bytes);
|
|
196
|
+
});
|
|
197
|
+
});
|
package/bin/commands/build.js
CHANGED
|
@@ -54,7 +54,7 @@ async function buildRecursive(baseDir, args, buildCmd) {
|
|
|
54
54
|
return false;
|
|
55
55
|
}
|
|
56
56
|
console.log(`Found ${filtered.length} package(s): ${filtered.map(p => p.friendlyName).join(', ')}`);
|
|
57
|
-
if (!args.silent) {
|
|
57
|
+
if (!args.silent && !args.s) {
|
|
58
58
|
const confirmed = await confirm(`\nBuild ${filtered.length} package(s)?`);
|
|
59
59
|
if (!confirmed) {
|
|
60
60
|
console.log('Aborted.');
|
package/bin/commands/create.js
CHANGED
|
@@ -26,7 +26,7 @@ const confTemplate = _jsYaml.default.load(_fs.default.readFileSync(confTemplateD
|
|
|
26
26
|
encoding: 'utf-8'
|
|
27
27
|
}));
|
|
28
28
|
const dependencies = [];
|
|
29
|
-
function createDirectoryContents(name, config, templateDir, packageDir, ide = '', ts = true, eslint = false, test = false) {
|
|
29
|
+
function createDirectoryContents(name, friendlyName, config, templateDir, packageDir, ide = '', ts = true, eslint = false, test = false) {
|
|
30
30
|
const filesToCreate = _fs.default.readdirSync(templateDir);
|
|
31
31
|
filesToCreate.forEach(file => {
|
|
32
32
|
const origFilePath = _path.default.join(templateDir, file);
|
|
@@ -38,6 +38,7 @@ function createDirectoryContents(name, config, templateDir, packageDir, ide = ''
|
|
|
38
38
|
return false;
|
|
39
39
|
}
|
|
40
40
|
let contents = _fs.default.readFileSync(file === 'webpack.config.js' && ts ? _path.default.join(templateDir, 'ts.webpack.config.js') : origFilePath, 'utf8');
|
|
41
|
+
contents = contents.replace(/#{PACKAGE_FRIENDLY_NAME}/g, friendlyName);
|
|
41
42
|
contents = contents.replace(/#{PACKAGE_NAME}/g, name);
|
|
42
43
|
contents = contents.replace(/#{PACKAGE_DETECTORS_NAME}/g, utils.kebabToCamelCase(name));
|
|
43
44
|
contents = contents.replace(/#{PACKAGE_NAME_LOWERCASE}/g, name.toLowerCase());
|
|
@@ -114,7 +115,7 @@ function createDirectoryContents(name, config, templateDir, packageDir, ide = ''
|
|
|
114
115
|
_fs.default.mkdirSync(copyFilePath);
|
|
115
116
|
// recursive call
|
|
116
117
|
if (_path.default.basename(origFilePath) === 'node_modules') return;
|
|
117
|
-
createDirectoryContents(name, config, origFilePath, copyFilePath, ide, ts, eslint, test);
|
|
118
|
+
createDirectoryContents(name, friendlyName, config, origFilePath, copyFilePath, ide, ts, eslint, test);
|
|
118
119
|
}
|
|
119
120
|
});
|
|
120
121
|
}
|
|
@@ -138,8 +139,10 @@ function create(args) {
|
|
|
138
139
|
color.error(confTest.message);
|
|
139
140
|
return false;
|
|
140
141
|
}
|
|
141
|
-
const
|
|
142
|
-
const validName = /^([A-Za-z\-_\d])+$/.test(
|
|
142
|
+
const rawName = nArgs === 2 ? args['_'][1] : curFolder;
|
|
143
|
+
const validName = /^([A-Za-z\-_\d])+$/.test(rawName);
|
|
144
|
+
const friendlyName = rawName;
|
|
145
|
+
const name = rawName.charAt(0).toUpperCase() + rawName.slice(1).toLowerCase();
|
|
143
146
|
if (validName) {
|
|
144
147
|
let packageDir = curDir;
|
|
145
148
|
let repositoryInfo = null;
|
|
@@ -183,7 +186,7 @@ function create(args) {
|
|
|
183
186
|
}
|
|
184
187
|
process.exit();
|
|
185
188
|
});
|
|
186
|
-
createDirectoryContents(name, config, templateDir, packageDir, args.ide, ts, !!args.eslint, !!args.test);
|
|
189
|
+
createDirectoryContents(name, friendlyName, config, templateDir, packageDir, args.ide, ts, !!args.eslint, !!args.test);
|
|
187
190
|
color.success('Successfully created package ' + name);
|
|
188
191
|
console.log(_entHelpers.help.package(ts));
|
|
189
192
|
console.log(`\nThe package has the following dependencies:\n${dependencies.join(' ')}\n`);
|
package/bin/commands/help.js
CHANGED
|
@@ -13,7 +13,7 @@ Commands:
|
|
|
13
13
|
add Add an object template
|
|
14
14
|
api Create wrapper functions
|
|
15
15
|
build Build a package or multiple packages
|
|
16
|
-
check Check package content (function signatures,
|
|
16
|
+
check Check package content (function signatures, etgc.)
|
|
17
17
|
claude Launch Claude Code in a Datagrok dev container
|
|
18
18
|
config Create and manage config files
|
|
19
19
|
create Create a package
|
|
@@ -26,6 +26,7 @@ Commands:
|
|
|
26
26
|
test Run package tests
|
|
27
27
|
testall Run packages tests
|
|
28
28
|
migrate Migrate legacy tags to meta.role
|
|
29
|
+
server (s) Manage a Datagrok server (list/get/delete entities, run functions)
|
|
29
30
|
|
|
30
31
|
To get help on a particular command, use:
|
|
31
32
|
grok <command> --help
|
|
@@ -359,6 +360,63 @@ Examples:
|
|
|
359
360
|
|
|
360
361
|
The instance name must match a server alias in ~/.grok/config.yaml.
|
|
361
362
|
`;
|
|
363
|
+
const HELP_SERVER = `
|
|
364
|
+
Usage: grok server <entity> <verb> [args] [options]
|
|
365
|
+
grok s <entity> <verb> [args] [options]
|
|
366
|
+
|
|
367
|
+
Manage a Datagrok server from the command line.
|
|
368
|
+
|
|
369
|
+
Entities:
|
|
370
|
+
users, groups, functions, connections, queries, scripts, packages, reports, files
|
|
371
|
+
|
|
372
|
+
Verbs:
|
|
373
|
+
list List entities
|
|
374
|
+
get Get a single entity by ID or name
|
|
375
|
+
delete Delete an entity by ID
|
|
376
|
+
|
|
377
|
+
Special commands:
|
|
378
|
+
grok s functions run <Name:func(args)> Call a function
|
|
379
|
+
grok s files list [path] [-r] List files (recursive with -r)
|
|
380
|
+
grok s shares add <entity> <group>[,<group>...] [--access View|Edit]
|
|
381
|
+
Share an entity with groups
|
|
382
|
+
grok s shares list <entity-id> List who an entity (UUID) is shared with
|
|
383
|
+
grok s users save --json user.json Create or update a user from JSON
|
|
384
|
+
grok s groups save --json group.json [--save-relations]
|
|
385
|
+
Create or update a group from JSON
|
|
386
|
+
grok s connections save --json conn.json [--save-credentials]
|
|
387
|
+
Create or update a connection from JSON
|
|
388
|
+
grok s connections test <id-or-name> Test connectivity of an existing connection
|
|
389
|
+
grok s connections test --json conn.json Test connectivity of a connection defined in JSON
|
|
390
|
+
grok s raw <METHOD> <path> Hit any API endpoint
|
|
391
|
+
grok s describe <entity-type> Show entity JSON schema
|
|
392
|
+
|
|
393
|
+
Options:
|
|
394
|
+
--host <alias|url> Server alias from config or full URL
|
|
395
|
+
--output <format> Output format: table (default), json, csv, quiet
|
|
396
|
+
--filter <text> Smart filter expression
|
|
397
|
+
--limit <n> Page size (default: 50)
|
|
398
|
+
--offset <n> Start offset (default: 0)
|
|
399
|
+
-r, --recursive Recursive (for files list)
|
|
400
|
+
--json <file> Read function parameters from JSON file
|
|
401
|
+
|
|
402
|
+
Examples:
|
|
403
|
+
grok s users list
|
|
404
|
+
grok s connections list --filter "PostgreSQL" --output json
|
|
405
|
+
grok s connections get <id>
|
|
406
|
+
grok s connections delete <id>
|
|
407
|
+
grok s connections save --json conn.json --save-credentials
|
|
408
|
+
grok s connections test "JohnDoe:MyConnection"
|
|
409
|
+
grok s connections test --json conn.json
|
|
410
|
+
grok s users save --json user.json
|
|
411
|
+
grok s groups save --json group.json --save-relations
|
|
412
|
+
grok s shares add "JohnDoe:MyConnection" Chemists,Admins --access Edit
|
|
413
|
+
grok s shares list <entity-uuid>
|
|
414
|
+
grok s functions run 'Chem:smilesToMw("ccc")'
|
|
415
|
+
grok s files list "System:AppData" -r
|
|
416
|
+
grok s raw GET /api/users/current
|
|
417
|
+
grok s describe connections
|
|
418
|
+
grok s users list --host dev
|
|
419
|
+
`;
|
|
362
420
|
const help = exports.help = {
|
|
363
421
|
add: HELP_ADD,
|
|
364
422
|
api: HELP_API,
|
|
@@ -376,5 +434,7 @@ const help = exports.help = {
|
|
|
376
434
|
test: HELP_TEST,
|
|
377
435
|
testall: HELP_TESTALL,
|
|
378
436
|
migrate: HELP_MIGRATE,
|
|
437
|
+
server: HELP_SERVER,
|
|
438
|
+
s: HELP_SERVER,
|
|
379
439
|
help: HELP
|
|
380
440
|
};
|