@reventlessdev/reventless-core 3.0.0-alpha.155 → 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 (75) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +49 -150
  3. package/package.json +13 -13
  4. package/src/admin/AdminApi.res +80 -10
  5. package/src/admin/AdminApi.res.mjs +45 -7
  6. package/src/admin/ApiFragmentRegistry/StateChangeSlice/ApiFragmentRegistry.res +65 -0
  7. package/src/admin/ApiFragmentRegistry/StateChangeSlice/ApiFragmentRegistry.res.mjs +87 -0
  8. package/src/admin/ApiFragmentRegistry/StateChangeSlice/ApiFragmentRegistry_Behavior.res +58 -0
  9. package/src/admin/ApiFragmentRegistry/StateChangeSlice/ApiFragmentRegistry_Behavior.res.mjs +135 -0
  10. package/src/admin/ApiFragmentRegistry/StateViewSlice/ApiFragments.res +44 -0
  11. package/src/admin/ApiFragmentRegistry/StateViewSlice/ApiFragments.res.mjs +74 -0
  12. package/src/admin/ApiFragmentRegistry/StateViewSlice/ApiFragments_Projection.res +50 -0
  13. package/src/admin/ApiFragmentRegistry/StateViewSlice/ApiFragments_Projection.res.mjs +80 -0
  14. package/src/admin/Platform_Admin.res +37 -1
  15. package/src/admin/Platform_Admin.res.mjs +18 -5
  16. package/src/admin/Platform_ApiFragmentsApi.res +32 -0
  17. package/src/admin/Platform_ApiFragmentsApi.res.mjs +52 -0
  18. package/src/admin/Platform_UIFragmentsApi.res +1 -1
  19. package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res +50 -0
  20. package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res.mjs +70 -0
  21. package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry_Behavior.res +33 -0
  22. package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry_Behavior.res.mjs +80 -0
  23. package/src/admin/UiFragmentRegistry/StateViewSlice/UiFragments.res +33 -0
  24. package/src/admin/{UIFragmentRegistryReadModelSpec.res.mjs → UiFragmentRegistry/StateViewSlice/UiFragments.res.mjs} +24 -2
  25. package/src/admin/UiFragmentRegistry/StateViewSlice/UiFragments_Projection.res +34 -0
  26. package/src/admin/UiFragmentRegistry/StateViewSlice/UiFragments_Projection.res.mjs +54 -0
  27. package/src/components/Api/Api_Naming.res +1 -1
  28. package/src/components/Api/GraphQL_FragmentGenerator.res +7 -11
  29. package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +8 -22
  30. package/src/components/Api/GraphQL_PushPlanner.res +65 -0
  31. package/src/components/Api/GraphQL_PushPlanner.res.mjs +46 -0
  32. package/src/components/Api/GraphQL_Stitcher.res +72 -1
  33. package/src/components/Api/GraphQL_Stitcher.res.mjs +53 -2
  34. package/src/components/Dcb/Dcb_Builder.res +16 -6
  35. package/src/components/Dcb/Dcb_Builder.res.mjs +10 -7
  36. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +6 -1
  37. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +1 -1
  38. package/src/plugin/api/PluginBaseFragment.res +5 -6
  39. package/src/plugin/api/PluginBaseFragment.res.mjs +0 -1
  40. package/src/plugin/component/Plugin_Builder.res +4 -1
  41. package/src/plugin/component/Plugin_Builder.res.mjs +7 -6
  42. package/src/plugin/component/Plugin_Helpers.res +30 -0
  43. package/src/plugin/component/Plugin_Helpers.res.mjs +13 -3
  44. package/src/plugin/component/Plugin_SubscriptionSchema.res +19 -9
  45. package/src/plugin/component/Plugin_SubscriptionSchema.res.mjs +23 -14
  46. package/src/plugin/connect/PluginConnectExtension_Mapping.res +28 -6
  47. package/src/plugin/connect/PluginConnectExtension_Mapping.res.mjs +34 -14
  48. package/src/plugin/connect/PluginExtensionPoint_Builder.res +7 -1
  49. package/src/plugin/connect/PluginExtensionPoint_Builder.res.mjs +5 -1
  50. package/src/plugin/connect/PluginExtensionPoint_Plugin.res +3 -1
  51. package/src/plugin/connect/PluginExtensionPoint_Plugin.res.mjs +4 -2
  52. package/src/plugin/connect/PluginExtensionPoint_UiFragment.res +52 -0
  53. package/src/plugin/connect/PluginExtensionPoint_UiFragment.res.mjs +74 -0
  54. package/src/plugin/lifecycle/PluginBehavior.res +9 -51
  55. package/src/plugin/lifecycle/PluginBehavior.res.mjs +39 -68
  56. package/src/plugin/lifecycle/PluginSpec.res +0 -18
  57. package/src/plugin/lifecycle/PluginSpec.res.mjs +0 -30
  58. package/src/plugin/lifecycle/PluginsProjection.res +2 -6
  59. package/src/plugin/lifecycle/PluginsProjection.res.mjs +0 -2
  60. package/src/plugin/lifecycle/PluginsReadModelSpec.res +0 -2
  61. package/src/plugin/lifecycle/PluginsReadModelSpec.res.mjs +0 -1
  62. package/tests/admin/Platform_ApiFragmentsApiTest.res +66 -0
  63. package/tests/admin/Platform_ApiFragmentsApiTest.res.mjs +68 -0
  64. package/tests/admin/Platform_UIFragmentsApiTest.res +1 -1
  65. package/tests/api/GraphQL_PushPlannerTest.res +62 -0
  66. package/tests/api/GraphQL_PushPlannerTest.res.mjs +80 -0
  67. package/tests/api/GraphQL_StitcherTest.res +63 -0
  68. package/tests/api/GraphQL_StitcherTest.res.mjs +75 -0
  69. package/tests/logger/LogFormatTest.res +1 -2
  70. package/tests/logger/LogFormatTest.res.mjs +1 -2
  71. package/tests/message/MessageTest.res +0 -3
  72. package/tests/message/MessageTest.res.mjs +0 -3
  73. package/src/admin/UIFragmentRegistryProjection.res +0 -43
  74. package/src/admin/UIFragmentRegistryProjection.res.mjs +0 -81
  75. package/src/admin/UIFragmentRegistryReadModelSpec.res +0 -13
