@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
|
@@ -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: "
|
|
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":"
|
|
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", () => {
|