@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.
@@ -0,0 +1,325 @@
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, Option } from "commander";
11
+ import { fail, requireDestructiveConfirmation } from "../utils/output";
12
+ import { connectOrFail } from "../utils/sdk-client";
13
+
14
+ // The directory/role support types (enums, option/response aliases) are missing
15
+ // from the SDK's export barrel: the main entry exports no DataFabric symbols, and
16
+ // the /entities subpath exports only the two service classes, not these types. So
17
+ // derive the principal-type input from the client's method signature instead of
18
+ // importing it. Fix belongs in @uipath/uipath-typescript (export these types).
19
+ type DataFabricClient = NonNullable<Awaited<ReturnType<typeof connectOrFail>>>;
20
+ type PrincipalTypeInput = Parameters<
21
+ DataFabricClient["entities"]["directory"]["assignRoles"]
22
+ >[1];
23
+
24
+ const PRINCIPAL_TYPES = ["User", "Group", "Application"] as const;
25
+
26
+ interface ListOptions {
27
+ tenant?: string;
28
+ limit: string;
29
+ offset: string;
30
+ }
31
+
32
+ interface AssignOptions {
33
+ tenant?: string;
34
+ type: string;
35
+ roleIds: string;
36
+ replace?: boolean;
37
+ uiAccess: boolean;
38
+ }
39
+
40
+ interface RevokeOptions {
41
+ tenant?: string;
42
+ yes?: boolean;
43
+ confirm?: boolean;
44
+ reason?: string;
45
+ }
46
+
47
+ const DIRECTORY_LIST_EXAMPLES: CommandExample[] = [
48
+ {
49
+ Description:
50
+ "List Data Fabric directory principals and their assigned roles.",
51
+ Command: "uip df directory list --limit 50",
52
+ Output: {
53
+ Code: "DirectoryList",
54
+ Data: {
55
+ TotalCount: 1,
56
+ Results: [
57
+ {
58
+ ExternalId: "d1e2f3a4-0000-0000-0000-000000000001",
59
+ Name: "Automation Users",
60
+ Type: "Group",
61
+ Roles: [
62
+ {
63
+ Id: "c1d2e3f4-0000-0000-0000-000000000001",
64
+ Name: "DataWriter",
65
+ },
66
+ ],
67
+ IsUIEnabled: true,
68
+ },
69
+ ],
70
+ },
71
+ },
72
+ },
73
+ ];
74
+
75
+ const DIRECTORY_ASSIGN_ROLES_EXAMPLES: CommandExample[] = [
76
+ {
77
+ Description: "Assign a Data Fabric role to an identity group.",
78
+ Command:
79
+ "uip df directory assign-roles d1e2f3a4-0000-0000-0000-000000000001 --type Group --role-ids c1d2e3f4-0000-0000-0000-000000000001",
80
+ Output: {
81
+ Code: "DirectoryRolesAssigned",
82
+ Data: [
83
+ {
84
+ PrincipalId: "d1e2f3a4-0000-0000-0000-000000000001",
85
+ RoleIds: ["c1d2e3f4-0000-0000-0000-000000000001"],
86
+ },
87
+ ],
88
+ },
89
+ },
90
+ ];
91
+
92
+ const DIRECTORY_REVOKE_ROLES_EXAMPLES: CommandExample[] = [
93
+ {
94
+ Description: "Revoke all direct Data Fabric roles from a principal.",
95
+ Command:
96
+ 'uip df directory revoke-roles d1e2f3a4-0000-0000-0000-000000000001 --yes --reason "Offboarding"',
97
+ Output: {
98
+ Code: "DirectoryRolesRevoked",
99
+ Data: {
100
+ PrincipalIds: ["d1e2f3a4-0000-0000-0000-000000000001"],
101
+ Reason: "Offboarding",
102
+ },
103
+ },
104
+ },
105
+ ];
106
+
107
+ const parsePrincipalType = (value: string): PrincipalTypeInput | null => {
108
+ const match = PRINCIPAL_TYPES.find(
109
+ (t) => t.toLowerCase() === value.toLowerCase(),
110
+ );
111
+ return match !== undefined ? (match as PrincipalTypeInput) : null;
112
+ };
113
+
114
+ const parseRoleIds = (value: string): string[] =>
115
+ value
116
+ .split(",")
117
+ .map((id) => id.trim())
118
+ .filter((id) => id !== "");
119
+
120
+ // Strict integer parse: `Number.parseInt` accepts trailing junk ("10abc" → 10)
121
+ // and truncates floats ("1.5" → 1), so validate the whole string is an integer
122
+ // first. Returns null on anything that isn't a plain (optionally signed) integer.
123
+ const parseIntegerOption = (value: string): number | null => {
124
+ const trimmed = value.trim();
125
+ if (!/^-?\d+$/.test(trimmed)) {
126
+ return null;
127
+ }
128
+ const parsed = Number(trimmed);
129
+ return Number.isSafeInteger(parsed) ? parsed : null;
130
+ };
131
+
132
+ /**
133
+ * Internal manage-access surface — hidden from help output and not part of
134
+ * the documented `uip df` command set.
135
+ */
136
+ export const registerDirectoryCommand = (program: Command) => {
137
+ const directory = program
138
+ .command("directory", { hidden: true })
139
+ .description(
140
+ "Manage Data Fabric directory principals and their role assignments",
141
+ );
142
+
143
+ directory
144
+ .command("list")
145
+ .description(
146
+ "List Data Fabric directory principals and their assigned roles",
147
+ )
148
+ .addOption(
149
+ createHiddenDeprecatedTenantOption("-t, --tenant <tenant-name>"),
150
+ )
151
+ .option(
152
+ "--limit <count>",
153
+ "Maximum number of principals to return",
154
+ "50",
155
+ )
156
+ .option("--offset <count>", "Number of principals to skip", "0")
157
+ .examples(DIRECTORY_LIST_EXAMPLES)
158
+ .trackedAction(processContext, async (options: ListOptions) => {
159
+ const top = parseIntegerOption(options.limit);
160
+ if (top === null || top <= 0) {
161
+ return fail(
162
+ `Invalid --limit value '${options.limit}'`,
163
+ "Provide a positive integer for --limit.",
164
+ );
165
+ }
166
+
167
+ const skip = parseIntegerOption(options.offset);
168
+ if (skip === null || skip < 0) {
169
+ return fail(
170
+ `Invalid --offset value '${options.offset}'`,
171
+ "Provide a non-negative integer for --offset.",
172
+ );
173
+ }
174
+
175
+ const sdk = await connectOrFail(options.tenant);
176
+ if (!sdk) return;
177
+
178
+ const [listError, result] = await catchError(
179
+ sdk.entities.directory.list({ skip, top }),
180
+ );
181
+
182
+ if (listError) {
183
+ return fail(
184
+ "Error listing directory principals",
185
+ await extractErrorMessage(listError),
186
+ );
187
+ }
188
+
189
+ OutputFormatter.success({
190
+ Result: RESULTS.Success,
191
+ Code: "DirectoryList",
192
+ Data: result,
193
+ });
194
+ });
195
+
196
+ directory
197
+ .command("assign-roles")
198
+ .description("Assign Data Fabric roles to one or more principals")
199
+ .argument("<principal-ids...>", "Principal external ID(s)")
200
+ .addOption(
201
+ createHiddenDeprecatedTenantOption("-t, --tenant <tenant-name>"),
202
+ )
203
+ .requiredOption(
204
+ "--type <type>",
205
+ "Principal type: User, Group, or Application",
206
+ )
207
+ .requiredOption(
208
+ "--role-ids <ids>",
209
+ "Comma-separated Data Fabric role IDs to assign (see 'df roles list')",
210
+ )
211
+ .option(
212
+ "--replace",
213
+ "Replace the principals' existing Data Fabric roles instead of preserving them",
214
+ )
215
+ .option(
216
+ "--no-ui-access",
217
+ "Do not enable Data Fabric UI access for the assigned principals",
218
+ )
219
+ .examples(DIRECTORY_ASSIGN_ROLES_EXAMPLES)
220
+ .trackedAction(
221
+ processContext,
222
+ async (principalIds: string[], options: AssignOptions) => {
223
+ const principalType = parsePrincipalType(options.type);
224
+ if (principalType === null) {
225
+ return fail(
226
+ `Invalid --type value '${options.type}'`,
227
+ `Provide one of: ${PRINCIPAL_TYPES.join(", ")}.`,
228
+ );
229
+ }
230
+
231
+ const roleIds = parseRoleIds(options.roleIds);
232
+ if (roleIds.length === 0) {
233
+ return fail(
234
+ "No role IDs provided",
235
+ "Provide at least one role ID via --role-ids. Use 'df roles list' to see available roles.",
236
+ );
237
+ }
238
+
239
+ const sdk = await connectOrFail(options.tenant);
240
+ if (!sdk) return;
241
+
242
+ const [assignError, result] = await catchError(
243
+ sdk.entities.directory.assignRoles(
244
+ principalIds,
245
+ principalType,
246
+ roleIds,
247
+ {
248
+ ...(options.replace === true && {
249
+ preserveExisting: false,
250
+ }),
251
+ ...(options.uiAccess === false && {
252
+ uiEnabled: false,
253
+ }),
254
+ },
255
+ ),
256
+ );
257
+
258
+ if (assignError) {
259
+ return fail(
260
+ "Error assigning roles",
261
+ await extractErrorMessage(assignError),
262
+ );
263
+ }
264
+
265
+ OutputFormatter.success({
266
+ Result: RESULTS.Success,
267
+ Code: "DirectoryRolesAssigned",
268
+ Data: result ?? [],
269
+ });
270
+ },
271
+ );
272
+
273
+ directory
274
+ .command("revoke-roles")
275
+ .description(
276
+ "Revoke all direct Data Fabric roles from one or more principals",
277
+ )
278
+ .argument("<principal-ids...>", "Principal external ID(s)")
279
+ .addOption(
280
+ createHiddenDeprecatedTenantOption("-t, --tenant <tenant-name>"),
281
+ )
282
+ .option("-y, --yes", "Acknowledge this is an irreversible operation")
283
+ .addOption(
284
+ new Option("--confirm", "Deprecated alias for --yes").hideHelp(),
285
+ )
286
+ .option(
287
+ "--reason <reason>",
288
+ "Reason for the revocation — echoed back in the response so the caller can log it",
289
+ )
290
+ .examples(DIRECTORY_REVOKE_ROLES_EXAMPLES)
291
+ .trackedAction(
292
+ processContext,
293
+ async (principalIds: string[], options: RevokeOptions) => {
294
+ const reason = requireDestructiveConfirmation(
295
+ options,
296
+ `revoke all Data Fabric roles from ${principalIds.length} principal(s)`,
297
+ 'Pass --reason "<text>" to record why the roles are being revoked.',
298
+ );
299
+ if (reason === null) return;
300
+
301
+ const sdk = await connectOrFail(options.tenant);
302
+ if (!sdk) return;
303
+
304
+ const [revokeError] = await catchError(
305
+ sdk.entities.directory.revokeRoles(principalIds),
306
+ );
307
+
308
+ if (revokeError) {
309
+ return fail(
310
+ "Error revoking roles",
311
+ await extractErrorMessage(revokeError),
312
+ );
313
+ }
314
+
315
+ OutputFormatter.success({
316
+ Result: RESULTS.Success,
317
+ Code: "DirectoryRolesRevoked",
318
+ Data: {
319
+ PrincipalIds: principalIds,
320
+ Reason: reason,
321
+ },
322
+ });
323
+ },
324
+ );
325
+ };
@@ -329,6 +329,44 @@ describe("entities create", () => {
329
329
  );
330
330
  });
