@reventlessdev/reventless-core 3.0.0-alpha.156 → 3.0.0-alpha.157

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 (74) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +6 -6
  3. package/src/admin/AdminApi.res +80 -10
  4. package/src/admin/AdminApi.res.mjs +45 -7
  5. package/src/admin/ApiFragmentRegistry/StateChangeSlice/ApiFragmentRegistry.res +65 -0
  6. package/src/admin/ApiFragmentRegistry/StateChangeSlice/ApiFragmentRegistry.res.mjs +87 -0
  7. package/src/admin/ApiFragmentRegistry/StateChangeSlice/ApiFragmentRegistry_Behavior.res +58 -0
  8. package/src/admin/ApiFragmentRegistry/StateChangeSlice/ApiFragmentRegistry_Behavior.res.mjs +135 -0
  9. package/src/admin/ApiFragmentRegistry/StateViewSlice/ApiFragments.res +44 -0
  10. package/src/admin/ApiFragmentRegistry/StateViewSlice/ApiFragments.res.mjs +74 -0
  11. package/src/admin/ApiFragmentRegistry/StateViewSlice/ApiFragments_Projection.res +50 -0
  12. package/src/admin/ApiFragmentRegistry/StateViewSlice/ApiFragments_Projection.res.mjs +80 -0
  13. package/src/admin/Platform_Admin.res +37 -1
  14. package/src/admin/Platform_Admin.res.mjs +18 -5
  15. package/src/admin/Platform_ApiFragmentsApi.res +32 -0
  16. package/src/admin/Platform_ApiFragmentsApi.res.mjs +52 -0
  17. package/src/admin/Platform_UIFragmentsApi.res +1 -1
  18. package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res +50 -0
  19. package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res.mjs +70 -0
  20. package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry_Behavior.res +33 -0
  21. package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry_Behavior.res.mjs +80 -0
  22. package/src/admin/UiFragmentRegistry/StateViewSlice/UiFragments.res +33 -0
  23. package/src/admin/{UIFragmentRegistryReadModelSpec.res.mjs → UiFragmentRegistry/StateViewSlice/UiFragments.res.mjs} +24 -2
  24. package/src/admin/UiFragmentRegistry/StateViewSlice/UiFragments_Projection.res +34 -0
  25. package/src/admin/UiFragmentRegistry/StateViewSlice/UiFragments_Projection.res.mjs +54 -0
  26. package/src/components/Api/Api_Naming.res +1 -1
  27. package/src/components/Api/GraphQL_FragmentGenerator.res +7 -11
  28. package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +8 -22
  29. package/src/components/Api/GraphQL_PushPlanner.res +65 -0
  30. package/src/components/Api/GraphQL_PushPlanner.res.mjs +46 -0
  31. package/src/components/Api/GraphQL_Stitcher.res +72 -1
  32. package/src/components/Api/GraphQL_Stitcher.res.mjs +53 -2
  33. package/src/components/Dcb/Dcb_Builder.res +16 -6
  34. package/src/components/Dcb/Dcb_Builder.res.mjs +10 -7
  35. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +6 -1
  36. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +1 -1
  37. package/src/plugin/api/PluginBaseFragment.res +5 -6
  38. package/src/plugin/api/PluginBaseFragment.res.mjs +0 -1
  39. package/src/plugin/component/Plugin_Builder.res +4 -1
  40. package/src/plugin/component/Plugin_Builder.res.mjs +7 -6
  41. package/src/plugin/component/Plugin_Helpers.res +30 -0
  42. package/src/plugin/component/Plugin_Helpers.res.mjs +13 -3
  43. package/src/plugin/component/Plugin_SubscriptionSchema.res +19 -9
  44. package/src/plugin/component/Plugin_SubscriptionSchema.res.mjs +23 -14
  45. package/src/plugin/connect/PluginConnectExtension_Mapping.res +28 -6
  46. package/src/plugin/connect/PluginConnectExtension_Mapping.res.mjs +34 -14
  47. package/src/plugin/connect/PluginExtensionPoint_Builder.res +7 -1
  48. package/src/plugin/connect/PluginExtensionPoint_Builder.res.mjs +5 -1
  49. package/src/plugin/connect/PluginExtensionPoint_Plugin.res +3 -1
  50. package/src/plugin/connect/PluginExtensionPoint_Plugin.res.mjs +4 -2
  51. package/src/plugin/connect/PluginExtensionPoint_UiFragment.res +52 -0
  52. package/src/plugin/connect/PluginExtensionPoint_UiFragment.res.mjs +74 -0
  53. package/src/plugin/lifecycle/PluginBehavior.res +9 -51
  54. package/src/plugin/lifecycle/PluginBehavior.res.mjs +39 -68
  55. package/src/plugin/lifecycle/PluginSpec.res +0 -18
  56. package/src/plugin/lifecycle/PluginSpec.res.mjs +0 -30
  57. package/src/plugin/lifecycle/PluginsProjection.res +2 -6
  58. package/src/plugin/lifecycle/PluginsProjection.res.mjs +0 -2
  59. package/src/plugin/lifecycle/PluginsReadModelSpec.res +0 -2
  60. package/src/plugin/lifecycle/PluginsReadModelSpec.res.mjs +0 -1
  61. package/tests/admin/Platform_ApiFragmentsApiTest.res +66 -0
  62. package/tests/admin/Platform_ApiFragmentsApiTest.res.mjs +68 -0
  63. package/tests/admin/Platform_UIFragmentsApiTest.res +1 -1
  64. package/tests/api/GraphQL_PushPlannerTest.res +62 -0
  65. package/tests/api/GraphQL_PushPlannerTest.res.mjs +80 -0
  66. package/tests/api/GraphQL_StitcherTest.res +63 -0
  67. package/tests/api/GraphQL_StitcherTest.res.mjs +75 -0
  68. package/tests/logger/LogFormatTest.res +1 -2
  69. package/tests/logger/LogFormatTest.res.mjs +1 -2
  70. package/tests/message/MessageTest.res +0 -3
  71. package/tests/message/MessageTest.res.mjs +0 -3
  72. package/src/admin/UIFragmentRegistryProjection.res +0 -43
  73. package/src/admin/UIFragmentRegistryProjection.res.mjs +0 -81
  74. package/src/admin/UIFragmentRegistryReadModelSpec.res +0 -13
