@uipath/data-fabric-tool 1.0.1 → 1.1.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.
@@ -0,0 +1,286 @@
1
+ import {
2
+ type CommandExample,
3
+ catchError,
4
+ extractErrorMessage,
5
+ OutputFormatter,
6
+ processContext,
7
+ RESULTS,
8
+ } from "@uipath/common";
9
+ import type { Command } from "commander";
10
+ import { extractCursorValue } from "../utils/pagination";
11
+ import { createDataFabricClient } from "../utils/sdk-client";
12
+
13
+ interface ListOptions {
14
+ tenant?: string;
15
+ }
16
+
17
+ interface GetOptions {
18
+ tenant?: string;
19
+ limit?: string;
20
+ offset?: string;
21
+ cursor?: string;
22
+ }
23
+
24
+ const CHOICE_SETS_LIST_EXAMPLES: CommandExample[] = [
25
+ {
26
+ Description:
27
+ "List all Data Fabric choice sets. The returned 'ID' is the value to pass as 'choiceSetId' on a CHOICE_SET_SINGLE/CHOICE_SET_MULTIPLE entity field, or to 'df choice-sets get <id>'.",
28
+ Command: "uip df choice-sets list",
29
+ Output: {
30
+ Code: "ChoiceSetList",
31
+ Data: [
32
+ {
33
+ ID: "c1d2e3f4-0000-0000-0000-000000000001",
34
+ Name: "ExpenseTypes",
35
+ DisplayName: "Expense Types",
36
+ Description: "Categories of expenses",
37
+ FolderId: "f1000000-0000-0000-0000-000000000001",
38
+ CreatedBy: "u1000000-0000-0000-0000-000000000001",
39
+ UpdatedBy: "u1000000-0000-0000-0000-000000000001",
40
+ CreatedTime: "2026-01-01T00:00:00Z",
41
+ UpdatedTime: "2026-01-02T00:00:00Z",
42
+ },
43
+ ],
44
+ },
45
+ },
46
+ ];
47
+
48
+ const CHOICE_SETS_GET_EXAMPLES: CommandExample[] = [
49
+ {
50
+ Description:
51
+ "Get values for a choice set. The 'NumberId' on each value is the integer to pass when inserting or updating a record into a CHOICE_SET_SINGLE field (or in an array for CHOICE_SET_MULTIPLE).",
52
+ Command:
53
+ "uip df choice-sets get c1d2e3f4-0000-0000-0000-000000000001 --limit 2",
54
+ Output: {
55
+ Code: "ChoiceSetValues",
56
+ Data: {
57
+ TotalCount: 2,
58
+ Values: [
59
+ {
60
+ Id: "v1000000-0000-0000-0000-000000000001",
61
+ Name: "travel",
62
+ DisplayName: "Travel",
63
+ NumberId: 1,
64
+ },
65
+ {
66
+ Id: "v1000000-0000-0000-0000-000000000002",
67
+ Name: "meals",
68
+ DisplayName: "Meals",
69
+ NumberId: 2,
70
+ },
71
+ ],
72
+ HasNextPage: false,
73
+ },
74
+ },
75
+ },
76
+ ];
77
+
78
+ export const registerChoiceSetsCommand = (program: Command) => {
79
+ const choiceSets = program
80
+ .command("choice-sets")
81
+ .description(
82
+ "Browse Data Fabric choice sets. Read-only: choice sets are authored in the Data Fabric web UI and cannot be created, updated, or deleted from the CLI. " +
83
+ "Use 'list' to find a choice set's ID (used as 'choiceSetId' on CHOICE_SET_SINGLE/CHOICE_SET_MULTIPLE entity fields), and 'get' to inspect its values.",
84
+ );
85
+
86
+ choiceSets
87
+ .command("list")
88
+ .description("List all Data Fabric choice sets")
89
+ .option("-t, --tenant <tenant-name>", "Tenant name")
90
+ .examples(CHOICE_SETS_LIST_EXAMPLES)
91
+ .trackedAction(processContext, async (options: ListOptions) => {
92
+ const [clientError, sdk] = await catchError(
93
+ createDataFabricClient(options.tenant),
94
+ );
95
+
96
+ if (clientError) {
97
+ OutputFormatter.error({
98
+ Result: RESULTS.Failure,
99
+ Message: "Error connecting to Data Fabric",
100
+ Instructions: await extractErrorMessage(clientError),
101
+ });
102
+ processContext.exit(1);
103
+ return;
104
+ }
105
+
106
+ const [listError, result] = await catchError(
107
+ sdk.entities.choicesets.getAll(),
108
+ );
109
+
110
+ if (listError) {
111
+ OutputFormatter.error({
112
+ Result: RESULTS.Failure,
113
+ Message: "Error listing choice sets",
114
+ Instructions: await extractErrorMessage(listError),
115
+ });
116
+ processContext.exit(1);
117
+ return;
118
+ }
119
+
120
+ const items = (result ?? []).map((cs) => ({
121
+ ID: (cs as { id?: string }).id,
122
+ Name: cs.name,
123
+ DisplayName: cs.displayName || cs.name,
124
+ Description: cs.description || "",
125
+ FolderId: cs.folderId,
126
+ CreatedBy: cs.createdBy,
127
+ UpdatedBy: cs.updatedBy,
128
+ CreatedTime: cs.createdTime,
129
+ UpdatedTime: cs.updatedTime,
130
+ }));
131
+
132
+ OutputFormatter.success({
133
+ Result: RESULTS.Success,
134
+ Code: "ChoiceSetList",
135
+ Data: items,
136
+ });
137
+ });
138
+
139
+ choiceSets
140
+ .command("get")
141
+ .description("Get values for a Data Fabric choice set")
142
+ .argument("<choice-set-id>", "Choice set ID")
143
+ .option("-t, --tenant <tenant-name>", "Tenant name")
144
+ .option(
145
+ "-l, --limit <number>",
146
+ "Number of values to return per page",
147
+ "50",
148
+ )
149
+ .option(
150
+ "-o, --offset <number>",
151
+ "Start from the page containing this record index (rounded down to the nearest page boundary; mutually exclusive with --cursor)",
152
+ )
153
+ .option(
154
+ "--cursor <cursor>",
155
+ "Pagination cursor from a previous response to fetch the next page",
156
+ )
157
+ .examples(CHOICE_SETS_GET_EXAMPLES)
158
+ .trackedAction(
159
+ processContext,
160
+ async (choiceSetId: string, options: GetOptions) => {
161
+ const pageSize = Number(options.limit ?? "50");
162
+ if (Number.isNaN(pageSize) || pageSize < 1) {
163
+ OutputFormatter.error({
164
+ Result: RESULTS.Failure,
165
+ Message: "Invalid --limit value",
166
+ Instructions: "Provide a positive integer for --limit.",
167
+ });
168
+ processContext.exit(1);
169
+ return;
170
+ }
171
+
172
+ if (
173
+ options.cursor !== undefined &&
174
+ options.offset !== undefined
175
+ ) {
176
+ OutputFormatter.error({
177
+ Result: RESULTS.Failure,
178
+ Message: "--offset and --cursor are mutually exclusive",
179
+ Instructions:
180
+ "Use --offset to jump to a position by record count, or --cursor to continue from a previous response.",
181
+ });
182
+ processContext.exit(1);
183
+ return;
184
+ }
185
+
186
+ let jumpToPage: number | undefined;
187
+ if (options.offset !== undefined) {
188
+ const offsetValue = Number(options.offset);
189
+ if (Number.isNaN(offsetValue) || offsetValue < 0) {
190
+ OutputFormatter.error({
191
+ Result: RESULTS.Failure,
192
+ Message: "Invalid --offset value",
193
+ Instructions:
194
+ "Provide a non-negative integer for --offset.",
195
+ });
196
+ processContext.exit(1);
197
+ return;
198
+ }
199
+ jumpToPage = Math.floor(offsetValue / pageSize) + 1;
200
+ }
201
+
202
+ const [clientError, sdk] = await catchError(
203
+ createDataFabricClient(options.tenant),
204
+ );
205
+
206
+ if (clientError) {
207
+ OutputFormatter.error({
208
+ Result: RESULTS.Failure,
209
+ Message: "Error connecting to Data Fabric",
210
+ Instructions: await extractErrorMessage(clientError),
211
+ });
212
+ processContext.exit(1);
213
+ return;
214
+ }
215
+
216
+ const paginationOptions =
217
+ options.cursor !== undefined
218
+ ? { pageSize, cursor: { value: options.cursor } }
219
+ : jumpToPage !== undefined
220
+ ? { pageSize, jumpToPage }
221
+ : { pageSize };
222
+
223
+ const [getError, result] = await catchError(
224
+ sdk.entities.choicesets.getById(
225
+ choiceSetId,
226
+ paginationOptions,
227
+ ),
228
+ );
229
+
230
+ if (getError) {
231
+ OutputFormatter.error({
232
+ Result: RESULTS.Failure,
233
+ Message: `Error getting choice set '${choiceSetId}'`,
234
+ Instructions: await extractErrorMessage(getError),
235
+ });
236
+ processContext.exit(1);
237
+ return;
238
+ }
239
+
240
+ type ChoiceSetValue = {
241
+ id?: string;
242
+ name?: string;
243
+ displayName?: string;
244
+ numberId?: number;
245
+ createdTime?: string;
246
+ updatedTime?: string;
247
+ };
248
+ const response = result as unknown as {
249
+ items?: ChoiceSetValue[];
250
+ totalCount?: number;
251
+ hasNextPage?: boolean;
252
+ nextCursor?: unknown;
253
+ currentPage?: number;
254
+ totalPages?: number;
255
+ };
256
+ const values = (response.items ?? []).map((v) => ({
257
+ Id: v.id,
258
+ Name: v.name,
259
+ DisplayName: v.displayName || v.name,
260
+ NumberId: v.numberId,
261
+ CreatedTime: v.createdTime,
262
+ UpdatedTime: v.updatedTime,
263
+ }));
264
+ const nextCursor = extractCursorValue(response.nextCursor);
265
+
266
+ OutputFormatter.success({
267
+ Result: RESULTS.Success,
268
+ Code: "ChoiceSetValues",
269
+ Data: {
270
+ TotalCount: response.totalCount ?? values.length,
271
+ Values: values,
272
+ HasNextPage: response.hasNextPage ?? false,
273
+ ...(nextCursor !== undefined && {
274
+ NextCursor: nextCursor,
275
+ }),
276
+ ...(response.currentPage !== undefined && {
277
+ CurrentPage: response.currentPage,
278
+ }),
279
+ ...(response.totalPages !== undefined && {
280
+ TotalPages: response.totalPages,
281
+ }),
282
+ },
283
+ });
284
+ },
285
+ );
286
+ };
@@ -465,6 +465,128 @@ describe("entities create", () => {
465
465
  );
466
466
  });
