@uipath/data-fabric-tool 1.195.0 → 1.196.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.
- package/dist/tool.js +4631 -4224
- package/package.json +2 -2
- package/src/commands/choice-sets.spec.ts +45 -12
- package/src/commands/choice-sets.ts +13 -3
- package/src/commands/entities.spec.ts +51 -14
- package/src/commands/entities.ts +21 -5
- package/src/commands/files.spec.ts +45 -3
- package/src/commands/files.ts +21 -2
- package/src/commands/records.spec.ts +87 -0
- package/src/commands/records.ts +24 -5
- 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 {
|
|
@@ -39,6 +39,9 @@ interface UpdateOptions {
|
|
|
39
39
|
|
|
40
40
|
interface DeleteOptions {
|
|
41
41
|
tenant?: string;
|
|
42
|
+
yes?: boolean;
|
|
43
|
+
confirm?: boolean;
|
|
44
|
+
reason?: string;
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
interface QueryOptions {
|
|
@@ -311,7 +314,7 @@ export const registerRecordsCommand = (program: Command) => {
|
|
|
311
314
|
)
|
|
312
315
|
.option(
|
|
313
316
|
"--body <json>",
|
|
314
|
-
"Inline JSON record data (object or array of objects)",
|
|
317
|
+
"Inline JSON record data (object or array of objects; use `-` to read from stdin)",
|
|
315
318
|
)
|
|
316
319
|
.examples(RECORDS_INSERT_EXAMPLES)
|
|
317
320
|
.trackedAction(
|
|
@@ -394,7 +397,7 @@ export const registerRecordsCommand = (program: Command) => {
|
|
|
394
397
|
)
|
|
395
398
|
.option(
|
|
396
399
|
"--body <json>",
|
|
397
|
-
"Inline JSON record data (must include Id field)",
|
|
400
|
+
"Inline JSON record data (must include Id field; use `-` to read from stdin)",
|
|
398
401
|
)
|
|
399
402
|
.examples(RECORDS_UPDATE_EXAMPLES)
|
|
400
403
|
.trackedAction(
|
|
@@ -509,7 +512,7 @@ export const registerRecordsCommand = (program: Command) => {
|
|
|
509
512
|
)
|
|
510
513
|
.option(
|
|
511
514
|
"--body <json>",
|
|
512
|
-
"Inline JSON query options (filterGroup, selectedFields, sortOptions, aggregates, groupBy)",
|
|
515
|
+
"Inline JSON query options (filterGroup, selectedFields, sortOptions, aggregates, groupBy; use `-` to read from stdin)",
|
|
513
516
|
)
|
|
514
517
|
.option(
|
|
515
518
|
"-l, --limit <number>",
|
|
@@ -689,6 +692,14 @@ export const registerRecordsCommand = (program: Command) => {
|
|
|
689
692
|
.addOption(
|
|
690
693
|
createHiddenDeprecatedTenantOption("-t, --tenant <tenant-name>"),
|
|
691
694
|
)
|
|
695
|
+
.option("-y, --yes", "Acknowledge this is an irreversible operation")
|
|
696
|
+
.addOption(
|
|
697
|
+
new Option("--confirm", "Deprecated alias for --yes").hideHelp(),
|
|
698
|
+
)
|
|
699
|
+
.option(
|
|
700
|
+
"--reason <reason>",
|
|
701
|
+
"Reason for the deletion — echoed back in the response so the caller can log it",
|
|
702
|
+
)
|
|
692
703
|
.examples(RECORDS_DELETE_EXAMPLES)
|
|
693
704
|
.trackedAction(
|
|
694
705
|
processContext,
|
|
@@ -697,6 +708,13 @@ export const registerRecordsCommand = (program: Command) => {
|
|
|
697
708
|
recordIds: string[],
|
|
698
709
|
options: DeleteOptions,
|
|
699
710
|
) => {
|
|
711
|
+
const reason = requireDestructiveConfirmation(
|
|
712
|
+
options,
|
|
713
|
+
`delete ${recordIds.length} record(s) from entity '${entityId}'`,
|
|
714
|
+
'Pass --reason "<text>" to record why the records are being deleted.',
|
|
715
|
+
);
|
|
716
|
+
if (reason === null) return;
|
|
717
|
+
|
|
700
718
|
const sdk = await connectOrFail(options.tenant);
|
|
701
719
|
if (!sdk) return;
|
|
702
720
|
|
|
@@ -721,6 +739,7 @@ export const registerRecordsCommand = (program: Command) => {
|
|
|
721
739
|
FailureCount: failureCount,
|
|
722
740
|
SuccessRecords: r.successRecords ?? [],
|
|
723
741
|
FailureRecords: r.failureRecords ?? [],
|
|
742
|
+
Reason: reason,
|
|
724
743
|
},
|
|
725
744
|
});
|
|
726
745
|
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
|
|