331
331
 
332
+ it("should accept MULTILINE_MAX as a field type on create", async () => {
333
+ const sdk = mockSdk();
334
+ vi.mocked(sdk.entities.create).mockResolvedValue("doc-entity-id");
335
+ vi.mocked(readJsonInput).mockResolvedValue({
336
+ fields: [
337
+ { fieldName: "title", type: "STRING" },
338
+ { fieldName: "body", type: "MULTILINE_MAX" },
339
+ ],
340
+ });
341
+
342
+ const program = buildProgram();
343
+ await program.parseAsync([
344
+ "node",
345
+ "test",
346
+ "entities",
347
+ "create",
348
+ "Document",
349
+ "--body",
350
+ '{"fields":[{"fieldName":"title","type":"STRING"},{"fieldName":"body","type":"MULTILINE_MAX"}]}',
351
+ ]);
352
+
353
+ expect(sdk.entities.create).toHaveBeenCalledWith(
354
+ "Document",
355
+ [
356
+ { fieldName: "title", type: "STRING" },
357
+ { fieldName: "body", type: "MULTILINE_MAX" },
358
+ ],
359
+ undefined,
360
+ );
361
+ expect(OutputFormatter.success).toHaveBeenCalledWith(
362
+ expect.objectContaining({
363
+ Result: "Success",
364
+ Code: "EntityCreated",
365
+ Data: expect.objectContaining({ ID: "doc-entity-id" }),
366
+ }),
367
+ );
368
+ });
369
+
332
370
  it("should create entity with displayName, description, and isRbacEnabled", async () => {
333
371
  const sdk = mockSdk();
334
372
  vi.mocked(sdk.entities.create).mockResolvedValue("entity-with-opts");
@@ -733,6 +771,117 @@ describe("entities create", () => {
733
771
  );
734
772
  expect(process.exitCode).toBe(1);
735
773
  });
774
+
775
+ it("should reject an entity name that starts with a digit", async () => {
776
+ const sdk = mockSdk();
777
+ vi.mocked(readJsonInput).mockResolvedValue({
778
+ fields: [{ fieldName: "title", type: "STRING" }],
779
+ });
780
+
781
+ const program = buildProgram();
782
+ await program.parseAsync([
783
+ "node",
784
+ "test",
785
+ "entities",
786
+ "create",
787
+ "1Bad",
788
+ "--body",
789
+ '{"fields":[{"fieldName":"title","type":"STRING"}]}',
790
+ ]);
791
+
792
+ expect(sdk.entities.create).not.toHaveBeenCalled();
793
+ expect(OutputFormatter.error).toHaveBeenCalledWith(
794
+ expect.objectContaining({
795
+ Result: "Failure",
796
+ Message: expect.stringContaining("Invalid entity name '1Bad'"),
797
+ }),
798
+ );
799
+ expect(process.exitCode).toBe(1);
800
+ });
801
+
802
+ it("should reject an entity name that is a reserved keyword", async () => {
803
+ const sdk = mockSdk();
804
+ vi.mocked(readJsonInput).mockResolvedValue({
805
+ fields: [{ fieldName: "title", type: "STRING" }],
806
+ });
807
+
808
+ const program = buildProgram();
809
+ await program.parseAsync([
810
+ "node",
811
+ "test",
812
+ "entities",
813
+ "create",
814
+ "Class",
815
+ "--body",
816
+ '{"fields":[{"fieldName":"title","type":"STRING"}]}',
817
+ ]);
818
+
819
+ expect(sdk.entities.create).not.toHaveBeenCalled();
820
+ expect(OutputFormatter.error).toHaveBeenCalledWith(
821
+ expect.objectContaining({
822
+ Result: "Failure",
823
+ Message: expect.stringContaining("reserved C# or VB keyword"),
824
+ }),
825
+ );
826
+ expect(process.exitCode).toBe(1);
827
+ });
828
+
829
+ it("should reject a fieldName that is a reserved system field", async () => {
830
+ const sdk = mockSdk();
831
+ vi.mocked(readJsonInput).mockResolvedValue({
832
+ fields: [{ fieldName: "Id", type: "STRING" }],
833
+ });
834
+
835
+ const program = buildProgram();
836
+ await program.parseAsync([
837
+ "node",
838
+ "test",
839
+ "entities",
840
+ "create",
841
+ "MyEntity",
842
+ "--body",
843
+ '{"fields":[{"fieldName":"Id","type":"STRING"}]}',
844
+ ]);
845
+
846
+ expect(sdk.entities.create).not.toHaveBeenCalled();
847
+ expect(OutputFormatter.error).toHaveBeenCalledWith(
848
+ expect.objectContaining({
849
+ Result: "Failure",
850
+ Message: expect.stringContaining("Field name 'Id' is reserved"),
851
+ }),
852
+ );
853
+ expect(process.exitCode).toBe(1);
854
+ });
855
+
856
+ it("should reject a UI-broken field type", async () => {
857
+ const sdk = mockSdk();
858
+ vi.mocked(readJsonInput).mockResolvedValue({
859
+ fields: [{ fieldName: "count", type: "INTEGER" }],
860
+ });
861
+
862
+ const program = buildProgram();
863
+ await program.parseAsync([
864
+ "node",
865
+ "test",
866
+ "entities",
867
+ "create",
868
+ "MyEntity",
869
+ "--body",
870
+ '{"fields":[{"fieldName":"count","type":"INTEGER"}]}',
871
+ ]);
872
+
873
+ expect(sdk.entities.create).not.toHaveBeenCalled();
874
+ expect(OutputFormatter.error).toHaveBeenCalledWith(
875
+ expect.objectContaining({
876
+ Result: "Failure",
877
+ Message: expect.stringContaining(
878
+ "type 'INTEGER' which the Data Fabric UI cannot render",
879
+ ),
880
+ Instructions: expect.stringContaining("DECIMAL"),
881
+ }),
882
+ );
883
+ expect(process.exitCode).toBe(1);
884
+ });
736
885
  });
