@unito/integration-cli 0.65.0 → 0.67.0
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.
|
@@ -16,6 +16,9 @@ export default class Graph extends BaseCommand<typeof Graph> {
|
|
|
16
16
|
'credential-id': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
17
17
|
'config-path': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
18
18
|
output: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
19
|
+
body: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
20
|
+
'body-file': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
21
|
+
yes: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
19
22
|
};
|
|
20
23
|
run(): Promise<void>;
|
|
21
24
|
}
|
|
@@ -4,6 +4,7 @@ const tslib_1 = require("tslib");
|
|
|
4
4
|
const crypto_1 = tslib_1.__importDefault(require("crypto"));
|
|
5
5
|
const fs_1 = tslib_1.__importDefault(require("fs"));
|
|
6
6
|
const core_1 = require("@oclif/core");
|
|
7
|
+
const inquirer_1 = tslib_1.__importDefault(require("inquirer"));
|
|
7
8
|
const baseCommand_1 = require("../baseCommand");
|
|
8
9
|
const errors_1 = require("../errors");
|
|
9
10
|
const GlobalConfiguration = tslib_1.__importStar(require("../resources/globalConfiguration"));
|
|
@@ -11,11 +12,22 @@ const integrations_1 = require("../resources/integrations");
|
|
|
11
12
|
const configuration_1 = require("../resources/configuration");
|
|
12
13
|
const decryption_1 = require("../resources/decryption");
|
|
13
14
|
const credentials_1 = require("../resources/credentials");
|
|
15
|
+
const METHOD_MAP = {
|
|
16
|
+
get: 'GET',
|
|
17
|
+
getItem: 'GET',
|
|
18
|
+
getCollection: 'GET',
|
|
19
|
+
createItem: 'POST',
|
|
20
|
+
updateItem: 'PATCH',
|
|
21
|
+
deleteItem: 'DELETE',
|
|
22
|
+
};
|
|
14
23
|
class Graph extends baseCommand_1.BaseCommand {
|
|
15
24
|
static description = 'Query a running integration graph and print the response';
|
|
16
25
|
static examples = [
|
|
17
26
|
'<%= config.bin %> <%= command.id %> --path=/sobjects/Opportunity/records/abc123',
|
|
18
27
|
'<%= config.bin %> <%= command.id %> get --path=/sobjects/Opportunity --port=9201',
|
|
28
|
+
`<%= config.bin %> <%= command.id %> createItem --path=/sobjects/Task/records --body='{"Name":"New Task"}'`,
|
|
29
|
+
'<%= config.bin %> <%= command.id %> updateItem --path=/sobjects/Task/records/abc123 --body=\'{"Name":"Updated"}\'',
|
|
30
|
+
'<%= config.bin %> <%= command.id %> deleteItem --path=/sobjects/Task/records/abc123',
|
|
19
31
|
];
|
|
20
32
|
static args = {
|
|
21
33
|
operation: core_1.Args.string({
|
|
@@ -72,13 +84,32 @@ class Graph extends baseCommand_1.BaseCommand {
|
|
|
72
84
|
char: 'o',
|
|
73
85
|
description: 'Write response body to file instead of stdout',
|
|
74
86
|
}),
|
|
87
|
+
body: core_1.Flags.string({
|
|
88
|
+
char: 'b',
|
|
89
|
+
description: 'JSON request body for createItem/updateItem operations',
|
|
90
|
+
exclusive: ['body-file'],
|
|
91
|
+
}),
|
|
92
|
+
'body-file': core_1.Flags.string({
|
|
93
|
+
description: 'Path to a JSON file to use as request body',
|
|
94
|
+
exclusive: ['body'],
|
|
95
|
+
}),
|
|
96
|
+
yes: core_1.Flags.boolean({
|
|
97
|
+
char: 'y',
|
|
98
|
+
description: 'Skip confirmation prompt for destructive operations (deleteItem)',
|
|
99
|
+
default: false,
|
|
100
|
+
}),
|
|
75
101
|
};
|
|
76
102
|
async run() {
|
|
77
103
|
(0, integrations_1.validateIsIntegrationDirectory)();
|
|
78
104
|
const { args, flags } = await this.parse(Graph);
|
|
79
|
-
const operation = args.operation
|
|
80
|
-
|
|
81
|
-
|
|
105
|
+
const operation = args.operation;
|
|
106
|
+
const method = METHOD_MAP[operation];
|
|
107
|
+
const needsBody = method === 'POST' || method === 'PATCH';
|
|
108
|
+
if (needsBody && !flags.body && !flags['body-file']) {
|
|
109
|
+
this.error('--body or --body-file is required for ' + operation, { exit: 1 });
|
|
110
|
+
}
|
|
111
|
+
if (operation === 'deleteItem' && (flags.body || flags['body-file'])) {
|
|
112
|
+
this.error('--body and --body-file are not supported for deleteItem', { exit: 1 });
|
|
82
113
|
}
|
|
83
114
|
const environment = flags.environment ?? GlobalConfiguration.Environment.Production;
|
|
84
115
|
const configuration = await (0, configuration_1.getConfiguration)(environment, flags['config-path']);
|
|
@@ -122,22 +153,65 @@ class Graph extends baseCommand_1.BaseCommand {
|
|
|
122
153
|
'X-Unito-Correlation-Id': crypto_1.default.randomUUID(),
|
|
123
154
|
'Content-Type': 'application/json',
|
|
124
155
|
};
|
|
156
|
+
let body;
|
|
157
|
+
if (needsBody) {
|
|
158
|
+
if (flags.body) {
|
|
159
|
+
try {
|
|
160
|
+
JSON.parse(flags.body);
|
|
161
|
+
body = flags.body;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
this.error('Invalid JSON in --body: ' + flags.body, { exit: 1 });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else if (flags['body-file']) {
|
|
168
|
+
const raw = await fs_1.default.promises.readFile(flags['body-file'], 'utf8').catch(() => {
|
|
169
|
+
this.error('Could not read --body-file: ' + flags['body-file'], { exit: 1 });
|
|
170
|
+
});
|
|
171
|
+
try {
|
|
172
|
+
JSON.parse(raw);
|
|
173
|
+
body = raw;
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
this.error('Invalid JSON in --body-file: ' + flags['body-file'], { exit: 1 });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
125
180
|
const url = `http://localhost:${flags.port}${flags.path}`;
|
|
126
|
-
|
|
181
|
+
if (operation === 'deleteItem' && !flags.yes) {
|
|
182
|
+
const { proceed } = await inquirer_1.default.prompt({
|
|
183
|
+
name: 'proceed',
|
|
184
|
+
message: `About to DELETE ${flags.path}. Continue?`,
|
|
185
|
+
type: 'confirm',
|
|
186
|
+
default: false,
|
|
187
|
+
});
|
|
188
|
+
if (!proceed) {
|
|
189
|
+
this.log('Aborted.');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const fetchOptions = { method, headers };
|
|
194
|
+
if (body) {
|
|
195
|
+
fetchOptions.body = body;
|
|
196
|
+
}
|
|
197
|
+
const response = await fetch(url, fetchOptions);
|
|
127
198
|
if (!response.ok) {
|
|
128
|
-
const
|
|
129
|
-
if (
|
|
130
|
-
this.logToStderr(JSON.stringify(
|
|
199
|
+
const errorBody = await response.json().catch(() => undefined);
|
|
200
|
+
if (errorBody !== undefined) {
|
|
201
|
+
this.logToStderr(JSON.stringify(errorBody, null, 2));
|
|
131
202
|
}
|
|
132
203
|
this.error(`HTTP ${response.status} from ${url}`, { exit: response.status });
|
|
133
204
|
}
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
205
|
+
const hasContent = response.status !== 204;
|
|
206
|
+
if (hasContent) {
|
|
207
|
+
const responseBody = (await response.json());
|
|
208
|
+
const json = JSON.stringify(responseBody, null, 2);
|
|
209
|
+
if (flags.output) {
|
|
210
|
+
await fs_1.default.promises.writeFile(flags.output, json, 'utf8');
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
this.log(json);
|
|
214
|
+
}
|
|
141
215
|
}
|
|
142
216
|
}
|
|
143
217
|
}
|
|
@@ -50,6 +50,10 @@ class SchemaSnapshot extends baseCommand_1.BaseCommand {
|
|
|
50
50
|
description: 'fail if any fields are unmapped and not in the allowlist',
|
|
51
51
|
default: false,
|
|
52
52
|
}),
|
|
53
|
+
port: core_1.Flags.integer({
|
|
54
|
+
description: 'port to start the integration on (default: 9200)',
|
|
55
|
+
default: 9200,
|
|
56
|
+
}),
|
|
53
57
|
};
|
|
54
58
|
async catch(error) {
|
|
55
59
|
if ((0, errors_1.handleError)(this, error)) {
|
|
@@ -88,7 +92,7 @@ class SchemaSnapshot extends baseCommand_1.BaseCommand {
|
|
|
88
92
|
throw new errors_1.EntryDecryptionError(failedSecrets.at(0), environment);
|
|
89
93
|
}
|
|
90
94
|
core_1.ux.action.start('Starting integration', undefined, { stdout: true });
|
|
91
|
-
const integrationPort =
|
|
95
|
+
const integrationPort = String(flags.port);
|
|
92
96
|
let modernProcess;
|
|
93
97
|
const recordingPath = path_1.default.join(os_1.default.tmpdir(), `schema-snapshot-recording-${Date.now()}.jsonl`);
|
|
94
98
|
// Pre-create recording file with restrictive permissions (owner-only read/write).
|
|
@@ -123,9 +127,12 @@ class SchemaSnapshot extends baseCommand_1.BaseCommand {
|
|
|
123
127
|
readOnly: true,
|
|
124
128
|
timeout: 20,
|
|
125
129
|
[Operation.GetCollection]: {
|
|
126
|
-
itemsPerPage:
|
|
130
|
+
itemsPerPage: 5,
|
|
127
131
|
followNextPage: false,
|
|
128
132
|
},
|
|
133
|
+
retryOnEmptyRelation: {
|
|
134
|
+
maxSiblings: 3,
|
|
135
|
+
},
|
|
129
136
|
});
|
|
130
137
|
// Only crawl graph structure, skip all step checks.
|
|
131
138
|
crawlerDriver.stepCheckKeys = [];
|
|
@@ -3,10 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
const tslib_1 = require("tslib");
|
|
4
4
|
const fs_1 = tslib_1.__importDefault(require("fs"));
|
|
5
5
|
const test_1 = require("@oclif/test");
|
|
6
|
+
const inquirer_1 = tslib_1.__importDefault(require("inquirer"));
|
|
6
7
|
const sinon_1 = tslib_1.__importDefault(require("sinon"));
|
|
7
8
|
const Configuration = tslib_1.__importStar(require("../../src/resources/configuration"));
|
|
8
9
|
const Decryption = tslib_1.__importStar(require("../../src/resources/decryption"));
|
|
9
10
|
const Integrations = tslib_1.__importStar(require("../../src/resources/integrations"));
|
|
11
|
+
const originalReadFile = fs_1.default.promises.readFile.bind(fs_1.default.promises);
|
|
10
12
|
const MOCK_CONFIG = {
|
|
11
13
|
name: 'salesforce-v2',
|
|
12
14
|
testAccounts: {
|
|
@@ -169,15 +171,203 @@ describe('graph', () => {
|
|
|
169
171
|
.it('treats "getCollection" as a get operation', ctx => {
|
|
170
172
|
(0, test_1.expect)(ctx.stdout).to.contain('"label": "Opportunity"');
|
|
171
173
|
});
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
|
|
174
|
+
// --- createItem ---
|
|
175
|
+
test_1.test
|
|
176
|
+
.stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
|
|
177
|
+
.stub(global, 'fetch', stub => stub.resolves({
|
|
178
|
+
ok: true,
|
|
179
|
+
status: 201,
|
|
180
|
+
json: () => Promise.resolve({ path: '/sobjects/Task/records/new123' }),
|
|
181
|
+
}))
|
|
182
|
+
.stdout()
|
|
183
|
+
.command(['graph', 'createItem', '--path=/sobjects/Task/records', '--body={"Name":"Test Task"}'])
|
|
184
|
+
.it('createItem sends POST with parsed JSON body', ctx => {
|
|
185
|
+
const fetchStub = global.fetch;
|
|
186
|
+
(0, test_1.expect)(fetchStub.calledOnce).to.be.true;
|
|
187
|
+
const [url, options] = fetchStub.firstCall.args;
|
|
188
|
+
(0, test_1.expect)(url).to.equal('http://localhost:9200/sobjects/Task/records');
|
|
189
|
+
(0, test_1.expect)(options.method).to.equal('POST');
|
|
190
|
+
(0, test_1.expect)(JSON.parse(options.body)).to.deep.equal({ Name: 'Test Task' });
|
|
191
|
+
(0, test_1.expect)(ctx.stdout).to.contain('/sobjects/Task/records/new123');
|
|
192
|
+
});
|
|
193
|
+
test_1.test
|
|
194
|
+
.stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
|
|
195
|
+
.command(['graph', 'createItem', '--path=/sobjects/Task/records'])
|
|
196
|
+
.catch(err => {
|
|
197
|
+
(0, test_1.expect)(err.message).to.contain('--body or --body-file is required');
|
|
198
|
+
})
|
|
199
|
+
.it('createItem without --body or --body-file errors');
|
|
200
|
+
test_1.test
|
|
201
|
+
.stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
|
|
202
|
+
.command(['graph', 'createItem', '--path=/sobjects/Task/records', '--body=not-json'])
|
|
203
|
+
.catch(err => {
|
|
204
|
+
(0, test_1.expect)(err.message).to.contain('Invalid JSON in --body');
|
|
205
|
+
})
|
|
206
|
+
.it('createItem with invalid JSON in --body errors');
|
|
207
|
+
test_1.test
|
|
208
|
+
.stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
|
|
209
|
+
.stub(global, 'fetch', stub => stub.resolves({
|
|
210
|
+
ok: true,
|
|
211
|
+
status: 201,
|
|
212
|
+
json: () => Promise.resolve({ path: '/sobjects/Task/records/new456' }),
|
|
213
|
+
}))
|
|
214
|
+
.stub(fs_1.default.promises, 'readFile', stub => stub.callsFake((...args) => {
|
|
215
|
+
if (args[0] === '/tmp/body.json')
|
|
216
|
+
return Promise.resolve('{"Name":"From File"}');
|
|
217
|
+
return originalReadFile(...args);
|
|
218
|
+
}))
|
|
219
|
+
.stdout()
|
|
220
|
+
.command(['graph', 'createItem', '--path=/sobjects/Task/records', '--body-file=/tmp/body.json'])
|
|
221
|
+
.it('--body-file reads and parses file as request body', () => {
|
|
222
|
+
const fetchStub = global.fetch;
|
|
223
|
+
const [, options] = fetchStub.firstCall.args;
|
|
224
|
+
(0, test_1.expect)(options.method).to.equal('POST');
|
|
225
|
+
(0, test_1.expect)(JSON.parse(options.body)).to.deep.equal({ Name: 'From File' });
|
|
226
|
+
});
|
|
227
|
+
test_1.test
|
|
228
|
+
.stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
|
|
229
|
+
.stub(fs_1.default.promises, 'readFile', stub => stub.callsFake((...args) => {
|
|
230
|
+
if (args[0] === '/tmp/bad.json')
|
|
231
|
+
return Promise.resolve('not valid json');
|
|
232
|
+
return originalReadFile(...args);
|
|
233
|
+
}))
|
|
234
|
+
.command(['graph', 'createItem', '--path=/sobjects/Task/records', '--body-file=/tmp/bad.json'])
|
|
235
|
+
.catch(err => {
|
|
236
|
+
(0, test_1.expect)(err.message).to.contain('Invalid JSON in --body-file');
|
|
237
|
+
})
|
|
238
|
+
.it('--body-file with invalid JSON errors');
|
|
239
|
+
test_1.test
|
|
240
|
+
.stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
|
|
241
|
+
.stub(fs_1.default.promises, 'readFile', stub => stub.callsFake((...args) => {
|
|
242
|
+
if (args[0] === '/tmp/missing.json')
|
|
243
|
+
return Promise.reject(new Error('ENOENT: no such file'));
|
|
244
|
+
return originalReadFile(...args);
|
|
245
|
+
}))
|
|
246
|
+
.command(['graph', 'createItem', '--path=/sobjects/Task/records', '--body-file=/tmp/missing.json'])
|
|
247
|
+
.catch(err => {
|
|
248
|
+
(0, test_1.expect)(err.message).to.contain('Could not read --body-file');
|
|
249
|
+
})
|
|
250
|
+
.it('--body-file with non-existent file errors');
|
|
251
|
+
// --- updateItem ---
|
|
252
|
+
test_1.test
|
|
253
|
+
.stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
|
|
254
|
+
.stub(global, 'fetch', stub => stub.resolves({
|
|
255
|
+
ok: true,
|
|
256
|
+
status: 200,
|
|
257
|
+
json: () => Promise.resolve({ path: '/sobjects/Task/records/abc123' }),
|
|
258
|
+
}))
|
|
259
|
+
.stdout()
|
|
260
|
+
.command(['graph', 'updateItem', '--path=/sobjects/Task/records/abc123', '--body={"Name":"Updated"}'])
|
|
261
|
+
.it('updateItem sends PATCH with parsed JSON body', () => {
|
|
262
|
+
const fetchStub = global.fetch;
|
|
263
|
+
const [url, options] = fetchStub.firstCall.args;
|
|
264
|
+
(0, test_1.expect)(url).to.equal('http://localhost:9200/sobjects/Task/records/abc123');
|
|
265
|
+
(0, test_1.expect)(options.method).to.equal('PATCH');
|
|
266
|
+
(0, test_1.expect)(JSON.parse(options.body)).to.deep.equal({ Name: 'Updated' });
|
|
267
|
+
});
|
|
268
|
+
test_1.test
|
|
269
|
+
.stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
|
|
270
|
+
.command(['graph', 'updateItem', '--path=/sobjects/Task/records/abc123'])
|
|
271
|
+
.catch(err => {
|
|
272
|
+
(0, test_1.expect)(err.message).to.contain('--body or --body-file is required');
|
|
273
|
+
})
|
|
274
|
+
.it('updateItem without --body or --body-file errors');
|
|
275
|
+
// --- deleteItem ---
|
|
276
|
+
test_1.test
|
|
277
|
+
.stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
|
|
278
|
+
.stub(global, 'fetch', stub => stub.resolves({
|
|
279
|
+
ok: true,
|
|
280
|
+
status: 204,
|
|
281
|
+
json: () => Promise.reject(new Error('No body')),
|
|
282
|
+
}))
|
|
283
|
+
.stdout()
|
|
284
|
+
.command(['graph', 'deleteItem', '--path=/sobjects/Task/records/abc123', '--yes'])
|
|
285
|
+
.it('deleteItem sends DELETE with no body and handles 204', ctx => {
|
|
286
|
+
const fetchStub = global.fetch;
|
|
287
|
+
const [url, options] = fetchStub.firstCall.args;
|
|
288
|
+
(0, test_1.expect)(url).to.equal('http://localhost:9200/sobjects/Task/records/abc123');
|
|
289
|
+
(0, test_1.expect)(options.method).to.equal('DELETE');
|
|
290
|
+
(0, test_1.expect)(options.body).to.be.undefined;
|
|
291
|
+
(0, test_1.expect)(ctx.stdout).to.equal('');
|
|
292
|
+
});
|
|
293
|
+
test_1.test
|
|
294
|
+
.stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
|
|
295
|
+
.command(['graph', 'deleteItem', '--path=/sobjects/Task/records/abc123', '--body={"x":1}'])
|
|
296
|
+
.catch(err => {
|
|
297
|
+
(0, test_1.expect)(err.message).to.contain('--body and --body-file are not supported for deleteItem');
|
|
298
|
+
})
|
|
299
|
+
.it('deleteItem rejects --body flag');
|
|
300
|
+
test_1.test
|
|
301
|
+
.stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
|
|
302
|
+
.command(['graph', 'deleteItem', '--path=/sobjects/Task/records/abc123', '--body-file=/tmp/body.json'])
|
|
303
|
+
.catch(err => {
|
|
304
|
+
(0, test_1.expect)(err.message).to.contain('--body and --body-file are not supported for deleteItem');
|
|
305
|
+
})
|
|
306
|
+
.it('deleteItem rejects --body-file flag');
|
|
307
|
+
// --- deleteItem confirmation prompt ---
|
|
308
|
+
test_1.test
|
|
309
|
+
.stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
|
|
310
|
+
.stub(global, 'fetch', stub => stub.resolves({
|
|
311
|
+
ok: true,
|
|
312
|
+
status: 204,
|
|
313
|
+
json: () => Promise.reject(new Error('No body')),
|
|
314
|
+
}))
|
|
315
|
+
.stub(inquirer_1.default, 'prompt', stub => stub.resolves({ proceed: true }))
|
|
316
|
+
.stdout()
|
|
317
|
+
.command(['graph', 'deleteItem', '--path=/sobjects/Task/records/abc123'])
|
|
318
|
+
.it('deleteItem prompts for confirmation and proceeds when accepted', () => {
|
|
319
|
+
const promptStub = inquirer_1.default.prompt;
|
|
320
|
+
(0, test_1.expect)(promptStub.calledOnce).to.be.true;
|
|
321
|
+
const [question] = promptStub.firstCall.args;
|
|
322
|
+
(0, test_1.expect)(question.message).to.contain('About to DELETE /sobjects/Task/records/abc123');
|
|
323
|
+
const fetchStub = global.fetch;
|
|
324
|
+
(0, test_1.expect)(fetchStub.calledOnce).to.be.true;
|
|
325
|
+
});
|
|
326
|
+
test_1.test
|
|
327
|
+
.stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
|
|
328
|
+
.stub(global, 'fetch', stub => stub.resolves({
|
|
329
|
+
ok: true,
|
|
330
|
+
status: 204,
|
|
331
|
+
json: () => Promise.reject(new Error('No body')),
|
|
332
|
+
}))
|
|
333
|
+
.stub(inquirer_1.default, 'prompt', stub => stub.resolves({ proceed: false }))
|
|
334
|
+
.stdout()
|
|
335
|
+
.command(['graph', 'deleteItem', '--path=/sobjects/Task/records/abc123'])
|
|
336
|
+
.it('deleteItem aborts when confirmation is declined', ctx => {
|
|
337
|
+
(0, test_1.expect)(ctx.stdout).to.contain('Aborted.');
|
|
338
|
+
const fetchStub = global.fetch;
|
|
339
|
+
(0, test_1.expect)(fetchStub.called).to.be.false;
|
|
340
|
+
});
|
|
341
|
+
test_1.test
|
|
342
|
+
.stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
|
|
343
|
+
.stub(global, 'fetch', stub => stub.resolves({
|
|
344
|
+
ok: true,
|
|
345
|
+
status: 204,
|
|
346
|
+
json: () => Promise.reject(new Error('No body')),
|
|
347
|
+
}))
|
|
348
|
+
.stdout()
|
|
349
|
+
.command(['graph', 'deleteItem', '--path=/sobjects/Task/records/abc123', '--yes'])
|
|
350
|
+
.it('deleteItem --yes bypasses confirmation prompt', () => {
|
|
351
|
+
const fetchStub = global.fetch;
|
|
352
|
+
(0, test_1.expect)(fetchStub.calledOnce).to.be.true;
|
|
353
|
+
});
|
|
354
|
+
// --- GET ignores body ---
|
|
355
|
+
test_1.test
|
|
356
|
+
.stub(Configuration, 'getConfiguration', stub => stub.resolves(MOCK_CONFIG))
|
|
357
|
+
.stub(global, 'fetch', stub => stub.resolves({
|
|
358
|
+
ok: true,
|
|
359
|
+
status: 200,
|
|
360
|
+
json: () => Promise.resolve(MOCK_RESPONSE),
|
|
361
|
+
}))
|
|
362
|
+
.stdout()
|
|
363
|
+
.command(['graph', 'get', '--path=/sobjects/Opportunity', '--body={"ignored":true}'])
|
|
364
|
+
.it('GET operations ignore --body flag', () => {
|
|
365
|
+
const fetchStub = global.fetch;
|
|
366
|
+
const [, options] = fetchStub.firstCall.args;
|
|
367
|
+
(0, test_1.expect)(options.method).to.equal('GET');
|
|
368
|
+
(0, test_1.expect)(options.body).to.be.undefined;
|
|
369
|
+
});
|
|
370
|
+
// --- invalid operation ---
|
|
181
371
|
test_1.test
|
|
182
372
|
.stderr()
|
|
183
373
|
.command(['graph', 'put', '--path=/sobjects/Opportunity'])
|
package/oclif.manifest.json
CHANGED
|
@@ -274,7 +274,10 @@
|
|
|
274
274
|
"description": "Query a running integration graph and print the response",
|
|
275
275
|
"examples": [
|
|
276
276
|
"<%= config.bin %> <%= command.id %> --path=/sobjects/Opportunity/records/abc123",
|
|
277
|
-
"<%= config.bin %> <%= command.id %> get --path=/sobjects/Opportunity --port=9201"
|
|
277
|
+
"<%= config.bin %> <%= command.id %> get --path=/sobjects/Opportunity --port=9201",
|
|
278
|
+
"<%= config.bin %> <%= command.id %> createItem --path=/sobjects/Task/records --body='{\"Name\":\"New Task\"}'",
|
|
279
|
+
"<%= config.bin %> <%= command.id %> updateItem --path=/sobjects/Task/records/abc123 --body='{\"Name\":\"Updated\"}'",
|
|
280
|
+
"<%= config.bin %> <%= command.id %> deleteItem --path=/sobjects/Task/records/abc123"
|
|
278
281
|
],
|
|
279
282
|
"flags": {
|
|
280
283
|
"json": {
|
|
@@ -360,6 +363,34 @@
|
|
|
360
363
|
"hasDynamicHelp": false,
|
|
361
364
|
"multiple": false,
|
|
362
365
|
"type": "option"
|
|
366
|
+
},
|
|
367
|
+
"body": {
|
|
368
|
+
"char": "b",
|
|
369
|
+
"description": "JSON request body for createItem/updateItem operations",
|
|
370
|
+
"exclusive": [
|
|
371
|
+
"body-file"
|
|
372
|
+
],
|
|
373
|
+
"name": "body",
|
|
374
|
+
"hasDynamicHelp": false,
|
|
375
|
+
"multiple": false,
|
|
376
|
+
"type": "option"
|
|
377
|
+
},
|
|
378
|
+
"body-file": {
|
|
379
|
+
"description": "Path to a JSON file to use as request body",
|
|
380
|
+
"exclusive": [
|
|
381
|
+
"body"
|
|
382
|
+
],
|
|
383
|
+
"name": "body-file",
|
|
384
|
+
"hasDynamicHelp": false,
|
|
385
|
+
"multiple": false,
|
|
386
|
+
"type": "option"
|
|
387
|
+
},
|
|
388
|
+
"yes": {
|
|
389
|
+
"char": "y",
|
|
390
|
+
"description": "Skip confirmation prompt for destructive operations (deleteItem)",
|
|
391
|
+
"name": "yes",
|
|
392
|
+
"allowNo": false,
|
|
393
|
+
"type": "boolean"
|
|
363
394
|
}
|
|
364
395
|
},
|
|
365
396
|
"hasDynamicHelp": false,
|
|
@@ -737,6 +768,14 @@
|
|
|
737
768
|
"name": "strict",
|
|
738
769
|
"allowNo": false,
|
|
739
770
|
"type": "boolean"
|
|
771
|
+
},
|
|
772
|
+
"port": {
|
|
773
|
+
"description": "port to start the integration on (default: 9200)",
|
|
774
|
+
"name": "port",
|
|
775
|
+
"default": 9200,
|
|
776
|
+
"hasDynamicHelp": false,
|
|
777
|
+
"multiple": false,
|
|
778
|
+
"type": "option"
|
|
740
779
|
}
|
|
741
780
|
},
|
|
742
781
|
"hasDynamicHelp": false,
|
|
@@ -961,5 +1000,5 @@
|
|
|
961
1000
|
]
|
|
962
1001
|
}
|
|
963
1002
|
},
|
|
964
|
-
"version": "0.
|
|
1003
|
+
"version": "0.67.0"
|
|
965
1004
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unito/integration-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.67.0",
|
|
4
4
|
"description": "Integration CLI",
|
|
5
5
|
"bin": {
|
|
6
6
|
"integration-cli": "./bin/run"
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"@ngrok/ngrok": "^1.4.1",
|
|
40
40
|
"@oazapfts/runtime": "1.x",
|
|
41
41
|
"@oclif/core": "3.x",
|
|
42
|
-
"@unito/integration-debugger": "0.
|
|
42
|
+
"@unito/integration-debugger": "0.29.0",
|
|
43
43
|
"ajv": "8.x",
|
|
44
44
|
"ajv-formats": "3.x",
|
|
45
45
|
"better-ajv-errors": "1.x",
|