@uipath/data-fabric-tool 0.9.1 → 1.0.1
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/index.js +18263 -30988
- package/dist/tool.js +18269 -30994
- package/package.json +8 -8
- package/src/commands/entities.spec.ts +649 -8
- package/src/commands/entities.ts +272 -24
- package/src/commands/records.spec.ts +251 -10
- package/src/commands/records.ts +80 -7
package/src/commands/entities.ts
CHANGED
|
@@ -37,6 +37,14 @@ interface UpdateEntityOptions {
|
|
|
37
37
|
tenant?: string;
|
|
38
38
|
file?: string;
|
|
39
39
|
body?: string;
|
|
40
|
+
confirm?: boolean;
|
|
41
|
+
reason?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface DeleteOptions {
|
|
45
|
+
tenant?: string;
|
|
46
|
+
confirm?: boolean;
|
|
47
|
+
reason?: string;
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
const ENTITIES_LIST_EXAMPLES: CommandExample[] = [
|
|
@@ -106,6 +114,22 @@ const ENTITIES_GET_EXAMPLES: CommandExample[] = [
|
|
|
106
114
|
},
|
|
107
115
|
];
|
|
108
116
|
|
|
117
|
+
const ENTITIES_DELETE_EXAMPLES: CommandExample[] = [
|
|
118
|
+
{
|
|
119
|
+
Description:
|
|
120
|
+
"Delete an entity (irreversible — requires --confirm and --reason)",
|
|
121
|
+
Command:
|
|
122
|
+
'uip df entities delete a1b2c3d4-0000-0000-0000-000000000001 --confirm --reason "test entity cleanup"',
|
|
123
|
+
Output: {
|
|
124
|
+
Code: "EntityDeleted",
|
|
125
|
+
Data: {
|
|
126
|
+
ID: "a1b2c3d4-0000-0000-0000-000000000001",
|
|
127
|
+
Reason: "test entity cleanup",
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
];
|
|
132
|
+
|
|
109
133
|
const VALID_FIELD_TYPES = new Set(Object.values(EntityFieldDataType));
|
|
110
134
|
const VALID_FIELD_TYPES_LIST = [...VALID_FIELD_TYPES].join(", ");
|
|
111
135
|
|
|
@@ -119,6 +143,25 @@ function hasInvalidFieldType(fields: unknown[]): boolean {
|
|
|
119
143
|
});
|
|
120
144
|
}
|
|
121
145
|
|
|
146
|
+
const KNOWN_UPDATE_KEYS = [
|
|
147
|
+
"addFields",
|
|
148
|
+
"removeFields",
|
|
149
|
+
"updateFields",
|
|
150
|
+
"displayName",
|
|
151
|
+
"description",
|
|
152
|
+
"isRbacEnabled",
|
|
153
|
+
] as const;
|
|
154
|
+
|
|
155
|
+
function pickKnownUpdateKeys(
|
|
156
|
+
input: Record<string, unknown>,
|
|
157
|
+
): EntityUpdateByIdOptions {
|
|
158
|
+
const out: Record<string, unknown> = {};
|
|
159
|
+
for (const key of KNOWN_UPDATE_KEYS) {
|
|
160
|
+
if (input[key] !== undefined) out[key] = input[key];
|
|
161
|
+
}
|
|
162
|
+
return out as EntityUpdateByIdOptions;
|
|
163
|
+
}
|
|
164
|
+
|
|
122
165
|
export const registerEntitiesCommand = (program: Command) => {
|
|
123
166
|
const entities = program
|
|
124
167
|
.command("entities")
|
|
@@ -417,9 +460,17 @@ export const registerEntitiesCommand = (program: Command) => {
|
|
|
417
460
|
.option("-t, --tenant <tenant-name>", "Tenant name")
|
|
418
461
|
.option(
|
|
419
462
|
"-f, --file <path>",
|
|
420
|
-
"Path to JSON file with update options (addFields, updateFields, displayName, description, isRbacEnabled)",
|
|
463
|
+
"Path to JSON file with update options (addFields, updateFields, removeFields, displayName, description, isRbacEnabled)",
|
|
421
464
|
)
|
|
422
465
|
.option("--body <json>", "Inline JSON update options")
|
|
466
|
+
.option(
|
|
467
|
+
"--confirm",
|
|
468
|
+
"Required when 'removeFields' is non-empty — acknowledges the field deletion is irreversible",
|
|
469
|
+
)
|
|
470
|
+
.option(
|
|
471
|
+
"--reason <reason>",
|
|
472
|
+
"Required when 'removeFields' is non-empty — echoed back in the response so the caller can log it",
|
|
473
|
+
)
|
|
423
474
|
.trackedAction(
|
|
424
475
|
processContext,
|
|
425
476
|
async (id: string, options: UpdateEntityOptions) => {
|
|
@@ -450,37 +501,125 @@ export const registerEntitiesCommand = (program: Command) => {
|
|
|
450
501
|
Result: RESULTS.Failure,
|
|
451
502
|
Message: "Update options must be a JSON object",
|
|
452
503
|
Instructions:
|
|
453
|
-
"Provide a JSON object with addFields, updateFields, displayName, description, or isRbacEnabled.",
|
|
504
|
+
"Provide a JSON object with addFields, updateFields, removeFields, displayName, description, or isRbacEnabled.",
|
|
454
505
|
});
|
|
455
506
|
processContext.exit(1);
|
|
456
507
|
return;
|
|
457
508
|
}
|
|
458
509
|
|
|
459
510
|
const input = parsed as Record<string, unknown>;
|
|
460
|
-
|
|
511
|
+
|
|
512
|
+
if (
|
|
513
|
+
input.addFields !== undefined &&
|
|
514
|
+
!Array.isArray(input.addFields)
|
|
515
|
+
) {
|
|
516
|
+
OutputFormatter.error({
|
|
517
|
+
Result: RESULTS.Failure,
|
|
518
|
+
Message: "'addFields' must be an array",
|
|
519
|
+
Instructions:
|
|
520
|
+
'Example: {"addFields":[{"fieldName":"title","type":"STRING"}]}',
|
|
521
|
+
});
|
|
522
|
+
processContext.exit(1);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (
|
|
527
|
+
input.updateFields !== undefined &&
|
|
528
|
+
!Array.isArray(input.updateFields)
|
|
529
|
+
) {
|
|
461
530
|
OutputFormatter.error({
|
|
462
531
|
Result: RESULTS.Failure,
|
|
463
|
-
Message: "
|
|
532
|
+
Message: "'updateFields' must be an array",
|
|
464
533
|
Instructions:
|
|
465
|
-
"
|
|
534
|
+
'Example: {"updateFields":[{"id":"<fieldId>","displayName":"New Name"}]}',
|
|
466
535
|
});
|
|
467
536
|
processContext.exit(1);
|
|
468
537
|
return;
|
|
469
538
|
}
|
|
470
539
|
|
|
540
|
+
if (
|
|
541
|
+
input.removeFields !== undefined &&
|
|
542
|
+
!Array.isArray(input.removeFields)
|
|
543
|
+
) {
|
|
544
|
+
OutputFormatter.error({
|
|
545
|
+
Result: RESULTS.Failure,
|
|
546
|
+
Message: "'removeFields' must be an array",
|
|
547
|
+
Instructions:
|
|
548
|
+
'Example: {"removeFields":[{"fieldName":"old_field"}]}',
|
|
549
|
+
});
|
|
550
|
+
processContext.exit(1);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
let trimmedReason: string | undefined;
|
|
555
|
+
let removedFieldNames: string[] = [];
|
|
556
|
+
const hasRemove =
|
|
557
|
+
Array.isArray(input.removeFields) &&
|
|
558
|
+
input.removeFields.length > 0;
|
|
559
|
+
|
|
560
|
+
if (hasRemove) {
|
|
561
|
+
const removeFields = input.removeFields as unknown[];
|
|
562
|
+
const hasInvalidRemove = removeFields.some((f) => {
|
|
563
|
+
if (typeof f !== "object" || f === null) return true;
|
|
564
|
+
const fn = (f as Record<string, unknown>).fieldName;
|
|
565
|
+
return typeof fn !== "string" || fn.trim() === "";
|
|
566
|
+
});
|
|
567
|
+
if (hasInvalidRemove) {
|
|
568
|
+
OutputFormatter.error({
|
|
569
|
+
Result: RESULTS.Failure,
|
|
570
|
+
Message:
|
|
571
|
+
"Each field in removeFields must include a non-empty 'fieldName' string",
|
|
572
|
+
Instructions:
|
|
573
|
+
'Example: {"removeFields":[{"fieldName":"old_field"}]}',
|
|
574
|
+
});
|
|
575
|
+
processContext.exit(1);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (options.confirm !== true) {
|
|
580
|
+
OutputFormatter.error({
|
|
581
|
+
Result: RESULTS.Failure,
|
|
582
|
+
Message:
|
|
583
|
+
"Confirmation required for destructive operation",
|
|
584
|
+
Instructions:
|
|
585
|
+
"Pass --confirm to acknowledge field deletion is irreversible.",
|
|
586
|
+
});
|
|
587
|
+
processContext.exit(1);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
trimmedReason = options.reason?.trim();
|
|
592
|
+
if (trimmedReason === undefined || trimmedReason === "") {
|
|
593
|
+
OutputFormatter.error({
|
|
594
|
+
Result: RESULTS.Failure,
|
|
595
|
+
Message:
|
|
596
|
+
"Reason required for destructive operation",
|
|
597
|
+
Instructions:
|
|
598
|
+
'Pass --reason "<text>" to record why fields are being removed.',
|
|
599
|
+
});
|
|
600
|
+
processContext.exit(1);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
removedFieldNames = (
|
|
605
|
+
removeFields as { fieldName: string }[]
|
|
606
|
+
).map((f) => f.fieldName);
|
|
607
|
+
}
|
|
608
|
+
|
|
471
609
|
if (Array.isArray(input.addFields)) {
|
|
472
610
|
const hasInvalidField = (input.addFields as unknown[]).some(
|
|
473
|
-
(f) =>
|
|
474
|
-
typeof f !== "object" ||
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
611
|
+
(f) => {
|
|
612
|
+
if (typeof f !== "object" || f === null)
|
|
613
|
+
return true;
|
|
614
|
+
const fn = (f as Record<string, unknown>).fieldName;
|
|
615
|
+
return typeof fn !== "string" || fn.trim() === "";
|
|
616
|
+
},
|
|
478
617
|
);
|
|
479
618
|
if (hasInvalidField) {
|
|
480
619
|
OutputFormatter.error({
|
|
481
620
|
Result: RESULTS.Failure,
|
|
482
621
|
Message:
|
|
483
|
-
"Each field in addFields must include a 'fieldName' string",
|
|
622
|
+
"Each field in addFields must include a non-empty 'fieldName' string",
|
|
484
623
|
Instructions:
|
|
485
624
|
'Example: {"fieldName":"title","type":"STRING"}',
|
|
486
625
|
});
|
|
@@ -497,23 +636,54 @@ export const registerEntitiesCommand = (program: Command) => {
|
|
|
497
636
|
processContext.exit(1);
|
|
498
637
|
return;
|
|
499
638
|
}
|
|
639
|
+
|
|
640
|
+
const addNames = (
|
|
641
|
+
input.addFields as { fieldName: string }[]
|
|
642
|
+
).map((f) => f.fieldName);
|
|
643
|
+
const duplicateAdd = addNames.find(
|
|
644
|
+
(name, i) => addNames.indexOf(name) !== i,
|
|
645
|
+
);
|
|
646
|
+
if (duplicateAdd !== undefined) {
|
|
647
|
+
OutputFormatter.error({
|
|
648
|
+
Result: RESULTS.Failure,
|
|
649
|
+
Message: `Duplicate fieldName '${duplicateAdd}' in addFields`,
|
|
650
|
+
Instructions:
|
|
651
|
+
"Each entry in addFields must have a unique fieldName.",
|
|
652
|
+
});
|
|
653
|
+
processContext.exit(1);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (removedFieldNames.length > 0) {
|
|
658
|
+
const conflict = addNames.find((name) =>
|
|
659
|
+
removedFieldNames.includes(name),
|
|
660
|
+
);
|
|
661
|
+
if (conflict !== undefined) {
|
|
662
|
+
OutputFormatter.error({
|
|
663
|
+
Result: RESULTS.Failure,
|
|
664
|
+
Message: `Field '${conflict}' appears in both addFields and removeFields`,
|
|
665
|
+
Instructions:
|
|
666
|
+
"A single update cannot add and remove the same field. Split into two calls if you need to recreate it.",
|
|
667
|
+
});
|
|
668
|
+
processContext.exit(1);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
500
672
|
}
|
|
501
673
|
|
|
502
674
|
if (Array.isArray(input.updateFields)) {
|
|
503
675
|
const hasInvalidField = (
|
|
504
676
|
input.updateFields as unknown[]
|
|
505
|
-
).some(
|
|
506
|
-
(f)
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
"string",
|
|
511
|
-
);
|
|
677
|
+
).some((f) => {
|
|
678
|
+
if (typeof f !== "object" || f === null) return true;
|
|
679
|
+
const fid = (f as Record<string, unknown>).id;
|
|
680
|
+
return typeof fid !== "string" || fid.trim() === "";
|
|
681
|
+
});
|
|
512
682
|
if (hasInvalidField) {
|
|
513
683
|
OutputFormatter.error({
|
|
514
684
|
Result: RESULTS.Failure,
|
|
515
685
|
Message:
|
|
516
|
-
"Each field in updateFields must include
|
|
686
|
+
"Each field in updateFields must include a non-empty 'id' string",
|
|
517
687
|
Instructions:
|
|
518
688
|
'Use \'df entities get <entityId>\' to find field IDs. Example: {"id":"<fieldId>","displayName":"Total Amount","isRequired":true}',
|
|
519
689
|
});
|
|
@@ -538,10 +708,7 @@ export const registerEntitiesCommand = (program: Command) => {
|
|
|
538
708
|
|
|
539
709
|
const entityService: EntityServiceModel = sdk.entities;
|
|
540
710
|
const [updateError] = await catchError(
|
|
541
|
-
entityService.updateById(
|
|
542
|
-
id,
|
|
543
|
-
input as EntityUpdateByIdOptions,
|
|
544
|
-
),
|
|
711
|
+
entityService.updateById(id, pickKnownUpdateKeys(input)),
|
|
545
712
|
);
|
|
546
713
|
|
|
547
714
|
if (updateError) {
|
|
@@ -557,7 +724,88 @@ export const registerEntitiesCommand = (program: Command) => {
|
|
|
557
724
|
OutputFormatter.success({
|
|
558
725
|
Result: RESULTS.Success,
|
|
559
726
|
Code: "EntityUpdated",
|
|
560
|
-
Data: {
|
|
727
|
+
Data: {
|
|
728
|
+
ID: id,
|
|
729
|
+
...(removedFieldNames.length > 0 && {
|
|
730
|
+
RemovedFields: removedFieldNames,
|
|
731
|
+
Reason: trimmedReason,
|
|
732
|
+
}),
|
|
733
|
+
},
|
|
734
|
+
});
|
|
735
|
+
},
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
entities
|
|
739
|
+
.command("delete")
|
|
740
|
+
.description("Delete a Data Fabric entity (irreversible)")
|
|
741
|
+
.argument("<id>", "Entity ID")
|
|
742
|
+
.option("-t, --tenant <tenant-name>", "Tenant name")
|
|
743
|
+
.option("--confirm", "Acknowledge this is an irreversible operation")
|
|
744
|
+
.option(
|
|
745
|
+
"--reason <reason>",
|
|
746
|
+
"Reason for the deletion — echoed back in the response so the caller can log it",
|
|
747
|
+
)
|
|
748
|
+
.examples(ENTITIES_DELETE_EXAMPLES)
|
|
749
|
+
.trackedAction(
|
|
750
|
+
processContext,
|
|
751
|
+
async (id: string, options: DeleteOptions) => {
|
|
752
|
+
if (options.confirm !== true) {
|
|
753
|
+
OutputFormatter.error({
|
|
754
|
+
Result: RESULTS.Failure,
|
|
755
|
+
Message:
|
|
756
|
+
"Confirmation required for destructive operation",
|
|
757
|
+
Instructions:
|
|
758
|
+
"Pass --confirm to acknowledge this is irreversible.",
|
|
759
|
+
});
|
|
760
|
+
processContext.exit(1);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const reason = options.reason?.trim();
|
|
765
|
+
if (reason === undefined || reason === "") {
|
|
766
|
+
OutputFormatter.error({
|
|
767
|
+
Result: RESULTS.Failure,
|
|
768
|
+
Message: "Reason required for destructive operation",
|
|
769
|
+
Instructions:
|
|
770
|
+
'Pass --reason "<text>" to record why the entity is being deleted.',
|
|
771
|
+
});
|
|
772
|
+
processContext.exit(1);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const [clientError, sdk] = await catchError(
|
|
777
|
+
createDataFabricClient(options.tenant),
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
if (clientError) {
|
|
781
|
+
OutputFormatter.error({
|
|
782
|
+
Result: RESULTS.Failure,
|
|
783
|
+
Message: "Error connecting to Data Fabric",
|
|
784
|
+
Instructions: await extractErrorMessage(clientError),
|
|
785
|
+
});
|
|
786
|
+
processContext.exit(1);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const entityService: EntityServiceModel = sdk.entities;
|
|
791
|
+
const [deleteError] = await catchError(
|
|
792
|
+
entityService.deleteById(id),
|
|
793
|
+
);
|
|
794
|
+
|
|
795
|
+
if (deleteError) {
|
|
796
|
+
OutputFormatter.error({
|
|
797
|
+
Result: RESULTS.Failure,
|
|
798
|
+
Message: `Error deleting entity '${id}'`,
|
|
799
|
+
Instructions: await extractErrorMessage(deleteError),
|
|
800
|
+
});
|
|
801
|
+
processContext.exit(1);
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
OutputFormatter.success({
|
|
806
|
+
Result: RESULTS.Success,
|
|
807
|
+
Code: "EntityDeleted",
|
|
808
|
+
Data: { ID: id, Reason: reason },
|
|
561
809
|
});
|
|
562
810
|
},
|
|
563
811
|
);
|
|
@@ -233,6 +233,126 @@ describe("records list", () => {
|
|
|
233
233
|
);
|
|
234
234
|
expect(process.exitCode).toBe(1);
|
|
235
235
|
});
|
|
236
|
+
|
|
237
|
+
it("should pass jumpToPage to SDK when --offset is given", async () => {
|
|
238
|
+
const sdk = mockSdk();
|
|
239
|
+
vi.mocked(sdk.entities.getAllRecords).mockResolvedValue({
|
|
240
|
+
items: [{ Id: "rec-51" }],
|
|
241
|
+
totalCount: 100,
|
|
242
|
+
hasNextPage: true,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const program = buildProgram();
|
|
246
|
+
await program.parseAsync([
|
|
247
|
+
"node",
|
|
248
|
+
"test",
|
|
249
|
+
"records",
|
|
250
|
+
"list",
|
|
251
|
+
"entity-id",
|
|
252
|
+
"--offset",
|
|
253
|
+
"50",
|
|
254
|
+
"--limit",
|
|
255
|
+
"50",
|
|
256
|
+
]);
|
|
257
|
+
|
|
258
|
+
expect(sdk.entities.getAllRecords).toHaveBeenCalledWith("entity-id", {
|
|
259
|
+
pageSize: 50,
|
|
260
|
+
jumpToPage: 2,
|
|
261
|
+
});
|
|
262
|
+
expect(OutputFormatter.success).toHaveBeenCalledWith(
|
|
263
|
+
expect.objectContaining({ Result: "Success", Code: "RecordList" }),
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("should use jumpToPage 1 when --offset is 0", async () => {
|
|
268
|
+
const sdk = mockSdk();
|
|
269
|
+
vi.mocked(sdk.entities.getAllRecords).mockResolvedValue({
|
|
270
|
+
items: [{ Id: "rec-1" }],
|
|
271
|
+
totalCount: 10,
|
|
272
|
+
hasNextPage: false,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const program = buildProgram();
|
|
276
|
+
await program.parseAsync([
|
|
277
|
+
"node",
|
|
278
|
+
"test",
|
|
279
|
+
"records",
|
|
280
|
+
"list",
|
|
281
|
+
"entity-id",
|
|
282
|
+
"--offset",
|
|
283
|
+
"0",
|
|
284
|
+
]);
|
|
285
|
+
|
|
286
|
+
expect(sdk.entities.getAllRecords).toHaveBeenCalledWith("entity-id", {
|
|
287
|
+
pageSize: 50,
|
|
288
|
+
jumpToPage: 1,
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("should error when --offset and --cursor are both given on list", async () => {
|
|
293
|
+
mockSdk();
|
|
294
|
+
const program = buildProgram();
|
|
295
|
+
await program.parseAsync([
|
|
296
|
+
"node",
|
|
297
|
+
"test",
|
|
298
|
+
"records",
|
|
299
|
+
"list",
|
|
300
|
+
"entity-id",
|
|
301
|
+
"--offset",
|
|
302
|
+
"50",
|
|
303
|
+
"--cursor",
|
|
304
|
+
"some-cursor",
|
|
305
|
+
]);
|
|
306
|
+
expect(OutputFormatter.error).toHaveBeenCalledWith(
|
|
307
|
+
expect.objectContaining({
|
|
308
|
+
Result: "Failure",
|
|
309
|
+
Message: "--offset and --cursor are mutually exclusive",
|
|
310
|
+
}),
|
|
311
|
+
);
|
|
312
|
+
expect(process.exitCode).toBe(1);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("should error when --offset is not a number on list", async () => {
|
|
316
|
+
mockSdk();
|
|
317
|
+
const program = buildProgram();
|
|
318
|
+
await program.parseAsync([
|
|
319
|
+
"node",
|
|
320
|
+
"test",
|
|
321
|
+
"records",
|
|
322
|
+
"list",
|
|
323
|
+
"entity-id",
|
|
324
|
+
"--offset",
|
|
325
|
+
"abc",
|
|
326
|
+
]);
|
|
327
|
+
expect(OutputFormatter.error).toHaveBeenCalledWith(
|
|
328
|
+
expect.objectContaining({
|
|
329
|
+
Result: "Failure",
|
|
330
|
+
Message: "Invalid --offset value",
|
|
331
|
+
}),
|
|
332
|
+
);
|
|
333
|
+
expect(process.exitCode).toBe(1);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("should error when --offset is negative on list", async () => {
|
|
337
|
+
mockSdk();
|
|
338
|
+
const program = buildProgram();
|
|
339
|
+
await program.parseAsync([
|
|
340
|
+
"node",
|
|
341
|
+
"test",
|
|
342
|
+
"records",
|
|
343
|
+
"list",
|
|
344
|
+
"entity-id",
|
|
345
|
+
"--offset",
|
|
346
|
+
"-1",
|
|
347
|
+
]);
|
|
348
|
+
expect(OutputFormatter.error).toHaveBeenCalledWith(
|
|
349
|
+
expect.objectContaining({
|
|
350
|
+
Result: "Failure",
|
|
351
|
+
Message: "Invalid --offset value",
|
|
352
|
+
}),
|
|
353
|
+
);
|
|
354
|
+
expect(process.exitCode).toBe(1);
|
|
355
|
+
});
|
|
236
356
|
});
|
|
237
357
|
|
|
238
358
|
describe("records list client error", () => {
|
|
@@ -1237,6 +1357,60 @@ describe("records query", () => {
|
|
|
1237
1357
|
);
|
|
1238
1358
|
});
|
|
1239
1359
|
|
|
1360
|
+
it("should query records with aggregates and groupBy via --body", async () => {
|
|
1361
|
+
const sdk = mockSdk();
|
|
1362
|
+
vi.mocked(sdk.entities.queryRecordsById).mockResolvedValue({
|
|
1363
|
+
items: [
|
|
1364
|
+
{ status: "Open", total: 12 },
|
|
1365
|
+
{ status: "Closed", total: 5 },
|
|
1366
|
+
],
|
|
1367
|
+
totalCount: 2,
|
|
1368
|
+
hasNextPage: false,
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
const aggregateBody = JSON.stringify({
|
|
1372
|
+
selectedFields: ["status"],
|
|
1373
|
+
groupBy: ["status"],
|
|
1374
|
+
aggregates: [{ function: "COUNT", field: "Id", alias: "total" }],
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1377
|
+
const program = buildProgram();
|
|
1378
|
+
await program.parseAsync([
|
|
1379
|
+
"node",
|
|
1380
|
+
"test",
|
|
1381
|
+
"records",
|
|
1382
|
+
"query",
|
|
1383
|
+
"entity-id",
|
|
1384
|
+
"--body",
|
|
1385
|
+
aggregateBody,
|
|
1386
|
+
]);
|
|
1387
|
+
|
|
1388
|
+
expect(sdk.entities.queryRecordsById).toHaveBeenCalledWith(
|
|
1389
|
+
"entity-id",
|
|
1390
|
+
expect.objectContaining({
|
|
1391
|
+
selectedFields: ["status"],
|
|
1392
|
+
groupBy: ["status"],
|
|
1393
|
+
aggregates: [
|
|
1394
|
+
{ function: "COUNT", field: "Id", alias: "total" },
|
|
1395
|
+
],
|
|
1396
|
+
pageSize: 50,
|
|
1397
|
+
}),
|
|
1398
|
+
);
|
|
1399
|
+
expect(OutputFormatter.success).toHaveBeenCalledWith(
|
|
1400
|
+
expect.objectContaining({
|
|
1401
|
+
Result: "Success",
|
|
1402
|
+
Code: "RecordQuery",
|
|
1403
|
+
Data: expect.objectContaining({
|
|
1404
|
+
TotalCount: 2,
|
|
1405
|
+
Records: [
|
|
1406
|
+
{ status: "Open", total: 12 },
|
|
1407
|
+
{ status: "Closed", total: 5 },
|
|
1408
|
+
],
|
|
1409
|
+
}),
|
|
1410
|
+
}),
|
|
1411
|
+
);
|
|
1412
|
+
});
|
|
1413
|
+
|
|
1240
1414
|
it("should query records with filterGroup and sortOptions combined", async () => {
|
|
1241
1415
|
const sdk = mockSdk();
|
|
1242
1416
|
vi.mocked(sdk.entities.queryRecordsById).mockResolvedValue({
|
|
@@ -1920,6 +2094,83 @@ describe("records query — pagination", () => {
|
|
|
1920
2094
|
);
|
|
1921
2095
|
});
|
|
1922
2096
|
|
|
2097
|
+
it("should pass jumpToPage to SDK when --offset is given on query", async () => {
|
|
2098
|
+
const sdk = mockSdk();
|
|
2099
|
+
vi.mocked(sdk.entities.queryRecordsById).mockResolvedValue({
|
|
2100
|
+
items: [{ Id: "r101" }],
|
|
2101
|
+
totalCount: 200,
|
|
2102
|
+
hasNextPage: true,
|
|
2103
|
+
});
|
|
2104
|
+
|
|
2105
|
+
const program = buildProgram();
|
|
2106
|
+
await program.parseAsync([
|
|
2107
|
+
"node",
|
|
2108
|
+
"test",
|
|
2109
|
+
"records",
|
|
2110
|
+
"query",
|
|
2111
|
+
"entity-id",
|
|
2112
|
+
"--offset",
|
|
2113
|
+
"100",
|
|
2114
|
+
"--limit",
|
|
2115
|
+
"50",
|
|
2116
|
+
]);
|
|
2117
|
+
|
|
2118
|
+
expect(sdk.entities.queryRecordsById).toHaveBeenCalledWith(
|
|
2119
|
+
"entity-id",
|
|
2120
|
+
expect.objectContaining({
|
|
2121
|
+
pageSize: 50,
|
|
2122
|
+
jumpToPage: 3,
|
|
2123
|
+
}),
|
|
2124
|
+
);
|
|
2125
|
+
expect(OutputFormatter.success).toHaveBeenCalledWith(
|
|
2126
|
+
expect.objectContaining({ Result: "Success", Code: "RecordQuery" }),
|
|
2127
|
+
);
|
|
2128
|
+
});
|
|
2129
|
+
|
|
2130
|
+
it("should error when --offset and --cursor are both given on query", async () => {
|
|
2131
|
+
mockSdk();
|
|
2132
|
+
const program = buildProgram();
|
|
2133
|
+
await program.parseAsync([
|
|
2134
|
+
"node",
|
|
2135
|
+
"test",
|
|
2136
|
+
"records",
|
|
2137
|
+
"query",
|
|
2138
|
+
"entity-id",
|
|
2139
|
+
"--offset",
|
|
2140
|
+
"50",
|
|
2141
|
+
"--cursor",
|
|
2142
|
+
"some-cursor",
|
|
2143
|
+
]);
|
|
2144
|
+
expect(OutputFormatter.error).toHaveBeenCalledWith(
|
|
2145
|
+
expect.objectContaining({
|
|
2146
|
+
Result: "Failure",
|
|
2147
|
+
Message: "--offset and --cursor are mutually exclusive",
|
|
2148
|
+
}),
|
|
2149
|
+
);
|
|
2150
|
+
expect(process.exitCode).toBe(1);
|
|
2151
|
+
});
|
|
2152
|
+
|
|
2153
|
+
it("should error when --offset is invalid on query", async () => {
|
|
2154
|
+
mockSdk();
|
|
2155
|
+
const program = buildProgram();
|
|
2156
|
+
await program.parseAsync([
|
|
2157
|
+
"node",
|
|
2158
|
+
"test",
|
|
2159
|
+
"records",
|
|
2160
|
+
"query",
|
|
2161
|
+
"entity-id",
|
|
2162
|
+
"--offset",
|
|
2163
|
+
"bad",
|
|
2164
|
+
]);
|
|
2165
|
+
expect(OutputFormatter.error).toHaveBeenCalledWith(
|
|
2166
|
+
expect.objectContaining({
|
|
2167
|
+
Result: "Failure",
|
|
2168
|
+
Message: "Invalid --offset value",
|
|
2169
|
+
}),
|
|
2170
|
+
);
|
|
2171
|
+
expect(process.exitCode).toBe(1);
|
|
2172
|
+
});
|
|
2173
|
+
|
|
1923
2174
|
it("should handle last page correctly — no NextCursor even if SDK emits undefined", async () => {
|
|
1924
2175
|
const sdk = mockSdk();
|
|
1925
2176
|
vi.mocked(sdk.entities.queryRecordsById).mockResolvedValue({
|
|
@@ -1963,16 +2214,6 @@ describe("records — negative scenarios", () => {
|
|
|
1963
2214
|
process.exitCode = undefined;
|
|
1964
2215
|
});
|
|
1965
2216
|
|
|
1966
|
-
it("entities delete is not a registered command", async () => {
|
|
1967
|
-
const { registerEntitiesCommand } = await import("./entities");
|
|
1968
|
-
const { Command } = await import("commander");
|
|
1969
|
-
const prog = new Command().exitOverride();
|
|
1970
|
-
registerEntitiesCommand(prog);
|
|
1971
|
-
const cmd = prog.commands.find((c) => c.name() === "entities");
|
|
1972
|
-
const subNames = cmd?.commands.map((c) => c.name());
|
|
1973
|
-
expect(subNames).not.toContain("delete");
|
|
1974
|
-
});
|
|
1975
|
-
|
|
1976
2217
|
it("records query — entity not found returns error", async () => {
|
|
1977
2218
|
const sdk = mockSdk();
|
|
1978
2219
|
vi.mocked(sdk.entities.queryRecordsById).mockRejectedValue(
|