@@ -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,
@@ -68,6 +68,15 @@ module Make = (
68
68
  let construct = (
69
69
  ~name: string,
70
70
  ~childName: string,
71
+ // API mutation-field prefix for a slice's GraphQL command fields. Defaults to `name`
72
+ // (`${plugin}_${command}`), so plugins are unchanged. The admin builder passes "Platform"
73
+ // so its exposed slice commands render as `Platform_RegisterApiFragment` — byte-equal to
74
+ // `Api_Naming.adminField("RegisterApiFragment")`, keeping the hand-declared admin SDL and
75
+ // the auto-bound DCB resolver field names aligned.
76
+ ~apiNamePrefix: string=name,
77
+ // True for the platform admin build — routes the DCB mutation resolvers to the Platform API
78
+ // (via the provider hook's `hooks.adminApi`) in split mode. Plugins leave it false.
79
+ ~onAdminApi: bool=false,
71
80
  ~environment: string="",
72
81
  ~platformName: string="",
73
82
  ~aggregateEventTopics: EventTopic.allOutputs=Dict.make(),
@@ -411,7 +420,7 @@ module Make = (
411
420
  registerResolver(
412
421
  ~kind=Dcb,
413
422
  ~fields=Api_Naming.sliceMutationFields(
414
- ~plugin=name,
423
+ ~plugin=apiNamePrefix,
415
424
  ~slice=S.Spec.name,
416
425
  ~commandSchema,
417
426
  )->Array.map(((f, _)) => f),
@@ -428,7 +437,7 @@ module Make = (
428
437
  registerResolver(
429
438
  ~kind=Dcb,
430
439
  ~fields=Api_Naming.sliceMutationFields(
431
- ~plugin=name,
440
+ ~plugin=apiNamePrefix,
432
441
  ~slice=S.Spec.name,
433
442
  ~commandSchema,
434
443
  )->Array.map(((f, _)) => f),
@@ -464,7 +473,7 @@ module Make = (
464
473
  ~componentKind=CommandGenerator_Callback.StateChangeSlice,
465
474
  ~stripIdFromParams=false,
466
475
  )
467
- Api_Naming.sliceMutationFields(~plugin=name, ~slice=S.Spec.name, ~commandSchema)
476
+ Api_Naming.sliceMutationFields(~plugin=apiNamePrefix, ~slice=S.Spec.name, ~commandSchema)
468
477
  ->Array.forEach(((fieldName, _)) => bindHandler(~field=fieldName, ~generateCommand))
469
478
  }
470
479
  })
@@ -491,7 +500,7 @@ module Make = (
491
500
  ~componentKind=CommandGenerator_Callback.StateChangeSlice,
492
501
  ~stripIdFromParams=false,
493
502
  )
494
- Api_Naming.sliceMutationFields(~plugin=name, ~slice=S.Spec.name, ~commandSchema)
503
+ Api_Naming.sliceMutationFields(~plugin=apiNamePrefix, ~slice=S.Spec.name, ~commandSchema)
495
504
  ->Array.forEach(((fieldName, _)) => bindHandler(~field=fieldName, ~generateCommand))
496
505
  }
497
506
  })
