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.
@@ -0,0 +1,467 @@
1
+ "use strict";
2
+
3
+ var _vitest = require("vitest");
4
+ var _nodeDapi = require("../utils/node-dapi");
5
+ function makeMock(responder) {
6
+ const calls = [];
7
+ const client = {
8
+ async request(method, path, body) {
9
+ calls.push({
10
+ method,
11
+ path,
12
+ body
13
+ });
14
+ return responder(method, path, body);
15
+ },
16
+ get(path) {
17
+ return this.request('GET', path);
18
+ },
19
+ post(path, body) {
20
+ return this.request('POST', path, body);
21
+ },
22
+ del(path) {
23
+ return this.request('DELETE', path);
24
+ }
25
+ };
26
+ return {
27
+ client,
28
+ calls
29
+ };
30
+ }
31
+ const PARENT_ID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';
32
+ const ALICE_ID = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb';
33
+ const BOB_ID = 'cccccccc-cccc-cccc-cccc-cccccccccccc';
34
+ function makeParent(children = []) {
35
+ return {
36
+ id: PARENT_ID,
37
+ friendlyName: 'Admins',
38
+ children
39
+ };
40
+ }
41
+ (0, _vitest.describe)('NodeGroupsDataSource.resolve', () => {
42
+ (0, _vitest.it)('treats a UUID as a direct ID and calls find()', async () => {
43
+ const {
44
+ client,
45
+ calls
46
+ } = makeMock((_m, path) => {
47
+ if (path.startsWith(`/public/v1/groups/${PARENT_ID}`)) return makeParent();
48
+ throw new Error(`unexpected ${path}`);
49
+ });
50
+ const ds = new _nodeDapi.NodeGroupsDataSource(client);
51
+ const g = await ds.resolve(PARENT_ID);
52
+ (0, _vitest.expect)(g.id).toBe(PARENT_ID);
53
+ (0, _vitest.expect)(calls[0].path).toBe(`/public/v1/groups/${PARENT_ID}`);
54
+ });
55
+ (0, _vitest.it)('uses /lookup for non-UUID names and returns the unique match', async () => {
56
+ const {
57
+ client,
58
+ calls
59
+ } = makeMock((_m, path) => {
60
+ if (path.startsWith('/public/v1/groups/lookup')) return [{
61
+ id: ALICE_ID,
62
+ name: 'alice',
63
+ friendlyName: 'alice',
64
+ personal: true
65
+ }];
66
+ throw new Error(`unexpected ${path}`);
67
+ });
68
+ const ds = new _nodeDapi.NodeGroupsDataSource(client);
69
+ const g = await ds.resolve('alice');
70
+ (0, _vitest.expect)(g.id).toBe(ALICE_ID);
71
+ (0, _vitest.expect)(calls[0].path).toBe('/public/v1/groups/lookup?query=alice');
72
+ });
73
+ (0, _vitest.it)('errors with a list when lookup returns multiple matches', async () => {
74
+ const {
75
+ client
76
+ } = makeMock((_m, path) => {
77
+ if (path.startsWith('/public/v1/groups/lookup')) return [{
78
+ id: 'g1',
79
+ friendlyName: 'alice',
80
+ personal: false
81
+ }, {
82
+ id: 'g2',
83
+ friendlyName: 'alice',
84
+ personal: true
85
+ }];
86
+ throw new Error(`unexpected ${path}`);
87
+ });
88
+ const ds = new _nodeDapi.NodeGroupsDataSource(client);
89
+ await (0, _vitest.expect)(ds.resolve('alice')).rejects.toThrow(/Multiple groups match 'alice'/);
90
+ });
91
+ (0, _vitest.it)('personalOnly filters lookup results to personal groups', async () => {
92
+ const {
93
+ client
94
+ } = makeMock((_m, path) => {
95
+ if (path.startsWith('/public/v1/groups/lookup')) return [{
96
+ id: 'g1',
97
+ friendlyName: 'alice',
98
+ personal: false
99
+ }, {
100
+ id: ALICE_ID,
101
+ friendlyName: 'alice',
102
+ personal: true
103
+ }];
104
+ throw new Error(`unexpected ${path}`);
105
+ });
106
+ const ds = new _nodeDapi.NodeGroupsDataSource(client);
107
+ const g = await ds.resolve('alice', {
108
+ personalOnly: true
109
+ });
110
+ (0, _vitest.expect)(g.id).toBe(ALICE_ID);
111
+ });
112
+ (0, _vitest.it)('errors when personalOnly yields no matches', async () => {
113
+ const {
114
+ client
115
+ } = makeMock((_m, path) => {
116
+ if (path.startsWith('/public/v1/groups/lookup')) return [{
117
+ id: 'g1',
118
+ friendlyName: 'alice',
119
+ personal: false
120
+ }];
121
+ throw new Error(`unexpected ${path}`);
122
+ });
123
+ const ds = new _nodeDapi.NodeGroupsDataSource(client);
124
+ await (0, _vitest.expect)(ds.resolve('alice', {
125
+ personalOnly: true
126
+ })).rejects.toThrow(/No group matching 'alice' \(personal\)/);
127
+ });
128
+ });
129
+ (0, _vitest.describe)('NodeGroupsDataSource.save', () => {
130
+ (0, _vitest.it)('POSTs the group body to /public/v1/groups and auto-generates an id when missing', async () => {
131
+ const {
132
+ client,
133
+ calls
134
+ } = makeMock((method, path, body) => {
135
+ if (method === 'POST' && path === '/public/v1/groups') return {
136
+ ...body,
137
+ friendlyName: 'Chemists'
138
+ };
139
+ throw new Error(`unexpected ${method} ${path}`);
140
+ });
141
+ const ds = new _nodeDapi.NodeGroupsDataSource(client);
142
+ await ds.save({
143
+ friendlyName: 'Chemists'
144
+ });
145
+ (0, _vitest.expect)(calls[0].path).toBe('/public/v1/groups');
146
+ (0, _vitest.expect)(calls[0].body.id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
147
+ });
148
+ (0, _vitest.it)('appends saveRelations=true when requested', async () => {
149
+ const {
150
+ client,
151
+ calls
152
+ } = makeMock((method, path) => {
153
+ if (method === 'POST' && path === '/public/v1/groups?saveRelations=true') return {
154
+ id: 'g'
155
+ };
156
+ throw new Error(`unexpected ${method} ${path}`);
157
+ });
158
+ const ds = new _nodeDapi.NodeGroupsDataSource(client);
159
+ await ds.save({
160
+ id: 'g',
161
+ friendlyName: 'Chemists'
162
+ }, true);
163
+ (0, _vitest.expect)(calls[0].path).toBe('/public/v1/groups?saveRelations=true');
164
+ });
165
+ });
166
+ (0, _vitest.describe)('NodeGroupsDataSource.addMembers', () => {
167
+ (0, _vitest.it)('appends new relations and POSTs with saveRelations=true', async () => {
168
+ const {
169
+ client,
170
+ calls
171
+ } = makeMock((method, path, _body) => {
172
+ if (method === 'GET' && path === `/public/v1/groups/${PARENT_ID}`) return makeParent();
173
+ if (method === 'GET' && path.startsWith('/public/v1/groups/lookup')) return [{
174
+ id: ALICE_ID,
175
+ friendlyName: 'alice',
176
+ personal: true
177
+ }];
178
+ if (method === 'POST' && path === '/public/v1/groups?saveRelations=true') return {
179
+ id: PARENT_ID
180
+ };
181
+ throw new Error(`unexpected ${method} ${path}`);
182
+ });
183
+ const ds = new _nodeDapi.NodeGroupsDataSource(client);
184
+ const results = await ds.addMembers(PARENT_ID, ['alice']);
185
+ (0, _vitest.expect)(results).toEqual([{
186
+ member: 'alice',
187
+ status: 'added'
188
+ }]);
189
+ const post = calls.find(c => c.method === 'POST');
190
+ (0, _vitest.expect)(post.path).toBe('/public/v1/groups?saveRelations=true');
191
+ (0, _vitest.expect)(post.body.children).toEqual([{
192
+ parent: {
193
+ id: PARENT_ID
194
+ },
195
+ child: {
196
+ id: ALICE_ID
197
+ },
198
+ isAdmin: false
199
+ }]);
200
+ });
201
+ (0, _vitest.it)('supports --admin by setting isAdmin on the new relation', async () => {
202
+ const {
203
+ client,
204
+ calls
205
+ } = makeMock((method, path) => {
206
+ if (method === 'GET' && path === `/public/v1/groups/${PARENT_ID}`) return makeParent();
207
+ if (method === 'GET' && path.startsWith('/public/v1/groups/lookup')) return [{
208
+ id: ALICE_ID,
209
+ friendlyName: 'alice',
210
+ personal: true
211
+ }];
212
+ if (method === 'POST') return {
213
+ id: PARENT_ID
214
+ };
215
+ throw new Error(`unexpected ${method} ${path}`);
216
+ });
217
+ const ds = new _nodeDapi.NodeGroupsDataSource(client);
218
+ await ds.addMembers(PARENT_ID, ['alice'], true);
219
+ const post = calls.find(c => c.method === 'POST');
220
+ (0, _vitest.expect)(post.body.children[0].isAdmin).toBe(true);
221
+ });
222
+ (0, _vitest.it)('is idempotent: re-adding with the same role reports noop and skips POST', async () => {
223
+ const existing = {
224
+ parent: {
225
+ id: PARENT_ID
226
+ },
227
+ child: {
228
+ id: ALICE_ID
229
+ },
230
+ isAdmin: false
231
+ };
232
+ const {
233
+ client,
234
+ calls
235
+ } = makeMock((method, path) => {
236
+ if (method === 'GET' && path === `/public/v1/groups/${PARENT_ID}`) return makeParent([existing]);
237
+ if (method === 'GET' && path.startsWith('/public/v1/groups/lookup')) return [{
238
+ id: ALICE_ID,
239
+ friendlyName: 'alice',
240
+ personal: true
241
+ }];
242
+ if (method === 'POST') return {
243
+ id: PARENT_ID
244
+ };
245
+ throw new Error(`unexpected ${method} ${path}`);
246
+ });
247
+ const ds = new _nodeDapi.NodeGroupsDataSource(client);
248
+ const results = await ds.addMembers(PARENT_ID, ['alice']);
249
+ (0, _vitest.expect)(results).toEqual([{
250
+ member: 'alice',
251
+ status: 'noop'
252
+ }]);
253
+ (0, _vitest.expect)(calls.find(c => c.method === 'POST')).toBeUndefined();
254
+ });
255
+ (0, _vitest.it)('flips isAdmin when re-adding an existing member with a different role', async () => {
256
+ const existing = {
257
+ parent: {
258
+ id: PARENT_ID
259
+ },
260
+ child: {
261
+ id: ALICE_ID
262
+ },
263
+ isAdmin: false
264
+ };
265
+ const {
266
+ client,
267
+ calls
268
+ } = makeMock((method, path) => {
269
+ if (method === 'GET' && path === `/public/v1/groups/${PARENT_ID}`) return makeParent([existing]);
270
+ if (method === 'GET' && path.startsWith('/public/v1/groups/lookup')) return [{
271
+ id: ALICE_ID,
272
+ friendlyName: 'alice',
273
+ personal: true
274
+ }];
275
+ if (method === 'POST') return {
276
+ id: PARENT_ID
277
+ };
278
+ throw new Error(`unexpected ${method} ${path}`);
279
+ });
280
+ const ds = new _nodeDapi.NodeGroupsDataSource(client);
281
+ const results = await ds.addMembers(PARENT_ID, ['alice'], true);
282
+ (0, _vitest.expect)(results).toEqual([{
283
+ member: 'alice',
284
+ status: 'updated'
285
+ }]);
286
+ const post = calls.find(c => c.method === 'POST');
287
+ (0, _vitest.expect)(post.body.children).toHaveLength(1);
288
+ (0, _vitest.expect)(post.body.children[0].isAdmin).toBe(true);
289
+ });
290
+ (0, _vitest.it)('processes a mixed batch: one valid, one unresolvable', async () => {
291
+ const {
292
+ client,
293
+ calls
294
+ } = makeMock((method, path) => {
295
+ if (method === 'GET' && path === `/public/v1/groups/${PARENT_ID}`) return makeParent();
296
+ if (method === 'GET' && path === '/public/v1/groups/lookup?query=alice') return [{
297
+ id: ALICE_ID,
298
+ friendlyName: 'alice',
299
+ personal: true
300
+ }];
301
+ if (method === 'GET' && path === '/public/v1/groups/lookup?query=nobody') return [];
302
+ if (method === 'POST') return {
303
+ id: PARENT_ID
304
+ };
305
+ throw new Error(`unexpected ${method} ${path}`);
306
+ });
307
+ const ds = new _nodeDapi.NodeGroupsDataSource(client);
308
+ const results = await ds.addMembers(PARENT_ID, ['alice', 'nobody']);
309
+ (0, _vitest.expect)(results[0]).toEqual({
310
+ member: 'alice',
311
+ status: 'added'
312
+ });
313
+ (0, _vitest.expect)(results[1].member).toBe('nobody');
314
+ (0, _vitest.expect)(results[1].status).toBe('error');
315
+ (0, _vitest.expect)(results[1].error).toMatch(/No group matching 'nobody'/);
316
+ // POST should still happen for the valid member
317
+ const post = calls.find(c => c.method === 'POST');
318
+ (0, _vitest.expect)(post.body.children).toHaveLength(1);
319
+ (0, _vitest.expect)(post.body.children[0].child.id).toBe(ALICE_ID);
320
+ });
321
+ (0, _vitest.it)('handles multiple members in a single call', async () => {
322
+ const {
323
+ client,
324
+ calls
325
+ } = makeMock((method, path) => {
326
+ if (method === 'GET' && path === `/public/v1/groups/${PARENT_ID}`) return makeParent();
327
+ if (method === 'GET' && path === '/public/v1/groups/lookup?query=alice') return [{
328
+ id: ALICE_ID,
329
+ friendlyName: 'alice',
330
+ personal: true
331
+ }];
332
+ if (method === 'GET' && path === '/public/v1/groups/lookup?query=bob') return [{
333
+ id: BOB_ID,
334
+ friendlyName: 'bob',
335
+ personal: true
336
+ }];
337
+ if (method === 'POST') return {
338
+ id: PARENT_ID
339
+ };
340
+ throw new Error(`unexpected ${method} ${path}`);
341
+ });
342
+ const ds = new _nodeDapi.NodeGroupsDataSource(client);
343
+ const results = await ds.addMembers(PARENT_ID, ['alice', 'bob']);
344
+ (0, _vitest.expect)(results.map(r => r.status)).toEqual(['added', 'added']);
345
+ const post = calls.find(c => c.method === 'POST');
346
+ (0, _vitest.expect)(post.body.children.map(c => c.child.id)).toEqual([ALICE_ID, BOB_ID]);
347
+ // exactly one POST regardless of batch size
348
+ (0, _vitest.expect)(calls.filter(c => c.method === 'POST')).toHaveLength(1);
349
+ });
350
+ });
351
+ (0, _vitest.describe)('NodeGroupsDataSource.removeMembers', () => {
352
+ (0, _vitest.it)('filters the relation out and POSTs with saveRelations=true', async () => {
353
+ const existing = {
354
+ parent: {
355
+ id: PARENT_ID
356
+ },
357
+ child: {
358
+ id: ALICE_ID
359
+ },
360
+ isAdmin: false
361
+ };
362
+ const {
363
+ client,
364
+ calls
365
+ } = makeMock((method, path) => {
366
+ if (method === 'GET' && path === `/public/v1/groups/${PARENT_ID}`) return makeParent([existing]);
367
+ if (method === 'GET' && path.startsWith('/public/v1/groups/lookup')) return [{
368
+ id: ALICE_ID,
369
+ friendlyName: 'alice',
370
+ personal: true
371
+ }];
372
+ if (method === 'POST') return {
373
+ id: PARENT_ID
374
+ };
375
+ throw new Error(`unexpected ${method} ${path}`);
376
+ });
377
+ const ds = new _nodeDapi.NodeGroupsDataSource(client);
378
+ const results = await ds.removeMembers(PARENT_ID, ['alice']);
379
+ (0, _vitest.expect)(results).toEqual([{
380
+ member: 'alice',
381
+ status: 'removed'
382
+ }]);
383
+ const post = calls.find(c => c.method === 'POST');
384
+ (0, _vitest.expect)(post.path).toBe('/public/v1/groups?saveRelations=true');
385
+ (0, _vitest.expect)(post.body.children).toEqual([]);
386
+ });
387
+ (0, _vitest.it)('reports not-member and skips POST when the member isn\'t in the group', async () => {
388
+ const {
389
+ client,
390
+ calls
391
+ } = makeMock((method, path) => {
392
+ if (method === 'GET' && path === `/public/v1/groups/${PARENT_ID}`) return makeParent();
393
+ if (method === 'GET' && path.startsWith('/public/v1/groups/lookup')) return [{
394
+ id: ALICE_ID,
395
+ friendlyName: 'alice',
396
+ personal: true
397
+ }];
398
+ if (method === 'POST') return {
399
+ id: PARENT_ID
400
+ };
401
+ throw new Error(`unexpected ${method} ${path}`);
402
+ });
403
+ const ds = new _nodeDapi.NodeGroupsDataSource(client);
404
+ const results = await ds.removeMembers(PARENT_ID, ['alice']);
405
+ (0, _vitest.expect)(results).toEqual([{
406
+ member: 'alice',
407
+ status: 'not-member'
408
+ }]);
409
+ (0, _vitest.expect)(calls.find(c => c.method === 'POST')).toBeUndefined();
410
+ });
411
+ });
412
+ (0, _vitest.describe)('NodeGroupsDataSource.getMembers', () => {
413
+ (0, _vitest.it)('calls /members without an admin query when admin is undefined', async () => {
414
+ const {
415
+ client,
416
+ calls
417
+ } = makeMock((method, path) => {
418
+ if (method === 'GET' && path === `/public/v1/groups/${PARENT_ID}`) return makeParent();
419
+ if (method === 'GET' && path === `/public/v1/groups/${PARENT_ID}/members`) return [];
420
+ throw new Error(`unexpected ${method} ${path}`);
421
+ });
422
+ const ds = new _nodeDapi.NodeGroupsDataSource(client);
423
+ await ds.getMembers(PARENT_ID);
424
+ (0, _vitest.expect)(calls.map(c => c.path)).toContain(`/public/v1/groups/${PARENT_ID}/members`);
425
+ });
426
+ (0, _vitest.it)('passes admin=true', async () => {
427
+ const {
428
+ client,
429
+ calls
430
+ } = makeMock((method, path) => {
431
+ if (method === 'GET' && path === `/public/v1/groups/${PARENT_ID}`) return makeParent();
432
+ if (method === 'GET' && path === `/public/v1/groups/${PARENT_ID}/members?admin=true`) return [];
433
+ throw new Error(`unexpected ${method} ${path}`);
434
+ });
435
+ const ds = new _nodeDapi.NodeGroupsDataSource(client);
436
+ await ds.getMembers(PARENT_ID, true);
437
+ (0, _vitest.expect)(calls.map(c => c.path)).toContain(`/public/v1/groups/${PARENT_ID}/members?admin=true`);
438
+ });
439
+ (0, _vitest.it)('passes admin=false', async () => {
440
+ const {
441
+ client,
442
+ calls
443
+ } = makeMock((method, path) => {
444
+ if (method === 'GET' && path === `/public/v1/groups/${PARENT_ID}`) return makeParent();
445
+ if (method === 'GET' && path === `/public/v1/groups/${PARENT_ID}/members?admin=false`) return [];
446
+ throw new Error(`unexpected ${method} ${path}`);
447
+ });
448
+ const ds = new _nodeDapi.NodeGroupsDataSource(client);
449
+ await ds.getMembers(PARENT_ID, false);
450
+ (0, _vitest.expect)(calls.map(c => c.path)).toContain(`/public/v1/groups/${PARENT_ID}/members?admin=false`);
451
+ });
452
+ });
453
+ (0, _vitest.describe)('NodeGroupsDataSource.getMemberships', () => {
454
+ (0, _vitest.it)('calls /memberships with admin flag when provided', async () => {
455
+ const {
456
+ client,
457
+ calls
458
+ } = makeMock((method, path) => {
459
+ if (method === 'GET' && path === `/public/v1/groups/${PARENT_ID}`) return makeParent();
460
+ if (method === 'GET' && path === `/public/v1/groups/${PARENT_ID}/memberships?admin=true`) return [];
461
+ throw new Error(`unexpected ${method} ${path}`);
462
+ });
463
+ const ds = new _nodeDapi.NodeGroupsDataSource(client);
464
+ await ds.getMemberships(PARENT_ID, true);
465
+ (0, _vitest.expect)(calls.map(c => c.path)).toContain(`/public/v1/groups/${PARENT_ID}/memberships?admin=true`);
466
+ });
467
+ });