@reventlessdev/reventless-core 3.0.0-alpha.236 → 3.0.0-alpha.237

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.
Files changed (30) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/package.json +7 -7
  3. package/src/admin/Platform_Admin_Structure.res +5 -1
  4. package/src/admin/Platform_Admin_Structure.res.mjs +4 -2
  5. package/src/admin/Platform_ComponentDefinitionsApi.res +4 -2
  6. package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +11 -3
  7. package/src/components/Api/GraphQL_FragmentGenerator.res +14 -1
  8. package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +1 -1
  9. package/src/components/Api/QueryDbListQuery.res +25 -1
  10. package/src/components/Api/QueryDbListQuery.res.mjs +11 -4
  11. package/src/components/Api/SuryToJsonSchema.res +39 -0
  12. package/src/components/Api/SuryToJsonSchema.res.mjs +34 -3
  13. package/src/plugin/component/Plugin_Structure.res +112 -15
  14. package/src/plugin/component/Plugin_Structure.res.mjs +77 -9
  15. package/src/plugin/lifecycle/PluginsReadModelSpec.res.mjs +3 -2
  16. package/tests/admin/Platform_Admin_StructureTest.res +49 -0
  17. package/tests/admin/Platform_Admin_StructureTest.res.mjs +24 -0
  18. package/tests/admin/Platform_BakedManifestTest.res +3 -1
  19. package/tests/admin/Platform_BakedManifestTest.res.mjs +3 -1
  20. package/tests/admin/Platform_ComponentDefinitionsApiTest.res +14 -8
  21. package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +24 -12
  22. package/tests/admin/Platform_PluginStructuresApiTest.res +3 -1
  23. package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +6 -2
  24. package/tests/api/Api_IdsTest.res.mjs +1 -1
  25. package/tests/api/SuryToJsonSchemaTest.res +141 -1
  26. package/tests/api/SuryToJsonSchemaTest.res.mjs +269 -54
  27. package/tests/plugin/PluginStructureTest.res +168 -12
  28. package/tests/plugin/PluginStructureTest.res.mjs +146 -9
  29. package/tests/plugin/StateViewSlice/PsAnnotatedView.res.mjs +3 -2
  30. package/tests/plugin/pluginDefinitionRequiredScalars.txt +2 -0
@@ -670,33 +670,189 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
670
670
  )
671
671
  })
672
672
 