@@ -730,7 +739,7 @@ module Make = (
730
739
  if ApiNoApiHelpers.isNoApi(commandSchema) {
731
740
  []
732
741
  } else {
733
- Api_Naming.sliceMutationFields(~plugin=name, ~slice=S.Spec.name, ~commandSchema)
742
+ Api_Naming.sliceMutationFields(~plugin=apiNamePrefix, ~slice=S.Spec.name, ~commandSchema)
734
743
  }
735
744
  })
736
745
  let dcbFieldNames = dcbMutationData->Array.map(((f, _)) => f)
@@ -759,6 +768,7 @@ module Make = (
759
768
  runtime: runtime->Obj.magic,
760
769
  fieldNames: dcbFieldNames,
761
770
  tags: dcbTags,
771
+ onAdminApi,
762
772
  opts,
763
773
  })
764
774
  | None => ()
@@ -825,7 +835,7 @@ module Make = (
825
835
  ) => {
826
836
  let commandSchema = S.Spec.commandSchema->Reventless.DcbTag.toUnknownSchema
827
837
  let fieldSpecs =
828
- Api_Naming.sliceMutationFields(~plugin=name, ~slice=S.Spec.name, ~commandSchema)
838
+ Api_Naming.sliceMutationFields(~plugin=apiNamePrefix, ~slice=S.Spec.name, ~commandSchema)
829
839
  if ApiNoApiHelpers.isNoApi(commandSchema) || fieldSpecs->Array.length == 0 {
830
840
  None
831
841
  } else {
@@ -78,7 +78,9 @@ let emptyResult = {
78
78
 
79
79
  function Make(DcbEventLogStorage) {
80
80
  return DcbEventTopicPublisher => (DcbCommandTopicChannel => (DcbCommandTopicChannelAsync => (RuntimeBuilder => (HooksConfig => {
81
- let construct = (name, childName, environmentOpt, platformNameOpt, aggregateEventTopicsOpt, stateChangeSlices, stateViewSlices, automationSlices, outboundTranslationSlices, inboundTranslationSlices, systemCallableComponentsOpt, pluginStructure, opts) => {
81
+ let construct = (name, childName, apiNamePrefixOpt, onAdminApiOpt, environmentOpt, platformNameOpt, aggregateEventTopicsOpt, stateChangeSlices, stateViewSlices, automationSlices, outboundTranslationSlices, inboundTranslationSlices, systemCallableComponentsOpt, pluginStructure, opts) => {
82
+ let apiNamePrefix = apiNamePrefixOpt !== undefined ? apiNamePrefixOpt : name;
83
+ let onAdminApi = onAdminApiOpt !== undefined ? onAdminApiOpt : false;
82
84
  let environment = environmentOpt !== undefined ? environmentOpt : "";
83
85
  let platformName = platformNameOpt !== undefined ? platformNameOpt : "";
84
86
  let aggregateEventTopics = aggregateEventTopicsOpt !== undefined ? aggregateEventTopicsOpt : ({});
@@ -225,13 +227,13 @@ function Make(DcbEventLogStorage) {
225
227
  syncSlices.forEach(S => {
226
228
  let commandSchema = S.Spec.commandSchema;
227
229
  if (!ApiNoApiHelpers$ReventlessCore.isNoApi(commandSchema)) {
228
- return registerResolver("Dcb", Api_Naming$ReventlessCore.sliceMutationFields(name, S.Spec.name, commandSchema).map(param => param[0]), commandSchema, S.Spec.commandAuthorization);
230
+ return registerResolver("Dcb", Api_Naming$ReventlessCore.sliceMutationFields(apiNamePrefix, S.Spec.name, commandSchema).map(param => param[0]), commandSchema, S.Spec.commandAuthorization);
229
231
  }
230
232
  });
231
233
  asyncSlices.forEach(S => {
232
234
  let commandSchema = S.Spec.commandSchema;
233
235
  if (!ApiNoApiHelpers$ReventlessCore.isNoApi(commandSchema)) {
234
- return registerResolver("Dcb", Api_Naming$ReventlessCore.sliceMutationFields(name, S.Spec.name, commandSchema).map(param => param[0]), commandSchema, S.Spec.commandAuthorization);
236
+ return registerResolver("Dcb", Api_Naming$ReventlessCore.sliceMutationFields(apiNamePrefix, S.Spec.name, commandSchema).map(param => param[0]), commandSchema, S.Spec.commandAuthorization);
235
237
  }
236
238
  });
237
239
  }
@@ -244,7 +246,7 @@ function Make(DcbEventLogStorage) {
244
246
  return;
245
247
  }
246
248
  let generateCommand = CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(ops.publishJsons, ops.publishJsonsAndWait, S.Spec.name, S.Spec.commandSchema, "StateChangeSlice", false);
247
- Api_Naming$ReventlessCore.sliceMutationFields(name, S.Spec.name, commandSchema).forEach(param => bindHandler(param[0], generateCommand));
249
+ Api_Naming$ReventlessCore.sliceMutationFields(apiNamePrefix, S.Spec.name, commandSchema).forEach(param => bindHandler(param[0], generateCommand));
248
250
  });
249
251
  });
250
252
  if (asyncDcbCommandTopicOpt !== undefined) {
@@ -255,7 +257,7 @@ function Make(DcbEventLogStorage) {
255
257
  return;
256
258
  }
257
259
  let generateCommand = CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(asyncOps.publishJsons, asyncOps.publishJsonsAndWait, S.Spec.name, S.Spec.commandSchema, "StateChangeSlice", false);
258
- Api_Naming$ReventlessCore.sliceMutationFields(name, S.Spec.name, commandSchema).forEach(param => bindHandler(param[0], generateCommand));
260
+ Api_Naming$ReventlessCore.sliceMutationFields(apiNamePrefix, S.Spec.name, commandSchema).forEach(param => bindHandler(param[0], generateCommand));
259
261
  });
260
262
  });
261
263
  }
@@ -397,7 +399,7 @@ function Make(DcbEventLogStorage) {
397
399
  if (ApiNoApiHelpers$ReventlessCore.isNoApi(commandSchema)) {
398
400
  return [];
399
401
  } else {
400
- return Api_Naming$ReventlessCore.sliceMutationFields(name, S.Spec.name, commandSchema);
402
+ return Api_Naming$ReventlessCore.sliceMutationFields(apiNamePrefix, S.Spec.name, commandSchema);
401
403
  }
402
404
  });
403
405
  let dcbFieldNames = dcbMutationData.map(param => param[0]);
@@ -424,6 +426,7 @@ function Make(DcbEventLogStorage) {
424
426
  runtime: runtime,
425
427
  fieldNames: dcbFieldNames,
426
428
  tags: dcbTags,
429
+ onAdminApi: onAdminApi,
427
430
  opts: opts
428
431
  });
429
432
  }
