@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,555 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
findFilePayloadKeys,
|
|
4
|
+
findInvalidFieldConstraints,
|
|
5
|
+
findInvalidFieldName,
|
|
6
|
+
findIsUniqueInUpdateFields,
|
|
7
|
+
findMissingComplexFieldExtras,
|
|
8
|
+
findReferenceFolderKeyOnChoiceSet,
|
|
9
|
+
findUiBrokenFieldType,
|
|
10
|
+
validateChoiceValueName,
|
|
11
|
+
validateName,
|
|
12
|
+
validateQueryFilter,
|
|
13
|
+
} from "./validate";
|
|
14
|
+
|
|
15
|
+
describe("validateName", () => {
|
|
16
|
+
it("accepts a plain field name", () => {
|
|
17
|
+
expect(validateName("total", "field")).toBeNull();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("accepts letters, digits, and underscores", () => {
|
|
21
|
+
expect(validateName("Order_2024_v1", "entity")).toBeNull();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("rejects a name starting with a digit", () => {
|
|
25
|
+
const err = validateName("1Order", "entity");
|
|
26
|
+
expect(err).not.toBeNull();
|
|
27
|
+
expect(err?.message).toContain("Invalid entity name");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("rejects a name with a hyphen", () => {
|
|
31
|
+
expect(validateName("my-field", "field")).not.toBeNull();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("rejects a two-character name", () => {
|
|
35
|
+
expect(validateName("ab", "field")).not.toBeNull();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("rejects the system-generated field name 'Id'", () => {
|
|
39
|
+
const err = validateName("Id", "field");
|
|
40
|
+
expect(err).not.toBeNull();
|
|
41
|
+
expect(err?.message).toContain("reserved");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("does not treat 'CreateTime' as reserved for an entity name", () => {
|
|
45
|
+
// The reserved-field-name list is field-specific; entity names go
|
|
46
|
+
// through the keyword check only.
|
|
47
|
+
expect(validateName("CreateTime", "entity")).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("rejects a reserved C#/VB keyword case-insensitively", () => {
|
|
51
|
+
for (const name of ["Class", "class", "CLASS", "New", "Case"]) {
|
|
52
|
+
const err = validateName(name, "field");
|
|
53
|
+
expect(err).not.toBeNull();
|
|
54
|
+
expect(err?.message).toContain("reserved C# or VB keyword");
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("labels the message with the kind", () => {
|
|
59
|
+
expect(validateName("Class", "entity")?.message).toContain("Entity");
|
|
60
|
+
expect(validateName("Class", "field")?.message).toContain("Field");
|
|
61
|
+
expect(validateName("Class", "choice set")?.message).toContain(
|
|
62
|
+
"Choice set",
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("findInvalidFieldName", () => {
|
|
68
|
+
it("returns null when every fieldName is valid", () => {
|
|
69
|
+
expect(
|
|
70
|
+
findInvalidFieldName([
|
|
71
|
+
{ fieldName: "title", type: "STRING" },
|
|
72
|
+
{ fieldName: "amount", type: "DECIMAL" },
|
|
73
|
+
]),
|
|
74
|
+
).toBeNull();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("flags the first invalid fieldName", () => {
|
|
78
|
+
const err = findInvalidFieldName([
|
|
79
|
+
{ fieldName: "title", type: "STRING" },
|
|
80
|
+
{ fieldName: "Class", type: "STRING" },
|
|
81
|
+
{ fieldName: "amount", type: "DECIMAL" },
|
|
82
|
+
]);
|
|
83
|
+
expect(err?.message).toContain("'Class'");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("skips entries where fieldName is not a string", () => {
|
|
87
|
+
// A shape-level check catches non-string fieldName earlier in the
|
|
88
|
+
// command handler. This helper simply ignores them.
|
|
89
|
+
expect(
|
|
90
|
+
findInvalidFieldName([{ fieldName: 42 }, { fieldName: "good" }]),
|
|
91
|
+
).toBeNull();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("findUiBrokenFieldType", () => {
|
|
96
|
+
it("returns null for UI-compatible types", () => {
|
|
97
|
+
expect(
|
|
98
|
+
findUiBrokenFieldType([
|
|
99
|
+
{ fieldName: "title", type: "STRING" },
|
|
100
|
+
{ fieldName: "amount", type: "DECIMAL" },
|
|
101
|
+
{ fieldName: "receipt", type: "FILE" },
|
|
102
|
+
]),
|
|
103
|
+
).toBeNull();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("flags INTEGER with a DECIMAL substitution", () => {
|
|
107
|
+
const err = findUiBrokenFieldType([
|
|
108
|
+
{ fieldName: "count", type: "INTEGER" },
|
|
109
|
+
]);
|
|
110
|
+
expect(err?.message).toContain("'INTEGER'");
|
|
111
|
+
expect(err?.instructions).toContain("DECIMAL");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("flags each UI-broken type", () => {
|
|
115
|
+
for (const type of [
|
|
116
|
+
"INTEGER",
|
|
117
|
+
"BIG_INTEGER",
|
|
118
|
+
"FLOAT",
|
|
119
|
+
"DOUBLE",
|
|
120
|
+
"UUID",
|
|
121
|
+
"DATETIME",
|
|
122
|
+
]) {
|
|
123
|
+
const err = findUiBrokenFieldType([{ fieldName: "x", type }]);
|
|
124
|
+
expect(err, `${type} should be flagged`).not.toBeNull();
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("flags the first offender when multiple are present", () => {
|
|
129
|
+
const err = findUiBrokenFieldType([
|
|
130
|
+
{ fieldName: "a", type: "STRING" },
|
|
131
|
+
{ fieldName: "b", type: "FLOAT" },
|
|
132
|
+
{ fieldName: "c", type: "DATETIME" },
|
|
133
|
+
]);
|
|
134
|
+
expect(err?.message).toContain("'b'");
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("validateChoiceValueName", () => {
|
|
139
|
+
it("accepts a plain snake_case name", () => {
|
|
140
|
+
expect(validateChoiceValueName("internal_audit")).toBeNull();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("accepts a capitalized keyword — the validator is case-sensitive", () => {
|
|
144
|
+
// `Class` is rejected as an entity/field name but passes as a
|
|
145
|
+
// choice-set value name because the server-side validator here is
|
|
146
|
+
// case-sensitive.
|
|
147
|
+
expect(validateChoiceValueName("Class")).toBeNull();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("rejects a lowercase reserved keyword", () => {
|
|
151
|
+
for (const name of ["internal", "class", "case", "new", "object"]) {
|
|
152
|
+
const err = validateChoiceValueName(name);
|
|
153
|
+
expect(err, `${name} should be flagged`).not.toBeNull();
|
|
154
|
+
expect(err?.message).toContain("reserved C# keyword");
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("rejects a name that fails format validation", () => {
|
|
159
|
+
expect(validateChoiceValueName("1bad")?.message).toContain("Invalid");
|
|
160
|
+
expect(validateChoiceValueName("has-hyphen")?.message).toContain(
|
|
161
|
+
"Invalid",
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("suggests --display-name in the fix instructions", () => {
|
|
166
|
+
const err = validateChoiceValueName("internal");
|
|
167
|
+
expect(err?.instructions).toContain("--display-name");
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("validateQueryFilter", () => {
|
|
172
|
+
it("returns null when filterGroup is undefined", () => {
|
|
173
|
+
expect(validateQueryFilter(undefined)).toBeNull();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("accepts a well-formed filter group", () => {
|
|
177
|
+
expect(
|
|
178
|
+
validateQueryFilter({
|
|
179
|
+
logicalOperator: 0,
|
|
180
|
+
queryFilters: [
|
|
181
|
+
{ fieldName: "Status", operator: "=", value: "Active" },
|
|
182
|
+
],
|
|
183
|
+
}),
|
|
184
|
+
).toBeNull();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("accepts the string form of logicalOperator, case-insensitive", () => {
|
|
188
|
+
for (const op of ["AND", "and", "OR", "or"]) {
|
|
189
|
+
expect(
|
|
190
|
+
validateQueryFilter({
|
|
191
|
+
logicalOperator: op,
|
|
192
|
+
queryFilters: [
|
|
193
|
+
{ fieldName: "x", operator: "=", value: "y" },
|
|
194
|
+
],
|
|
195
|
+
}),
|
|
196
|
+
).toBeNull();
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("rejects an unsupported operator", () => {
|
|
201
|
+
for (const op of ["==", "equals", "Equals", "like", "BETWEEN"]) {
|
|
202
|
+
const err = validateQueryFilter({
|
|
203
|
+
queryFilters: [{ fieldName: "x", operator: op, value: "y" }],
|
|
204
|
+
});
|
|
205
|
+
expect(err, `${op} should be flagged`).not.toBeNull();
|
|
206
|
+
expect(err?.message).toContain("unsupported operator");
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("requires valueList on 'in' / 'not in'", () => {
|
|
211
|
+
const err = validateQueryFilter({
|
|
212
|
+
queryFilters: [
|
|
213
|
+
{ fieldName: "Status", operator: "in", value: "Active" },
|
|
214
|
+
],
|
|
215
|
+
});
|
|
216
|
+
expect(err?.message).toContain("requires 'valueList'");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("requires value on operators other than in / not in", () => {
|
|
220
|
+
const err = validateQueryFilter({
|
|
221
|
+
queryFilters: [{ fieldName: "Status", operator: "=" }],
|
|
222
|
+
});
|
|
223
|
+
expect(err?.message).toContain("requires 'value'");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("accepts null value (is-empty / is-not-empty)", () => {
|
|
227
|
+
expect(
|
|
228
|
+
validateQueryFilter({
|
|
229
|
+
queryFilters: [
|
|
230
|
+
{ fieldName: "Status", operator: "=", value: null },
|
|
231
|
+
],
|
|
232
|
+
}),
|
|
233
|
+
).toBeNull();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("recurses into nested filterGroups", () => {
|
|
237
|
+
const err = validateQueryFilter({
|
|
238
|
+
queryFilters: [{ fieldName: "x", operator: "=", value: "y" }],
|
|
239
|
+
filterGroups: [
|
|
240
|
+
{
|
|
241
|
+
queryFilters: [
|
|
242
|
+
{ fieldName: "y", operator: "like", value: "z" },
|
|
243
|
+
],
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
});
|
|
247
|
+
expect(err?.message).toContain("unsupported operator 'like'");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("rejects a bad logicalOperator", () => {
|
|
251
|
+
const err = validateQueryFilter({
|
|
252
|
+
logicalOperator: 2,
|
|
253
|
+
queryFilters: [{ fieldName: "x", operator: "=", value: "y" }],
|
|
254
|
+
});
|
|
255
|
+
expect(err?.message).toContain("logicalOperator");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("rejects a queryFilters entry with missing fieldName", () => {
|
|
259
|
+
const err = validateQueryFilter({
|
|
260
|
+
queryFilters: [{ operator: "=", value: "y" }],
|
|
261
|
+
});
|
|
262
|
+
expect(err?.message).toContain("fieldName");
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe("findReferenceFolderKeyOnChoiceSet", () => {
|
|
267
|
+
it("returns null when CHOICE_SET_* fields omit referenceFolderKey", () => {
|
|
268
|
+
expect(
|
|
269
|
+
findReferenceFolderKeyOnChoiceSet([
|
|
270
|
+
{
|
|
271
|
+
fieldName: "status",
|
|
272
|
+
type: "CHOICE_SET_SINGLE",
|
|
273
|
+
choiceSetId: "cs-1",
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
fieldName: "tags",
|
|
277
|
+
type: "CHOICE_SET_MULTIPLE",
|
|
278
|
+
choiceSetId: "cs-2",
|
|
279
|
+
},
|
|
280
|
+
]),
|
|
281
|
+
).toBeNull();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("ignores referenceFolderKey on RELATIONSHIP / FILE (allowed there)", () => {
|
|
285
|
+
expect(
|
|
286
|
+
findReferenceFolderKeyOnChoiceSet([
|
|
287
|
+
{
|
|
288
|
+
fieldName: "customerId",
|
|
289
|
+
type: "RELATIONSHIP",
|
|
290
|
+
referenceEntityId: "e-1",
|
|
291
|
+
referenceFieldId: "f-1",
|
|
292
|
+
referenceFolderKey: "folder-1",
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
fieldName: "receipt",
|
|
296
|
+
type: "FILE",
|
|
297
|
+
referenceFolderKey: "folder-1",
|
|
298
|
+
},
|
|
299
|
+
]),
|
|
300
|
+
).toBeNull();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("flags referenceFolderKey on CHOICE_SET_SINGLE", () => {
|
|
304
|
+
const err = findReferenceFolderKeyOnChoiceSet([
|
|
305
|
+
{
|
|
306
|
+
fieldName: "status",
|
|
307
|
+
type: "CHOICE_SET_SINGLE",
|
|
308
|
+
choiceSetId: "cs-1",
|
|
309
|
+
referenceFolderKey: "folder-1",
|
|
310
|
+
},
|
|
311
|
+
]);
|
|
312
|
+
expect(err?.message).toContain("CHOICE_SET_* field 'status'");
|
|
313
|
+
expect(err?.instructions).toContain("choiceSetId");
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("flags referenceFolderKey on CHOICE_SET_MULTIPLE", () => {
|
|
317
|
+
const err = findReferenceFolderKeyOnChoiceSet([
|
|
318
|
+
{
|
|
319
|
+
fieldName: "tags",
|
|
320
|
+
type: "CHOICE_SET_MULTIPLE",
|
|
321
|
+
choiceSetId: "cs-2",
|
|
322
|
+
referenceFolderKey: "folder-1",
|
|
323
|
+
},
|
|
324
|
+
]);
|
|
325
|
+
expect(err).not.toBeNull();
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
describe("findMissingComplexFieldExtras", () => {
|
|
330
|
+
it("returns null when complex fields carry their extras", () => {
|
|
331
|
+
expect(
|
|
332
|
+
findMissingComplexFieldExtras([
|
|
333
|
+
{
|
|
334
|
+
fieldName: "status",
|
|
335
|
+
type: "CHOICE_SET_SINGLE",
|
|
336
|
+
choiceSetId: "cs-1",
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
fieldName: "tags",
|
|
340
|
+
type: "CHOICE_SET_MULTIPLE",
|
|
341
|
+
choiceSetId: "cs-2",
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
fieldName: "customerId",
|
|
345
|
+
type: "RELATIONSHIP",
|
|
346
|
+
referenceEntityId: "e-1",
|
|
347
|
+
referenceFieldId: "f-1",
|
|
348
|
+
},
|
|
349
|
+
{ fieldName: "receipt", type: "FILE" },
|
|
350
|
+
{ fieldName: "title", type: "STRING" },
|
|
351
|
+
]),
|
|
352
|
+
).toBeNull();
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("flags CHOICE_SET_SINGLE without choiceSetId", () => {
|
|
356
|
+
const err = findMissingComplexFieldExtras([
|
|
357
|
+
{ fieldName: "status", type: "CHOICE_SET_SINGLE" },
|
|
358
|
+
]);
|
|
359
|
+
expect(err?.message).toContain("'status'");
|
|
360
|
+
expect(err?.message).toContain("requires 'choiceSetId'");
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("flags CHOICE_SET_MULTIPLE with empty-string choiceSetId", () => {
|
|
364
|
+
const err = findMissingComplexFieldExtras([
|
|
365
|
+
{ fieldName: "tags", type: "CHOICE_SET_MULTIPLE", choiceSetId: "" },
|
|
366
|
+
]);
|
|
367
|
+
expect(err).not.toBeNull();
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("flags RELATIONSHIP without referenceEntityId", () => {
|
|
371
|
+
const err = findMissingComplexFieldExtras([
|
|
372
|
+
{
|
|
373
|
+
fieldName: "customerId",
|
|
374
|
+
type: "RELATIONSHIP",
|
|
375
|
+
referenceFieldId: "f-1",
|
|
376
|
+
},
|
|
377
|
+
]);
|
|
378
|
+
expect(err?.message).toContain("requires 'referenceEntityId'");
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("flags RELATIONSHIP without referenceFieldId", () => {
|
|
382
|
+
const err = findMissingComplexFieldExtras([
|
|
383
|
+
{
|
|
384
|
+
fieldName: "customerId",
|
|
385
|
+
type: "RELATIONSHIP",
|
|
386
|
+
referenceEntityId: "e-1",
|
|
387
|
+
},
|
|
388
|
+
]);
|
|
389
|
+
expect(err?.message).toContain("requires 'referenceFieldId'");
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("stops at the first offender", () => {
|
|
393
|
+
const err = findMissingComplexFieldExtras([
|
|
394
|
+
{ fieldName: "ok", type: "CHOICE_SET_SINGLE", choiceSetId: "cs-1" },
|
|
395
|
+
{ fieldName: "bad", type: "RELATIONSHIP" },
|
|
396
|
+
]);
|
|
397
|
+
expect(err?.message).toContain("'bad'");
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
describe("findInvalidFieldConstraints", () => {
|
|
402
|
+
it("returns null when constraints match their type + ranges", () => {
|
|
403
|
+
expect(
|
|
404
|
+
findInvalidFieldConstraints([
|
|
405
|
+
{ fieldName: "title", type: "STRING", lengthLimit: 500 },
|
|
406
|
+
{
|
|
407
|
+
fieldName: "body",
|
|
408
|
+
type: "MULTILINE_TEXT",
|
|
409
|
+
lengthLimit: 8000,
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
fieldName: "note",
|
|
413
|
+
type: "MULTILINE_MAX",
|
|
414
|
+
lengthLimit: 100000,
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
fieldName: "price",
|
|
418
|
+
type: "DECIMAL",
|
|
419
|
+
decimalPrecision: 2,
|
|
420
|
+
minValue: 0,
|
|
421
|
+
maxValue: 999999,
|
|
422
|
+
},
|
|
423
|
+
]),
|
|
424
|
+
).toBeNull();
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it("rejects lengthLimit on a non-text type", () => {
|
|
428
|
+
const err = findInvalidFieldConstraints([
|
|
429
|
+
{ fieldName: "score", type: "DECIMAL", lengthLimit: 100 },
|
|
430
|
+
]);
|
|
431
|
+
expect(err?.message).toContain(
|
|
432
|
+
"'DECIMAL' does not accept 'lengthLimit'",
|
|
433
|
+
);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("rejects lengthLimit outside the STRING range", () => {
|
|
437
|
+
const err = findInvalidFieldConstraints([
|
|
438
|
+
{ fieldName: "title", type: "STRING", lengthLimit: 5000 },
|
|
439
|
+
]);
|
|
440
|
+
expect(err?.message).toContain("outside the allowed range");
|
|
441
|
+
expect(err?.instructions).toContain("1 and 4000");
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it("rejects lengthLimit outside the MULTILINE_MAX byte budget", () => {
|
|
445
|
+
const err = findInvalidFieldConstraints([
|
|
446
|
+
{ fieldName: "body", type: "MULTILINE_MAX", lengthLimit: 200000 },
|
|
447
|
+
]);
|
|
448
|
+
expect(err?.message).toContain("MULTILINE_MAX");
|
|
449
|
+
expect(err?.instructions).toContain("131072");
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("rejects maxValue on non-DECIMAL", () => {
|
|
453
|
+
const err = findInvalidFieldConstraints([
|
|
454
|
+
{ fieldName: "title", type: "STRING", maxValue: 100 },
|
|
455
|
+
]);
|
|
456
|
+
expect(err?.message).toContain("does not accept 'maxValue'");
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it("rejects minValue >= maxValue on DECIMAL", () => {
|
|
460
|
+
const err = findInvalidFieldConstraints([
|
|
461
|
+
{
|
|
462
|
+
fieldName: "price",
|
|
463
|
+
type: "DECIMAL",
|
|
464
|
+
minValue: 100,
|
|
465
|
+
maxValue: 100,
|
|
466
|
+
},
|
|
467
|
+
]);
|
|
468
|
+
expect(err?.message).toContain("must be strictly less than");
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it("rejects decimalPrecision outside 0-10", () => {
|
|
472
|
+
const err = findInvalidFieldConstraints([
|
|
473
|
+
{ fieldName: "price", type: "DECIMAL", decimalPrecision: 15 },
|
|
474
|
+
]);
|
|
475
|
+
expect(err?.message).toContain("decimalPrecision");
|
|
476
|
+
expect(err?.instructions).toContain("0 and 10");
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it("rejects decimalPrecision on non-DECIMAL", () => {
|
|
480
|
+
const err = findInvalidFieldConstraints([
|
|
481
|
+
{
|
|
482
|
+
fieldName: "title",
|
|
483
|
+
type: "STRING",
|
|
484
|
+
decimalPrecision: 2,
|
|
485
|
+
},
|
|
486
|
+
]);
|
|
487
|
+
expect(err?.message).toContain("does not accept 'decimalPrecision'");
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
describe("findFilePayloadKeys", () => {
|
|
492
|
+
it("returns null when there are no FILE fields", () => {
|
|
493
|
+
expect(
|
|
494
|
+
findFilePayloadKeys(
|
|
495
|
+
[{ title: "x", receipt: "/tmp/r.pdf" }],
|
|
496
|
+
new Set(),
|
|
497
|
+
),
|
|
498
|
+
).toBeNull();
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it("returns null when no record key matches a FILE field", () => {
|
|
502
|
+
expect(
|
|
503
|
+
findFilePayloadKeys(
|
|
504
|
+
[{ title: "x", score: 5 }],
|
|
505
|
+
new Set(["receipt"]),
|
|
506
|
+
),
|
|
507
|
+
).toBeNull();
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it("flags a matching FILE key and names the field + record index", () => {
|
|
511
|
+
const err = findFilePayloadKeys(
|
|
512
|
+
[{ title: "ok" }, { title: "bad", receipt: "/tmp/r.pdf" }],
|
|
513
|
+
new Set(["receipt"]),
|
|
514
|
+
);
|
|
515
|
+
expect(err?.message).toContain("Record #1");
|
|
516
|
+
expect(err?.message).toContain("'receipt'");
|
|
517
|
+
expect(err?.instructions).toContain("files upload");
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it("stops at the first offender", () => {
|
|
521
|
+
const err = findFilePayloadKeys(
|
|
522
|
+
[
|
|
523
|
+
{ title: "x", receipt: "/tmp/a.pdf" },
|
|
524
|
+
{ title: "y", cover: "/tmp/b.pdf" },
|
|
525
|
+
],
|
|
526
|
+
new Set(["receipt", "cover"]),
|
|
527
|
+
);
|
|
528
|
+
expect(err?.message).toContain("Record #0");
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
describe("findIsUniqueInUpdateFields", () => {
|
|
533
|
+
it("returns null when isUnique is absent", () => {
|
|
534
|
+
expect(
|
|
535
|
+
findIsUniqueInUpdateFields([
|
|
536
|
+
{ id: "f1", displayName: "New Name" },
|
|
537
|
+
{ id: "f2", isRequired: true },
|
|
538
|
+
]),
|
|
539
|
+
).toBeNull();
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it("flags isUnique: true", () => {
|
|
543
|
+
const err = findIsUniqueInUpdateFields([{ id: "f1", isUnique: true }]);
|
|
544
|
+
expect(err?.message).toContain("'f1'");
|
|
545
|
+
expect(err?.instructions).toContain("fixed at field creation");
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it("flags isUnique: false too", () => {
|
|
549
|
+
// The server silently ignores the value regardless of what's passed.
|
|
550
|
+
// Both true and false are unsafe — the caller can't rely on the toggle.
|
|
551
|
+
expect(
|
|
552
|
+
findIsUniqueInUpdateFields([{ id: "f1", isUnique: false }]),
|
|
553
|
+
).not.toBeNull();
|
|
554
|
+
});
|
|
555
|
+
});
|