673
- describe("statusField — the shape rule", () => {
674
- let statusOf = schema =>
675
- Plugin_Structure.statusFieldFromStateSchema(~entityName="Test", schema->S.castToUnknown)
673
+ describe("lifecycleField — the name rule and the shape rule", () => {
674
+ let lifecycleOf = schema =>
675
+ Plugin_Structure.lifecycleFieldFromStateSchema(~entityName="Test", schema->S.castToUnknown)
676
676
 
677
- testSync("a `status` field holding a closed set of values is the lifecycle field", () => {
677
+ let withLifecycle = (schema, ~field) =>
678
+ schema->S.castToUnknown->S.Metadata.set(
679
+ ~id=Reventless.StateAnnotations.stateAnnotationsId,
680
+ {
681
+ ids: [],
682
+ compositeIds: [],
683
+ subIds: [],
684
+ compositeSubIds: [],
685
+ indexes: [],
686
+ hidden: [],
687
+ summary: [],
688
+ drillTargets: [],
689
+ drillTargetKeys: [],
690
+ collapsed: [],
691
+ scan: [],
692
+ scanSort: [],
693
+ semantic: [],
694
+ metric: [],
695
+ lifecycle: Some(field),
696
+ groupBy: None,
697
+ visibility: None,
698
+ live: None,
699
+ retired: None,
700
+ },
701
+ )
702
+
703
+ testSync("a `lifecycle` field holding a closed set of values is the lifecycle field", () => {
678
704
  let schema = S.schema(s =>
679
705
  {
680
- "status": s.matches(S.union([S.literal("Placed"), S.literal("Shipped")])),
706
+ "lifecycle": s.matches(S.union([S.literal("Placed"), S.literal("Shipped")])),
681
707
  }
682
708
  )
683
- expect(statusOf(schema))->toEqual(Some("status"))
709
+ expect(lifecycleOf(schema))->toEqual(Some("lifecycle"))
684
710
  })
685
711
 
686
712
  testSync("an optional one counts too", () => {
687
713
  let schema = S.schema(s =>
688
714
  {
689
- "status": s.matches(S.option(S.union([S.literal("Placed"), S.literal("Shipped")]))),
715
+ "lifecycle": s.matches(S.option(S.union([S.literal("Placed"), S.literal("Shipped")]))),
690
716
  }
691
717
  )
692
- expect(statusOf(schema))->toEqual(Some("status"))
718
+ expect(lifecycleOf(schema))->toEqual(Some("lifecycle"))
693
719
  })
694
720
 
695
- testSync("free text named `status` is not a lifecycle", () => {
721
+ testSync("free text named `lifecycle` is not a lifecycle", () => {
696
722
  // `allowedStates` filtering needs states to compare against; a string
697
- // field named `status` gives a command menu nothing to match.
698
- let schema = S.schema(s => {"status": s.matches(S.string)})
699
- expect(statusOf(schema))->toEqual(None)
723
+ // field named `lifecycle` gives a command menu nothing to match.
724
+ let schema = S.schema(s => {"lifecycle": s.matches(S.string)})
725
+ expect(lifecycleOf(schema))->toEqual(None)
726
+ })
727
+
728
+ // The one behaviour the rename deliberately changes, so it is asserted rather
729
+ // than assumed. `status` is a promiscuous name — geocoding progress, todo-queue
730
+ // progress, translation audit outcome — and a convention keyed on it guessed
731
+ // often. A record whose lifecycle really does live in a field called `status`
732
+ // says so with `@lifecycle`.
733
+ testSync("an unannotated field named `status` resolves to None", () => {
734
+ let schema = S.schema(s =>
735
+ {
736
+ "status": s.matches(S.union([S.literal("Placed"), S.literal("Shipped")])),
737
+ }
738
+ )
739
+ expect(lifecycleOf(schema))->toEqual(None)
740
+ })
741
+
742
+ testSync("the annotation names a field the convention would never reach", () => {
743
+ let schema = S.schema(s =>
744
+ {
745
+ "locationStatus": s.matches(S.union([S.literal("Pending"), S.literal("Located")])),
746
+ }
747
+ )
748
+ expect(lifecycleOf(schema->withLifecycle(~field="locationStatus")))->toEqual(
749
+ Some("locationStatus"),
750
+ )
751
+ })
752
+ })
753
+
754
+ describe("retiredField — annotation and nothing else", () => {
755
+ let retiredOf = schema =>
756
+ Plugin_Structure.retiredFieldFromStateSchema(schema->S.castToUnknown)
757
+
758
+ let withRetired = (schema, ~field) =>
759
+ schema->S.castToUnknown->S.Metadata.set(
760
+ ~id=Reventless.StateAnnotations.stateAnnotationsId,
761
+ {
762
+ ids: [],
763
+ compositeIds: [],
764
+ subIds: [],
765
+ compositeSubIds: [],
766
+ indexes: [],
767
+ hidden: [],
768
+ summary: [],
769
+ drillTargets: [],
770
+ drillTargetKeys: [],
771
+ collapsed: [],
772
+ scan: [],
773
+ scanSort: [],
774
+ semantic: [],
775
+ metric: [],
776
+ lifecycle: None,
777
+ groupBy: None,
778
+ visibility: None,
779
+ live: None,
780
+ retired: Some({field, label: "", showWhenFalse: false, values: None}),
781
+ },
782
+ )
783
+
784
+ // The state form. The value is published beside the field so a client holding
785
+ // the def holds the whole predicate — two places deriving one comparison is
786
+ // how they come to disagree about which rows a caller may see.
787
+ testSync("publishes the retirement state beside the field", () => {
788
+ let withState = schema =>
789
+ schema->S.castToUnknown->S.Metadata.set(
790
+ ~id=Reventless.StateAnnotations.stateAnnotationsId,
791
+ {
792
+ ids: [],
793
+ compositeIds: [],
794
+ subIds: [],
795
+ compositeSubIds: [],
796
+ indexes: [],
797
+ hidden: [],
798
+ summary: [],
799
+ drillTargets: [],
800
+ drillTargetKeys: [],
801
+ collapsed: [],
802
+ scan: [],
803
+ scanSort: [],
804
+ semantic: [],
805
+ metric: [],
806
+ lifecycle: Some("accountStatus"),
807
+ groupBy: None,
808
+ visibility: None,
809
+ live: None,
810
+ retired: Some({
811
+ field: "accountStatus",
812
+ label: "",
813
+ showWhenFalse: false,
814
+ values: Some(["Deactivated", "Closed"]),
815
+ }),
816
+ },
817
+ )
818
+ let schema = S.schema(s => {"accountStatus": s.matches(S.string)})->withState
819
+ expect((
820
+ Plugin_Structure.retiredFieldFromStateSchema(schema),
821
+ Plugin_Structure.retiredValuesFromStateSchema(schema),
822
+ ))->toEqual((Some("accountStatus"), Some(["Deactivated", "Closed"])))
823
+ })
824
+
825
+ // Absent is what says "boolean form", so it has to stay tellable from a state
826
+ // form — including one naming nothing, which is `Some([])`.
827
+ testSync("publishes no states on the boolean form", () => {
828
+ let schema = S.schema(s => {"archived": s.matches(S.bool)})
829
+ expect(
830
+ Plugin_Structure.retiredValuesFromStateSchema(schema->withRetired(~field="archived")),
831
+ )->toEqual(None)
832
+ })
833
+
834
+ testSync("names the annotated field", () => {
835
+ let schema = S.schema(s => {"archived": s.matches(S.bool)})
836
+ expect(retiredOf(schema->withRetired(~field="archived")))->toEqual(Some("archived"))
837
+ })
838
+
839
+ // The whole reason this has no convention rung. `lifecycleField` may still fall
840
+ // back to a field literally named `lifecycle` because guessing wrong there makes
841
+ // a command menu filter oddly; guessing wrong here makes rows disappear for every caller who is not
842
+ // elevated, so an unannotated boolean stays as visible as it was.
843
+ testSync("declines a conventionally-named boolean nobody annotated", () => {
844
+ let schema = S.schema(s =>
845
+ {
846
+ "archived": s.matches(S.bool),
847
+ "deactivated": s.matches(S.bool),
848
+ }
849
+ )
850
+ expect(retiredOf(schema))->toEqual(None)
851
+ })
852
+
853
+ testSync("declines a schema carrying no annotation spec at all", () => {
854
+ let schema = S.schema(s => {"id": s.matches(S.string)})
855
+ expect(retiredOf(schema))->toEqual(None)
700
856
  })
701
857
  })
702
858
 
@@ -14,6 +14,7 @@ import * as Api_Naming$ReventlessCore from "../../src/components/Api/Api_Naming.
14
14
  import * as PsShipOrder$ReventlessCore from "./StateChangeSlice/PsShipOrder.res.mjs";
15
15
  import * as PsOrdersView$ReventlessCore from "./StateViewSlice/PsOrdersView.res.mjs";
16
16
  import * as PsPlaceOrder$ReventlessCore from "./StateChangeSlice/PsPlaceOrder.res.mjs";
17
+ import * as StateAnnotations$Reventless from "@reventlessdev/reventless-spec/src/components/StateAnnotations.res.mjs";
17
18
  import * as PsChangePhoto$ReventlessCore from "./StateChangeSlice/PsChangePhoto.res.mjs";
18
19
  import * as CapabilityManifest$Reventless from "@reventlessdev/reventless-spec/src/components/CapabilityManifest.res.mjs";
19
20
  import * as PsReserveStock$ReventlessCore from "./StateChangeSlice/PsReserveStock.res.mjs";
@@ -796,30 +797,166 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
796
797
  ]);
797
798
  });
798
799
  });