@@ -451,7 +454,7 @@ function Make(DcbEventLogStorage) {
451
454
  };
452
455
  let mutationEntriesFromSlices = Stdlib_Array.filterMap(stateChangeSlices, S => {
453
456
  let commandSchema = S.Spec.commandSchema;
454
- let fieldSpecs = Api_Naming$ReventlessCore.sliceMutationFields(name, S.Spec.name, commandSchema);
457
+ let fieldSpecs = Api_Naming$ReventlessCore.sliceMutationFields(apiNamePrefix, S.Spec.name, commandSchema);
455
458
  if (ApiNoApiHelpers$ReventlessCore.isNoApi(commandSchema) || fieldSpecs.length === 0) {
456
459
  return;
457
460
  }
@@ -34,7 +34,12 @@ module Make = (
34
34
  ~tagKeysByEventType: Dict.t<array<string>>,
35
35
  event: Spec.event,
36
36
  ): ReventlessInfra.DcbEventLog.rawEvent => {
37
- let json = event->JSON.stringifyAny->Option.getOrThrow->JSON.parseOrThrow
37
+ // Encode through the event schema (like the Aggregate path's Message.encode),
38
+ // NOT JSON.stringifyAny: the runtime representation drops `None` option
39
+ // fields entirely, while the consumer side (DcbDecode) parses with the sury
40
+ // schema — a js_nullable option field would reject the missing key and the
41
+ // event would be dropped as schema drift.
42
+ let json = event->S.reverseConvertToJsonOrThrow(Spec.eventSchema)
38
43
  let (eventType, data) = json->Message.splitMessage
39
44
  // Use `extractTagsExpanded` (not `extractTags`) so per-element tags on
40
45
  // `array<string>` fields are emitted — e.g. OrderPlaced's
@@ -182,7 +182,7 @@ function Make(Spec) {
182
182
  }
183
183
  let rawEvents = newEvents$1.map(e => {
184
184
  let parentMeta = command$p.meta;
185
- let json = JSON.parse(Stdlib_Option.getOrThrow(JSON.stringify(e), undefined));
185
+ let json = S.reverseConvertToJsonOrThrow(e, Spec.eventSchema);
186
186
  let match = Message$ReventlessCore.splitMessage(json);
187
187
  let eventType = match[0];
188
188
  let baseTags = DcbTag$Reventless.extractTagsExpanded(Spec.eventSchema, e);
@@ -18,13 +18,12 @@ let pluginExcludeFields: array<string> = [
18
18
 
19
19
  // Additional fields the SDL exposes but the Plugin list view should NOT
20
20
  // surface — they're option-of-nested-object types (e.g. apiSchemaFragment,
21
- // uiFragments, structure) which AutoUI currently renders as scalar columns
21
+ // structure) which AutoUI currently renders as scalar columns
22
22
  // and queries without a sub-selection, failing schema validation. Other
23
- // callers (host-shell's Platform_ComponentDefinitions / Platform_UIFragments)
24
- // keep querying them via dedicated fields and resolver paths.
23
+ // callers (host-shell's Platform_ComponentDefinitions) keep querying them via
24
+ // dedicated fields and resolver paths.
25
25
  let pluginUIOnlyExcludeFields: array<string> = pluginExcludeFields->Array.concat([
26
26
  "apiSchemaFragment",
27
- "uiFragments",
28
27
  "structure",
29
28
  ])
30
29
 
@@ -40,10 +39,10 @@ let pluginUIOnlyExcludeFields: array<string> = pluginExcludeFields->Array.concat
40
39
  let indexQueriesOfConfig = (config: Reventless.ReadModel.config) =>
41
40
  config.indexes->Array.length > 0 ? Some(config.indexes) : None
42
41
 
43
- // The UIFragmentRegistry read model is queried exclusively via the explicit
42
+ // The UiFragments StateViewSlice is queried exclusively via the explicit
44
43
  // flat `Platform_UIFragments: [Platform_UIFragmentEntry!]!` field declared in
45
44
  // `Platform_UIFragmentsApi.res` (and resolved by `Platform_UIFragments_Lambda.res`
46
- // on AWS / the seeded QueryDb in-memory). Reintroducing an auto-generated
45
+ // on AWS / the slice QueryDb in-memory). Reintroducing an auto-generated
47
46
  // connection field here collides with that name on SDL composition.
48
47
  let queryEntries: array<querySchemaEntry> = [
49
48
  {
@@ -20,7 +20,6 @@ let pluginExcludeFields = [
20
20
 
21
21
  let pluginUIOnlyExcludeFields = pluginExcludeFields.concat([
22
22
  "apiSchemaFragment",
23
- "uiFragments",
24
23
  "structure"
25
24
  ]);
26
25
 
@@ -295,6 +295,7 @@ module Make = (
295
295
  ...parts,
296
296
  types: Array.concat(parts.types, subResult.extraTypes),
297
297
  subscriptions: subResult.subscriptionFields,
298
+ subscriptionSources: subResult.subscriptionSources,
298
299
  })
299
300
  }
300
301
 
@@ -750,7 +751,6 @@ module Make = (
750
751
  extensionProtocols: [],
751
752
  apiSchemaFragment: Some(apiSchemaFragment),
752
753
  apiTarget: Some(capturedDeployTarget),
753
- uiFragments,
754
754
  structure: pluginStructure,
755
755
  dcbEventLog: dcbEventLogDef,
756
756
  // Business role from the deploy-time metadata registry (set via
@@ -807,6 +807,7 @@ module Make = (
807
807
  connectPluginExtensionIncomingEventHandler,
808
808
  ) = createConnectPluginExtension(
809
809
  ~pluginDefinition,
810
+ ~uiFragments,
810
811
  ~publishToPluginExtensionPoint,
811
812
  ~publishToAggregates,
812
813
  ~readModelNamesForSourceName,
@@ -821,6 +822,7 @@ module Make = (
821
822
  ~extensionsOutputs,
822
823
  ~pluginExtensionPointUnwrapped,
823
824
  ~pluginDefinition,
825
+ ~uiFragments,
824
826
  ~connectPluginExtensionIncomingEventHandler,
825
827
  ~extensionsHandlers,
826
828
  ~extensionPointsHandlers,
@@ -840,6 +842,7 @@ module Make = (
840
842
  ~extensionPointsOutputs,
841
843
  ~extensionsOutputs,
842
844
  ~pluginDefinition,
845
+ ~uiFragments,
843
846
  ~extensionsHandlers,
844
847
  ~extensionPointsHandlers,
845
848
  ~extensionRegistryInfos,
@@ -6,6 +6,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
6
  import * as Pulumi from "@pulumi/pulumi";
7
7
  import * as Belt_SetString from "@rescript/runtime/lib/es6/Belt_SetString.js";
8
8
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
9
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
9
10
  import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
10
11
  import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
11
12
  import * as Plugin$ReventlessCore from "./Plugin.res.mjs";
@@ -146,7 +147,7 @@ function Make(Spec) {
146
147
  let DcbBuilder = Dcb_Builder$ReventlessCore.Make(DcbEventLogStorage)(DcbEventTopicPublisher)(DcbCommandTopicChannel)(DcbCommandTopicChannelAsync)(PluginRuntimeBuilder)({
147
148
  hooks: Spec.hooks
148
149
  });
149
- let dcbResult = DcbBuilder.construct(extra$1, childName, Spec.environment, Spec.platformName, aggregateEventTopics, stateChangeSlices, stateViewSlices, automationSlices, outboundTranslationSlices, inboundTranslationSlices, systemCallableComponents, pluginStructure, opts);
150
+ let dcbResult = DcbBuilder.construct(extra$1, childName, undefined, undefined, Spec.environment, Spec.platformName, aggregateEventTopics, stateChangeSlices, stateViewSlices, automationSlices, outboundTranslationSlices, inboundTranslationSlices, systemCallableComponents, pluginStructure, opts);
150
151
  let mutationEntriesFromAggregates = aggregates.flatMap(M => {
151
152
  let commandSchema = M.Spec.commandSchema;
152
153
  if (ApiNoApiHelpers$ReventlessCore.isNoApi(commandSchema)) {
@@ -227,7 +228,8 @@ function Make(Spec) {
227
228
  types: parts.types.concat(subResult.extraTypes),
228
229
  mutations: parts.mutations,
229
230
  queries: parts.queries,
230
- subscriptions: subResult.subscriptionFields
231
+ subscriptions: subResult.subscriptionFields,
232
+ subscriptionSources: subResult.subscriptionSources
231
233
  });
232
234
  Stdlib_Option.forEach(Spec.hooks.schemaTypeRegistrationHook, registerTypes => {
233
235
  let parts = GraphQL_Stitcher$ReventlessCore.decode(apiSchemaFragment);
@@ -496,7 +498,6 @@ function Make(Spec) {
496
498
  extensionProtocols: [],
497
499
  apiSchemaFragment: apiSchemaFragment,
498
500
  apiTarget: capturedDeployTarget,
499
- uiFragments: uiFragments,
500
501
  structure: pluginStructure,
501
502
  dcbEventLog: param[2],
502
503
  kind: Stdlib_Option.getOr(Stdlib_Option.flatMap(Plugin_Helpers$ReventlessCore.pluginMetadataRegistry.contents, m => m.kind), "Domain")
@@ -528,10 +529,10 @@ function Make(Spec) {
528
529
  readModelQueueUrls[param[0]] = urlOutput;
529
530
  });
530
531
  if (coreSetup !== undefined) {
531
- let match$4 = Plugin_Helpers$ReventlessCore.createConnectPluginExtension(pluginDefinition, publishToPluginExtensionPoint, publishToAggregates, Plugin_Helpers$ReventlessCore.readModelNamesForSourceName, publishToReadModels, queryEngine, opts);
532
- EventCollectorHelper.connect(eventCollector, eventTopics, extensionPointsOutputs, extensionsOutputs, coreSetup[0], pluginDefinition, match$4[1], extensionsHandlers, extensionPointsHandlers, match$4[0], extensionRegistryInfos, match$1[2], aggregateQueueUrls, readModelQueueUrls, Plugin_Helpers$ReventlessCore.readModelNamesForSourceName);
532
+ let match$4 = Plugin_Helpers$ReventlessCore.createConnectPluginExtension(pluginDefinition, uiFragments, publishToPluginExtensionPoint, publishToAggregates, Plugin_Helpers$ReventlessCore.readModelNamesForSourceName, publishToReadModels, queryEngine, opts);
533
+ EventCollectorHelper.connect(eventCollector, eventTopics, extensionPointsOutputs, extensionsOutputs, coreSetup[0], pluginDefinition, Primitive_option.some(uiFragments), match$4[1], extensionsHandlers, extensionPointsHandlers, match$4[0], extensionRegistryInfos, match$1[2], aggregateQueueUrls, readModelQueueUrls, Plugin_Helpers$ReventlessCore.readModelNamesForSourceName);
533
534
  } else {
534
- EventCollectorHelper.connect(eventCollector, eventTopics, extensionPointsOutputs, extensionsOutputs, undefined, pluginDefinition, undefined, extensionsHandlers, extensionPointsHandlers, undefined, extensionRegistryInfos, undefined, aggregateQueueUrls, readModelQueueUrls, Plugin_Helpers$ReventlessCore.readModelNamesForSourceName);
535
+ EventCollectorHelper.connect(eventCollector, eventTopics, extensionPointsOutputs, extensionsOutputs, undefined, pluginDefinition, Primitive_option.some(uiFragments), undefined, extensionsHandlers, extensionPointsHandlers, undefined, extensionRegistryInfos, undefined, aggregateQueueUrls, readModelQueueUrls, Plugin_Helpers$ReventlessCore.readModelNamesForSourceName);
535
536
  }
536
537
  let tasksOutputs = Plugin_Helpers$ReventlessCore.createTasks(tasks, aggregatesOutputs, scheduler, schedulerRoleUrn, publishToAggregates, queryEngine, Spec.resourceNaming, opts);
537
538
  let resolvers = Plugin_Helpers$ReventlessCore.createResolvers(allQueryDbs);
@@ -146,6 +146,12 @@ type eventCollectorContext = {
146
146
  // Pre-serialised JSON of the pluginDefinition (sury-encoded then JSON.stringify).
147
147
  // Output because it's assembled from resolved component outputs at deploy time.
148
148
  pluginDefinitionJson: Pulumi.Output.t<string>,
149
+ // Pre-serialised JSON of the plugin's UI-fragment manifest ("null" when the
150
+ // plugin ships none). Shipped as its own asset next to pluginDefinition.json —
151
+ // the manifest no longer rides pluginDefinition (the UiFragmentRegistry slice
152
+ // owns fragment state); the bundled Connect extension needs it at cold start
153
+ // to emit RegisterUiFragment in the handshake answer.
154
+ uiFragmentsJson: Pulumi.Output.t<string>,
149
155
  // Outgoing EPs whose events this Lambda's mapping handles. Admin Lambda: 1
150
156
  // entry (its own Plugin EP). Plugin Lambda: 0 (plugins don't process their own
151
157
  // EP outgoing events here).
@@ -368,6 +374,7 @@ let extractExtensionDefinitions = (extensionsOutputs: array<Extension.outputs>)
368
374
 
369
375
  let createConnectPluginExtension = (
370
376
  ~pluginDefinition,
377
+ ~uiFragments: option<Reventless.Plugin.uiFragmentManifest>,
371
378
  ~publishToPluginExtensionPoint,
372
379
  ~publishToAggregates,
373
380
  ~readModelNamesForSourceName,
@@ -379,6 +386,7 @@ let createConnectPluginExtension = (
379
386
  ->Pulumi.Output.apply(pluginDefinition => {
380
387
  module ConnectPluginExtension = PluginConnectExtension_Builder.Make({
381
388
  let pluginDefinition = pluginDefinition
389
+ let uiFragments = uiFragments
382
390
  })
383
391
  let connectPluginExtension = ConnectPluginExtension.make(
384
392
  ~publishToPluginExtensionPoint,
@@ -472,6 +480,10 @@ module MakeEventCollectorHelper = (
472
480
  ~extensionsOutputs: array<Extension.outputs>,
473
481
  ~pluginExtensionPointUnwrapped: option<ReventlessInterop.ExtensionPoint.resolvedOutputs>=?,
474
482
  ~pluginDefinition,
483
+ // The plugin's UI-fragment manifest — serialised into the per-plugin
484
+ // eventCollectorContext (uiFragmentsJson) so the AWS adapter ships it as a
485
+ // uiFragments.json asset for the bundled Connect extension's cold start.
486
+ ~uiFragments: option<Reventless.Plugin.uiFragmentManifest>=None,
475
487
  ~connectPluginExtensionIncomingEventHandler: option<Pulumi.Output.t<Pulumi.Output.t<Plugin_Callback.jsonEventsHandler>>>=?,
476
488
  ~extensionsHandlers,
477
489
  ~extensionPointsHandlers,
@@ -589,6 +601,11 @@ module MakeEventCollectorHelper = (
589
601
  ->S.reverseConvertToJsonOrThrow(Reventless.Plugin.pluginDefinitionSchema)
590
602
  ->JSON.stringify
591
603
  ->Pulumi.Output.make
604
+ let uiFragmentsJson =
605
+ uiFragments
606
+ ->S.reverseConvertToJsonOrThrow(Reventless.Plugin.uiFragmentManifestOptionSchema)
607
+ ->JSON.stringify
608
+ ->Pulumi.Output.make
592
609
  let pluginExtensionPointCmdTopicUrl = switch pluginExtensionPointUnwrapped {
593
610
  | Some(unwrapped) =>
594
611
  switch unwrapped.commandTopic.resources->Array.get(0) {
@@ -680,6 +697,7 @@ module MakeEventCollectorHelper = (
680
697
  ~componentName=ecName,
681
698
  ~context={
682
699
  pluginDefinitionJson,
700
+ uiFragmentsJson,
683
701
  extensionPoints: extensionPointEntries,
684
702
  connectExtension,
685
703
  extensions,
@@ -829,6 +847,11 @@ type dcbAppSyncResolverParams = {
829
847
  runtime: Runtime.environment<unknown>,
830
848
  fieldNames: array<string>,
831
849
  tags: array<string>,
850
+ // True when the slice belongs to the platform admin build (Platform_Admin.construct). In split
851
+ // mode admin resolvers live on the Platform API, so the provider hook binds them to `hooks.adminApi`
852
+ // instead of `hooks.api` (which is the deploy-target/Domain API and is read in the same deferred
853
+ // callback by plugin builds). See platformHooks.adminApi.
854
+ onAdminApi: bool,
832
855
  opts: Pulumi.ComponentResource.options,
833
856
  }
834
857
 
@@ -952,6 +975,12 @@ type platformHooks = {
952
975
  schedulerRoleUrn: ref<Pulumi.Output.t<string>>,
953
976
  api: ref<option<hookedValue<unknown>>>,
954
977
  apiRole: ref<option<hookedValue<unknown>>>,
978
+ // The API admin resolvers bind to (split mode: the Platform API; unified: same as `api`).
979
+ // Populated by makePlatform once the Platform API resource exists, and read — via the
980
+ // `onAdminApi` payload flag — by the DCB resolver hook so admin DCB mutations land on the
981
+ // Platform API even though `api` (read in the same deferred callback by plugin builds) points at
982
+ // the Domain/deploy-target API. `None` on platforms with no separate admin API (in-memory).
983
+ adminApi: ref<option<hookedValue<unknown>>>,
955
984
  // Current deploy target, set by deployPlugin before plugin.make() is called.
956
985
  // "Domain" (default) → resolvers/schema go to the DomainApi.
957
986
  // "Platform" → resolvers/schema go to the PlatformApi.
@@ -967,6 +996,7 @@ let noHooks: platformHooks = {
967
996
  schedulerRoleUrn: ref(Pulumi.Output.make("")),
968
997
  api: ref(None),
969
998
  apiRole: ref(None),
999
+ adminApi: ref(None),
970
1000
  deployTarget: ref("Domain"),
971
1001
  }
972
1002