@@ -0,0 +1,80 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
4
+
5
+ function evolve(_state, event) {
6
+ switch (event.TAG) {
7
+ case "UiFragmentRegistered" :
8
+ return event.manifest;
9
+ case "UiFragmentUpdated" :
10
+ return event.newManifest;
11
+ case "UiFragmentDeregistered" :
12
+ return;
13
+ }
14
+ }
15
+
16
+ function decide(state, command) {
17
+ if (command.TAG !== "RegisterUiFragment") {
18
+ if (state !== undefined) {
19
+ return {
20
+ TAG: "Ok",
21
+ _0: [{
22
+ TAG: "UiFragmentDeregistered",
23
+ pluginId: command.pluginId
24
+ }]
25
+ };
26
+ } else {
27
+ return {
28
+ TAG: "Ok",
29
+ _0: []
30
+ };
31
+ }
32
+ }
33
+ let at = command.at;
34
+ let manifest = command.manifest;
35
+ let pluginId = command.pluginId;
36
+ if (state !== undefined) {
37
+ if (Primitive_object.equal(state, manifest)) {
38
+ return {
39
+ TAG: "Ok",
40
+ _0: []
41
+ };
42
+ } else {
43
+ return {
44
+ TAG: "Ok",
45
+ _0: [{
46
+ TAG: "UiFragmentUpdated",
47
+ pluginId: pluginId,
48
+ previousManifest: state,
49
+ newManifest: manifest,
50
+ at: at
51
+ }]
52
+ };
53
+ }
54
+ } else {
55
+ return {
56
+ TAG: "Ok",
57
+ _0: [{
58
+ TAG: "UiFragmentRegistered",
59
+ pluginId: pluginId,
60
+ manifest: manifest,
61
+ at: at
62
+ }]
63
+ };
64
+ }
65
+ }
66
+
67
+ let Spec;
68
+
69
+ let initialState;
70
+
71
+ let moduleUrl = "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry_Behavior.res.mjs";
72
+
73
+ export {
74
+ Spec,
75
+ initialState,
76
+ evolve,
77
+ decide,
78
+ moduleUrl,
79
+ }
80
+ /* No side effect */
@@ -0,0 +1,33 @@
1
+ // UiFragments StateViewSlice — the read side of the platform UI-fragment registry,
2
+ // replacing UIFragmentRegistryReadModelSpec + UIFragmentRegistryProjection. Projects the
3
+ // UiFragmentRegistry slice's events (from the shared admin DcbEventLog) into the
4
+ // manifest-per-plugin table that backs the Platform_UIFragments query. The wire shape is
5
+ // owned by Platform_UIFragmentsApi.res (SDL + encoder), pinned by the golden test
6
+ // Platform_UIFragmentsApiTest.res — `state` here matches that encoder byte-for-byte.
7
+ @@reventless.spec
8
+
9
+ open Reventless.Plugin
10
+
11
+ @schema
12
+ type state = {
13
+ pluginId: string,
14
+ remoteEntryUrl: string,
15
+ panels: array<panelManifestEntry>,
16
+ pages: array<pageManifestEntry>,
17
+ registeredAt: string,
18
+ updatedAt: string,
19
+ }
20
+
21
+ // Consumes the UiFragmentRegistry slice's events (from the shared admin DcbEventLog). Inline
22
+ // records with the same constructors/fields as the producer's `event` — decodes byte-identically
23
+ // and the ppx auto-tags `pluginId`.
24
+ @schema
25
+ type consumedEvent =
26
+ | UiFragmentRegistered({pluginId: string, manifest: uiFragmentManifest, at: string})
27
+ | UiFragmentUpdated({
28
+ pluginId: string,
29
+ previousManifest: uiFragmentManifest,
30
+ newManifest: uiFragmentManifest,
31
+ at: string,
32
+ })
33
+ | UiFragmentDeregistered({pluginId: string})
@@ -1,6 +1,7 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as S from "sury/src/S.res.mjs";
4
+ import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
4
5
  import * as Plugin$Reventless from "@reventlessdev/reventless-spec/src/components/Plugin.res.mjs";