467
467
 
468
+ it("should create entity with a CHOICE_SET_SINGLE field carrying choiceSetId", async () => {
469
+ const sdk = mockSdk();
470
+ vi.mocked(sdk.entities.create).mockResolvedValue(
471
+ "entity-choice-single",
472
+ );
473
+ const fields = [
474
+ {
475
+ fieldName: "opType",
476
+ type: "CHOICE_SET_SINGLE",
477
+ choiceSetId: "9db00b66-9952-f111-8ef3-6045bd07956d",
478
+ isRequired: true,
479
+ },
480
+ ];
481
+ vi.mocked(readJsonInput).mockResolvedValue({ fields });
482
+
483
+ const program = buildProgram();
484
+ await program.parseAsync([
485
+ "node",
486
+ "test",
487
+ "entities",
488
+ "create",
489
+ "CliTestExpense",
490
+ "--body",
491
+ JSON.stringify({ fields }),
492
+ ]);
493
+
494
+ expect(sdk.entities.create).toHaveBeenCalledWith(
495
+ "CliTestExpense",
496
+ expect.arrayContaining([
497
+ expect.objectContaining({
498
+ fieldName: "opType",
499
+ type: "CHOICE_SET_SINGLE",
500
+ choiceSetId: "9db00b66-9952-f111-8ef3-6045bd07956d",
501
+ }),
502
+ ]),
503
+ undefined,
504
+ );
505
+ expect(OutputFormatter.success).toHaveBeenCalledWith(
506
+ expect.objectContaining({ Code: "EntityCreated" }),
507
+ );
508
+ });
509
+
510
+ it("should create entity with a CHOICE_SET_MULTIPLE field carrying choiceSetId", async () => {
511
+ const sdk = mockSdk();
512
+ vi.mocked(sdk.entities.create).mockResolvedValue("entity-choice-multi");
513
+ const fields = [
514
+ {
515
+ fieldName: "multiOpType",
516
+ type: "CHOICE_SET_MULTIPLE",
517
+ choiceSetId: "9db00b66-9952-f111-8ef3-6045bd07956d",
518
+ },
519
+ ];
520
+ vi.mocked(readJsonInput).mockResolvedValue({ fields });
521
+
522
+ const program = buildProgram();
523
+ await program.parseAsync([
524
+ "node",
525
+ "test",
526
+ "entities",
527
+ "create",
528
+ "CliTestExpense",
529
+ "--body",
530
+ JSON.stringify({ fields }),
531
+ ]);
532
+
533
+ expect(sdk.entities.create).toHaveBeenCalledWith(
534
+ "CliTestExpense",
535
+ expect.arrayContaining([
536
+ expect.objectContaining({
537
+ fieldName: "multiOpType",
538
+ type: "CHOICE_SET_MULTIPLE",
539
+ choiceSetId: "9db00b66-9952-f111-8ef3-6045bd07956d",
540
+ }),
541
+ ]),
542
+ undefined,
543
+ );
544
+ expect(OutputFormatter.success).toHaveBeenCalledWith(
545
+ expect.objectContaining({ Code: "EntityCreated" }),
546
+ );
547
+ });
548
+
549
+ it("should create entity with a RELATIONSHIP field carrying referenceEntityName and referenceFieldName", async () => {
550
+ const sdk = mockSdk();
551
+ vi.mocked(sdk.entities.create).mockResolvedValue("entity-rel");
552
+ const fields = [
553
+ {
554
+ fieldName: "rel1",
555
+ type: "RELATIONSHIP",
556
+ referenceEntityName: "CodeEvalTestEntity",
557
+ referenceFieldName: "Email",
558
+ },
559
+ ];
560
+ vi.mocked(readJsonInput).mockResolvedValue({ fields });
561
+
562
+ const program = buildProgram();
563
+ await program.parseAsync([
564
+ "node",
565
+ "test",
566
+ "entities",
567
+ "create",
568
+ "CliTestExpense",
569
+ "--body",
570
+ JSON.stringify({ fields }),
571
+ ]);
572
+
573
+ expect(sdk.entities.create).toHaveBeenCalledWith(
574
+ "CliTestExpense",
575
+ expect.arrayContaining([
576
+ expect.objectContaining({
577
+ fieldName: "rel1",
578
+ type: "RELATIONSHIP",
579
+ referenceEntityName: "CodeEvalTestEntity",
580
+ referenceFieldName: "Email",
581
+ }),
582
+ ]),
583
+ undefined,
584
+ );
585
+ expect(OutputFormatter.success).toHaveBeenCalledWith(
586
+ expect.objectContaining({ Code: "EntityCreated" }),
587
+ );
588
+ });
589
+
468
590
  it("should error when no --body or --file provided", async () => {
469
591
  vi.mocked(readJsonInput).mockRejectedValue(
470
592
  new Error("Provide entity definition via --file or --body."),
@@ -684,6 +806,69 @@ describe("entities update", () => {
684
806
  );
685
807
  });
686
808
 
809
+ it("should update entity by adding a RELATIONSHIP field with referenceFieldName=Email", async () => {
810
+ const sdk = mockSdk();
811
+ const addFields = [
812
+ {
813
+ fieldName: "rel1",
814
+ type: "RELATIONSHIP",
815
+ referenceEntityName: "CodeEvalTestEntity",
816
+ referenceFieldName: "Email",
817
+ },
818
+ ];
819
+ vi.mocked(readJsonInput).mockResolvedValue({ addFields });
820
+
821
+ const program = buildProgram();
822
+ await program.parseAsync([
823
+ "node",
824
+ "test",
825
+ "entities",
826
+ "update",
827
+ "entity-id",
828
+ "--body",
829
+ JSON.stringify({ addFields }),
830
+ ]);
831
+
832
+ expect(sdk.entities.updateById).toHaveBeenCalledWith(
833
+ "entity-id",
834
+ expect.objectContaining({ addFields }),
835
+ );
836
+ expect(OutputFormatter.success).toHaveBeenCalledWith(
837
+ expect.objectContaining({ Code: "EntityUpdated" }),
838
+ );
839
+ });
840
+
841
+ it("should update entity by adding a CHOICE_SET_MULTIPLE field with choiceSetId", async () => {
842
+ const sdk = mockSdk();
843
+ const addFields = [
844
+ {
845
+ fieldName: "multiOpType",
846
+ type: "CHOICE_SET_MULTIPLE",
847
+ choiceSetId: "9db00b66-9952-f111-8ef3-6045bd07956d",
848
+ },
849
+ ];
850
+ vi.mocked(readJsonInput).mockResolvedValue({ addFields });
851
+
852
+ const program = buildProgram();
853
+ await program.parseAsync([
854
+ "node",
855
+ "test",
856
+ "entities",
857
+ "update",
858
+ "entity-id",
859
+ "--body",
860
+ JSON.stringify({ addFields }),
861
+ ]);
862
+
863
+ expect(sdk.entities.updateById).toHaveBeenCalledWith(
864
+ "entity-id",
865
+ expect.objectContaining({ addFields }),
866
+ );
867
+ expect(OutputFormatter.success).toHaveBeenCalledWith(
868
+ expect.objectContaining({ Code: "EntityUpdated" }),
869
+ );
870
+ });
871
+
687
872
  it("should update entity by modifying existing fields (updateFields)", async () => {
688
873
  const sdk = mockSdk();
689
874
  vi.mocked(readJsonInput).mockResolvedValue({
@@ -727,6 +912,182 @@ describe("entities update", () => {
727
912
  );
728
913
  });
729
914
 
915
+ it("should update displayName on a CHOICE_SET_SINGLE field via updateFields", async () => {
916
+ const sdk = mockSdk();
917
+ const updateFields = [
918
+ {
919
+ id: "f9ccd648-ac52-f111-8ef3-6045bd07956d",
920
+ displayName: "Operator Type",
921
+ },
922
+ ];
923
+ vi.mocked(readJsonInput).mockResolvedValue({ updateFields });
924
+
925
+ const program = buildProgram();
926
+ await program.parseAsync([
927
+ "node",
928
+ "test",
929
+ "entities",
930
+ "update",
931
+ "entity-id",
932
+ "--body",
933
+ JSON.stringify({ updateFields }),
934
+ ]);
935
+
936
+ expect(sdk.entities.updateById).toHaveBeenCalledWith(
937
+ "entity-id",
938
+ expect.objectContaining({ updateFields }),
939
+ );
940
+ expect(OutputFormatter.success).toHaveBeenCalledWith(
941
+ expect.objectContaining({ Code: "EntityUpdated" }),
942
+ );
943
+ });
944
+
945
+ it("should update displayName and isRequired on a CHOICE_SET_MULTIPLE field via updateFields", async () => {
946
+ const sdk = mockSdk();
947
+ const updateFields = [
948
+ {
949
+ id: "8750af08-b252-f111-8ef3-6045bd07956d",
950
+ displayName: "Multi Operator Types",
951
+ isRequired: true,
952
+ },
953
+ ];
954
+ vi.mocked(readJsonInput).mockResolvedValue({ updateFields });
955
+
956
+ const program = buildProgram();
957
+ await program.parseAsync([
958
+ "node",
959
+ "test",
960
+ "entities",
961
+ "update",
962
+ "entity-id",
963
+ "--body",
964
+ JSON.stringify({ updateFields }),
965
+ ]);
966
+
967
+ expect(sdk.entities.updateById).toHaveBeenCalledWith(
968
+ "entity-id",
969
+ expect.objectContaining({ updateFields }),
970
+ );
971
+ expect(OutputFormatter.success).toHaveBeenCalledWith(
972
+ expect.objectContaining({ Code: "EntityUpdated" }),
973
+ );
974
+ });
975
+
976
+ it("should remove a CHOICE_SET_SINGLE field via removeFields with --confirm and --reason", async () => {
977
+ const sdk = mockSdk();
978
+ vi.mocked(readJsonInput).mockResolvedValue({
979
+ removeFields: [{ fieldName: "opType" }],
980
+ });
981
+
982
+ const program = buildProgram();
983
+ await program.parseAsync([
984
+ "node",
985
+ "test",
986
+ "entities",
987
+ "update",
988
+ "entity-id",
989
+ "--body",
990
+ '{"removeFields":[{"fieldName":"opType"}]}',
991
+ "--confirm",
992
+ "--reason",
993
+ "dropping single-select choice-set field",
994
+ ]);
995
+
996
+ expect(sdk.entities.updateById).toHaveBeenCalledWith(
997
+ "entity-id",
998
+ expect.objectContaining({
999
+ removeFields: [{ fieldName: "opType" }],
1000
+ }),
1001
+ );
1002
+ expect(OutputFormatter.success).toHaveBeenCalledWith(
1003
+ expect.objectContaining({
1004
+ Result: "Success",
1005
+ Code: "EntityUpdated",
1006
+ Data: expect.objectContaining({
1007
+ RemovedFields: ["opType"],
1008
+ Reason: "dropping single-select choice-set field",
1009
+ }),
1010
+ }),
1011
+ );
1012
+ });
1013
+
1014
+ it("should remove a CHOICE_SET_MULTIPLE field via removeFields with --confirm and --reason", async () => {
1015
+ const sdk = mockSdk();
1016
+ vi.mocked(readJsonInput).mockResolvedValue({
1017
+ removeFields: [{ fieldName: "multiOpType" }],
1018
+ });
1019
+
1020
+ const program = buildProgram();
1021
+ await program.parseAsync([
1022
+ "node",
1023
+ "test",
1024
+ "entities",
1025
+ "update",
1026
+ "entity-id",
1027
+ "--body",
1028
+ '{"removeFields":[{"fieldName":"multiOpType"}]}',
1029
+ "--confirm",
1030
+ "--reason",
1031
+ "dropping multi-select choice-set field",
1032
+ ]);
1033
+
1034
+ expect(sdk.entities.updateById).toHaveBeenCalledWith(
1035
+ "entity-id",
1036
+ expect.objectContaining({
1037
+ removeFields: [{ fieldName: "multiOpType" }],
1038
+ }),
1039
+ );
1040
+ expect(OutputFormatter.success).toHaveBeenCalledWith(
1041
+ expect.objectContaining({
1042
+ Data: expect.objectContaining({
1043
+ RemovedFields: ["multiOpType"],
1044
+ Reason: "dropping multi-select choice-set field",
1045
+ }),
1046
+ }),
1047
+ );
1048
+ });
1049
+
1050
+ it("should remove both choice-set fields in a single removeFields call", async () => {
1051
+ const sdk = mockSdk();
1052
+ vi.mocked(readJsonInput).mockResolvedValue({
1053
+ removeFields: [
1054
+ { fieldName: "opType" },
1055
+ { fieldName: "multiOpType" },
1056
+ ],
1057
+ });
1058
+
1059
+ const program = buildProgram();
1060
+ await program.parseAsync([
1061
+ "node",
1062
+ "test",
1063
+ "entities",
1064
+ "update",
1065
+ "entity-id",
1066
+ "--body",
1067
+ '{"removeFields":[{"fieldName":"opType"},{"fieldName":"multiOpType"}]}',
1068
+ "--confirm",
1069
+ "--reason",
1070
+ "dropping all choice-set fields",
1071
+ ]);
1072
+
1073
+ expect(sdk.entities.updateById).toHaveBeenCalledWith(
1074
+ "entity-id",
1075
+ expect.objectContaining({
1076
+ removeFields: [
1077
+ { fieldName: "opType" },
1078
+ { fieldName: "multiOpType" },
1079
+ ],
1080
+ }),
1081
+ );
1082
+ expect(OutputFormatter.success).toHaveBeenCalledWith(
1083
+ expect.objectContaining({
1084
+ Data: expect.objectContaining({
1085
+ RemovedFields: ["opType", "multiOpType"],
1086
+ }),
1087
+ }),
1088
+ );
1089
+ });
1090
+
730
1091
  it("should update entity metadata only (displayName and description)", async () => {
731
1092
  const sdk = mockSdk();
732
1093
  vi.mocked(readJsonInput).mockResolvedValue({