@uipath/data-fabric-tool 1.198.0 → 1.199.0-preview.104
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 +774 -28
- package/package.json +2 -2
- package/src/commands/choice-sets.spec.ts +107 -4
- package/src/commands/choice-sets.ts +15 -3
- package/src/commands/directory.spec.ts +544 -0
- package/src/commands/directory.ts +325 -0
- package/src/commands/entities.spec.ts +239 -2
- package/src/commands/entities.ts +47 -3
- package/src/commands/records.spec.ts +119 -3
- package/src/commands/records.ts +83 -2
- package/src/commands/roles.spec.ts +153 -0
- package/src/commands/roles.ts +88 -0
- package/src/tool.ts +4 -0
- package/src/utils/validate.spec.ts +555 -0
- package/src/utils/validate.ts +611 -0
- package/tests/access-management.e2e.test.ts +347 -0
- package/tests/choice-sets-lifecycle.e2e.test.ts +741 -0
- package/tests/entities-lifecycle.e2e.test.ts +614 -0
- package/tests/files-management.e2e.test.ts +518 -0
- package/tests/records-management.e2e.test.ts +646 -0
package/src/commands/entities.ts
CHANGED
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
OutputFormatter,
|
|
7
7
|
processContext,
|
|
8
8
|
RESULTS,
|
|
9
|
+
runChecks,
|
|
10
|
+
type ValidationError,
|
|
9
11
|
} from "@uipath/common";
|
|
10
12
|
import type {
|
|
11
13
|
EntityCreateFieldOptions,
|
|
@@ -20,6 +22,15 @@ import { type Command, Option } from "commander";
|
|
|
20
22
|
import { readJsonInput } from "../utils/input";
|
|
21
23
|
import { fail, requireDestructiveConfirmation } from "../utils/output";
|
|
22
24
|
import { connectOrFail } from "../utils/sdk-client";
|
|
25
|
+
import {
|
|
26
|
+
findInvalidFieldConstraints,
|
|
27
|
+
findInvalidFieldName,
|
|
28
|
+
findIsUniqueInUpdateFields,
|
|
29
|
+
findMissingComplexFieldExtras,
|
|
30
|
+
findReferenceFolderKeyOnChoiceSet,
|
|
31
|
+
findUiBrokenFieldType,
|
|
32
|
+
validateName,
|
|
33
|
+
} from "../utils/validate";
|
|
23
34
|
|
|
24
35
|
interface ListOptions {
|
|
25
36
|
tenant?: string;
|
|
@@ -145,13 +156,14 @@ const ENTITIES_DELETE_EXAMPLES: CommandExample[] = [
|
|
|
145
156
|
const ENTITIES_CREATE_EXAMPLES: CommandExample[] = [
|
|
146
157
|
{
|
|
147
158
|
Description:
|
|
148
|
-
"Create an entity with choice-set, relationship, and
|
|
159
|
+
"Create an entity with choice-set, relationship, file, and large-text fields. " +
|
|
149
160
|
"CHOICE_SET_SINGLE/CHOICE_SET_MULTIPLE require 'choiceSetId' (from 'df choice-sets list'). " +
|
|
150
161
|
"RELATIONSHIP requires 'referenceEntityId' (UUID of the target entity, from 'df entities list') and 'referenceFieldId' (UUID of the field on the target entity, from 'df entities get <target-id>'). " +
|
|
151
162
|
"Note: a RELATIONSHIP column on a record always stores the target record's UUID 'Id' (regardless of which 'referenceFieldId' configured the join) — see 'df records insert' for how to write the value. " +
|
|
152
|
-
"FILE fields take only 'fieldName' and 'type' — the server auto-wires the internal attachment reference; populate them later with 'uip df files upload <entity-id> <record-id> <field-name> --file <path>'."
|
|
163
|
+
"FILE fields take only 'fieldName' and 'type' — the server auto-wires the internal attachment reference; populate them later with 'uip df files upload <entity-id> <record-id> <field-name> --file <path>'. " +
|
|
164
|
+
"MULTILINE_MAX holds very large text ('lengthLimit' = UTF-16 byte budget, 1-131072; default max ~65,536 chars). Not filterable/sortable; 'records list'/'query' return a size marker — full content via 'records get'. Tenant-gated by the 'MultilineMax' feature flag.",
|
|
153
165
|
Command:
|
|
154
|
-
'uip df entities create Expense --body \'{"displayName":"Expense","fields":[{"fieldName":"category","type":"CHOICE_SET_SINGLE","choiceSetId":"c1d2e3f4-0000-0000-0000-000000000001","isRequired":true},{"fieldName":"tags","type":"CHOICE_SET_MULTIPLE","choiceSetId":"c1d2e3f4-0000-0000-0000-000000000002"},{"fieldName":"submitter","type":"RELATIONSHIP","referenceEntityId":"a1b2c3d4-0000-0000-0000-000000000010","referenceFieldId":"f1000000-0000-0000-0000-000000000100","isRequired":true},{"fieldName":"receipt","type":"FILE"}]}\'',
|
|
166
|
+
'uip df entities create Expense --body \'{"displayName":"Expense","fields":[{"fieldName":"category","type":"CHOICE_SET_SINGLE","choiceSetId":"c1d2e3f4-0000-0000-0000-000000000001","isRequired":true},{"fieldName":"tags","type":"CHOICE_SET_MULTIPLE","choiceSetId":"c1d2e3f4-0000-0000-0000-000000000002"},{"fieldName":"submitter","type":"RELATIONSHIP","referenceEntityId":"a1b2c3d4-0000-0000-0000-000000000010","referenceFieldId":"f1000000-0000-0000-0000-000000000100","isRequired":true},{"fieldName":"receipt","type":"FILE"},{"fieldName":"notes","type":"MULTILINE_MAX"}]}\'',
|
|
155
167
|
Output: {
|
|
156
168
|
Code: "EntityCreated",
|
|
157
169
|
Data: { ID: "a1b2c3d4-0000-0000-0000-000000000004" },
|
|
@@ -204,6 +216,20 @@ function pickKnownUpdateKeys(
|
|
|
204
216
|
return out as EntityUpdateByIdOptions;
|
|
205
217
|
}
|
|
206
218
|
|
|
219
|
+
// Run every check that applies to a `fields` / `addFields` payload:
|
|
220
|
+
// name rules and UI-broken-type substitution. The SDK-enum type check
|
|
221
|
+
// stays inline at the call site because it needs `hasInvalidFieldType`
|
|
222
|
+
// (bound to the SDK enum, not portable to @uipath/common).
|
|
223
|
+
function validateFieldsPayload(fields: unknown[]): ValidationError | null {
|
|
224
|
+
return runChecks([
|
|
225
|
+
() => findInvalidFieldName(fields),
|
|
226
|
+
() => findUiBrokenFieldType(fields),
|
|
227
|
+
() => findReferenceFolderKeyOnChoiceSet(fields),
|
|
228
|
+
() => findMissingComplexFieldExtras(fields),
|
|
229
|
+
() => findInvalidFieldConstraints(fields),
|
|
230
|
+
]);
|
|
231
|
+
}
|
|
232
|
+
|
|
207
233
|
export const registerEntitiesCommand = (program: Command) => {
|
|
208
234
|
const entities = program
|
|
209
235
|
.command("entities")
|
|
@@ -408,6 +434,12 @@ export const registerEntitiesCommand = (program: Command) => {
|
|
|
408
434
|
);
|
|
409
435
|
}
|
|
410
436
|
|
|
437
|
+
const err = runChecks([
|
|
438
|
+
() => validateName(name, "entity"),
|
|
439
|
+
() => validateFieldsPayload(definition.fields as unknown[]),
|
|
440
|
+
]);
|
|
441
|
+
if (err) return fail(err.message, err.instructions);
|
|
442
|
+
|
|
411
443
|
const sdk = await connectOrFail(options.tenant);
|
|
412
444
|
if (!sdk) return;
|
|
413
445
|
|
|
@@ -599,6 +631,12 @@ export const registerEntitiesCommand = (program: Command) => {
|
|
|
599
631
|
);
|
|
600
632
|
}
|
|
601
633
|
|
|
634
|
+
const addErr = validateFieldsPayload(
|
|
635
|
+
input.addFields as unknown[],
|
|
636
|
+
);
|
|
637
|
+
if (addErr)
|
|
638
|
+
return fail(addErr.message, addErr.instructions);
|
|
639
|
+
|
|
602
640
|
const addNames = (
|
|
603
641
|
input.addFields as { fieldName: string }[]
|
|
604
642
|
).map((f) => f.fieldName);
|
|
@@ -639,6 +677,12 @@ export const registerEntitiesCommand = (program: Command) => {
|
|
|
639
677
|
'Use \'df entities get <entityId>\' to find field IDs. Example: {"id":"<fieldId>","displayName":"Total Amount","isRequired":true}',
|
|
640
678
|
);
|
|
641
679
|
}
|
|
680
|
+
|
|
681
|
+
const updateErr = findIsUniqueInUpdateFields(
|
|
682
|
+
input.updateFields as unknown[],
|
|
683
|
+
);
|
|
684
|
+
if (updateErr)
|
|
685
|
+
return fail(updateErr.message, updateErr.instructions);
|
|
642
686
|
}
|
|
643
687
|
|
|
644
688
|
const sdk = await connectOrFail(options.tenant);
|
|
@@ -37,6 +37,9 @@ function buildProgram(): Command {
|
|
|
37
37
|
function mockSdk(overrides: Record<string, unknown> = {}) {
|
|
38
38
|
const sdk = {
|
|
39
39
|
entities: {
|
|
40
|
+
// Default schema has no FILE fields — insert/update FILE-payload
|
|
41
|
+
// guard is a no-op unless a test overrides this.
|
|
42
|
+
getById: vi.fn().mockResolvedValue({ fields: [] }),
|
|
40
43
|
getAllRecords: vi.fn().mockResolvedValue({
|
|
41
44
|
items: [],
|
|
42
45
|
totalCount: 0,
|
|
@@ -781,6 +784,66 @@ describe("records insert", () => {
|
|
|
781
784
|
);
|
|
782
785
|
expect(process.exitCode).toBe(1);
|
|
783
786
|
});
|
|
787
|
+
|
|
788
|
+
it("skips FILE-payload validation when schema fetch fails and lets the SDK error surface", async () => {
|
|
789
|
+
const sdk = mockSdk();
|
|
790
|
+
vi.mocked(sdk.entities.getById).mockRejectedValue(
|
|
791
|
+
new Error("Entity not found"),
|
|
792
|
+
);
|
|
793
|
+
vi.mocked(sdk.entities.insertRecordById).mockRejectedValue(
|
|
794
|
+
new Error("Entity not found"),
|
|
795
|
+
);
|
|
796
|
+
|
|
797
|
+
const program = buildProgram();
|
|
798
|
+
await program.parseAsync([
|
|
799
|
+
"node",
|
|
800
|
+
"test",
|
|
801
|
+
"records",
|
|
802
|
+
"insert",
|
|
803
|
+
"missing-entity-id",
|
|
804
|
+
"--body",
|
|
805
|
+
'{"title":"Q1"}',
|
|
806
|
+
]);
|
|
807
|
+
|
|
808
|
+
expect(sdk.entities.insertRecordById).toHaveBeenCalled();
|
|
809
|
+
expect(OutputFormatter.error).toHaveBeenCalledWith(
|
|
810
|
+
expect.objectContaining({
|
|
811
|
+
Result: "Failure",
|
|
812
|
+
Message: "Error inserting record",
|
|
813
|
+
}),
|
|
814
|
+
);
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
it("rejects a FILE-typed key in the insert body", async () => {
|
|
818
|
+
const sdk = mockSdk();
|
|
819
|
+
vi.mocked(sdk.entities.getById).mockResolvedValue({
|
|
820
|
+
fields: [
|
|
821
|
+
{ name: "title", fieldDataType: { name: "STRING" } },
|
|
822
|
+
{ name: "receipt", fieldDataType: { name: "FILE" } },
|
|
823
|
+
],
|
|
824
|
+
} as never);
|
|
825
|
+
|
|
826
|
+
const program = buildProgram();
|
|
827
|
+
await program.parseAsync([
|
|
828
|
+
"node",
|
|
829
|
+
"test",
|
|
830
|
+
"records",
|
|
831
|
+
"insert",
|
|
832
|
+
"entity-id",
|
|
833
|
+
"--body",
|
|
834
|
+
'{"title":"Q1","receipt":"/tmp/r.pdf"}',
|
|
835
|
+
]);
|
|
836
|
+
|
|
837
|
+
expect(sdk.entities.insertRecordById).not.toHaveBeenCalled();
|
|
838
|
+
expect(OutputFormatter.error).toHaveBeenCalledWith(
|
|
839
|
+
expect.objectContaining({
|
|
840
|
+
Result: "Failure",
|
|
841
|
+
Message: expect.stringContaining("FILE-typed field 'receipt'"),
|
|
842
|
+
Instructions: expect.stringContaining("files upload"),
|
|
843
|
+
}),
|
|
844
|
+
);
|
|
845
|
+
expect(process.exitCode).toBe(1);
|
|
846
|
+
});
|
|
784
847
|
});
|
|
785
848
|
|
|
786
849
|
describe("records update", () => {
|
|
@@ -1408,8 +1471,8 @@ describe("records query", () => {
|
|
|
1408
1471
|
queryFilters: [
|
|
1409
1472
|
{
|
|
1410
1473
|
fieldName: "amount",
|
|
1411
|
-
operator: "
|
|
1412
|
-
value: 100,
|
|
1474
|
+
operator: ">",
|
|
1475
|
+
value: "100",
|
|
1413
1476
|
},
|
|
1414
1477
|
],
|
|
1415
1478
|
},
|
|
@@ -1550,7 +1613,7 @@ describe("records query", () => {
|
|
|
1550
1613
|
queryFilters: [
|
|
1551
1614
|
{
|
|
1552
1615
|
fieldName: "status",
|
|
1553
|
-
operator: "
|
|
1616
|
+
operator: "=",
|
|
1554
1617
|
value: "Active",
|
|
1555
1618
|
},
|
|
1556
1619
|
],
|
|
@@ -1953,6 +2016,59 @@ describe("records import", () => {
|
|
|
1953
2016
|
});
|
|
1954
2017
|
});
|
|
1955
2018
|
|
|
2019
|
+
describe("records query — filter validation", () => {
|
|
2020
|
+
beforeEach(() => {
|
|
2021
|
+
vi.resetAllMocks();
|
|
2022
|
+
process.exitCode = undefined;
|
|
2023
|
+
});
|
|
2024
|
+
|
|
2025
|
+
it("rejects an unsupported operator", async () => {
|
|
2026
|
+
const sdk = mockSdk();
|
|
2027
|
+
const program = buildProgram();
|
|
2028
|
+
await program.parseAsync([
|
|
2029
|
+
"node",
|
|
2030
|
+
"test",
|
|
2031
|
+
"records",
|
|
2032
|
+
"query",
|
|
2033
|
+
"entity-id",
|
|
2034
|
+
"--body",
|
|
2035
|
+
'{"filterGroup":{"queryFilters":[{"fieldName":"Status","operator":"==","value":"Active"}]}}',
|
|
2036
|
+
]);
|
|
2037
|
+
|
|
2038
|
+
expect(sdk.entities.queryRecordsById).not.toHaveBeenCalled();
|
|
2039
|
+
expect(OutputFormatter.error).toHaveBeenCalledWith(
|
|
2040
|
+
expect.objectContaining({
|
|
2041
|
+
Result: "Failure",
|
|
2042
|
+
Message: expect.stringContaining("unsupported operator '=='"),
|
|
2043
|
+
}),
|
|
2044
|
+
);
|
|
2045
|
+
expect(process.exitCode).toBe(1);
|
|
2046
|
+
});
|
|
2047
|
+
|
|
2048
|
+
it("rejects 'in' without a valueList", async () => {
|
|
2049
|
+
const sdk = mockSdk();
|
|
2050
|
+
const program = buildProgram();
|
|
2051
|
+
await program.parseAsync([
|
|
2052
|
+
"node",
|
|
2053
|
+
"test",
|
|
2054
|
+
"records",
|
|
2055
|
+
"query",
|
|
2056
|
+
"entity-id",
|
|
2057
|
+
"--body",
|
|
2058
|
+
'{"filterGroup":{"queryFilters":[{"fieldName":"Status","operator":"in","value":"Active"}]}}',
|
|
2059
|
+
]);
|
|
2060
|
+
|
|
2061
|
+
expect(sdk.entities.queryRecordsById).not.toHaveBeenCalled();
|
|
2062
|
+
expect(OutputFormatter.error).toHaveBeenCalledWith(
|
|
2063
|
+
expect.objectContaining({
|
|
2064
|
+
Result: "Failure",
|
|
2065
|
+
Message: expect.stringContaining("requires 'valueList'"),
|
|
2066
|
+
}),
|
|
2067
|
+
);
|
|
2068
|
+
expect(process.exitCode).toBe(1);
|
|
2069
|
+
});
|
|
2070
|
+
});
|
|
2071
|
+
|
|
1956
2072
|
describe("records query — pagination", () => {
|
|
1957
2073
|
beforeEach(() => {
|
|
1958
2074
|
vi.resetAllMocks();
|
package/src/commands/records.ts
CHANGED
|
@@ -13,6 +13,60 @@ import { type Command, Option } from "commander";
|
|
|
13
13
|
import { readFileBinary, readJsonInput } from "../utils/input";
|
|
14
14
|
import { fail, requireDestructiveConfirmation } from "../utils/output";
|
|
15
15
|
import { connectOrFail } from "../utils/sdk-client";
|
|
16
|
+
import { findFilePayloadKeys, validateQueryFilter } from "../utils/validate";
|
|
17
|
+
|
|
18
|
+
// Fetch the entity schema and return the set of FILE-typed field names.
|
|
19
|
+
// One RTT — worth it: without this, `records insert/update` silently
|
|
20
|
+
// strips FILE keys server-side and returns Success unchanged.
|
|
21
|
+
//
|
|
22
|
+
// On any fetch error, returns an empty set instead of surfacing the error.
|
|
23
|
+
// The SDK write call that follows will hit the same failure and produce
|
|
24
|
+
// the authoritative message ("Entity not found", auth issue, network,
|
|
25
|
+
// etc.) — pre-empting it here would hide the real error behind a
|
|
26
|
+
// misleading "validation" wrapper.
|
|
27
|
+
async function loadFileFieldNames(
|
|
28
|
+
sdk: Awaited<ReturnType<typeof connectOrFail>>,
|
|
29
|
+
entityId: string,
|
|
30
|
+
folderKey?: string,
|
|
31
|
+
): Promise<Set<string>> {
|
|
32
|
+
const names = new Set<string>();
|
|
33
|
+
if (!sdk) return names;
|
|
34
|
+
const [schemaErr, entity] = await catchError(
|
|
35
|
+
sdk.entities.getById(
|
|
36
|
+
entityId,
|
|
37
|
+
folderKey !== undefined ? { folderKey } : undefined,
|
|
38
|
+
),
|
|
39
|
+
);
|
|
40
|
+
if (schemaErr) return names;
|
|
41
|
+
// Detect FILE fields by their data-type name. The `isAttachment` boolean
|
|
42
|
+
// exists on the SDK type but the server does not reliably set it (all
|
|
43
|
+
// fields — including FILE — come back with `IsAttachment: false`), so
|
|
44
|
+
// relying on it silently skipped the FILE check in live tests.
|
|
45
|
+
//
|
|
46
|
+
// The response also uses PascalCase at runtime while the SDK's TypeScript
|
|
47
|
+
// type declares camelCase — read both to survive that skew.
|
|
48
|
+
const rec = entity as unknown as {
|
|
49
|
+
fields?: Array<Record<string, unknown>>;
|
|
50
|
+
Fields?: Array<Record<string, unknown>>;
|
|
51
|
+
};
|
|
52
|
+
const fieldList = rec.fields ?? rec.Fields ?? [];
|
|
53
|
+
for (const f of fieldList) {
|
|
54
|
+
const dataType = (f.fieldDataType ?? f.FieldDataType) as
|
|
55
|
+
| { name?: string; Name?: string }
|
|
56
|
+
| undefined;
|
|
57
|
+
const typeName = dataType?.name ?? dataType?.Name;
|
|
58
|
+
const fieldName =
|
|
59
|
+
typeof f.name === "string"
|
|
60
|
+
? f.name
|
|
61
|
+
: typeof f.Name === "string"
|
|
62
|
+
? f.Name
|
|
63
|
+
: undefined;
|
|
64
|
+
if (typeName === "FILE" && fieldName !== undefined) {
|
|
65
|
+
names.add(fieldName);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return names;
|
|
69
|
+
}
|
|
16
70
|
|
|
17
71
|
interface ListOptions {
|
|
18
72
|
tenant?: string;
|
|
@@ -72,7 +126,8 @@ interface BatchResult {
|
|
|
72
126
|
|
|
73
127
|
const RECORDS_LIST_EXAMPLES: CommandExample[] = [
|
|
74
128
|
{
|
|
75
|
-
Description:
|
|
129
|
+
Description:
|
|
130
|
+
"List records in an entity. MULTILINE_MAX fields return a size marker (see 'notes' below) — full value via 'records get'.",
|
|
76
131
|
Command:
|
|
77
132
|
"uip df records list a1b2c3d4-0000-0000-0000-000000000001 --limit 2",
|
|
78
133
|
Output: {
|
|
@@ -82,10 +137,12 @@ const RECORDS_LIST_EXAMPLES: CommandExample[] = [
|
|
|
82
137
|
{
|
|
83
138
|
Id: "b2c3d4e5-0000-0000-0000-000000000001",
|
|
84
139
|
amount: 1500,
|
|
140
|
+
notes: "HasValue=true Length=20000",
|
|
85
141
|
},
|
|
86
142
|
{
|
|
87
143
|
Id: "b2c3d4e5-0000-0000-0000-000000000002",
|
|
88
144
|
amount: 2750,
|
|
145
|
+
notes: null,
|
|
89
146
|
},
|
|
90
147
|
],
|
|
91
148
|
totalCount: 2,
|
|
@@ -97,7 +154,8 @@ const RECORDS_LIST_EXAMPLES: CommandExample[] = [
|
|
|
97
154
|
|
|
98
155
|
const RECORDS_GET_EXAMPLES: CommandExample[] = [
|
|
99
156
|
{
|
|
100
|
-
Description:
|
|
157
|
+
Description:
|
|
158
|
+
"Get a record by ID. Returns MULTILINE_MAX fields in full (see 'notes' below).",
|
|
101
159
|
Command:
|
|
102
160
|
"uip df records get a1b2c3d4-0000-0000-0000-000000000001 b2c3d4e5-0000-0000-0000-000000000001",
|
|
103
161
|
Output: {
|
|
@@ -106,6 +164,7 @@ const RECORDS_GET_EXAMPLES: CommandExample[] = [
|
|
|
106
164
|
Id: "b2c3d4e5-0000-0000-0000-000000000001",
|
|
107
165
|
amount: 1500,
|
|
108
166
|
status: "Paid",
|
|
167
|
+
notes: "Customer disputed the Q1 late fee. Timeline of the case: … (the complete 20,000-character text, returned in full)",
|
|
109
168
|
},
|
|
110
169
|
},
|
|
111
170
|
},
|
|
@@ -411,6 +470,14 @@ export const registerRecordsCommand = (program: Command) => {
|
|
|
411
470
|
? { folderKey: options.folderKey }
|
|
412
471
|
: undefined;
|
|
413
472
|
|
|
473
|
+
const fileNames = await loadFileFieldNames(
|
|
474
|
+
sdk,
|
|
475
|
+
entityId,
|
|
476
|
+
options.folderKey,
|
|
477
|
+
);
|
|
478
|
+
const fileErr = findFilePayloadKeys(recordsList, fileNames);
|
|
479
|
+
if (fileErr) return fail(fileErr.message, fileErr.instructions);
|
|
480
|
+
|
|
414
481
|
if (recordsList.length === 1) {
|
|
415
482
|
const [insertError, result] = await catchError(
|
|
416
483
|
sdk.entities.insertRecordById(
|
|
@@ -511,6 +578,14 @@ export const registerRecordsCommand = (program: Command) => {
|
|
|
511
578
|
? { folderKey: options.folderKey }
|
|
512
579
|
: undefined;
|
|
513
580
|
|
|
581
|
+
const fileNames = await loadFileFieldNames(
|
|
582
|
+
sdk,
|
|
583
|
+
entityId,
|
|
584
|
+
options.folderKey,
|
|
585
|
+
);
|
|
586
|
+
const fileErr = findFilePayloadKeys(recordsList, fileNames);
|
|
587
|
+
if (fileErr) return fail(fileErr.message, fileErr.instructions);
|
|
588
|
+
|
|
514
589
|
if (recordsList.length === 1) {
|
|
515
590
|
const record = recordsList[0];
|
|
516
591
|
const recordId =
|
|
@@ -685,6 +760,12 @@ export const registerRecordsCommand = (program: Command) => {
|
|
|
685
760
|
);
|
|
686
761
|
}
|
|
687
762
|
queryBody = parsed as Record<string, unknown>;
|
|
763
|
+
|
|
764
|
+
const filterErr = validateQueryFilter(
|
|
765
|
+
queryBody.filterGroup,
|
|
766
|
+
);
|
|
767
|
+
if (filterErr)
|
|
768
|
+
return fail(filterErr.message, filterErr.instructions);
|
|
688
769
|
}
|
|
689
770
|
|
|
690
771
|
const sdk = await connectOrFail(options.tenant);
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
vi.mock("../utils/sdk-client", () => ({
|
|
5
|
+
createDataFabricClient: vi.fn(),
|
|
6
|
+
connectOrFail: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
vi.mock("@uipath/common", async (importOriginal) => {
|
|
10
|
+
const actual = await importOriginal<typeof import("@uipath/common")>();
|
|
11
|
+
return {
|
|
12
|
+
...actual,
|
|
13
|
+
OutputFormatter: {
|
|
14
|
+
success: vi.fn(),
|
|
15
|
+
error: vi.fn(),
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
import { OutputFormatter } from "@uipath/common";
|
|
21
|
+
import { connectOrFail } from "../utils/sdk-client";
|
|
22
|
+
import { registerRolesCommand } from "./roles";
|
|
23
|
+
|
|
24
|
+
function buildProgram(): Command {
|
|
25
|
+
const program = new Command();
|
|
26
|
+
program.name("test").exitOverride();
|
|
27
|
+
registerRolesCommand(program);
|
|
28
|
+
return program;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function mockSdk(overrides: Record<string, unknown> = {}) {
|
|
32
|
+
const sdk = {
|
|
33
|
+
entities: {
|
|
34
|
+
roles: {
|
|
35
|
+
getAll: vi.fn().mockResolvedValue([]),
|
|
36
|
+
...overrides,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
vi.mocked(connectOrFail).mockResolvedValue(sdk as never);
|
|
41
|
+
return sdk;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe("roles", () => {
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
vi.resetAllMocks();
|
|
47
|
+
process.exitCode = undefined;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should register the roles command as hidden with a list subcommand", () => {
|
|
51
|
+
const program = buildProgram();
|
|
52
|
+
const cmd = program.commands.find((c) => c.name() === "roles");
|
|
53
|
+
expect(cmd).toBeDefined();
|
|
54
|
+
expect(program.commands.some((c) => c.name() === "roles")).toBe(true);
|
|
55
|
+
const subNames = cmd?.commands.map((c) => c.name()) ?? [];
|
|
56
|
+
expect(subNames).toContain("list");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should list roles with stats enabled by default", async () => {
|
|
60
|
+
const sdk = mockSdk();
|
|
61
|
+
const rawRoles = [
|
|
62
|
+
{
|
|
63
|
+
id: "c1d2e3f4-0000-0000-0000-000000000001",
|
|
64
|
+
name: "DataWriter",
|
|
65
|
+
type: "System",
|
|
66
|
+
directoryEntityCount: 3,
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
vi.mocked(sdk.entities.roles.getAll).mockResolvedValue(
|
|
70
|
+
rawRoles as never,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const program = buildProgram();
|
|
74
|
+
await program.parseAsync(["node", "test", "roles", "list"]);
|
|
75
|
+
|
|
76
|
+
expect(sdk.entities.roles.getAll).toHaveBeenCalledWith({
|
|
77
|
+
stats: true,
|
|
78
|
+
});
|
|
79
|
+
expect(OutputFormatter.success).toHaveBeenCalledWith(
|
|
80
|
+
expect.objectContaining({
|
|
81
|
+
Result: "Success",
|
|
82
|
+
Code: "RoleList",
|
|
83
|
+
Data: rawRoles,
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should pass stats false with --no-stats", async () => {
|
|
89
|
+
const sdk = mockSdk();
|
|
90
|
+
|
|
91
|
+
const program = buildProgram();
|
|
92
|
+
await program.parseAsync([
|
|
93
|
+
"node",
|
|
94
|
+
"test",
|
|
95
|
+
"roles",
|
|
96
|
+
"list",
|
|
97
|
+
"--no-stats",
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
expect(sdk.entities.roles.getAll).toHaveBeenCalledWith({
|
|
101
|
+
stats: false,
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should forward --folder-key", async () => {
|
|
106
|
+
const sdk = mockSdk();
|
|
107
|
+
|
|
108
|
+
const program = buildProgram();
|
|
109
|
+
await program.parseAsync([
|
|
110
|
+
"node",
|
|
111
|
+
"test",
|
|
112
|
+
"roles",
|
|
113
|
+
"list",
|
|
114
|
+
"--folder-key",
|
|
115
|
+
"f-key-1",
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
expect(sdk.entities.roles.getAll).toHaveBeenCalledWith({
|
|
119
|
+
stats: true,
|
|
120
|
+
folderKey: "f-key-1",
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("should fail with exit 1 when the SDK call errors", async () => {
|
|
125
|
+
const sdk = mockSdk();
|
|
126
|
+
vi.mocked(sdk.entities.roles.getAll).mockRejectedValue(
|
|
127
|
+
new Error("boom"),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const program = buildProgram();
|
|
131
|
+
await program.parseAsync(["node", "test", "roles", "list"]);
|
|
132
|
+
|
|
133
|
+
expect(OutputFormatter.error).toHaveBeenCalledWith(
|
|
134
|
+
expect.objectContaining({
|
|
135
|
+
Result: "Failure",
|
|
136
|
+
Message: "Error listing roles",
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
expect(OutputFormatter.success).not.toHaveBeenCalled();
|
|
140
|
+
expect(process.exitCode).toBe(1);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should bail when SDK connection fails", async () => {
|
|
144
|
+
const sdk = mockSdk();
|
|
145
|
+
vi.mocked(connectOrFail).mockResolvedValue(undefined);
|
|
146
|
+
|
|
147
|
+
const program = buildProgram();
|
|
148
|
+
await program.parseAsync(["node", "test", "roles", "list"]);
|
|
149
|
+
|
|
150
|
+
expect(sdk.entities.roles.getAll).not.toHaveBeenCalled();
|
|
151
|
+
expect(OutputFormatter.success).not.toHaveBeenCalled();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CommandExample,
|
|
3
|
+
catchError,
|
|
4
|
+
createHiddenDeprecatedTenantOption,
|
|
5
|
+
extractErrorMessage,
|
|
6
|
+
OutputFormatter,
|
|
7
|
+
processContext,
|
|
8
|
+
RESULTS,
|
|
9
|
+
} from "@uipath/common";
|
|
10
|
+
import type { Command } from "commander";
|
|
11
|
+
import { fail } from "../utils/output";
|
|
12
|
+
import { connectOrFail } from "../utils/sdk-client";
|
|
13
|
+
|
|
14
|
+
interface ListOptions {
|
|
15
|
+
tenant?: string;
|
|
16
|
+
stats: boolean;
|
|
17
|
+
folderKey?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ROLES_LIST_EXAMPLES: CommandExample[] = [
|
|
21
|
+
{
|
|
22
|
+
Description: "List Data Fabric access roles.",
|
|
23
|
+
Command: "uip df roles list",
|
|
24
|
+
Output: {
|
|
25
|
+
Code: "RoleList",
|
|
26
|
+
Data: [
|
|
27
|
+
{
|
|
28
|
+
Id: "c1d2e3f4-0000-0000-0000-000000000001",
|
|
29
|
+
Name: "DataWriter",
|
|
30
|
+
Type: "System",
|
|
31
|
+
DirectoryEntityCount: 3,
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Internal manage-access surface — hidden from help output and not part of
|
|
40
|
+
* the documented `uip df` command set.
|
|
41
|
+
*/
|
|
42
|
+
export const registerRolesCommand = (program: Command) => {
|
|
43
|
+
const roles = program
|
|
44
|
+
.command("roles", { hidden: true })
|
|
45
|
+
.description("Manage Data Fabric access roles");
|
|
46
|
+
|
|
47
|
+
roles
|
|
48
|
+
.command("list")
|
|
49
|
+
.description("List Data Fabric access roles")
|
|
50
|
+
.addOption(
|
|
51
|
+
createHiddenDeprecatedTenantOption("-t, --tenant <tenant-name>"),
|
|
52
|
+
)
|
|
53
|
+
.option(
|
|
54
|
+
"--no-stats",
|
|
55
|
+
"Skip role statistics (directory entity counts) in the response",
|
|
56
|
+
)
|
|
57
|
+
.option(
|
|
58
|
+
"--folder-key <key>",
|
|
59
|
+
"Folder key (GUID) for folder-scoped role requests",
|
|
60
|
+
)
|
|
61
|
+
.examples(ROLES_LIST_EXAMPLES)
|
|
62
|
+
.trackedAction(processContext, async (options: ListOptions) => {
|
|
63
|
+
const sdk = await connectOrFail(options.tenant);
|
|
64
|
+
if (!sdk) return;
|
|
65
|
+
|
|
66
|
+
const [listError, result] = await catchError(
|
|
67
|
+
sdk.entities.roles.getAll({
|
|
68
|
+
stats: options.stats,
|
|
69
|
+
...(options.folderKey !== undefined && {
|
|
70
|
+
folderKey: options.folderKey,
|
|
71
|
+
}),
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
if (listError) {
|
|
76
|
+
return fail(
|
|
77
|
+
"Error listing roles",
|
|
78
|
+
await extractErrorMessage(listError),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
OutputFormatter.success({
|
|
83
|
+
Result: RESULTS.Success,
|
|
84
|
+
Code: "RoleList",
|
|
85
|
+
Data: result ?? [],
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
};
|
package/src/tool.ts
CHANGED
|
@@ -4,9 +4,11 @@ import {
|
|
|
4
4
|
registerChoiceSetsCommand,
|
|
5
5
|
registerChoiceSetValuesCommand,
|
|
6
6
|
} from "./commands/choice-sets";
|
|
7
|
+
import { registerDirectoryCommand } from "./commands/directory";
|
|
7
8
|
import { registerEntitiesCommand } from "./commands/entities";
|
|
8
9
|
import { registerFilesCommand } from "./commands/files";
|
|
9
10
|
import { registerRecordsCommand } from "./commands/records";
|
|
11
|
+
import { registerRolesCommand } from "./commands/roles";
|
|
10
12
|
|
|
11
13
|
export const metadata = {
|
|
12
14
|
name: "data-fabric-tool",
|
|
@@ -22,4 +24,6 @@ export const registerCommands = async (program: Command): Promise<void> => {
|
|
|
22
24
|
registerFilesCommand(program);
|
|
23
25
|
registerChoiceSetsCommand(program);
|
|
24
26
|
registerChoiceSetValuesCommand(program);
|
|
27
|
+
registerRolesCommand(program);
|
|
28
|
+
registerDirectoryCommand(program);
|
|
25
29
|
};
|