@reventlessdev/reventless-core 3.0.0-alpha.245 → 3.0.0-alpha.247
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/CHANGELOG.md +23 -0
- package/package.json +6 -6
- package/src/components/Api/Api_Naming.res +58 -0
- package/src/components/Api/Api_Naming.res.mjs +44 -0
- package/src/components/Api/GraphQL_FragmentGenerator.res +61 -1
- package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +44 -3
- package/src/components/Api/GraphQL_SchemaInspector.res.mjs +2 -2
- package/src/components/AutomationSlice/AutomationSlice_Callback.res +88 -8
- package/src/components/AutomationSlice/AutomationSlice_Callback.res.mjs +86 -28
- package/src/components/Dcb/Dcb_Builder.res +3 -0
- package/src/components/Dcb/Dcb_Builder.res.mjs +4 -1
- package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Callback.res +108 -12
- package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Callback.res.mjs +89 -22
- package/src/plugin/component/Plugin_Builder.res +2 -0
- package/src/plugin/component/Plugin_Builder.res.mjs +3 -1
- package/tests/api/GraphQL_FragmentGeneratorTest.res +141 -0
- package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +109 -2
|
@@ -572,3 +572,144 @@ describe("@internal keeps a field off the generated type", () => {
|
|
|
572
572
|
))
|
|
573
573
|
})
|
|
574
574
|
})
|
|
575
|
+
|
|
576
|
+
// `@resolves` / `@resolvesMany` add a field to a queryable's object type that the
|
|
577
|
+
// state record does not carry — the row on the other side of a foreign key. The
|
|
578
|
+
// PPX has written those configs into `config.idResolvers` / `config.idsResolvers`
|
|
579
|
+
// all along and nothing emitted the field, so every backend provisioned a
|
|
580
|
+
// resolver for a field its own schema never declared.
|
|
581
|
+
|
|
582
|
+
module ResolvedFields = {
|
|
583
|
+
@schema
|
|
584
|
+
type product = {productId: string, name: string}
|
|
585
|
+
|
|
586
|
+
@schema
|
|
587
|
+
type order = {orderId: string, productId: string, productIds: array<string>}
|
|
588
|
+
|
|
589
|
+
let entries = (
|
|
590
|
+
~resolvedFields,
|
|
591
|
+
~targetPermission: option<Reventless.Authorization.permission>=?,
|
|
592
|
+
): array<ReventlessInfra.Api.querySchemaEntry> => [
|
|
593
|
+
{
|
|
594
|
+
singleFieldName: "Catalog_Order",
|
|
595
|
+
listFieldName: "Catalog_Orders",
|
|
596
|
+
returnTypeName: "Catalog_Order",
|
|
597
|
+
stateSchema: orderSchema->S.castToUnknown,
|
|
598
|
+
authorization: None,
|
|
599
|
+
resolvedFields: ?resolvedFields,
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
singleFieldName: "Catalog_Product",
|
|
603
|
+
listFieldName: "Catalog_Products",
|
|
604
|
+
returnTypeName: "Catalog_Product",
|
|
605
|
+
stateSchema: productSchema->S.castToUnknown,
|
|
606
|
+
authorization: None,
|
|
607
|
+
permission: ?targetPermission,
|
|
608
|
+
},
|
|
609
|
+
]
|
|
610
|
+
|
|
611
|
+
let bothFields: array<ReventlessInfra.Api.resolvedFieldEntry> = [
|
|
612
|
+
{fieldName: "product", typeName: "Catalog_Product", multi: false},
|
|
613
|
+
{fieldName: "products", typeName: "Catalog_Product", multi: true},
|
|
614
|
+
]
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
describe("@resolves / @resolvesMany reach the SDL", () => {
|
|
618
|
+
let refused = f =>
|
|
619
|
+
try {
|
|
620
|
+
let _ = f()
|
|
621
|
+
false
|
|
622
|
+
} catch {
|
|
623
|
+
| _ => true
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
let orderType = (~resolvedFields, ~targetPermission=?) =>
|
|
627
|
+
GraphQL_Stitcher.decode(
|
|
628
|
+
GraphQL_FragmentGenerator.generate(
|
|
629
|
+
~mutationEntries=[],
|
|
630
|
+
~queryEntries=ResolvedFields.entries(~resolvedFields, ~targetPermission?),
|
|
631
|
+
),
|
|
632
|
+
).types
|
|
633
|
+
->Array.find(t => t->String.startsWith("type Catalog_Order "))
|
|
634
|
+
->Option.getOr("")
|
|
635
|
+
|
|
636
|
+
testSync("the single form returns the target's type", () =>
|
|
637
|
+
expect(
|
|
638
|
+
orderType(~resolvedFields=Some(ResolvedFields.bothFields))->String.includes(
|
|
639
|
+
"product: Catalog_Product",
|
|
640
|
+
),
|
|
641
|
+
)->toBe(true)
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
// A list of non-null items rather than `[T]`: the batch read drops ids it did
|
|
645
|
+
// not find instead of padding the gap with nulls, so the field is shorter.
|
|
646
|
+
testSync("the batch form returns a list of the target's type", () =>
|
|
647
|
+
expect(
|
|
648
|
+
orderType(~resolvedFields=Some(ResolvedFields.bothFields))->String.includes(
|
|
649
|
+
"products: [Catalog_Product!]",
|
|
650
|
+
),
|
|
651
|
+
)->toBe(true)
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
testSync("the state's own fields are untouched", () => {
|
|
655
|
+
let sdl = orderType(~resolvedFields=Some(ResolvedFields.bothFields))
|
|
656
|
+
expect((sdl->String.includes("orderId:"), sdl->String.includes("id: ID!")))->toEqual((
|
|
657
|
+
true,
|
|
658
|
+
true,
|
|
659
|
+
))
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
// The control: without the entry the type is exactly what it was, which is what
|
|
663
|
+
// made the annotation inert rather than wrong.
|
|
664
|
+
testSync("a view that declares none gets no extra field", () =>
|
|
665
|
+
expect(orderType(~resolvedFields=None)->String.includes("Catalog_Product"))->toBe(false)
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
// A plugin's document is validated standalone before the merge, so a field
|
|
669
|
+
// returning a name nothing declares fails the whole document — every type in
|
|
670
|
+
// it, not just this field. Refused where the declaration is, naming it.
|
|
671
|
+
testSync("a target this plugin does not expose is refused", () =>
|
|
672
|
+
expect(
|
|
673
|
+
refused(() =>
|
|
674
|
+
orderType(
|
|
675
|
+
~resolvedFields=Some([{fieldName: "supplier", typeName: "Supply_Supplier", multi: false}]),
|
|
676
|
+
)
|
|
677
|
+
),
|
|
678
|
+
)->toBe(true)
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
// A nested field is read through its parent, so it answers under the parent's
|
|
682
|
+
// rule. A target guarded more narrowly than the view that embeds it would be
|
|
683
|
+
// handed over by the wider door.
|
|
684
|
+
testSync("a target guarded differently from the parent is refused", () =>
|
|
685
|
+
expect(
|
|
686
|
+
refused(() =>
|
|
687
|
+
orderType(
|
|
688
|
+
~resolvedFields=Some(ResolvedFields.bothFields),
|
|
689
|
+
~targetPermission=AllowGroups(["Admin"]),
|
|
690
|
+
)
|
|
691
|
+
),
|
|
692
|
+
)->toBe(true)
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
// A target open to everyone can only narrow, so it needs no refusal.
|
|
696
|
+
testSync("a target open to everyone is allowed under an authenticated parent", () =>
|
|
697
|
+
expect(
|
|
698
|
+
refused(() =>
|
|
699
|
+
orderType(~resolvedFields=Some(ResolvedFields.bothFields), ~targetPermission=AllowAnonymous)
|
|
700
|
+
),
|
|
701
|
+
)->toBe(false)
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
testSync("a resolved field colliding with a state field is refused", () =>
|
|
705
|
+
expect(
|
|
706
|
+
refused(() =>
|
|
707
|
+
orderType(
|
|
708
|
+
~resolvedFields=Some([
|
|
709
|
+
{fieldName: "productId", typeName: "Catalog_Product", multi: false},
|
|
710
|
+
]),
|
|
711
|
+
)
|
|
712
|
+
),
|
|
713
|
+
)->toBe(true)
|
|
714
|
+
)
|
|
715
|
+
})
|
|
@@ -246,7 +246,7 @@ globalThis.describe("semantic composites are named once, not once per field", ()
|
|
|
246
246
|
globalThis.expect(fragmentTypes.some(t => t.includes("AddProductPrice"))).toBe(false);
|
|
247
247
|
});
|
|
248
248
|
globalThis.test("the output position keeps the bare name", () => {
|
|
249
|
-
let types = GraphQL_FragmentGenerator$ReventlessCore.deriveObjectTypeWithNested("Catalog_Product", undefined, false, Sury.$schema(s => ({
|
|
249
|
+
let types = GraphQL_FragmentGenerator$ReventlessCore.deriveObjectTypeWithNested("Catalog_Product", undefined, false, undefined, Sury.$schema(s => ({
|
|
250
250
|
productId: s.m(Sury.string),
|
|
251
251
|
price: s.m(Money$Reventless.schema)
|
|
252
252
|
})));
|
|
@@ -430,7 +430,7 @@ globalThis.describe("the by-index door", () => {
|
|
|
430
430
|
});
|
|
431
431
|
|
|
432
432
|
globalThis.describe("@internal keeps a field off the generated type", () => {
|
|
433
|
-
let types = excludeFields => GraphQL_FragmentGenerator$ReventlessCore.deriveObjectTypeWithNested("Platform_Widget", excludeFields, undefined, PsInternalFieldsReadModel$ReventlessCore.stateSchema).join("\n");
|
|
433
|
+
let types = excludeFields => GraphQL_FragmentGenerator$ReventlessCore.deriveObjectTypeWithNested("Platform_Widget", excludeFields, undefined, undefined, PsInternalFieldsReadModel$ReventlessCore.stateSchema).join("\n");
|
|
434
434
|
globalThis.test("the declared fields are absent", () => {
|
|
435
435
|
globalThis.expect([
|
|
436
436
|
types(undefined).includes("eventCollector"),
|
|
@@ -471,6 +471,112 @@ globalThis.describe("@internal keeps a field off the generated type", () => {
|
|
|
471
471
|
});
|
|
472
472
|
});
|
|
473
473
|
|
|
474
|
+
let productSchema = Sury.$schema(s => ({
|
|
475
|
+
productId: s.m(Sury.string),
|
|
476
|
+
name: s.m(Sury.string)
|
|
477
|
+
}));
|
|
478
|
+
|
|
479
|
+
let orderSchema = Sury.$schema(s => ({
|
|
480
|
+
orderId: s.m(Sury.string),
|
|
481
|
+
productId: s.m(Sury.string),
|
|
482
|
+
productIds: s.m(Sury.array(Sury.string))
|
|
483
|
+
}));
|
|
484
|
+
|
|
485
|
+
function entries(resolvedFields, targetPermission) {
|
|
486
|
+
return [
|
|
487
|
+
{
|
|
488
|
+
singleFieldName: "Catalog_Order",
|
|
489
|
+
listFieldName: "Catalog_Orders",
|
|
490
|
+
returnTypeName: "Catalog_Order",
|
|
491
|
+
stateSchema: orderSchema,
|
|
492
|
+
authorization: undefined,
|
|
493
|
+
resolvedFields: resolvedFields
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
singleFieldName: "Catalog_Product",
|
|
497
|
+
listFieldName: "Catalog_Products",
|
|
498
|
+
returnTypeName: "Catalog_Product",
|
|
499
|
+
stateSchema: productSchema,
|
|
500
|
+
authorization: undefined,
|
|
501
|
+
permission: targetPermission
|
|
502
|
+
}
|
|
503
|
+
];
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
let bothFields = [
|
|
507
|
+
{
|
|
508
|
+
fieldName: "product",
|
|
509
|
+
typeName: "Catalog_Product",
|
|
510
|
+
multi: false
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
fieldName: "products",
|
|
514
|
+
typeName: "Catalog_Product",
|
|
515
|
+
multi: true
|
|
516
|
+
}
|
|
517
|
+
];
|
|
518
|
+
|
|
519
|
+
let ResolvedFields = {
|
|
520
|
+
productSchema: productSchema,
|
|
521
|
+
orderSchema: orderSchema,
|
|
522
|
+
entries: entries,
|
|
523
|
+
bothFields: bothFields
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
globalThis.describe("@resolves / @resolvesMany reach the SDL", () => {
|
|
527
|
+
let refused = f => {
|
|
528
|
+
try {
|
|
529
|
+
f();
|
|
530
|
+
return false;
|
|
531
|
+
} catch (exn) {
|
|
532
|
+
return true;
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
let orderType = (resolvedFields, targetPermission) => Stdlib_Option.getOr(GraphQL_Stitcher$ReventlessCore.decode(GraphQL_FragmentGenerator$ReventlessCore.generate([], entries(resolvedFields, targetPermission))).types.find(t => t.startsWith("type Catalog_Order ")), "");
|
|
536
|
+
globalThis.test("the single form returns the target's type", () => {
|
|
537
|
+
globalThis.expect(orderType(bothFields, undefined).includes("product: Catalog_Product")).toBe(true);
|
|
538
|
+
});
|
|
539
|
+
globalThis.test("the batch form returns a list of the target's type", () => {
|
|
540
|
+
globalThis.expect(orderType(bothFields, undefined).includes("products: [Catalog_Product!]")).toBe(true);
|
|
541
|
+
});
|
|
542
|
+
globalThis.test("the state's own fields are untouched", () => {
|
|
543
|
+
let sdl = orderType(bothFields, undefined);
|
|
544
|
+
globalThis.expect([
|
|
545
|
+
sdl.includes("orderId:"),
|
|
546
|
+
sdl.includes("id: ID!")
|
|
547
|
+
]).toEqual([
|
|
548
|
+
true,
|
|
549
|
+
true
|
|
550
|
+
]);
|
|
551
|
+
});
|
|
552
|
+
globalThis.test("a view that declares none gets no extra field", () => {
|
|
553
|
+
globalThis.expect(orderType(undefined, undefined).includes("Catalog_Product")).toBe(false);
|
|
554
|
+
});
|
|
555
|
+
globalThis.test("a target this plugin does not expose is refused", () => {
|
|
556
|
+
globalThis.expect(refused(() => orderType([{
|
|
557
|
+
fieldName: "supplier",
|
|
558
|
+
typeName: "Supply_Supplier",
|
|
559
|
+
multi: false
|
|
560
|
+
}], undefined))).toBe(true);
|
|
561
|
+
});
|
|
562
|
+
globalThis.test("a target guarded differently from the parent is refused", () => {
|
|
563
|
+
globalThis.expect(refused(() => orderType(bothFields, {
|
|
564
|
+
TAG: "AllowGroups",
|
|
565
|
+
_0: ["Admin"]
|
|
566
|
+
}))).toBe(true);
|
|
567
|
+
});
|
|
568
|
+
globalThis.test("a target open to everyone is allowed under an authenticated parent", () => {
|
|
569
|
+
globalThis.expect(refused(() => orderType(bothFields, "AllowAnonymous"))).toBe(false);
|
|
570
|
+
});
|
|
571
|
+
globalThis.test("a resolved field colliding with a state field is refused", () => {
|
|
572
|
+
globalThis.expect(refused(() => orderType([{
|
|
573
|
+
fieldName: "productId",
|
|
574
|
+
typeName: "Catalog_Product",
|
|
575
|
+
multi: false
|
|
576
|
+
}], undefined))).toBe(true);
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
|
|
474
580
|
export {
|
|
475
581
|
OrderSliceCmd,
|
|
476
582
|
mutationFor,
|
|
@@ -482,5 +588,6 @@ export {
|
|
|
482
588
|
RefDoorRow,
|
|
483
589
|
queryFor,
|
|
484
590
|
refDoorFragment,
|
|
591
|
+
ResolvedFields,
|
|
485
592
|
}
|
|
486
593
|
/* commandSchema Not a pure module */
|