5
6
  import * as ReadModel$Reventless from "@reventlessdev/reventless-spec/src/components/ReadModel.res.mjs";
6
7
 
@@ -13,15 +14,35 @@ let stateSchema = S.schema(s => ({
13
14
  updatedAt: s.m(S.string)
14
15
  }));
15
16
 
17
+ let consumedEventSchema = S.union([
18
+ S.schema(s => ({
19
+ TAG: "UiFragmentRegistered",
20
+ pluginId: s.m(DcbTag$Reventless.string),
21
+ manifest: s.m(Plugin$Reventless.uiFragmentManifestSchema),
22
+ at: s.m(S.string)
23
+ })),
24
+ S.schema(s => ({
25
+ TAG: "UiFragmentUpdated",
26
+ pluginId: s.m(DcbTag$Reventless.string),
27
+ previousManifest: s.m(Plugin$Reventless.uiFragmentManifestSchema),
28
+ newManifest: s.m(Plugin$Reventless.uiFragmentManifestSchema),
29
+ at: s.m(S.string)
30
+ })),
31
+ S.schema(s => ({
32
+ TAG: "UiFragmentDeregistered",
33
+ pluginId: s.m(DcbTag$Reventless.string)
34
+ }))
35
+ ]);
36
+
16
37
  let config = ReadModel$Reventless.config(undefined, undefined, undefined);
17
38
 
18
- let name = "UIFragmentRegistry";
39
+ let name = "UiFragments";
19
40
 
20
41
  let Id;
21
42
 
22
43
  let subIdConfig;
23
44
 
24
- let moduleUrl = "@reventlessdev/reventless-core/src/admin/UIFragmentRegistryReadModelSpec.res.mjs";
45
+ let moduleUrl = "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateViewSlice/UiFragments.res.mjs";
25
46
 
26
47
  let authorization = "AllowAuthenticated";
27
48
 
