@uipath/data-fabric-tool 1.199.0 → 1.201.0-preview.115

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.
@@ -2,14 +2,20 @@ import { describe, expect, it } from "vitest";
2
2
  import {
3
3
  findFilePayloadKeys,
4
4
  findInvalidFieldConstraints,
5
+ findInvalidFieldMetadata,
5
6
  findInvalidFieldName,
6
7
  findIsUniqueInUpdateFields,
7
8
  findMissingComplexFieldExtras,
9
+ findReferenceFieldsOnFile,
8
10
  findReferenceFolderKeyOnChoiceSet,
11
+ findTooManyMultilineMaxFields,
9
12
  findUiBrokenFieldType,
13
+ validateChoiceValueDisplayName,
10
14
  validateChoiceValueName,
11
15
  validateName,
16
+ validateQueryAggregates,
12
17
  validateQueryFilter,
18
+ validateSchemaMetadata,
13
19
  } from "./validate";
14
20
 
15
21
  describe("validateName", () => {
@@ -17,10 +23,16 @@ describe("validateName", () => {
17
23
  expect(validateName("total", "field")).toBeNull();
18
24
  });
19
25
 
20
- it("accepts letters, digits, and underscores", () => {
26
+ it("accepts underscores in entity names", () => {
21
27
  expect(validateName("Order_2024_v1", "entity")).toBeNull();
22
28
  });
23
29
 
30
+ it("rejects underscores in field names", () => {
31
+ const err = validateName("order_total", "field");
32
+ expect(err?.message).toContain("Invalid field name");
33
+ expect(err?.instructions).toContain("no underscores");
34
+ });
35
+
24
36
  it("rejects a name starting with a digit", () => {
25
37
  const err = validateName("1Order", "entity");
26
38
  expect(err).not.toBeNull();
@@ -41,6 +53,19 @@ describe("validateName", () => {
41
53
  expect(err?.message).toContain("reserved");
42
54
  });
43
55
 
56
+ it("rejects every system field name case-insensitively", () => {
57
+ for (const name of [
58
+ "id",
59
+ "CREATEDBY",
60
+ "createTime",
61
+ "UpdatedBy",
62
+ "updatetime",
63
+ "RecordOwner",
64
+ ]) {
65
+ expect(validateName(name, "field")?.message).toContain("reserved");
66
+ }
67
+ });
68
+
44
69
  it("does not treat 'CreateTime' as reserved for an entity name", () => {
45
70
  // The reserved-field-name list is field-specific; entity names go
46
71
  // through the keyword check only.
@@ -55,6 +80,31 @@ describe("validateName", () => {
55
80
  }
56
81
  });
57
82
 
83
+ it("rejects confirmed SQL keywords for entity names only", () => {
84
+ for (const name of ["Order", "order", "Group", "GROUP"]) {
85
+ expect(validateName(name, "entity")?.message).toContain(
86
+ "reserved SQL keyword",
87
+ );
88
+ }
89
+ });
90
+
91
+ it("accepts SQL keywords as field names — server only blocks them on entity create", () => {
92
+ for (const name of ["Order", "order", "Group", "GROUP"]) {
93
+ expect(validateName(name, "field")).toBeNull();
94
+ }
95
+ });
96
+
97
+ it("accepts SQL-like names the platform allows", () => {
98
+ for (const name of ["Status", "Key", "Type", "OrderStatus"]) {
99
+ expect(validateName(name, "field")).toBeNull();
100
+ }
101
+ });
102
+
103
+ it("does not apply entity and field SQL rules to choice-set names", () => {
104
+ expect(validateName("Order", "choice set")).toBeNull();
105
+ expect(validateName("Group", "choice set")).toBeNull();
106
+ });
107
+
58
108
  it("labels the message with the kind", () => {
59
109
  expect(validateName("Class", "entity")?.message).toContain("Entity");
60
110
  expect(validateName("Class", "field")?.message).toContain("Field");
@@ -92,6 +142,51 @@ describe("findInvalidFieldName", () => {
92
142
  });
93
143
  });
94
144
 
145
+ describe("schema metadata validation", () => {
146
+ it("enforces fixed entity metadata limits", () => {
147
+ expect(
148
+ validateSchemaMetadata({ displayName: "x".repeat(129) }, "Entity")
149
+ ?.message,
150
+ ).toContain("128");
151
+ expect(
152
+ validateSchemaMetadata({ description: "x".repeat(513) }, "Entity")
153
+ ?.message,
154
+ ).toContain("512");
155
+ });
156
+
157
+ it("rejects case-insensitive field name and display-name collisions", () => {
158
+ expect(
159
+ findInvalidFieldMetadata([
160
+ { fieldName: "Amount" },
161
+ { fieldName: "amount" },
162
+ ])?.message,
163
+ ).toContain("Duplicate field name");
164
+ expect(
165
+ findInvalidFieldMetadata([
166
+ { fieldName: "Amount", displayName: "Total" },
167
+ { fieldName: "Price", displayName: "total" },
168
+ ])?.message,
169
+ ).toContain("Duplicate field display name");
170
+ });
171
+
172
+ it("rejects a field name matching its entity name", () => {
173
+ expect(
174
+ findInvalidFieldMetadata([{ fieldName: "invoice" }], "Invoice")
175
+ ?.message,
176
+ ).toContain("cannot match entity name");
177
+ });
178
+
179
+ it("rejects more than ten MULTILINE_MAX fields", () => {
180
+ const fields = Array.from({ length: 11 }, (_, i) => ({
181
+ fieldName: `Body${i}`,
182
+ type: "MULTILINE_MAX",
183
+ }));
184
+ expect(findTooManyMultilineMaxFields(fields)?.message).toContain(
185
+ "maximum is 10",
186
+ );
187
+ });
188
+ });
189
+
95
190
  describe("findUiBrokenFieldType", () => {
96
191
  it("returns null for UI-compatible types", () => {
97
192
  expect(
@@ -136,8 +231,15 @@ describe("findUiBrokenFieldType", () => {
136
231
  });
137
232
 
138
233
  describe("validateChoiceValueName", () => {
139
- it("accepts a plain snake_case name", () => {
140
- expect(validateChoiceValueName("internal_audit")).toBeNull();
234
+ it("accepts one-character and alphanumeric names", () => {
235
+ expect(validateChoiceValueName("A")).toBeNull();
236
+ expect(validateChoiceValueName("internalAudit2")).toBeNull();
237
+ });
238
+
239
+ it("rejects underscores", () => {
240
+ expect(validateChoiceValueName("internal_audit")?.message).toContain(
241
+ "Invalid",
242
+ );
141
243
  });
142
244
 
143
245
  it("accepts a capitalized keyword — the validator is case-sensitive", () => {
@@ -166,6 +268,12 @@ describe("validateChoiceValueName", () => {
166
268
  const err = validateChoiceValueName("internal");
167
269
  expect(err?.instructions).toContain("--display-name");
168
270
  });
271
+
272
+ it("enforces the 500-character display-name limit", () => {
273
+ expect(
274
+ validateChoiceValueDisplayName("x".repeat(501))?.message,
275
+ ).toContain("500");
276
+ });
169
277
  });
170
278
 
171
279
  describe("validateQueryFilter", () => {
@@ -216,6 +324,15 @@ describe("validateQueryFilter", () => {
216
324
  expect(err?.message).toContain("requires 'valueList'");
217
325
  });
218
326
 
327
+ it("rejects an empty valueList", () => {
328
+ const err = validateQueryFilter({
329
+ queryFilters: [
330
+ { fieldName: "Status", operator: "in", valueList: [] },
331
+ ],
332
+ });
333
+ expect(err?.message).toContain("non-empty");
334
+ });
335
+
219
336
  it("requires value on operators other than in / not in", () => {
220
337
  const err = validateQueryFilter({
221
338
  queryFilters: [{ fieldName: "Status", operator: "=" }],
@@ -233,6 +350,22 @@ describe("validateQueryFilter", () => {
233
350
  ).toBeNull();
234
351
  });
235
352
 
353
+ it("rejects null with operators other than = and !=", () => {
354
+ const err = validateQueryFilter({
355
+ queryFilters: [{ fieldName: "Score", operator: ">", value: null }],
356
+ });
357
+ expect(err?.message).toContain("cannot use a null value");
358
+ });
359
+
360
+ it("rejects invalid filter field paths", () => {
361
+ const err = validateQueryFilter({
362
+ queryFilters: [
363
+ { fieldName: "bad-field", operator: "=", value: "x" },
364
+ ],
365
+ });
366
+ expect(err?.message).toContain("fieldName");
367
+ });
368
+
236
369
  it("recurses into nested filterGroups", () => {
237
370
  const err = validateQueryFilter({
238
371
  queryFilters: [{ fieldName: "x", operator: "=", value: "y" }],
@@ -263,6 +396,76 @@ describe("validateQueryFilter", () => {
263
396
  });
264
397
  });
265
398
 
399
+ describe("validateQueryAggregates", () => {
400
+ it("accepts a valid grouped aggregate", () => {
401
+ expect(
402
+ validateQueryAggregates({
403
+ selectedFields: ["Status"],
404
+ groupBy: ["Status"],
405
+ aggregates: [
406
+ { function: "COUNT", field: "Id", alias: "total" },
407
+ ],
408
+ }),
409
+ ).toBeNull();
410
+ });
411
+
412
+ it("enforces aggregate and groupBy count limits", () => {
413
+ expect(
414
+ validateQueryAggregates({
415
+ aggregates: Array.from({ length: 6 }, (_, i) => ({
416
+ function: "COUNT",
417
+ field: `Field${i}`,
418
+ })),
419
+ })?.message,
420
+ ).toContain("at most 5");
421
+ expect(
422
+ validateQueryAggregates({
423
+ aggregates: [{ function: "COUNT", field: "Id" }],
424
+ groupBy: ["A", "B", "C", "D", "E", "F"],
425
+ })?.message,
426
+ ).toContain("groupBy");
427
+ });
428
+
429
+ it("rejects invalid functions, aliases, and duplicate aliases", () => {
430
+ expect(
431
+ validateQueryAggregates({
432
+ aggregates: [{ function: "MEDIAN", field: "Score" }],
433
+ })?.message,
434
+ ).toContain("invalid function");
435
+ expect(
436
+ validateQueryAggregates({
437
+ aggregates: [
438
+ { function: "COUNT", field: "Id", alias: "bad]alias" },
439
+ ],
440
+ })?.message,
441
+ ).toContain("Invalid aggregate alias");
442
+ expect(
443
+ validateQueryAggregates({
444
+ aggregates: [
445
+ { function: "COUNT", field: "Id", alias: "total" },
446
+ { function: "SUM", field: "Score", alias: "TOTAL" },
447
+ ],
448
+ })?.message,
449
+ ).toContain("Duplicate aggregate alias");
450
+ });
451
+
452
+ it("requires selected fields to be grouped", () => {
453
+ expect(
454
+ validateQueryAggregates({
455
+ selectedFields: ["Status"],
456
+ aggregates: [{ function: "COUNT", field: "Id" }],
457
+ })?.message,
458
+ ).toContain("'groupBy' is required");
459
+ expect(
460
+ validateQueryAggregates({
461
+ selectedFields: ["Status", "Region"],
462
+ groupBy: ["Status"],
463
+ aggregates: [{ function: "COUNT", field: "Id" }],
464
+ })?.message,
465
+ ).toContain("Region");
466
+ });
467
+ });
468
+
266
469
  describe("findReferenceFolderKeyOnChoiceSet", () => {
267
470
  it("returns null when CHOICE_SET_* fields omit referenceFolderKey", () => {
268
471
  expect(
@@ -281,7 +484,7 @@ describe("findReferenceFolderKeyOnChoiceSet", () => {
281
484
  ).toBeNull();
282
485
  });
283
486
 
284
- it("ignores referenceFolderKey on RELATIONSHIP / FILE (allowed there)", () => {
487
+ it("ignores referenceFolderKey on RELATIONSHIP", () => {
285
488
  expect(
286
489
  findReferenceFolderKeyOnChoiceSet([
287
490
  {
@@ -291,11 +494,6 @@ describe("findReferenceFolderKeyOnChoiceSet", () => {
291
494
  referenceFieldId: "f-1",
292
495
  referenceFolderKey: "folder-1",
293
496
  },
294
- {
295
- fieldName: "receipt",
296
- type: "FILE",
297
- referenceFolderKey: "folder-1",
298
- },
299
497
  ]),
300
498
  ).toBeNull();
301
499
  });
@@ -326,6 +524,48 @@ describe("findReferenceFolderKeyOnChoiceSet", () => {
326
524
  });
327
525
  });
328
526
 
527
+ describe("findReferenceFieldsOnFile", () => {
528
+ it("accepts a FILE field without reference metadata", () => {
529
+ expect(
530
+ findReferenceFieldsOnFile([{ fieldName: "receipt", type: "FILE" }]),
531
+ ).toBeNull();
532
+ });
533
+
534
+ it("rejects reference metadata on FILE fields", () => {
535
+ for (const key of [
536
+ "referenceFolderKey",
537
+ "referenceEntityId",
538
+ "referenceFieldId",
539
+ "choiceSetId",
540
+ ]) {
541
+ const err = findReferenceFieldsOnFile([
542
+ {
543
+ fieldName: "receipt",
544
+ type: "FILE",
545
+ [key]: "value",
546
+ },
547
+ ]);
548
+ expect(err, `${key} should be rejected`).not.toBeNull();
549
+ expect(err?.message).toContain("FILE field 'receipt'");
550
+ expect(err?.instructions).toContain(`'${key}'`);
551
+ }
552
+ });
553
+
554
+ it("ignores reference metadata on non-FILE fields", () => {
555
+ expect(
556
+ findReferenceFieldsOnFile([
557
+ {
558
+ fieldName: "customer",
559
+ type: "RELATIONSHIP",
560
+ referenceEntityId: "entity-id",
561
+ referenceFieldId: "field-id",
562
+ referenceFolderKey: "folder-id",
563
+ },
564
+ ]),
565
+ ).toBeNull();
566
+ });
567
+ });
568
+
329
569
  describe("findMissingComplexFieldExtras", () => {
330
570
  it("returns null when complex fields carry their extras", () => {
331
571
  expect(
@@ -456,16 +696,43 @@ describe("findInvalidFieldConstraints", () => {
456
696
  expect(err?.message).toContain("does not accept 'maxValue'");
457
697
  });
458
698
 
459
- it("rejects minValue >= maxValue on DECIMAL", () => {
699
+ it("allows equal numeric bounds and rejects minValue > maxValue", () => {
700
+ expect(
701
+ findInvalidFieldConstraints([
702
+ {
703
+ fieldName: "fixed",
704
+ type: "DECIMAL",
705
+ minValue: 100,
706
+ maxValue: 100,
707
+ },
708
+ ]),
709
+ ).toBeNull();
460
710
  const err = findInvalidFieldConstraints([
461
711
  {
462
712
  fieldName: "price",
463
713
  type: "DECIMAL",
464
- minValue: 100,
714
+ minValue: 101,
465
715
  maxValue: 100,
466
716
  },
467
717
  ]);
468
- expect(err?.message).toContain("must be strictly less than");
718
+ expect(err?.message).toContain("must not exceed");
719
+ });
720
+
721
+ it("rejects fractional length limits and out-of-range numeric bounds", () => {
722
+ expect(
723
+ findInvalidFieldConstraints([
724
+ { fieldName: "title", type: "STRING", lengthLimit: 1.5 },
725
+ ])?.message,
726
+ ).toContain("integer");
727
+ expect(
728
+ findInvalidFieldConstraints([
729
+ {
730
+ fieldName: "amount",
731
+ type: "DECIMAL",
732
+ maxValue: 9_007_199_254_740_992,
733
+ },
734
+ ])?.message,
735
+ ).toContain("supported numeric range");
469
736
  });
470
737
 
471
738
  it("rejects decimalPrecision outside 0-10", () => {