799
- globalThis.describe("statusField — the shape rule", () => {
800
- globalThis.test("a `status` field holding a closed set of values is the lifecycle field", () => {
800
+ globalThis.describe("lifecycleField — the name rule and the shape rule", () => {
801
+ let withLifecycle = (schema, field) => S.Metadata.set(schema, StateAnnotations$Reventless.stateAnnotationsId, {
802
+ ids: [],
803
+ compositeIds: [],
804
+ subIds: [],
805
+ compositeSubIds: [],
806
+ indexes: [],
807
+ hidden: [],
808
+ summary: [],
809
+ drillTargets: [],
810
+ drillTargetKeys: [],
811
+ collapsed: [],
812
+ scan: [],
813
+ scanSort: [],
814
+ semantic: [],
815
+ metric: [],
816
+ lifecycle: field,
817
+ groupBy: undefined,
818
+ visibility: undefined,
819
+ live: undefined,
820
+ retired: undefined
821
+ });
822
+ globalThis.test("a `lifecycle` field holding a closed set of values is the lifecycle field", () => {
801
823
  let schema = S.schema(s => ({
802
- status: s.m(S.union([
824
+ lifecycle: s.m(S.union([
803
825
  S.literal("Placed"),
804
826
  S.literal("Shipped")
805
827
  ]))
806
828
  }));
807
- globalThis.expect(Plugin_Structure$ReventlessCore.statusFieldFromStateSchema("Test", schema)).toEqual("status");
829
+ globalThis.expect(Plugin_Structure$ReventlessCore.lifecycleFieldFromStateSchema("Test", schema)).toEqual("lifecycle");
808
830
  });
809
831
  globalThis.test("an optional one counts too", () => {
810
832
  let schema = S.schema(s => ({
811
- status: s.m(S.option(S.union([
833
+ lifecycle: s.m(S.option(S.union([
812
834
  S.literal("Placed"),
813
835
  S.literal("Shipped")
814
836
  ])))
815
837
  }));
816
- globalThis.expect(Plugin_Structure$ReventlessCore.statusFieldFromStateSchema("Test", schema)).toEqual("status");
838
+ globalThis.expect(Plugin_Structure$ReventlessCore.lifecycleFieldFromStateSchema("Test", schema)).toEqual("lifecycle");
839
+ });
840
+ globalThis.test("free text named `lifecycle` is not a lifecycle", () => {
841
+ let schema = S.schema(s => ({
842
+ lifecycle: s.m(S.string)
843
+ }));
844
+ globalThis.expect(Plugin_Structure$ReventlessCore.lifecycleFieldFromStateSchema("Test", schema)).toEqual(undefined);
845
+ });
846
+ globalThis.test("an unannotated field named `status` resolves to None", () => {
847
+ let schema = S.schema(s => ({
848
+ status: s.m(S.union([
849
+ S.literal("Placed"),
850
+ S.literal("Shipped")
851
+ ]))
852
+ }));
853
+ globalThis.expect(Plugin_Structure$ReventlessCore.lifecycleFieldFromStateSchema("Test", schema)).toEqual(undefined);
854
+ });
855
+ globalThis.test("the annotation names a field the convention would never reach", () => {
856
+ let schema = S.schema(s => ({
857
+ locationStatus: s.m(S.union([
858
+ S.literal("Pending"),
859
+ S.literal("Located")
860
+ ]))
861
+ }));
862
+ globalThis.expect(Plugin_Structure$ReventlessCore.lifecycleFieldFromStateSchema("Test", withLifecycle(schema, "locationStatus"))).toEqual("locationStatus");
863
+ });
864
+ });
865
+ globalThis.describe("retiredField — annotation and nothing else", () => {
866
+ let withRetired = (schema, field) => S.Metadata.set(schema, StateAnnotations$Reventless.stateAnnotationsId, {
867
+ ids: [],
868
+ compositeIds: [],
869
+ subIds: [],
870
+ compositeSubIds: [],
871
+ indexes: [],
872
+ hidden: [],
873
+ summary: [],
874
+ drillTargets: [],
875
+ drillTargetKeys: [],
876
+ collapsed: [],
877
+ scan: [],
878
+ scanSort: [],
879
+ semantic: [],
880
+ metric: [],
881
+ lifecycle: undefined,
882
+ groupBy: undefined,
883
+ visibility: undefined,
884
+ live: undefined,
885
+ retired: {
886
+ field: field,
887
+ label: "",
888
+ showWhenFalse: false,
889
+ values: undefined
890
+ }
891
+ });
892
+ globalThis.test("publishes the retirement state beside the field", () => {
893
+ let withState = schema => S.Metadata.set(schema, StateAnnotations$Reventless.stateAnnotationsId, {
894
+ ids: [],
895
+ compositeIds: [],
896
+ subIds: [],
897
+ compositeSubIds: [],
898
+ indexes: [],
899
+ hidden: [],
900
+ summary: [],
901
+ drillTargets: [],
902
+ drillTargetKeys: [],
903
+ collapsed: [],
904
+ scan: [],
905
+ scanSort: [],
906
+ semantic: [],
907
+ metric: [],
908
+ lifecycle: "accountStatus",
909
+ groupBy: undefined,
910
+ visibility: undefined,
911
+ live: undefined,
912
+ retired: {
913
+ field: "accountStatus",
914
+ label: "",
915
+ showWhenFalse: false,
916
+ values: [
917
+ "Deactivated",
918
+ "Closed"
919
+ ]
920
+ }
921
+ });
922
+ let schema = withState(S.schema(s => ({
923
+ accountStatus: s.m(S.string)
924
+ })));
925
+ globalThis.expect([
926
+ Plugin_Structure$ReventlessCore.retiredFieldFromStateSchema(schema),
927
+ Plugin_Structure$ReventlessCore.retiredValuesFromStateSchema(schema)
928
+ ]).toEqual([
929
+ "accountStatus",
930
+ [
931
+ "Deactivated",
932
+ "Closed"
933
+ ]
934
+ ]);
935
+ });
936
+ globalThis.test("publishes no states on the boolean form", () => {
937
+ let schema = S.schema(s => ({
938
+ archived: s.m(S.bool)
939
+ }));
940
+ globalThis.expect(Plugin_Structure$ReventlessCore.retiredValuesFromStateSchema(withRetired(schema, "archived"))).toEqual(undefined);
941
+ });
942
+ globalThis.test("names the annotated field", () => {
943
+ let schema = S.schema(s => ({
944
+ archived: s.m(S.bool)
945
+ }));
946
+ globalThis.expect(Plugin_Structure$ReventlessCore.retiredFieldFromStateSchema(withRetired(schema, "archived"))).toEqual("archived");
947
+ });
948
+ globalThis.test("declines a conventionally-named boolean nobody annotated", () => {
949
+ let schema = S.schema(s => ({
950
+ archived: s.m(S.bool),
951
+ deactivated: s.m(S.bool)
952
+ }));
953
+ globalThis.expect(Plugin_Structure$ReventlessCore.retiredFieldFromStateSchema(schema)).toEqual(undefined);
817
954
  });
818
- globalThis.test("free text named `status` is not a lifecycle", () => {
955
+ globalThis.test("declines a schema carrying no annotation spec at all", () => {
819
956
  let schema = S.schema(s => ({
820
- status: s.m(S.string)
957
+ id: s.m(S.string)
821
958
  }));
822
- globalThis.expect(Plugin_Structure$ReventlessCore.statusFieldFromStateSchema("Test", schema)).toEqual(undefined);
959
+ globalThis.expect(Plugin_Structure$ReventlessCore.retiredFieldFromStateSchema(schema)).toEqual(undefined);
823
960
  });
824
961
  });
825
962
  let getProperty = (json, key) => {
@@ -79,10 +79,11 @@ let stateSchema$1 = S.Metadata.set(stateSchema, StateAnnotations$Reventless.stat
79
79
  label: "Revenue"
80
80
  }
81
81
  ]],
82
- status: undefined,
82
+ lifecycle: undefined,
83
83
  groupBy: undefined,
84
84
  visibility: undefined,
85
- live: false
85
+ live: false,
86
+ retired: undefined
86
87
  });
87
88
 
88
89
  let name = "AnnotatedView";
@@ -79,6 +79,7 @@
79
79
  .structure.readModels[].name: string
80
80
  .structure.readModels[].queryField: string
81
81
  .structure.readModels[].requiredAccess[]: string
82
+ .structure.readModels[].retiredValues[]: string
82
83
  .structure.readModels[].schema: string
83
84
  .structure.readModels[].searchableFields[]: string
84
85
  .structure.requiredStoreDeclarations[].component: string
@@ -110,6 +111,7 @@
110
111
  .structure.stateViewSlices[].name: string
111
112
  .structure.stateViewSlices[].queryField: string
112
113
  .structure.stateViewSlices[].requiredAccess[]: string
114
+ .structure.stateViewSlices[].retiredValues[]: string
113
115
  .structure.stateViewSlices[].schema: string
114
116
  .structure.stateViewSlices[].searchableFields[]: string
115
117
  .version: string