@uipath/data-fabric-tool 1.195.0 → 1.197.0-preview.59
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/dist/index.js +2 -0
- package/dist/tool.js +6037 -6015
- package/package.json +2 -2
- package/src/commands/choice-sets.spec.ts +262 -13
- package/src/commands/choice-sets.ts +126 -8
- package/src/commands/entities.spec.ts +302 -14
- package/src/commands/entities.ts +111 -9
- package/src/commands/files.spec.ts +108 -3
- package/src/commands/files.ts +45 -2
- package/src/commands/records.spec.ts +323 -0
- package/src/commands/records.ts +120 -18
- package/src/utils/input.spec.ts +127 -0
- package/src/utils/input.ts +30 -1
- package/src/utils/output.spec.ts +22 -9
- package/src/utils/output.ts +27 -9
package/src/commands/records.ts
CHANGED
|
@@ -9,9 +9,9 @@ import {
|
|
|
9
9
|
} from "@uipath/common";
|
|
10
10
|
import { getFileSystem } from "@uipath/filesystem";
|
|
11
11
|
import type { EntityRecord } from "@uipath/uipath-typescript";
|
|
12
|
-
import type
|
|
12
|
+
import { type Command, Option } from "commander";
|
|
13
13
|
import { readFileBinary, readJsonInput } from "../utils/input";
|
|
14
|
-
import { fail } from "../utils/output";
|
|
14
|
+
import { fail, requireDestructiveConfirmation } from "../utils/output";
|
|
15
15
|
import { connectOrFail } from "../utils/sdk-client";
|
|
16
16
|
|
|
17
17
|
interface ListOptions {
|
|
@@ -19,26 +19,34 @@ interface ListOptions {
|
|
|
19
19
|
limit: string;
|
|
20
20
|
offset?: string;
|
|
21
21
|
cursor?: string;
|
|
22
|
+
folderKey?: string;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
interface GetOptions {
|
|
25
26
|
tenant?: string;
|
|
27
|
+
folderKey?: string;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
interface InsertOptions {
|
|
29
31
|
tenant?: string;
|
|
30
32
|
file?: string;
|
|
31
33
|
body?: string;
|
|
34
|
+
folderKey?: string;
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
interface UpdateOptions {
|
|
35
38
|
tenant?: string;
|
|
36
39
|
file?: string;
|
|
37
40
|
body?: string;
|
|
41
|
+
folderKey?: string;
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
interface DeleteOptions {
|
|
41
45
|
tenant?: string;
|
|
46
|
+
yes?: boolean;
|
|
47
|
+
confirm?: boolean;
|
|
48
|
+
reason?: string;
|
|
49
|
+
folderKey?: string;
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
interface QueryOptions {
|
|
@@ -48,11 +56,13 @@ interface QueryOptions {
|
|
|
48
56
|
limit: string;
|
|
49
57
|
offset?: string;
|
|
50
58
|
cursor?: string;
|
|
59
|
+
folderKey?: string;
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
interface ImportOptions {
|
|
54
63
|
tenant?: string;
|
|
55
64
|
file?: string;
|
|
65
|
+
folderKey?: string;
|
|
56
66
|
}
|
|
57
67
|
|
|
58
68
|
interface BatchResult {
|
|
@@ -195,6 +205,10 @@ export const registerRecordsCommand = (program: Command) => {
|
|
|
195
205
|
"--cursor <cursor>",
|
|
196
206
|
"Pagination cursor from a previous response to fetch the next page",
|
|
197
207
|
)
|
|
208
|
+
.option(
|
|
209
|
+
"--folder-key <key>",
|
|
210
|
+
"Folder key (GUID) of the folder containing the entity (for folder-scoped entities)",
|
|
211
|
+
)
|
|
198
212
|
.examples(RECORDS_LIST_EXAMPLES)
|
|
199
213
|
.trackedAction(
|
|
200
214
|
processContext,
|
|
@@ -232,13 +246,18 @@ export const registerRecordsCommand = (program: Command) => {
|
|
|
232
246
|
const sdk = await connectOrFail(options.tenant);
|
|
233
247
|
if (!sdk) return;
|
|
234
248
|
|
|
249
|
+
const listPaginationOptions =
|
|
250
|
+
options.cursor !== undefined
|
|
251
|
+
? { pageSize, cursor: { value: options.cursor } }
|
|
252
|
+
: jumpToPage !== undefined
|
|
253
|
+
? { pageSize, jumpToPage }
|
|
254
|
+
: { pageSize };
|
|
235
255
|
const [listError, result] = await catchError(
|
|
236
256
|
sdk.entities.getAllRecords(entityId, {
|
|
237
|
-
|
|
238
|
-
...(options.
|
|
239
|
-
|
|
257
|
+
...listPaginationOptions,
|
|
258
|
+
...(options.folderKey !== undefined && {
|
|
259
|
+
folderKey: options.folderKey,
|
|
240
260
|
}),
|
|
241
|
-
...(jumpToPage !== undefined && { jumpToPage }),
|
|
242
261
|
}),
|
|
243
262
|
);
|
|
244
263
|
|
|
@@ -265,6 +284,10 @@ export const registerRecordsCommand = (program: Command) => {
|
|
|
265
284
|
.addOption(
|
|
266
285
|
createHiddenDeprecatedTenantOption("-t, --tenant <tenant-name>"),
|
|
267
286
|
)
|
|
287
|
+
.option(
|
|
288
|
+
"--folder-key <key>",
|
|
289
|
+
"Folder key (GUID) of the folder containing the entity (for folder-scoped entities)",
|
|
290
|
+
)
|
|
268
291
|
.examples(RECORDS_GET_EXAMPLES)
|
|
269
292
|
.trackedAction(
|
|
270
293
|
processContext,
|
|
@@ -273,7 +296,13 @@ export const registerRecordsCommand = (program: Command) => {
|
|
|
273
296
|
if (!sdk) return;
|
|
274
297
|
|
|
275
298
|
const [getError, record] = await catchError(
|
|
276
|
-
sdk.entities.getRecordById(
|
|
299
|
+
sdk.entities.getRecordById(
|
|
300
|
+
entityId,
|
|
301
|
+
recordId,
|
|
302
|
+
options.folderKey !== undefined
|
|
303
|
+
? { folderKey: options.folderKey }
|
|
304
|
+
: undefined,
|
|
305
|
+
),
|
|
277
306
|
);
|
|
278
307
|
|
|
279
308
|
if (getError) {
|
|
@@ -311,7 +340,11 @@ export const registerRecordsCommand = (program: Command) => {
|
|
|
311
340
|
)
|
|
312
341
|
.option(
|
|
313
342
|
"--body <json>",
|
|
314
|
-
"Inline JSON record data (object or array of objects)",
|
|
343
|
+
"Inline JSON record data (object or array of objects; use `-` to read from stdin)",
|
|
344
|
+
)
|
|
345
|
+
.option(
|
|
346
|
+
"--folder-key <key>",
|
|
347
|
+
"Folder key (GUID) of the folder containing the entity (for folder-scoped entities)",
|
|
315
348
|
)
|
|
316
349
|
.examples(RECORDS_INSERT_EXAMPLES)
|
|
317
350
|
.trackedAction(
|
|
@@ -333,9 +366,18 @@ export const registerRecordsCommand = (program: Command) => {
|
|
|
333
366
|
| Record<string, unknown>[];
|
|
334
367
|
const recordsList = Array.isArray(data) ? data : [data];
|
|
335
368
|
|
|
369
|
+
const folderOptions =
|
|
370
|
+
options.folderKey !== undefined
|
|
371
|
+
? { folderKey: options.folderKey }
|
|
372
|
+
: undefined;
|
|
373
|
+
|
|
336
374
|
if (recordsList.length === 1) {
|
|
337
375
|
const [insertError, result] = await catchError(
|
|
338
|
-
sdk.entities.insertRecordById(
|
|
376
|
+
sdk.entities.insertRecordById(
|
|
377
|
+
entityId,
|
|
378
|
+
recordsList[0],
|
|
379
|
+
folderOptions,
|
|
380
|
+
),
|
|
339
381
|
);
|
|
340
382
|
|
|
341
383
|
if (insertError) {
|
|
@@ -352,7 +394,11 @@ export const registerRecordsCommand = (program: Command) => {
|
|
|
352
394
|
});
|
|
353
395
|
} else {
|
|
354
396
|
const [insertError, result] = await catchError(
|
|
355
|
-
sdk.entities.insertRecordsById(
|
|
397
|
+
sdk.entities.insertRecordsById(
|
|
398
|
+
entityId,
|
|
399
|
+
recordsList,
|
|
400
|
+
folderOptions,
|
|
401
|
+
),
|
|
356
402
|
);
|
|
357
403
|
|
|
358
404
|
if (insertError) {
|
|
@@ -394,7 +440,11 @@ export const registerRecordsCommand = (program: Command) => {
|
|
|
394
440
|
)
|
|
395
441
|
.option(
|
|
396
442
|
"--body <json>",
|
|
397
|
-
"Inline JSON record data (must include Id field)",
|
|
443
|
+
"Inline JSON record data (must include Id field; use `-` to read from stdin)",
|
|
444
|
+
)
|
|
445
|
+
.option(
|
|
446
|
+
"--folder-key <key>",
|
|
447
|
+
"Folder key (GUID) of the folder containing the entity (for folder-scoped entities)",
|
|
398
448
|
)
|
|
399
449
|
.examples(RECORDS_UPDATE_EXAMPLES)
|
|
400
450
|
.trackedAction(
|
|
@@ -416,6 +466,11 @@ export const registerRecordsCommand = (program: Command) => {
|
|
|
416
466
|
| Record<string, unknown>[];
|
|
417
467
|
const recordsList = Array.isArray(data) ? data : [data];
|
|
418
468
|
|
|
469
|
+
const folderOptions =
|
|
470
|
+
options.folderKey !== undefined
|
|
471
|
+
? { folderKey: options.folderKey }
|
|
472
|
+
: undefined;
|
|
473
|
+
|
|
419
474
|
if (recordsList.length === 1) {
|
|
420
475
|
const record = recordsList[0];
|
|
421
476
|
const recordId =
|
|
@@ -432,6 +487,7 @@ export const registerRecordsCommand = (program: Command) => {
|
|
|
432
487
|
entityId,
|
|
433
488
|
String(recordId),
|
|
434
489
|
record,
|
|
490
|
+
folderOptions,
|
|
435
491
|
),
|
|
436
492
|
);
|
|
437
493
|
|
|
@@ -462,6 +518,7 @@ export const registerRecordsCommand = (program: Command) => {
|
|
|
462
518
|
sdk.entities.updateRecordsById(
|
|
463
519
|
entityId,
|
|
464
520
|
recordsList as EntityRecord[],
|
|
521
|
+
folderOptions,
|
|
465
522
|
),
|
|
466
523
|
);
|
|
467
524
|
|
|
@@ -509,7 +566,7 @@ export const registerRecordsCommand = (program: Command) => {
|
|
|
509
566
|
)
|
|
510
567
|
.option(
|
|
511
568
|
"--body <json>",
|
|
512
|
-
"Inline JSON query options (filterGroup, selectedFields, sortOptions, aggregates, groupBy)",
|
|
569
|
+
"Inline JSON query options (filterGroup, selectedFields, sortOptions, aggregates, groupBy; use `-` to read from stdin)",
|
|
513
570
|
)
|
|
514
571
|
.option(
|
|
515
572
|
"-l, --limit <number>",
|
|
@@ -524,6 +581,10 @@ export const registerRecordsCommand = (program: Command) => {
|
|
|
524
581
|
"--cursor <cursor>",
|
|
525
582
|
"Pagination cursor from a previous response to fetch the next page",
|
|
526
583
|
)
|
|
584
|
+
.option(
|
|
585
|
+
"--folder-key <key>",
|
|
586
|
+
"Folder key (GUID) of the folder containing the entity (for folder-scoped entities)",
|
|
587
|
+
)
|
|
527
588
|
.trackedAction(
|
|
528
589
|
processContext,
|
|
529
590
|
async (entityId: string, options: QueryOptions) => {
|
|
@@ -588,14 +649,19 @@ export const registerRecordsCommand = (program: Command) => {
|
|
|
588
649
|
const sdk = await connectOrFail(options.tenant);
|
|
589
650
|
if (!sdk) return;
|
|
590
651
|
|
|
652
|
+
const queryPaginationOptions =
|
|
653
|
+
options.cursor !== undefined
|
|
654
|
+
? { pageSize, cursor: { value: options.cursor } }
|
|
655
|
+
: jumpToPage !== undefined
|
|
656
|
+
? { pageSize, jumpToPage }
|
|
657
|
+
: { pageSize };
|
|
591
658
|
const [queryError, result] = await catchError(
|
|
592
659
|
sdk.entities.queryRecordsById(entityId, {
|
|
593
660
|
...(queryBody !== null && queryBody),
|
|
594
|
-
|
|
595
|
-
...(options.
|
|
596
|
-
|
|
661
|
+
...queryPaginationOptions,
|
|
662
|
+
...(options.folderKey !== undefined && {
|
|
663
|
+
folderKey: options.folderKey,
|
|
597
664
|
}),
|
|
598
|
-
...(jumpToPage !== undefined && { jumpToPage }),
|
|
599
665
|
}),
|
|
600
666
|
);
|
|
601
667
|
|
|
@@ -622,6 +688,10 @@ export const registerRecordsCommand = (program: Command) => {
|
|
|
622
688
|
createHiddenDeprecatedTenantOption("-t, --tenant <tenant-name>"),
|
|
623
689
|
)
|
|
624
690
|
.option("-f, --file <path>", "Path to the CSV file to import")
|
|
691
|
+
.option(
|
|
692
|
+
"--folder-key <key>",
|
|
693
|
+
"Folder key (GUID) of the folder containing the entity (for folder-scoped entities)",
|
|
694
|
+
)
|
|
625
695
|
.trackedAction(
|
|
626
696
|
processContext,
|
|
627
697
|
async (entityId: string, options: ImportOptions) => {
|
|
@@ -651,7 +721,13 @@ export const registerRecordsCommand = (program: Command) => {
|
|
|
651
721
|
{ type: "text/csv" },
|
|
652
722
|
);
|
|
653
723
|
const [importError, result] = await catchError(
|
|
654
|
-
sdk.entities.importRecordsById(
|
|
724
|
+
sdk.entities.importRecordsById(
|
|
725
|
+
entityId,
|
|
726
|
+
csvFile,
|
|
727
|
+
options.folderKey !== undefined
|
|
728
|
+
? { folderKey: options.folderKey }
|
|
729
|
+
: undefined,
|
|
730
|
+
),
|
|
655
731
|
);
|
|
656
732
|
|
|
657
733
|
if (importError) {
|
|
@@ -689,6 +765,18 @@ export const registerRecordsCommand = (program: Command) => {
|
|
|
689
765
|
.addOption(
|
|
690
766
|
createHiddenDeprecatedTenantOption("-t, --tenant <tenant-name>"),
|
|
691
767
|
)
|
|
768
|
+
.option("-y, --yes", "Acknowledge this is an irreversible operation")
|
|
769
|
+
.addOption(
|
|
770
|
+
new Option("--confirm", "Deprecated alias for --yes").hideHelp(),
|
|
771
|
+
)
|
|
772
|
+
.option(
|
|
773
|
+
"--reason <reason>",
|
|
774
|
+
"Reason for the deletion — echoed back in the response so the caller can log it",
|
|
775
|
+
)
|
|
776
|
+
.option(
|
|
777
|
+
"--folder-key <key>",
|
|
778
|
+
"Folder key (GUID) of the folder containing the entity (for folder-scoped entities)",
|
|
779
|
+
)
|
|
692
780
|
.examples(RECORDS_DELETE_EXAMPLES)
|
|
693
781
|
.trackedAction(
|
|
694
782
|
processContext,
|
|
@@ -697,11 +785,24 @@ export const registerRecordsCommand = (program: Command) => {
|
|
|
697
785
|
recordIds: string[],
|
|
698
786
|
options: DeleteOptions,
|
|
699
787
|
) => {
|
|
788
|
+
const reason = requireDestructiveConfirmation(
|
|
789
|
+
options,
|
|
790
|
+
`delete ${recordIds.length} record(s) from entity '${entityId}'`,
|
|
791
|
+
'Pass --reason "<text>" to record why the records are being deleted.',
|
|
792
|
+
);
|
|
793
|
+
if (reason === null) return;
|
|
794
|
+
|
|
700
795
|
const sdk = await connectOrFail(options.tenant);
|
|
701
796
|
if (!sdk) return;
|
|
702
797
|
|
|
703
798
|
const [deleteError, result] = await catchError(
|
|
704
|
-
sdk.entities.deleteRecordsById(
|
|
799
|
+
sdk.entities.deleteRecordsById(
|
|
800
|
+
entityId,
|
|
801
|
+
recordIds,
|
|
802
|
+
options.folderKey !== undefined
|
|
803
|
+
? { folderKey: options.folderKey }
|
|
804
|
+
: undefined,
|
|
805
|
+
),
|
|
705
806
|
);
|
|
706
807
|
|
|
707
808
|
if (deleteError) {
|
|
@@ -721,6 +822,7 @@ export const registerRecordsCommand = (program: Command) => {
|
|
|
721
822
|
FailureCount: failureCount,
|
|
722
823
|
SuccessRecords: r.successRecords ?? [],
|
|
723
824
|
FailureRecords: r.failureRecords ?? [],
|
|
825
|
+
Reason: reason,
|
|
724
826
|
},
|
|
725
827
|
});
|
|
726
828
|
if (failureCount > 0) {
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
const readFile = vi.fn();
|
|
5
|
+
|
|
6
|
+
vi.mock("@uipath/filesystem", () => ({
|
|
7
|
+
getFileSystem: () => ({ readFile }),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
import { readJsonInput } from "./input";
|
|
11
|
+
|
|
12
|
+
const realStdin = process.stdin;
|
|
13
|
+
const realPlatform = process.platform;
|
|
14
|
+
|
|
15
|
+
// Replaces process.stdin with a fake stream. Pass null to simulate a TTY (no
|
|
16
|
+
// piped input); pass a string to simulate piped data.
|
|
17
|
+
function mockStdin(data: string | null): void {
|
|
18
|
+
const stream = new EventEmitter() as unknown as NodeJS.ReadStream;
|
|
19
|
+
(stream as unknown as { isTTY: boolean }).isTTY = data === null;
|
|
20
|
+
stream.setEncoding = vi.fn().mockReturnValue(stream);
|
|
21
|
+
Object.defineProperty(process, "stdin", {
|
|
22
|
+
value: stream,
|
|
23
|
+
configurable: true,
|
|
24
|
+
});
|
|
25
|
+
if (data !== null) {
|
|
26
|
+
queueMicrotask(() => {
|
|
27
|
+
stream.emit("data", data);
|
|
28
|
+
stream.emit("end");
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function setPlatform(platform: string): void {
|
|
34
|
+
Object.defineProperty(process, "platform", {
|
|
35
|
+
value: platform,
|
|
36
|
+
configurable: true,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("readJsonInput", () => {
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
vi.clearAllMocks();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
Object.defineProperty(process, "stdin", {
|
|
47
|
+
value: realStdin,
|
|
48
|
+
configurable: true,
|
|
49
|
+
});
|
|
50
|
+
Object.defineProperty(process, "platform", {
|
|
51
|
+
value: realPlatform,
|
|
52
|
+
configurable: true,
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("parses inline JSON", async () => {
|
|
57
|
+
await expect(readJsonInput(undefined, '{"a":1}')).resolves.toEqual({
|
|
58
|
+
a: 1,
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("reads and parses a JSON file", async () => {
|
|
63
|
+
readFile.mockResolvedValue('{"fromFile":true}');
|
|
64
|
+
await expect(readJsonInput("payload.json")).resolves.toEqual({
|
|
65
|
+
fromFile: true,
|
|
66
|
+
});
|
|
67
|
+
expect(readFile).toHaveBeenCalledWith("payload.json", "utf-8");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("throws the missing-input message when nothing is provided", async () => {
|
|
71
|
+
await expect(readJsonInput(undefined, undefined)).rejects.toThrow(
|
|
72
|
+
"Provide either --file <path> or inline data.",
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("uses a custom missing-input message", async () => {
|
|
77
|
+
await expect(
|
|
78
|
+
readJsonInput(undefined, undefined, "Provide --body or --file."),
|
|
79
|
+
).rejects.toThrow("Provide --body or --file.");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("reads JSON from stdin when --body is `-` (UV-14637)", async () => {
|
|
83
|
+
// Payload contains `&`, the char that cmd.exe mangles on the CLI line.
|
|
84
|
+
mockStdin('[{"name":"A&B"}]');
|
|
85
|
+
await expect(readJsonInput(undefined, "-")).resolves.toEqual([
|
|
86
|
+
{ name: "A&B" },
|
|
87
|
+
]);
|
|
88
|
+
expect(readFile).not.toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("reads JSON from stdin when --file is `-`", async () => {
|
|
92
|
+
mockStdin('{"piped":true}');
|
|
93
|
+
await expect(readJsonInput("-")).resolves.toEqual({ piped: true });
|
|
94
|
+
expect(readFile).not.toHaveBeenCalled();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("errors when `-` is given but stdin is a TTY", async () => {
|
|
98
|
+
mockStdin(null);
|
|
99
|
+
await expect(readJsonInput(undefined, "-")).rejects.toThrow(
|
|
100
|
+
"Expected JSON on stdin but got none",
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("errors when `-` is given but stdin is empty", async () => {
|
|
105
|
+
mockStdin(" ");
|
|
106
|
+
await expect(readJsonInput(undefined, "-")).rejects.toThrow(
|
|
107
|
+
"Expected JSON on stdin but got none",
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("reports invalid JSON without a Windows hint on non-Windows", async () => {
|
|
112
|
+
setPlatform("linux");
|
|
113
|
+
await expect(readJsonInput(undefined, "{bad")).rejects.toThrow(
|
|
114
|
+
/^Invalid JSON input:/,
|
|
115
|
+
);
|
|
116
|
+
await expect(readJsonInput(undefined, "{bad")).rejects.not.toThrow(
|
|
117
|
+
/cmd\/PowerShell/,
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("appends a shell-quoting hint on Windows", async () => {
|
|
122
|
+
setPlatform("win32");
|
|
123
|
+
await expect(readJsonInput(undefined, "{bad")).rejects.toThrow(
|
|
124
|
+
/cmd\/PowerShell.*--file.*--body -/s,
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
});
|
package/src/utils/input.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { readStdin } from "@uipath/common";
|
|
1
2
|
import { getFileSystem } from "@uipath/filesystem";
|
|
2
3
|
|
|
4
|
+
const STDIN_SENTINEL = "-";
|
|
5
|
+
|
|
3
6
|
export async function readFileBinary(filePath: string): Promise<Uint8Array> {
|
|
4
7
|
const fs = getFileSystem();
|
|
5
8
|
const content = await fs.readFile(filePath);
|
|
@@ -17,6 +20,19 @@ export async function readJsonInput(
|
|
|
17
20
|
if (!filePath && !inline) {
|
|
18
21
|
throw new Error(missingMsg);
|
|
19
22
|
}
|
|
23
|
+
|
|
24
|
+
// `--body -` / `--file -` reads the JSON payload from stdin. Stdin bypasses
|
|
25
|
+
// the shell, so characters like `&` are never mangled by cmd.exe (UV-14637).
|
|
26
|
+
if (inline === STDIN_SENTINEL || filePath === STDIN_SENTINEL) {
|
|
27
|
+
const stdinData = await readStdin();
|
|
28
|
+
if (stdinData === null) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
"Expected JSON on stdin but got none. Pipe data in, e.g. `... --body - < payload.json`.",
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return parseJson(stdinData);
|
|
34
|
+
}
|
|
35
|
+
|
|
20
36
|
let raw: string;
|
|
21
37
|
if (filePath) {
|
|
22
38
|
const fs = getFileSystem();
|
|
@@ -28,5 +44,18 @@ export async function readJsonInput(
|
|
|
28
44
|
} else {
|
|
29
45
|
raw = inline as string;
|
|
30
46
|
}
|
|
31
|
-
return
|
|
47
|
+
return parseJson(raw);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseJson(raw: string): unknown {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(raw);
|
|
53
|
+
} catch (err: unknown) {
|
|
54
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
55
|
+
const hint =
|
|
56
|
+
process.platform === "win32"
|
|
57
|
+
? " On Windows cmd/PowerShell, characters like & | < > ^ split the command — use `--file <path>` or pipe JSON via `--body -`."
|
|
58
|
+
: "";
|
|
59
|
+
throw new Error(`Invalid JSON input: ${detail}.${hint}`);
|
|
60
|
+
}
|
|
32
61
|
}
|
package/src/utils/output.spec.ts
CHANGED
|
@@ -31,6 +31,7 @@ describe("fail", () => {
|
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
describe("requireDestructiveConfirmation", () => {
|
|
34
|
+
const operation = "delete entity 'abc'";
|
|
34
35
|
const reasonInstruction = "Pass --reason to explain.";
|
|
35
36
|
|
|
36
37
|
beforeEach(() => {
|
|
@@ -38,32 +39,44 @@ describe("requireDestructiveConfirmation", () => {
|
|
|
38
39
|
process.exitCode = undefined;
|
|
39
40
|
});
|
|
40
41
|
|
|
41
|
-
it("returns the trimmed reason when --
|
|
42
|
+
it("returns the trimmed reason when --yes and --reason are present", () => {
|
|
42
43
|
const result = requireDestructiveConfirmation(
|
|
43
|
-
{
|
|
44
|
+
{ yes: true, reason: " cleanup " },
|
|
45
|
+
operation,
|
|
44
46
|
reasonInstruction,
|
|
45
47
|
);
|
|
46
48
|
expect(result).toBe("cleanup");
|
|
47
49
|
expect(OutputFormatter.error).not.toHaveBeenCalled();
|
|
48
50
|
});
|
|
49
51
|
|
|
50
|
-
it("
|
|
52
|
+
it("accepts the deprecated --confirm alias for --yes", () => {
|
|
53
|
+
const result = requireDestructiveConfirmation(
|
|
54
|
+
{ confirm: true, reason: "cleanup" },
|
|
55
|
+
operation,
|
|
56
|
+
reasonInstruction,
|
|
57
|
+
);
|
|
58
|
+
expect(result).toBe("cleanup");
|
|
59
|
+
expect(OutputFormatter.error).not.toHaveBeenCalled();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("fails and returns null when neither --yes nor --confirm is present", () => {
|
|
63
|
+
// The confirmation error is emitted by @uipath/common's
|
|
64
|
+
// requireConfirmation (covered by confirmation.spec.ts); it routes
|
|
65
|
+
// through common's own OutputFormatter, which this suite's module-level
|
|
66
|
+
// mock doesn't intercept. Assert the observable contract here.
|
|
51
67
|
const result = requireDestructiveConfirmation(
|
|
52
68
|
{ reason: "cleanup" },
|
|
69
|
+
operation,
|
|
53
70
|
reasonInstruction,
|
|
54
71
|
);
|
|
55
72
|
expect(result).toBeNull();
|
|
56
|
-
expect(OutputFormatter.error).toHaveBeenCalledWith(
|
|
57
|
-
expect.objectContaining({
|
|
58
|
-
Message: "Confirmation required for destructive operation",
|
|
59
|
-
}),
|
|
60
|
-
);
|
|
61
73
|
expect(process.exitCode).toBe(1);
|
|
62
74
|
});
|
|
63
75
|
|
|
64
76
|
it("fails and returns null when --reason is missing or blank", () => {
|
|
65
77
|
const result = requireDestructiveConfirmation(
|
|
66
|
-
{
|
|
78
|
+
{ yes: true, reason: " " },
|
|
79
|
+
operation,
|
|
67
80
|
reasonInstruction,
|
|
68
81
|
);
|
|
69
82
|
expect(result).toBeNull();
|
package/src/utils/output.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
OutputFormatter,
|
|
3
|
+
processContext,
|
|
4
|
+
RESULTS,
|
|
5
|
+
requireConfirmation,
|
|
6
|
+
warnDeprecatedOptionAlias,
|
|
7
|
+
} from "@uipath/common";
|
|
2
8
|
|
|
3
9
|
/**
|
|
4
10
|
* Emit a standard failure result and set a non-zero exit code.
|
|
@@ -17,27 +23,39 @@ export function fail(message: string, instructions: string): void {
|
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
export interface DestructiveOptions {
|
|
26
|
+
yes?: boolean;
|
|
27
|
+
// Legacy hidden alias for `yes`. The canonical flag is `--yes`; `--confirm`
|
|
28
|
+
// keeps working (with a deprecation warning) so existing scripts don't break.
|
|
20
29
|
confirm?: boolean;
|
|
21
30
|
reason?: string;
|
|
22
31
|
}
|
|
23
32
|
|
|
24
33
|
/**
|
|
25
|
-
* Validate the `--
|
|
26
|
-
*
|
|
27
|
-
*
|
|
34
|
+
* Validate the `--yes`/`--reason` flags required for a destructive operation.
|
|
35
|
+
* `--confirm` is accepted as a deprecated alias for `--yes`. On failure, emits
|
|
36
|
+
* the appropriate structured error, sets the exit code, and returns `null`. On
|
|
37
|
+
* success, returns the trimmed reason.
|
|
28
38
|
*
|
|
39
|
+
* @param operation - short description of what will happen, e.g.
|
|
40
|
+
* `"delete entity 'abc'"`, surfaced in the confirmation error.
|
|
29
41
|
* @param reasonInstruction - Instruction text shown when `--reason` is missing,
|
|
30
42
|
* e.g. `'Pass --reason "<text>" to record why the choice set is being deleted.'`
|
|
31
43
|
*/
|
|
32
44
|
export function requireDestructiveConfirmation(
|
|
33
45
|
options: DestructiveOptions,
|
|
46
|
+
operation: string,
|
|
34
47
|
reasonInstruction: string,
|
|
35
48
|
): string | null {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
// --confirm is a deprecated alias for the canonical --yes.
|
|
50
|
+
if (options.confirm === true && options.yes !== true) {
|
|
51
|
+
warnDeprecatedOptionAlias("--confirm", "--yes");
|
|
52
|
+
}
|
|
53
|
+
if (
|
|
54
|
+
!requireConfirmation(
|
|
55
|
+
{ yes: options.yes === true || options.confirm === true },
|
|
56
|
+
operation,
|
|
57
|
+
)
|
|
58
|
+
) {
|
|
41
59
|
return null;
|
|
42
60
|
}
|
|
43
61
|
|