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,120 @@
|
|
|
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 CONN = {
|
|
32
|
+
name: 'TestPg',
|
|
33
|
+
dataSource: 'PostgreSQL',
|
|
34
|
+
server: 'localhost',
|
|
35
|
+
port: 5432,
|
|
36
|
+
db: 'datagrok'
|
|
37
|
+
};
|
|
38
|
+
(0, _vitest.describe)('NodeConnectionsDataSource.save', () => {
|
|
39
|
+
(0, _vitest.it)('POSTs to /public/v1/connections without saveCredentials by default', async () => {
|
|
40
|
+
const {
|
|
41
|
+
client,
|
|
42
|
+
calls
|
|
43
|
+
} = makeMock((method, path, body) => {
|
|
44
|
+
if (method === 'POST' && path === '/public/v1/connections') return {
|
|
45
|
+
...body,
|
|
46
|
+
id: 'new-id'
|
|
47
|
+
};
|
|
48
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
49
|
+
});
|
|
50
|
+
const ds = new _nodeDapi.NodeConnectionsDataSource(client);
|
|
51
|
+
const saved = await ds.save(CONN);
|
|
52
|
+
(0, _vitest.expect)(saved.id).toBe('new-id');
|
|
53
|
+
(0, _vitest.expect)(calls[0].path).toBe('/public/v1/connections');
|
|
54
|
+
(0, _vitest.expect)(calls[0].body).toEqual(CONN);
|
|
55
|
+
});
|
|
56
|
+
(0, _vitest.it)('appends saveCredentials=true when requested', async () => {
|
|
57
|
+
const {
|
|
58
|
+
client,
|
|
59
|
+
calls
|
|
60
|
+
} = makeMock((method, path, body) => {
|
|
61
|
+
if (method === 'POST' && path === '/public/v1/connections?saveCredentials=true') return {
|
|
62
|
+
...body,
|
|
63
|
+
id: 'new-id'
|
|
64
|
+
};
|
|
65
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
66
|
+
});
|
|
67
|
+
const ds = new _nodeDapi.NodeConnectionsDataSource(client);
|
|
68
|
+
await ds.save(CONN, true);
|
|
69
|
+
(0, _vitest.expect)(calls[0].path).toBe('/public/v1/connections?saveCredentials=true');
|
|
70
|
+
});
|
|
71
|
+
(0, _vitest.it)('omits saveCredentials query param when false', async () => {
|
|
72
|
+
const {
|
|
73
|
+
client,
|
|
74
|
+
calls
|
|
75
|
+
} = makeMock((method, path) => {
|
|
76
|
+
if (method === 'POST' && path === '/public/v1/connections') return {
|
|
77
|
+
id: 'new-id'
|
|
78
|
+
};
|
|
79
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
80
|
+
});
|
|
81
|
+
const ds = new _nodeDapi.NodeConnectionsDataSource(client);
|
|
82
|
+
await ds.save(CONN, false);
|
|
83
|
+
(0, _vitest.expect)(calls[0].path).toBe('/public/v1/connections');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
(0, _vitest.describe)('NodeConnectionsDataSource.test', () => {
|
|
87
|
+
(0, _vitest.it)('POSTs the connection body to /public/v1/connections/test and resolves on "ok"', async () => {
|
|
88
|
+
const {
|
|
89
|
+
client,
|
|
90
|
+
calls
|
|
91
|
+
} = makeMock((method, path) => {
|
|
92
|
+
if (method === 'POST' && path === '/public/v1/connections/test') return 'ok';
|
|
93
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
94
|
+
});
|
|
95
|
+
const ds = new _nodeDapi.NodeConnectionsDataSource(client);
|
|
96
|
+
await ds.test(CONN);
|
|
97
|
+
(0, _vitest.expect)(calls[0].body).toEqual(CONN);
|
|
98
|
+
});
|
|
99
|
+
(0, _vitest.it)('strips surrounding quotes on a JSON-encoded "ok"', async () => {
|
|
100
|
+
const {
|
|
101
|
+
client
|
|
102
|
+
} = makeMock(() => '"ok"');
|
|
103
|
+
const ds = new _nodeDapi.NodeConnectionsDataSource(client);
|
|
104
|
+
await (0, _vitest.expect)(ds.test(CONN)).resolves.toBeUndefined();
|
|
105
|
+
});
|
|
106
|
+
(0, _vitest.it)('throws the server error text when the response is not ok', async () => {
|
|
107
|
+
const {
|
|
108
|
+
client
|
|
109
|
+
} = makeMock(() => 'connection refused: tcp://localhost:5432');
|
|
110
|
+
const ds = new _nodeDapi.NodeConnectionsDataSource(client);
|
|
111
|
+
await (0, _vitest.expect)(ds.test(CONN)).rejects.toThrow(/connection refused/);
|
|
112
|
+
});
|
|
113
|
+
(0, _vitest.it)('throws a generic message on empty response', async () => {
|
|
114
|
+
const {
|
|
115
|
+
client
|
|
116
|
+
} = makeMock(() => '');
|
|
117
|
+
const ds = new _nodeDapi.NodeConnectionsDataSource(client);
|
|
118
|
+
await (0, _vitest.expect)(ds.test(CONN)).rejects.toThrow(/failed/i);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {describe, it, expect} from 'vitest';
|
|
2
|
+
import {NodeConnectionsDataSource} from '../utils/node-dapi';
|
|
3
|
+
|
|
4
|
+
interface Call {method: string; path: string; body?: any}
|
|
5
|
+
|
|
6
|
+
function makeMock(responder: (method: string, path: string, body?: any) => any) {
|
|
7
|
+
const calls: Call[] = [];
|
|
8
|
+
const client: any = {
|
|
9
|
+
async request(method: string, path: string, body?: any) {
|
|
10
|
+
calls.push({method, path, body});
|
|
11
|
+
return responder(method, path, body);
|
|
12
|
+
},
|
|
13
|
+
get(path: string) { return this.request('GET', path); },
|
|
14
|
+
post(path: string, body?: any) { return this.request('POST', path, body); },
|
|
15
|
+
del(path: string) { return this.request('DELETE', path); },
|
|
16
|
+
};
|
|
17
|
+
return {client, calls};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const CONN = {name: 'TestPg', dataSource: 'PostgreSQL', server: 'localhost', port: 5432, db: 'datagrok'};
|
|
21
|
+
|
|
22
|
+
describe('NodeConnectionsDataSource.save', () => {
|
|
23
|
+
it('POSTs to /public/v1/connections without saveCredentials by default', async () => {
|
|
24
|
+
const {client, calls} = makeMock((method, path, body) => {
|
|
25
|
+
if (method === 'POST' && path === '/public/v1/connections') return {...body, id: 'new-id'};
|
|
26
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
27
|
+
});
|
|
28
|
+
const ds = new NodeConnectionsDataSource(client);
|
|
29
|
+
const saved = await ds.save(CONN);
|
|
30
|
+
expect(saved.id).toBe('new-id');
|
|
31
|
+
expect(calls[0].path).toBe('/public/v1/connections');
|
|
32
|
+
expect(calls[0].body).toEqual(CONN);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('appends saveCredentials=true when requested', async () => {
|
|
36
|
+
const {client, calls} = makeMock((method, path, body) => {
|
|
37
|
+
if (method === 'POST' && path === '/public/v1/connections?saveCredentials=true') return {...body, id: 'new-id'};
|
|
38
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
39
|
+
});
|
|
40
|
+
const ds = new NodeConnectionsDataSource(client);
|
|
41
|
+
await ds.save(CONN, true);
|
|
42
|
+
expect(calls[0].path).toBe('/public/v1/connections?saveCredentials=true');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('omits saveCredentials query param when false', async () => {
|
|
46
|
+
const {client, calls} = makeMock((method, path) => {
|
|
47
|
+
if (method === 'POST' && path === '/public/v1/connections') return {id: 'new-id'};
|
|
48
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
49
|
+
});
|
|
50
|
+
const ds = new NodeConnectionsDataSource(client);
|
|
51
|
+
await ds.save(CONN, false);
|
|
52
|
+
expect(calls[0].path).toBe('/public/v1/connections');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('NodeConnectionsDataSource.test', () => {
|
|
57
|
+
it('POSTs the connection body to /public/v1/connections/test and resolves on "ok"', async () => {
|
|
58
|
+
const {client, calls} = makeMock((method, path) => {
|
|
59
|
+
if (method === 'POST' && path === '/public/v1/connections/test') return 'ok';
|
|
60
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
61
|
+
});
|
|
62
|
+
const ds = new NodeConnectionsDataSource(client);
|
|
63
|
+
await ds.test(CONN);
|
|
64
|
+
expect(calls[0].body).toEqual(CONN);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('strips surrounding quotes on a JSON-encoded "ok"', async () => {
|
|
68
|
+
const {client} = makeMock(() => '"ok"');
|
|
69
|
+
const ds = new NodeConnectionsDataSource(client);
|
|
70
|
+
await expect(ds.test(CONN)).resolves.toBeUndefined();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('throws the server error text when the response is not ok', async () => {
|
|
74
|
+
const {client} = makeMock(() => 'connection refused: tcp://localhost:5432');
|
|
75
|
+
const ds = new NodeConnectionsDataSource(client);
|
|
76
|
+
await expect(ds.test(CONN)).rejects.toThrow(/connection refused/);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('throws a generic message on empty response', async () => {
|
|
80
|
+
const {client} = makeMock(() => '');
|
|
81
|
+
const ds = new NodeConnectionsDataSource(client);
|
|
82
|
+
await expect(ds.test(CONN)).rejects.toThrow(/failed/i);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -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
|
+
});
|