@@ -31,6 +52,7 @@ export {
31
52
  name,
32
53
  Id,
33
54
  stateSchema,
55
+ consumedEventSchema,
34
56
  config,
35
57
  subIdConfig,
36
58
  moduleUrl,
@@ -0,0 +1,34 @@
1
+ @@reventless.projection
2
+
3
+ // No `open Reventless.Plugin`: it would shadow the state labels (panels/pages/...) brought in
4
+ // by the projection's spec open. `manifest.<field>` resolves type-directed from the event's
5
+ // uiFragmentManifest payload.
6
+ let project = event =>
7
+ switch event {
8
+ | UiFragmentRegistered({pluginId, manifest, at}) => [
9
+ Set(
10
+ pluginId,
11
+ {
12
+ pluginId,
13
+ remoteEntryUrl: manifest.remoteEntryUrl,
14
+ panels: manifest.panels,
15
+ pages: manifest.pages,
16
+ registeredAt: at,
17
+ updatedAt: at,
18
+ },
19
+ ),
20
+ ]
21
+ | UiFragmentUpdated({pluginId, newManifest, at}) => [
22
+ Update(
23
+ pluginId,
24
+ state => {
25
+ ...state,
26
+ remoteEntryUrl: newManifest.remoteEntryUrl,
27
+ panels: newManifest.panels,
28
+ pages: newManifest.pages,
29
+ updatedAt: at,
30
+ },
31
+ ),
32
+ ]
33
+ | UiFragmentDeregistered({pluginId}) => [Delete(pluginId)]
34
+ }
@@ -0,0 +1,54 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+
4
+ function project(event) {
5
+ switch (event.TAG) {
6
+ case "UiFragmentRegistered" :
7
+ let at = event.at;
8
+ let manifest = event.manifest;
9
+ let pluginId = event.pluginId;
10
+ return [{
11
+ TAG: "Set",
12
+ _0: pluginId,
13
+ _1: {
14
+ pluginId: pluginId,
15
+ remoteEntryUrl: manifest.remoteEntryUrl,
16
+ panels: manifest.panels,
17
+ pages: manifest.pages,
18
+ registeredAt: at,
19
+ updatedAt: at
20
+ }
21
+ }];
22
+ case "UiFragmentUpdated" :
23
+ let at$1 = event.at;
24
+ let newManifest = event.newManifest;
25
+ return [{
26
+ TAG: "Update",
27
+ _0: event.pluginId,
28
+ _1: state => ({
29
+ pluginId: state.pluginId,
30
+ remoteEntryUrl: newManifest.remoteEntryUrl,
31
+ panels: newManifest.panels,
32
+ pages: newManifest.pages,
33
+ registeredAt: state.registeredAt,
34
+ updatedAt: at$1
35
+ })
36
+ }];
37
+ case "UiFragmentDeregistered" :
38
+ return [{
39
+ TAG: "Delete",
40
+ _0: event.pluginId
41
+ }];
42
+ }
43
+ }
44
+
45
+ let Spec;
46
+
47
+ let moduleUrl = "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateViewSlice/UiFragments_Projection.res.mjs";
48
+
49
+ export {
50
+ Spec,
51
+ project,
52
+ moduleUrl,
53
+ }
54
+ /* No side effect */
@@ -49,7 +49,7 @@ let sliceMutationField = (~plugin: string, ~slice: string) => `${plugin}_${slice
49
49
  let dcbCommandMutationField = (~plugin: string, ~command: string) => `${plugin}_${command}`
50
50
 
51
51
  // AppSync hard limit: subscription field names may be at most 50 chars. Each mutation
52
- // field `f` produces a `@aws_subscribe` subscription `on${f}` (see
52
+ // field `f` produces a mutation-sourced subscription `on${f}` (see
53
53
  // Plugin_SubscriptionSchema.sourceCFields), so the real cap on a mutation field is 48.
54
54
  // Fail at build with an actionable message rather than letting AppSync reject the
55
55
  // schema push at deploy time with an opaque 500.
@@ -598,15 +598,11 @@ let generate = (
598
598
  commandResultSdlTypes->Array.forEach(t => types->Array.push(t))
599
599
  }
600
600
 
601
- let encoded =
602
- JSON.Encode.object(
603
- Dict.fromArray([
604
- ("types", JSON.Encode.array(types->Array.map(JSON.Encode.string))),
605
- ("mutations", JSON.Encode.array(mutations->Array.map(JSON.Encode.string))),
606
- ("queries", JSON.Encode.array(queries->Array.map(JSON.Encode.string))),
607
- ("subscriptions", JSON.Encode.array([])),
608
- ]),
609
- )->JSON.stringify
610
-
611
- {Reventless.Plugin.encoded, protocol: "graphql"}
601
+ GraphQL_Stitcher.encode({
602
+ types,
603
+ mutations,
604
+ queries,
605
+ subscriptions: [],
606
+ subscriptionSources: [],
607
+ })
612
608
  }
@@ -5,6 +5,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
5
  import * as Stdlib_String from "@rescript/runtime/lib/es6/Stdlib_String.js";
6
6
  import * as SchemaType$ReventlessCore from "./SchemaType.res.mjs";
7
7
  import * as StateAnnotations$Reventless from "@reventlessdev/reventless-spec/src/components/StateAnnotations.res.mjs";
8
+ import * as GraphQL_Stitcher$ReventlessCore from "./GraphQL_Stitcher.res.mjs";
8
9
 
9
10
  let commandResultSdlTypes = [
10
11
  `union CommandResult = CommandAccepted | CommandRejected | CommandPending`,
@@ -453,28 +454,13 @@ function generate(mutationEntries, queryEntries) {
453
454
  types.push(t);
454
455
  });
455
456
  }
456
- let encoded = JSON.stringify(Object.fromEntries([
457
- [
458
- "types",
459
- types.map(prim => prim)
460
- ],
461
- [
462
- "mutations",
463
- mutations.map(prim => prim)
464
- ],
465
- [
466
- "queries",
467
- queries.map(prim => prim)
468
- ],
469
- [
470
- "subscriptions",
471
- []
472
- ]
473
- ]));
474
- return {
475
- encoded: encoded,
476
- protocol: "graphql"
477
- };
457
+ return GraphQL_Stitcher$ReventlessCore.encode({
458
+ types: types,
459
+ mutations: mutations,
460
+ queries: queries,
461
+ subscriptions: [],
462
+ subscriptionSources: []
463
+ });
478
464
  }
479
465
 
480
466
  export {
@@ -0,0 +1,65 @@
1
+ // Generic, provider-neutral push planner for the event-sourced API-schema fragment registry.
2
+ //
3
+ // Given the fragments currently in the ApiFragmentRegistry (each tagged with the API it belongs to)
4
+ // it groups them by target and produces one stitched-SDL push plan per API, honouring split vs.
5
+ // unified mode. It encodes the SAME base-selection rules the deploy-time push
6
+ // (`reventless-aws Platform.preResolversSchemaHook`) applies, so the reactive single writer stays
7
+ // consistent with the deploy path:
8
+ // - split mode: the Platform API carries the admin base + Platform-target fragments; the Domain
9
+ // API carries an EMPTY base + Domain-target fragments (admin lives only on the Platform API).
10
+ // - unified mode: a single API carries the admin base + ALL fragments regardless of target.
11
+ //
12
+ // Neutral SDL only — the AWS adapter injects @aws_subscribe per plan and pushes each to the
13
+ // matching AppSync API id; the local platform (scope 2) will consume the same plans in-process.
14
+
15
+ open Reventless.Plugin
16
+
17
+ type targetedFragment = {
18
+ fragment: apiSchemaFragment,
19
+ target: apiTarget,
20
+ }
21
+
22
+ // The destination API of a push plan. In unified mode every fragment collapses onto `DomainApi`
23
+ // (the single shared API); the AWS adapter maps `PlatformApi` → the Platform AppSync id and
24
+ // `DomainApi` → the Domain AppSync id.
25
+ @schema
26
+ type planTarget = DomainApi | PlatformApi
27
+
28
+ type pushPlan = {
29
+ api: planTarget,
30
+ // Stitched neutral SDL for that API (admin/empty base + the target's fragments).
31
+ sdl: string,
32
+ }
33
+
34
+ // An empty base fragment — the Domain API's base in split mode (admin is on the Platform API).
35
+ let emptyBase: apiSchemaFragment = GraphQL_Stitcher.encode({
36
+ types: [],
37
+ queries: [],
38
+ mutations: [],
39
+ subscriptions: [],
40
+ subscriptionSources: [],
41
+ })
42
+
43
+ let planPushes = (
44
+ ~adminBase: apiSchemaFragment,
45
+ ~fragments: array<targetedFragment>,
46
+ ~splitApi: bool,
47
+ ): array<pushPlan> =>
48
+ if splitApi {
49
+ let platformFrags =
50
+ fragments->Array.filter(f => f.target == Platform)->Array.map(f => f.fragment)
51
+ let domainFrags = fragments->Array.filter(f => f.target == Domain)->Array.map(f => f.fragment)
52
+ [
53
+ {
54
+ api: PlatformApi,
55
+ sdl: GraphQL_Stitcher.stitch(~baseFragment=adminBase, ~pluginFragments=platformFrags),
56
+ },
57
+ {
58
+ api: DomainApi,
59
+ sdl: GraphQL_Stitcher.stitch(~baseFragment=emptyBase, ~pluginFragments=domainFrags),
60
+ },
61
+ ]
62
+ } else {
63
+ let allFrags = fragments->Array.map(f => f.fragment)
64
+ [{api: DomainApi, sdl: GraphQL_Stitcher.stitch(~baseFragment=adminBase, ~pluginFragments=allFrags)}]
65
+ }
@@ -0,0 +1,46 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+ import * as GraphQL_Stitcher$ReventlessCore from "./GraphQL_Stitcher.res.mjs";
5
+
6
+ let planTargetSchema = S.union([
7
+ S.literal("DomainApi"),
8
+ S.literal("PlatformApi")
9
+ ]);
10
+
11
+ let emptyBase = GraphQL_Stitcher$ReventlessCore.encode({
12
+ types: [],
13
+ mutations: [],
14
+ queries: [],
15
+ subscriptions: [],
16
+ subscriptionSources: []
17
+ });
18
+
19
+ function planPushes(adminBase, fragments, splitApi) {
20
+ if (splitApi) {
21
+ let platformFrags = fragments.filter(f => f.target === "Platform").map(f => f.fragment);
22
+ let domainFrags = fragments.filter(f => f.target === "Domain").map(f => f.fragment);
23
+ return [
24
+ {
25
+ api: "PlatformApi",
26
+ sdl: GraphQL_Stitcher$ReventlessCore.stitch(adminBase, platformFrags)
27
+ },
28
+ {
29
+ api: "DomainApi",
30
+ sdl: GraphQL_Stitcher$ReventlessCore.stitch(emptyBase, domainFrags)
31
+ }
32
+ ];
33
+ }
34
+ let allFrags = fragments.map(f => f.fragment);
35
+ return [{
36
+ api: "DomainApi",
37
+ sdl: GraphQL_Stitcher$ReventlessCore.stitch(adminBase, allFrags)
38
+ }];
39
+ }
40
+
41
+ export {
42
+ planTargetSchema,
43
+ emptyBase,
44
+ planPushes,
45
+ }
46
+ /* planTargetSchema Not a pure module */
@@ -4,13 +4,33 @@
4
4
 
5
5
  let log = Logger.fromEnv()
6
6
 
7
+ // Neutral subscription→mutation source mapping. Core emits provider-free
8
+ // subscription SDL; this record carries which mutation(s) feed a subscription
9
+ // field so each provider can wire its own dialect (AppSync appends
10
+ // `@aws_subscribe(mutations: [...])` at push time; the local platform routes
11
+ // its PubSub bridge from the same mapping). Supports many-mutations-to-one
12
+ // fan-in (e.g. onUIFragmentChange ← three Platform_UIFragment* mutations).
13
+ type subscriptionSource = {
14
+ field: string,
15
+ mutations: array<string>,
16
+ }
17
+
7
18
  type fragmentParts = {
8
19
  types: array<string>,
9
20
  mutations: array<string>,
10
21
  queries: array<string>,
11
22
  subscriptions: array<string>,
23
+ subscriptionSources: array<subscriptionSource>,
12
24
  }
13
25
 
26
+ let encodeSubscriptionSource = (source: subscriptionSource): JSON.t =>
27
+ JSON.Encode.object(
28
+ Dict.fromArray([
29
+ ("field", JSON.Encode.string(source.field)),
30
+ ("mutations", JSON.Encode.array(source.mutations->Array.map(JSON.Encode.string))),
31
+ ]),
32
+ )
33
+
14
34
  let encode = (parts: fragmentParts): Reventless.Plugin.apiSchemaFragment => {
15
35
  let encoded =
16
36
  JSON.Encode.object(
@@ -19,6 +39,10 @@ let encode = (parts: fragmentParts): Reventless.Plugin.apiSchemaFragment => {
19
39
  ("mutations", JSON.Encode.array(parts.mutations->Array.map(JSON.Encode.string))),
20
40
  ("queries", JSON.Encode.array(parts.queries->Array.map(JSON.Encode.string))),
21
41
  ("subscriptions", JSON.Encode.array(parts.subscriptions->Array.map(JSON.Encode.string))),
42
+ (
43
+ "subscriptionSources",
44
+ JSON.Encode.array(parts.subscriptionSources->Array.map(encodeSubscriptionSource)),
45
+ ),
22
46
  ]),
23
47
  )->JSON.stringify
24
48
  {Reventless.Plugin.encoded, protocol: "graphql"}
@@ -26,7 +50,7 @@ let encode = (parts: fragmentParts): Reventless.Plugin.apiSchemaFragment => {
26
50
 
27
51
  let decode = (fragment: Reventless.Plugin.apiSchemaFragment): fragmentParts => {
28
52
  switch fragment.encoded->JSON.parseOrThrow->JSON.Decode.object {
29
- | None => {types: [], mutations: [], queries: [], subscriptions: []}
53
+ | None => {types: [], mutations: [], queries: [], subscriptions: [], subscriptionSources: []}
30
54
  | Some(dict) =>
31
55
  let getString = (key): array<string> =>
32
56
  switch dict->Dict.get(key) {
@@ -39,15 +63,62 @@ let decode = (fragment: Reventless.Plugin.apiSchemaFragment): fragmentParts => {
39
63
  )
40
64
  | _ => []
41
65
  }
66
+ // Tolerant of fragments encoded before the field existed — default [].
67
+ let subscriptionSources = switch dict->Dict.get("subscriptionSources") {
68
+ | Some(JSON.Array(arr)) =>
69
+ arr->Array.filterMap(j =>
70
+ j
71
+ ->JSON.Decode.object
72
+ ->Option.flatMap(obj => {
73
+ let field = obj->Dict.get("field")->Option.flatMap(JSON.Decode.string)
74
+ let mutations = switch obj->Dict.get("mutations") {
75
+ | Some(JSON.Array(ms)) =>
76
+ ms->Array.filterMap(m =>
77
+ switch m {
78
+ | JSON.String(s) => Some(s)
79
+ | _ => None
80
+ }
81
+ )
82
+ | _ => []
83
+ }
84
+ field->Option.map(field => {field, mutations})
85
+ })
86
+ )
87
+ | _ => []
88
+ }
42
89
  {
43
90
  types: getString("types"),
44
91
  mutations: getString("mutations"),
45
92
  queries: getString("queries"),
46
93
  subscriptions: getString("subscriptions"),
94
+ subscriptionSources,
47
95
  }
48
96
  }
49
97
  }
50
98
 
99
+ /**
100
+ Union of the subscription→mutation source mappings across a base fragment and
101
+ all plugin fragments, deduped by subscription field name (first wins — matching
102
+ `stitch`'s first-wins field dedupe). Providers use this to decorate or wire the
103
+ STITCHED schema's subscription fields.
104
+ */
105
+ let collectSubscriptionSources = (
106
+ ~baseFragment: Reventless.Plugin.apiSchemaFragment,
107
+ ~pluginFragments: array<Reventless.Plugin.apiSchemaFragment>,
108
+ ): array<subscriptionSource> => {
109
+ let seen: Set.t<string> = Set.make()
110
+ Array.concat([baseFragment], pluginFragments)
111
+ ->Array.flatMap(fragment => decode(fragment).subscriptionSources)
112
+ ->Array.filter(source =>
113
+ if seen->Set.has(source.field) {
114
+ false
115
+ } else {
116
+ seen->Set.add(source.field)
117
+ true
118
+ }
119
+ )
120
+ }
121
+
51
122
  // Extract the first identifier from a SDL field/type string.
52
123
  // e.g. " fieldName(args): Type" → "fieldName"
53
124
  // "type TypeName {" → "TypeName"
@@ -8,6 +8,19 @@ import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
8
8
 
9
9
  let log = Logger$ReventlessCore.fromEnv();
10
10
 
11
+ function encodeSubscriptionSource(source) {
12
+ return Object.fromEntries([
13
+ [
14
+ "field",
15
+ source.field
16
+ ],
17
+ [
18
+ "mutations",
19
+ source.mutations.map(prim => prim)
20
+ ]
21
+ ]);
22
+ }
23
+
11
24
  function encode(parts) {
12
25
  let encoded = JSON.stringify(Object.fromEntries([
13
26
  [
@@ -25,6 +38,10 @@ function encode(parts) {
25
38
  [
26
39
  "subscriptions",
27
40
  parts.subscriptions.map(prim => prim)
41
+ ],
42
+ [
43
+ "subscriptionSources",
44
+ parts.subscriptionSources.map(encodeSubscriptionSource)
28
45
  ]
29
46
  ]));
30
47
  return {
@@ -40,7 +57,8 @@ function decode(fragment) {
40
57
  types: [],
41
58
  mutations: [],
42
59
  queries: [],
43
- subscriptions: []
60
+ subscriptions: [],
61
+ subscriptionSources: []
44
62
  };
45
63
  }
46
64
  let getString = key => {
@@ -59,14 +77,45 @@ function decode(fragment) {
59
77
  return [];
60
78
  }
61
79
  };
80
+ let match = dict["subscriptionSources"];
81
+ let subscriptionSources = match !== undefined ? (
82
+ Array.isArray(match) ? Stdlib_Array.filterMap(match, j => Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(j), obj => {
83
+ let field = Stdlib_Option.flatMap(obj["field"], Stdlib_JSON.Decode.string);
84
+ let match = obj["mutations"];
85
+ let mutations = match !== undefined ? (
86
+ Array.isArray(match) ? Stdlib_Array.filterMap(match, m => {
87
+ if (typeof m === "string") {
88
+ return m;
89
+ }
90
+ }) : []
91
+ ) : [];
92
+ return Stdlib_Option.map(field, field => ({
93
+ field: field,
94
+ mutations: mutations
95
+ }));
96
+ })) : []
97
+ ) : [];
62
98
  return {
63
99
  types: getString("types"),
64
100
  mutations: getString("mutations"),
65
101
  queries: getString("queries"),
66
- subscriptions: getString("subscriptions")
102
+ subscriptions: getString("subscriptions"),
103
+ subscriptionSources: subscriptionSources
67
104
  };
68
105
  }
69
106
 
107
+ function collectSubscriptionSources(baseFragment, pluginFragments) {
108
+ let seen = new Set();
109
+ return [baseFragment].concat(pluginFragments).flatMap(fragment => decode(fragment).subscriptionSources).filter(source => {
110
+ if (seen.has(source.field)) {
111
+ return false;
112
+ } else {
113
+ seen.add(source.field);
114
+ return true;
115
+ }
116
+ });
117
+ }
118
+
70
119
  function extractLeadingName(str) {
71
120
  let trimmed = str.trim();
72
121
  let afterType = trimmed.startsWith("type ") || trimmed.startsWith("enum ") ? trimmed.slice(5, trimmed.length).trim() : (
@@ -252,8 +301,10 @@ function isCatastrophicSchemaShrink(currentSdl, newSdl, threshold) {
252
301
 
253
302
  export {
254
303
  log,
304
+ encodeSubscriptionSource,
255
305
  encode,
256
306
  decode,
307
+ collectSubscriptionSources,
257
308
  extractLeadingName,
258
309
  relayBaseTypes,
259
310
  relayBaseQueries,