737
886
 
738
887
  describe("entities update", () => {
@@ -1540,7 +1689,7 @@ describe("entities update", () => {
1540
1689
  vi.mocked(readJsonInput).mockResolvedValue({
1541
1690
  addFields: [
1542
1691
  { fieldName: "dup", type: "STRING" },
1543
- { fieldName: "dup", type: "INTEGER" },
1692
+ { fieldName: "dup", type: "DECIMAL" },
1544
1693
  ],
1545
1694
  });
1546
1695
 
@@ -1552,7 +1701,7 @@ describe("entities update", () => {
1552
1701
  "update",
1553
1702
  "entity-id",
1554
1703
  "--body",
1555
- '{"addFields":[{"fieldName":"dup","type":"STRING"},{"fieldName":"dup","type":"INTEGER"}]}',
1704
+ '{"addFields":[{"fieldName":"dup","type":"STRING"},{"fieldName":"dup","type":"DECIMAL"}]}',
1556
1705
  ]);
1557
1706
 
1558
1707
  expect(sdk.entities.updateById).not.toHaveBeenCalled();
@@ -1826,6 +1975,94 @@ describe("entities update", () => {
1826
1975
  );
1827
1976
  expect(process.exitCode).toBe(1);
1828
1977
  });
1978
+
1979
+ it("should reject a UI-broken type in addFields", async () => {
1980
+ const sdk = mockSdk();
1981
+ vi.mocked(readJsonInput).mockResolvedValue({
1982
+ addFields: [{ fieldName: "count", type: "FLOAT" }],
1983
+ });
1984
+
1985
+ const program = buildProgram();
1986
+ await program.parseAsync([
1987
+ "node",
1988
+ "test",
1989
+ "entities",
1990
+ "update",
1991
+ "entity-id",
1992
+ "--body",
1993
+ '{"addFields":[{"fieldName":"count","type":"FLOAT"}]}',
1994
+ ]);
1995
+
1996
+ expect(sdk.entities.updateById).not.toHaveBeenCalled();
1997
+ expect(OutputFormatter.error).toHaveBeenCalledWith(
1998
+ expect.objectContaining({
1999
+ Result: "Failure",
2000
+ Message: expect.stringContaining(
2001
+ "type 'FLOAT' which the Data Fabric UI cannot render",
2002
+ ),
2003
+ }),
2004
+ );
2005
+ expect(process.exitCode).toBe(1);
2006
+ });
2007
+
2008
+ it("should reject a reserved keyword in addFields", async () => {
2009
+ const sdk = mockSdk();
2010
+ vi.mocked(readJsonInput).mockResolvedValue({
2011
+ addFields: [{ fieldName: "New", type: "STRING" }],
2012
+ });
2013
+
2014
+ const program = buildProgram();
2015
+ await program.parseAsync([
2016
+ "node",
2017
+ "test",
2018
+ "entities",
2019
+ "update",
2020
+ "entity-id",
2021
+ "--body",
2022
+ '{"addFields":[{"fieldName":"New","type":"STRING"}]}',
2023
+ ]);
2024
+
2025
+ expect(sdk.entities.updateById).not.toHaveBeenCalled();
2026
+ expect(OutputFormatter.error).toHaveBeenCalledWith(
2027
+ expect.objectContaining({
2028
+ Result: "Failure",
2029
+ Message: expect.stringContaining("reserved C# or VB keyword"),
2030
+ }),
2031
+ );
2032
+ expect(process.exitCode).toBe(1);
2033
+ });
2034
+
2035
+ it("should reject isUnique in updateFields", async () => {
2036
+ const sdk = mockSdk();
2037
+ vi.mocked(readJsonInput).mockResolvedValue({
2038
+ updateFields: [{ id: "field-uuid", isUnique: true }],
2039
+ });
2040
+
2041
+ const program = buildProgram();
2042
+ await program.parseAsync([
2043
+ "node",
2044
+ "test",
2045
+ "entities",
2046
+ "update",
2047
+ "entity-id",
2048
+ "--body",
2049
+ '{"updateFields":[{"id":"field-uuid","isUnique":true}]}',
2050
+ ]);
2051
+
2052
+ expect(sdk.entities.updateById).not.toHaveBeenCalled();
2053
+ expect(OutputFormatter.error).toHaveBeenCalledWith(
2054
+ expect.objectContaining({
2055
+ Result: "Failure",
2056
+ Message: expect.stringContaining(
2057
+ "Cannot change 'isUnique' on field 'field-uuid'",
2058
+ ),
2059
+ Instructions: expect.stringContaining(
2060
+ "fixed at field creation",
2061
+ ),
2062
+ }),
2063
+ );
2064
+ expect(process.exitCode).toBe(1);
2065
+ });
1829
2066
  });
1830
2067
 
1831
2068
  describe("entities delete", () => {