@reventlessdev/reventless-core 3.0.0-alpha.238 → 3.0.0-alpha.240

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 (178) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/package.json +12 -12
  3. package/src/Message.res +11 -11
  4. package/src/Message.res.mjs +15 -14
  5. package/src/Projection.res +2 -2
  6. package/src/Projection.res.mjs +5 -4
  7. package/src/adapter/Monitoring/Monitoring.res +24 -2
  8. package/src/adapter/Monitoring/Monitoring.res.mjs +3 -3
  9. package/src/admin/Platform_AdminApi.res.mjs +3 -3
  10. package/src/admin/Platform_Admin_Structure.res +33 -112
  11. package/src/admin/Platform_Admin_Structure.res.mjs +8 -138
  12. package/src/admin/Platform_ComponentDefinitionsApi.res +2 -1
  13. package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +5 -1
  14. package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res.mjs +12 -12
  15. package/src/admin/UiFragmentRegistry/StateViewSlice/UiFragments.res.mjs +14 -14
  16. package/src/components/Aggregate/Aggregate_Callback.res +2 -2
  17. package/src/components/Aggregate/Aggregate_Callback.res.mjs +4 -4
  18. package/src/components/Api/ApiAllowedStatesHelpers.res.mjs +3 -3
  19. package/src/components/Api/ApiNoApiHelpers.res.mjs +5 -5
  20. package/src/components/Api/ApiTargetStateHelpers.res.mjs +3 -3
  21. package/src/components/Api/GraphQL_FragmentGenerator.res +140 -24
  22. package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +48 -21
  23. package/src/components/Api/GraphQL_SchemaInspector.res +1 -1
  24. package/src/components/Api/GraphQL_SchemaInspector.res.mjs +1 -1
  25. package/src/components/Api/MCP_SchemaGenerator.res +1 -1
  26. package/src/components/Api/MCP_SchemaGenerator.res.mjs +1 -1
  27. package/src/components/Api/SchemaType.res +6 -10
  28. package/src/components/Api/SchemaType.res.mjs +4 -4
  29. package/src/components/Api/StorageRefFields.res +1 -1
  30. package/src/components/Api/StorageRefFields.res.mjs +1 -1
  31. package/src/components/Api/SuryToJsonSchema.res +19 -0
  32. package/src/components/Api/SuryToJsonSchema.res.mjs +10 -5
  33. package/src/components/AutomationSlice/AutomationSlice_Callback.res +3 -4
  34. package/src/components/AutomationSlice/AutomationSlice_Callback.res.mjs +22 -23
  35. package/src/components/CommandTopic/CommandTopic_Callback.res +1 -2
  36. package/src/components/CommandTopic/CommandTopic_Callback.res.mjs +3 -4
  37. package/src/components/CommandTopic/CommandTopic_Helpers.res +1 -1
  38. package/src/components/CommandTopic/CommandTopic_Helpers.res.mjs +3 -3
  39. package/src/components/Counter/Counter.res.mjs +2 -2
  40. package/src/components/Counter/Counter_Callback.res.mjs +4 -4
  41. package/src/components/Counter/Counter_Operations.res.mjs +4 -4
  42. package/src/components/Dcb/Dcb_Builder.res +1 -1
  43. package/src/components/Dcb/Dcb_Builder.res.mjs +8 -6
  44. package/src/components/DcbEventLog/DcbEventLog_Builder.res.mjs +4 -3
  45. package/src/components/EventCollector/EventCollector_Builder.res +1 -1
  46. package/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res +1 -2
  47. package/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res.mjs +11 -12
  48. package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Callback.res +3 -4
  49. package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Callback.res.mjs +24 -25
  50. package/src/components/SideEffectHandler/SideEffectHandler_Callback.res +1 -1
  51. package/src/components/SideEffectHandler/SideEffectHandler_Callback.res.mjs +3 -3
  52. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +2 -2
  53. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +4 -4
  54. package/src/components/StateViewSlice/StateViewSlice_Builder.res +1 -1
  55. package/src/components/StateViewSlice/StateViewSlice_Builder.res.mjs +3 -3
  56. package/src/plugin/api/PluginBaseFragment.res +12 -29
  57. package/src/plugin/api/PluginBaseFragment.res.mjs +3 -22
  58. package/src/plugin/component/Capability_Inference.res +118 -1
  59. package/src/plugin/component/Capability_Inference.res.mjs +128 -1
  60. package/src/plugin/component/Plugin_Builder.res +9 -2
  61. package/src/plugin/component/Plugin_Builder.res.mjs +5 -2
  62. package/src/plugin/component/Plugin_Helpers.res +11 -11
  63. package/src/plugin/component/Plugin_Helpers.res.mjs +11 -10
  64. package/src/plugin/component/Plugin_Structure.res +458 -296
  65. package/src/plugin/component/Plugin_Structure.res.mjs +259 -182
  66. package/src/plugin/component/Plugin_SubscriptionSchema.res +1 -1
  67. package/src/plugin/component/Plugin_SubscriptionSchema.res.mjs +1 -1
  68. package/src/plugin/component/SchemaWalker.res +11 -11
  69. package/src/plugin/component/SchemaWalker.res.mjs +8 -9
  70. package/src/plugin/connect/PluginExtensionPoint_Plugin.res.mjs +15 -15
  71. package/src/plugin/lifecycle/PluginBehavior.res.mjs +55 -55
  72. package/src/plugin/lifecycle/PluginSpec.res +22 -7
  73. package/src/plugin/lifecycle/PluginSpec.res.mjs +91 -36
  74. package/src/plugin/lifecycle/PluginsReadModelSpec.res +36 -9
  75. package/src/plugin/lifecycle/PluginsReadModelSpec.res.mjs +33 -22
  76. package/src/util/Interstack.res +3 -3
  77. package/src/util/LogFormat.res +1 -2
  78. package/src/util/LogFormat.res.mjs +2 -2
  79. package/tests/IdentityTest.res +6 -7
  80. package/tests/IdentityTest.res.mjs +7 -9
  81. package/tests/adapter/MonitoringTest.res +30 -3
  82. package/tests/adapter/MonitoringTest.res.mjs +31 -12
  83. package/tests/adapter/RuntimeExtensionHookReachTest.res +0 -1
  84. package/tests/adapter/RuntimeExtensionHookReachTest.res.mjs +5 -7
  85. package/tests/admin/Platform_Admin_StructureTest.res +254 -0
  86. package/tests/admin/Platform_Admin_StructureTest.res.mjs +219 -0
  87. package/tests/admin/Platform_BakedManifestTest.res +1 -0
  88. package/tests/admin/Platform_BakedManifestTest.res.mjs +1 -0
  89. package/tests/admin/Platform_ComponentDefinitionsApiTest.res +3 -0
  90. package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +6 -0
  91. package/tests/admin/Platform_PluginStructuresApiTest.res +1 -0
  92. package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +2 -0
  93. package/tests/aggregate/AggregateCacheTest.res +0 -1
  94. package/tests/aggregate/AggregateCacheTest.res.mjs +13 -15
  95. package/tests/aggregate/AggregateConflictTest.res +0 -1
  96. package/tests/aggregate/AggregateConflictTest.res.mjs +7 -9
  97. package/tests/aggregate/AggregateFixtures.res +0 -1
  98. package/tests/aggregate/AggregateFixtures.res.mjs +19 -21
  99. package/tests/aggregate/AggregateSnapshotTest.res +1 -2
  100. package/tests/aggregate/AggregateSnapshotTest.res.mjs +15 -16
  101. package/tests/api/ApiNoApiTest.res.mjs +17 -9
  102. package/tests/api/GraphQL_FragmentGeneratorTest.res +232 -0
  103. package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +207 -55
  104. package/tests/api/PsInternalFieldsReadModel.res +21 -0
  105. package/tests/api/PsInternalFieldsReadModel.res.mjs +74 -0
  106. package/tests/api/StorageRefFieldsTest.res.mjs +13 -13
  107. package/tests/api/SuryToJsonSchemaTest.res +73 -3
  108. package/tests/api/SuryToJsonSchemaTest.res.mjs +364 -816
  109. package/tests/commandgenerator/CommandGeneratorFixtures.res +0 -1
  110. package/tests/commandgenerator/CommandGeneratorFixtures.res.mjs +19 -21
  111. package/tests/commandgenerator/OwnerStampingTest.res +0 -1
  112. package/tests/commandgenerator/OwnerStampingTest.res.mjs +9 -11
  113. package/tests/commandtopic/CommandTopicCallbackFixtures.res +0 -1
  114. package/tests/commandtopic/CommandTopicCallbackFixtures.res.mjs +7 -9
  115. package/tests/counter/CounterFixtures.res +0 -1
  116. package/tests/counter/CounterFixtures.res.mjs +1 -4
  117. package/tests/dcb/DcbDecodeTest.res.mjs +24 -24
  118. package/tests/dcb/DcbEventLogOperationsTest.res +2 -2
  119. package/tests/dcb/DcbEventLogOperationsTest.res.mjs +3 -3
  120. package/tests/dcb/DcbEventLogSchemaTest.res.mjs +7 -7
  121. package/tests/dcb/DcbFixtures.res +0 -1
  122. package/tests/dcb/DcbFixtures.res.mjs +87 -89
  123. package/tests/dcb/DcbTagTest.res.mjs +6 -6
  124. package/tests/dcb/DcbValidationTest.res.mjs +52 -52
  125. package/tests/eventlog/EventLogFixtures.res +0 -1
  126. package/tests/eventlog/EventLogFixtures.res.mjs +7 -9
  127. package/tests/eventmapper/EventMapperFixtures.res +0 -1
  128. package/tests/eventmapper/EventMapperFixtures.res.mjs +24 -26
  129. package/tests/eventtopic/EventTopicFixtures.res +0 -1
  130. package/tests/eventtopic/EventTopicFixtures.res.mjs +7 -9
  131. package/tests/extensionpoint/ExtensionPointFixtures.res +0 -1
  132. package/tests/extensionpoint/ExtensionPointFixtures.res.mjs +10 -12
  133. package/tests/extensionpoint/ExtensionPointOperationsTest.res +1 -2
  134. package/tests/extensionpoint/ExtensionPointOperationsTest.res.mjs +9 -10
  135. package/tests/logger/LogFormatTest.res +3 -2
  136. package/tests/logger/LogFormatTest.res.mjs +7 -9
  137. package/tests/message/MessageTest.res +2 -2
  138. package/tests/message/MessageTest.res.mjs +8 -7
  139. package/tests/message/MetaEnvelopeTest.res +5 -5
  140. package/tests/message/MetaEnvelopeTest.res.mjs +12 -11
  141. package/tests/plugin/ManifestVisibilityTest.res.mjs +7 -7
  142. package/tests/plugin/PluginDefinitionScalars.res +5 -3
  143. package/tests/plugin/PluginDefinitionScalars.res.mjs +2 -2
  144. package/tests/plugin/PluginLifecycleCorpusTest.res +4 -3
  145. package/tests/plugin/PluginLifecycleCorpusTest.res.mjs +3 -6
  146. package/tests/plugin/PluginSpecExperiment.res +0 -1
  147. package/tests/plugin/PluginSpecExperiment.res.mjs +12 -14
  148. package/tests/plugin/PluginStructureTest.res +292 -2
  149. package/tests/plugin/PluginStructureTest.res.mjs +391 -88
  150. package/tests/plugin/Plugin_EventQuerySchemaTest.res.mjs +3 -3
  151. package/tests/plugin/SchemaWalkerTest.res +0 -1
  152. package/tests/plugin/SchemaWalkerTest.res.mjs +17 -19
  153. package/tests/plugin/StateChangeSlice/PsAttachInvoice.res.mjs +5 -5
  154. package/tests/plugin/StateChangeSlice/PsChangePhoto.res.mjs +8 -8
  155. package/tests/plugin/StateChangeSlice/PsDispatchShipment.res.mjs +5 -5
  156. package/tests/plugin/StateChangeSlice/PsGatedCommands.res.mjs +9 -9
  157. package/tests/plugin/StateChangeSlice/PsPlaceOrder.res.mjs +5 -5
  158. package/tests/plugin/StateChangeSlice/PsReserveStock.res.mjs +8 -8
  159. package/tests/plugin/StateChangeSlice/PsShipOrder.res.mjs +15 -15
  160. package/tests/plugin/StateChangeSlice/PsTypoStore.res +31 -0
  161. package/tests/plugin/StateChangeSlice/PsTypoStore.res.mjs +62 -0
  162. package/tests/plugin/StateChangeSlice/PsUploadAvatar.res.mjs +8 -8
  163. package/tests/plugin/StateChangeSlice/PsUploadImages.res +40 -0
  164. package/tests/plugin/StateChangeSlice/PsUploadImages.res.mjs +66 -0
  165. package/tests/plugin/StateViewSlice/PsAnnotatedView.res.mjs +13 -12
  166. package/tests/plugin/StateViewSlice/PsAvailableProductsView.res.mjs +8 -8
  167. package/tests/plugin/StateViewSlice/PsCategoriesView.res.mjs +6 -6
  168. package/tests/plugin/StateViewSlice/PsCustomersView.res.mjs +10 -10
  169. package/tests/plugin/StateViewSlice/PsGatedView.res.mjs +6 -6
  170. package/tests/plugin/StateViewSlice/PsOrdersView.res.mjs +9 -9
  171. package/tests/plugin/StateViewSlice/PsShipmentsView.res.mjs +11 -10
  172. package/tests/plugin/pluginDefinitionRequiredScalars.txt +15 -0
  173. package/tests/querydb/QueryDbFixtures.res +0 -1
  174. package/tests/querydb/QueryDbFixtures.res.mjs +5 -7
  175. package/tests/sideeffecthandler/SideEffectHandlerFixtures.res +0 -1
  176. package/tests/sideeffecthandler/SideEffectHandlerFixtures.res.mjs +4 -6
  177. package/tests/util/CommandPublisherTest.res +0 -1
  178. package/tests/util/CommandPublisherTest.res.mjs +4 -6
@@ -15,6 +15,17 @@
15
15
  open JestGlobals
16
16
  open Reventless.Plugin
17
17
 
18
+ // The commands are derived from `PluginSpec.command` rather than hand-written, so
19
+ // a test names one by looking it up. Raises rather than defaulting: a missing
20
+ // variant is the failure these tests exist to catch, and a placeholder would let
21
+ // the assertions below pass against nothing.
22
+ let commandNamed = (name: string): commandDef =>
23
+ Platform_Admin_Structure.pluginCommands
24
+ ->Array.find(c => c.name == name)
25
+ ->Option.getOrThrow(~message=`Platform_Admin_Structure publishes no "${name}" command`)
26
+
27
+ let activate = () => commandNamed("Activate")
28
+
18
29
  describe("the Plugins read model declares its lifecycle", () => {
19
30
  let rm: queryableDef = Platform_Admin_Structure.pluginReadModel
20
31
 
@@ -47,3 +58,246 @@ describe("the Plugins read model declares its lifecycle", () => {
47
58
  )->toEqual(false)
48
59
  )
49
60
  })
61
+
62
+ // `Plugin_Structure.make` never sees the platform's own components, so the checks it
63
+ // runs over every ordinary plugin do not run here. A test rather than a startup call:
64
+ // both sides are compile-time constants in this package, so nothing varies at runtime.
65
+ describe("the admin structure's declared transitions are checked", () => {
66
+ let states = Plugin_Structure.lifecycleStatesFromStateSchema(
67
+ ~entityName=PluginsReadModelSpec.name,
68
+ PluginsReadModelSpec.stateSchema->S.castToUnknown,
69
+ )
70
+
71
+ let statesByView = () => {
72
+ let d = Dict.make()
73
+ states->Option.forEach(s => d->Dict.set(PluginsReadModelSpec.name, s))
74
+ d
75
+ }
76
+
77
+ let raises = writables =>
78
+ switch Plugin_Structure.checkDeclaredTransitions(
79
+ ~pluginName="Platform",
80
+ ~writables,
81
+ ~lifecycleStatesByView=statesByView(),
82
+ ) {
83
+ | () => false
84
+ | exception _ => true
85
+ }
86
+
87
+ // The precondition the rest of the block is worthless without: with no states the
88
+ // check counts the commands as unvalidated and passes.
89
+ testSync("the linked view yields its lifecycle states", () =>
90
+ expect(states)->toEqual(Some(["Connected", "Disconnected", "Inactive", "Retired"]))
91
+ )
92
+
93
+ testSync("every state the Plugin commands name is one the view declares", () =>
94
+ expect(raises([Platform_Admin_Structure.pluginAggregate]))->toEqual(false)
95
+ )
96
+
97
+ // A check that cannot fail is indistinguishable from one that is not running.
98
+ testSync("and a state the view does not declare is caught", () => {
99
+ let bogus: Reventless.Plugin.writableDef = {
100
+ ...Platform_Admin_Structure.pluginAggregate,
101
+ commands: [
102
+ {
103
+ ...activate(),
104
+ targetState: Some("Conected"),
105
+ },
106
+ ],
107
+ }
108
+ expect(raises([bogus]))->toEqual(true)
109
+ })
110
+ })
111
+
112
+ // The drift that made deriving these defs worth doing rather than tidy. The
113
+ // hand-written arg schema described only `id`, while the SDL generated off the
114
+ // same `commandSchema` declares `Platform_Plugin_Activate(_0: String!, id: ID!)`.
115
+ // AutoUI builds its mutation document from the published schema, so it sent a
116
+ // document missing a required argument and every admin row action was rejected at
117
+ // GraphQL validation, before the aggregate saw it.
118
+ describe("the Plugin command defs carry every argument the SDL requires", () => {
119
+ testSync("the version argument is published", () =>
120
+ expect(activate().schema->String.includes(`"_0"`))->toEqual(true)
121
+ )
122
+
123
+ testSync("and so is the aggregate id", () => {
124
+ // `id` is not in the command schema — it is the aggregate instance, injected
125
+ // by the auto-resolver flow. What the def has to get right is naming it, so
126
+ // the consumer declares the variable rather than guessing.
127
+ expect(activate().aggregateIdField)->toEqual(None)
128
+ })
129
+
130
+ testSync("the exposed commands name the field PluginBaseFragment generates", () =>
131
+ expect(
132
+ Platform_Admin_Structure.pluginCommands
133
+ ->Array.filter(c => c.apiExposed != Some(false))
134
+ ->Array.map(c => c.mutationField),
135
+ )->toEqual([
136
+ "Platform_Plugin_Activate",
137
+ "Platform_Plugin_Deactivate",
138
+ "Platform_Plugin_Retire",
139
+ ])
140
+ )
141
+
142
+ // The `@noApi` protocol variants come back from the same walk. They belong on
143
+ // the event graph — `Disconnect` is the only edge into `Disconnected` — but
144
+ // `Connect` carries a whole plugin definition, so publishing metadata about
145
+ // them must not publish a way to call them.
146
+ testSync("the internal commands are published as uncallable", () => {
147
+ let internal =
148
+ Platform_Admin_Structure.pluginCommands->Array.filter(c => c.apiExposed == Some(false))
149
+ expect(internal->Array.map(c => c.name))->toEqual([
150
+ "Connect",
151
+ "ReportIncompatibility",
152
+ "Heartbeat",
153
+ "Redetect",
154
+ "Disconnect",
155
+ ])
156
+ expect(internal->Array.every(c => c.mutationField == ""))->toEqual(true)
157
+ })
158
+ })
159
+
160
+ // A heartbeat timing out is the only way a row reaches `Disconnected`, and the
161
+ // command that does it carries `@noApi`. Until it declared its edge the topology
162
+ // check reported the state unreachable and was right to: nothing in the
163
+ // declarations said how a row got there.
164
+ describe("the internal commands declare the edges they move rows along", () => {
165
+ let states = Plugin_Structure.lifecycleStatesFromStateSchema(
166
+ ~entityName=PluginsReadModelSpec.name,
167
+ PluginsReadModelSpec.stateSchema->S.castToUnknown,
168
+ )
169
+
170
+ let statesByView = () => {
171
+ let d = Dict.make()
172
+ states->Option.forEach(s => d->Dict.set(PluginsReadModelSpec.name, s))
173
+ d
174
+ }
175
+
176
+ testSync("the only edge into Disconnected is declared", () =>
177
+ expect((commandNamed("Disconnect").allowedStates, commandNamed("Disconnect").targetState))
178
+ ->toEqual((Some(["Connected"]), Some("Disconnected")))
179
+ )
180
+
181
+ // The creating form's claim: a target and no from-set. `Connect` brings the
182
+ // version's row into being, so there is no state it runs from — and an empty
183
+ // `allowedStates` would say the opposite, that it is legal in none.
184
+ testSync("the handshake declares a target and no from-set", () =>
185
+ expect((commandNamed("Connect").allowedStates, commandNamed("Connect").targetState))
186
+ ->toEqual((None, Some("Connected")))
187
+ )
188
+
189
+ testSync("and no state is left unreachable", () =>
190
+ expect(
191
+ Plugin_Structure.lifecycleTopologyFindings(
192
+ ~writables=[Platform_Admin_Structure.pluginAggregate],
193
+ ~lifecycleStatesByView=statesByView(),
194
+ ),
195
+ )->toEqual([])
196
+ )
197
+ })
198
+
199
+ // One rule, read on both sides of the wire. `PluginBaseFragment` hand-wrote an
200
+ // `Admin` Cognito gate while the spec defaulted to `AllowAuthenticated`, and the
201
+ // published `requiredAccess` said `None` — so a shell was told the Plugins view
202
+ // was open to everyone and the server refused anyone outside `Admin`. The
203
+ // mismatch is invisible until a non-admin clicks the page.
204
+ describe("the Plugins view publishes the rule the server enforces", () => {
205
+ testSync("the spec declares the group", () =>
206
+ expect(PluginsReadModelSpec.authorization)->toEqual(
207
+ Reventless.Authorization.AllowGroups(["Admin"]),
208
+ )
209
+ )
210
+
211
+ testSync("and the published access keys are derived from it", () =>
212
+ expect(Platform_Admin_Structure.pluginReadModel.requiredAccess)->toEqual(Some(["Admin"]))
213
+ )
214
+
215
+ // The API entry reads the same binding rather than restating it, so the two
216
+ // cannot be edited apart.
217
+ testSync("the query entry carries the spec's rule and no hand-written pair", () => {
218
+ let entry = PluginBaseFragment.queryEntries->Array.getUnsafe(0)
219
+ expect((entry.permission, entry.authorization))->toEqual((
220
+ Some(PluginsReadModelSpec.authorization),
221
+ None,
222
+ ))
223
+ })
224
+ })
225
+
226
+ // The `queryableDef` is derived from the spec, not hand-written. The analysis
227
+ // that preceded this expected the admin's naming to be bespoke and it is not:
228
+ // `Api_Naming.queryFieldNamesForReadModel` returns byte-identical names, because
229
+ // `singularize` already handles the plural-spec-name / singular-type shape that
230
+ // looked special. Asserted rather than assumed — the whole case for deriving
231
+ // rests on the generic helpers reaching the same answers.
232
+ describe("the Plugins queryableDef is what the generic helpers produce", () => {
233
+ let rm: queryableDef = Platform_Admin_Structure.pluginReadModel
234
+
235
+ testSync("the field names are the generic ones", () =>
236
+ expect((rm.queryField, rm.singleQueryField))->toEqual((
237
+ "Platform_Plugins",
238
+ Some("Platform_Plugin"),
239
+ ))
240
+ )
241
+
242
+ testSync("the label ladder reaches the same field, by convention", () =>
243
+ expect((rm.labelField, rm.labelFieldSource, rm.searchableFields))->toEqual((
244
+ "name",
245
+ Some("convention"),
246
+ ["name"],
247
+ ))
248
+ )
249
+
250
+ // The row id is `name@version` and the state carries the two halves
251
+ // separately, never the composed key — so `None` is the honest answer, and it
252
+ // is the one the resolver ladder reaches on its own.
253
+ testSync("the key ladder finds no id field, as the hand-written def claimed", () =>
254
+ expect((rm.idField, rm.idFieldSource))->toEqual((None, None))
255
+ )
256
+
257
+ // `apiSchemaFragment` and `structure` are on the API — the shell fetches them
258
+ // through dedicated resolver paths — but each is a multi-kilobyte JSON blob
259
+ // that a generated list view must not name. They used to be cut out of the
260
+ // published schema by a hand-written list; they now carry `@hidden`, so they
261
+ // are described and the consumer decides.
262
+ //
263
+ // This is the difference between the two markers, asserted side by side: a
264
+ // `@hidden` field is present and flagged, an `@internal` one is not there at
265
+ // all. Cutting a hidden field out here would leave a consumer unable to tell
266
+ // "not for you" from "does not exist".
267
+ testSync("the blob fields are published, and marked hidden", () => {
268
+ // Read as property NAMES, not as substrings of the JSON: the schema carries
269
+ // nested object types whose own fields would match a naive `includes`.
270
+ let props =
271
+ rm.schema
272
+ ->JSON.parseExn
273
+ ->JSON.Decode.object
274
+ ->Option.flatMap(o => o->Dict.get("properties"))
275
+ ->Option.flatMap(JSON.Decode.object)
276
+ ->Option.getOr(Dict.make())
277
+ let isHidden = key =>
278
+ props
279
+ ->Dict.get(key)
280
+ ->Option.flatMap(JSON.Decode.object)
281
+ ->Option.flatMap(o => o->Dict.get("x-reventless-hidden"))
282
+ ->Option.flatMap(JSON.Decode.bool)
283
+ expect((isHidden("apiSchemaFragment"), isHidden("structure")))->toEqual((
284
+ Some(true),
285
+ Some(true),
286
+ ))
287
+ })
288
+
289
+ testSync("while the internal fields are not published at all", () => {
290
+ let props =
291
+ rm.schema
292
+ ->JSON.parseExn
293
+ ->JSON.Decode.object
294
+ ->Option.flatMap(o => o->Dict.get("properties"))
295
+ ->Option.flatMap(JSON.Decode.object)
296
+ ->Option.mapOr([], Dict.keysToArray)
297
+ expect((
298
+ props->Array.includes("eventCollector"),
299
+ props->Array.includes("extensionNames"),
300
+ props->Array.includes("statusChange"),
301
+ ))->toEqual((false, false, true))
302
+ })
303
+ })
@@ -1,9 +1,22 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
3
4
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
6
+ import * as Plugin_Structure$ReventlessCore from "../../src/plugin/component/Plugin_Structure.res.mjs";
7
+ import * as PluginBaseFragment$ReventlessCore from "../../src/plugin/api/PluginBaseFragment.res.mjs";
8
+ import * as PluginsReadModelSpec$ReventlessCore from "../../src/plugin/lifecycle/PluginsReadModelSpec.res.mjs";
4
9
  import * as Platform_Admin_Structure$ReventlessCore from "../../src/admin/Platform_Admin_Structure.res.mjs";
5
10
  import * as Platform_ComponentDefinitionsApi$ReventlessCore from "../../src/admin/Platform_ComponentDefinitionsApi.res.mjs";
6
11
 
12
+ function commandNamed(name) {
13
+ return Stdlib_Option.getOrThrow(Platform_Admin_Structure$ReventlessCore.pluginCommands.find(c => c.name === name), `Platform_Admin_Structure publishes no "` + name + `" command`);
14
+ }
15
+
16
+ function activate() {
17
+ return commandNamed("Activate");
18
+ }
19
+
7
20
  globalThis.describe("the Plugins read model declares its lifecycle", () => {
8
21
  globalThis.test("names the field commands branch on", () => {
9
22
  globalThis.expect(Platform_Admin_Structure$ReventlessCore.pluginReadModel.lifecycleField).toEqual("status");
@@ -21,4 +34,210 @@ globalThis.describe("the Plugins read model declares its lifecycle", () => {
21
34
  });
22
35
  });
23
36
 
37
+ globalThis.describe("the admin structure's declared transitions are checked", () => {
38
+ let states = Plugin_Structure$ReventlessCore.lifecycleStatesFromStateSchema(PluginsReadModelSpec$ReventlessCore.name, PluginsReadModelSpec$ReventlessCore.stateSchema);
39
+ let statesByView = () => {
40
+ let d = {};
41
+ Stdlib_Option.forEach(states, s => {
42
+ d[PluginsReadModelSpec$ReventlessCore.name] = s;
43
+ });
44
+ return d;
45
+ };
46
+ let raises = writables => {
47
+ try {
48
+ Plugin_Structure$ReventlessCore.checkDeclaredTransitions("Platform", writables, statesByView());
49
+ return false;
50
+ } catch (exn) {
51
+ return true;
52
+ }
53
+ };
54
+ globalThis.test("the linked view yields its lifecycle states", () => {
55
+ globalThis.expect(states).toEqual([
56
+ "Connected",
57
+ "Disconnected",
58
+ "Inactive",
59
+ "Retired"
60
+ ]);
61
+ });
62
+ globalThis.test("every state the Plugin commands name is one the view declares", () => {
63
+ globalThis.expect(raises([Platform_Admin_Structure$ReventlessCore.pluginAggregate])).toEqual(false);
64
+ });
65
+ globalThis.test("and a state the view does not declare is caught", () => {
66
+ let init = commandNamed("Activate");
67
+ let bogus_name = Platform_Admin_Structure$ReventlessCore.pluginAggregate.name;
68
+ let bogus_commands = [{
69
+ name: init.name,
70
+ schema: init.schema,
71
+ level: init.level,
72
+ aggregateIdField: init.aggregateIdField,
73
+ mutationField: init.mutationField,
74
+ references: init.references,
75
+ allowedStates: init.allowedStates,
76
+ targetState: "Conected",
77
+ apiExposed: init.apiExposed,
78
+ requiredAccess: init.requiredAccess,
79
+ ownerField: init.ownerField
80
+ }];
81
+ let bogus_producedEventTypes = Platform_Admin_Structure$ReventlessCore.pluginAggregate.producedEventTypes;
82
+ let bogus_consumedEventTypes = Platform_Admin_Structure$ReventlessCore.pluginAggregate.consumedEventTypes;
83
+ let bogus_linkedViews = Platform_Admin_Structure$ReventlessCore.pluginAggregate.linkedViews;
84
+ let bogus_consistencyRead = Platform_Admin_Structure$ReventlessCore.pluginAggregate.consistencyRead;
85
+ let bogus_events = Platform_Admin_Structure$ReventlessCore.pluginAggregate.events;
86
+ let bogus_errors = Platform_Admin_Structure$ReventlessCore.pluginAggregate.errors;
87
+ let bogus_chapter = Platform_Admin_Structure$ReventlessCore.pluginAggregate.chapter;
88
+ let bogus = {
89
+ name: bogus_name,
90
+ commands: bogus_commands,
91
+ producedEventTypes: bogus_producedEventTypes,
92
+ consumedEventTypes: bogus_consumedEventTypes,
93
+ linkedViews: bogus_linkedViews,
94
+ consistencyRead: bogus_consistencyRead,
95
+ events: bogus_events,
96
+ errors: bogus_errors,
97
+ chapter: bogus_chapter
98
+ };
99
+ globalThis.expect(raises([bogus])).toEqual(true);
100
+ });
101
+ });
102
+
103
+ globalThis.describe("the Plugin command defs carry every argument the SDL requires", () => {
104
+ globalThis.test("the version argument is published", () => {
105
+ globalThis.expect(commandNamed("Activate").schema.includes(`"_0"`)).toEqual(true);
106
+ });
107
+ globalThis.test("and so is the aggregate id", () => {
108
+ globalThis.expect(commandNamed("Activate").aggregateIdField).toEqual(undefined);
109
+ });
110
+ globalThis.test("the exposed commands name the field PluginBaseFragment generates", () => {
111
+ globalThis.expect(Platform_Admin_Structure$ReventlessCore.pluginCommands.filter(c => Primitive_object.notequal(c.apiExposed, false)).map(c => c.mutationField)).toEqual([
112
+ "Platform_Plugin_Activate",
113
+ "Platform_Plugin_Deactivate",
114
+ "Platform_Plugin_Retire"
115
+ ]);
116
+ });
117
+ globalThis.test("the internal commands are published as uncallable", () => {
118
+ let internal = Platform_Admin_Structure$ReventlessCore.pluginCommands.filter(c => Primitive_object.equal(c.apiExposed, false));
119
+ globalThis.expect(internal.map(c => c.name)).toEqual([
120
+ "Connect",
121
+ "ReportIncompatibility",
122
+ "Heartbeat",
123
+ "Redetect",
124
+ "Disconnect"
125
+ ]);
126
+ globalThis.expect(internal.every(c => c.mutationField === "")).toEqual(true);
127
+ });
128
+ });
129
+
130
+ globalThis.describe("the internal commands declare the edges they move rows along", () => {
131
+ let states = Plugin_Structure$ReventlessCore.lifecycleStatesFromStateSchema(PluginsReadModelSpec$ReventlessCore.name, PluginsReadModelSpec$ReventlessCore.stateSchema);
132
+ let statesByView = () => {
133
+ let d = {};
134
+ Stdlib_Option.forEach(states, s => {
135
+ d[PluginsReadModelSpec$ReventlessCore.name] = s;
136
+ });
137
+ return d;
138
+ };
139
+ globalThis.test("the only edge into Disconnected is declared", () => {
140
+ globalThis.expect([
141
+ commandNamed("Disconnect").allowedStates,
142
+ commandNamed("Disconnect").targetState
143
+ ]).toEqual([
144
+ ["Connected"],
145
+ "Disconnected"
146
+ ]);
147
+ });
148
+ globalThis.test("the handshake declares a target and no from-set", () => {
149
+ globalThis.expect([
150
+ commandNamed("Connect").allowedStates,
151
+ commandNamed("Connect").targetState
152
+ ]).toEqual([
153
+ undefined,
154
+ "Connected"
155
+ ]);
156
+ });
157
+ globalThis.test("and no state is left unreachable", () => {
158
+ globalThis.expect(Plugin_Structure$ReventlessCore.lifecycleTopologyFindings([Platform_Admin_Structure$ReventlessCore.pluginAggregate], statesByView())).toEqual([]);
159
+ });
160
+ });
161
+
162
+ globalThis.describe("the Plugins view publishes the rule the server enforces", () => {
163
+ globalThis.test("the spec declares the group", () => {
164
+ globalThis.expect(PluginsReadModelSpec$ReventlessCore.authorization).toEqual({
165
+ TAG: "AllowGroups",
166
+ _0: ["Admin"]
167
+ });
168
+ });
169
+ globalThis.test("and the published access keys are derived from it", () => {
170
+ globalThis.expect(Platform_Admin_Structure$ReventlessCore.pluginReadModel.requiredAccess).toEqual(["Admin"]);
171
+ });
172
+ globalThis.test("the query entry carries the spec's rule and no hand-written pair", () => {
173
+ let entry = PluginBaseFragment$ReventlessCore.queryEntries[0];
174
+ globalThis.expect([
175
+ entry.permission,
176
+ entry.authorization
177
+ ]).toEqual([
178
+ PluginsReadModelSpec$ReventlessCore.authorization,
179
+ undefined
180
+ ]);
181
+ });
182
+ });
183
+
184
+ globalThis.describe("the Plugins queryableDef is what the generic helpers produce", () => {
185
+ globalThis.test("the field names are the generic ones", () => {
186
+ globalThis.expect([
187
+ Platform_Admin_Structure$ReventlessCore.pluginReadModel.queryField,
188
+ Platform_Admin_Structure$ReventlessCore.pluginReadModel.singleQueryField
189
+ ]).toEqual([
190
+ "Platform_Plugins",
191
+ "Platform_Plugin"
192
+ ]);
193
+ });
194
+ globalThis.test("the label ladder reaches the same field, by convention", () => {
195
+ globalThis.expect([
196
+ Platform_Admin_Structure$ReventlessCore.pluginReadModel.labelField,
197
+ Platform_Admin_Structure$ReventlessCore.pluginReadModel.labelFieldSource,
198
+ Platform_Admin_Structure$ReventlessCore.pluginReadModel.searchableFields
199
+ ]).toEqual([
200
+ "name",
201
+ "convention",
202
+ ["name"]
203
+ ]);
204
+ });
205
+ globalThis.test("the key ladder finds no id field, as the hand-written def claimed", () => {
206
+ globalThis.expect([
207
+ Platform_Admin_Structure$ReventlessCore.pluginReadModel.idField,
208
+ Platform_Admin_Structure$ReventlessCore.pluginReadModel.idFieldSource
209
+ ]).toEqual([
210
+ undefined,
211
+ undefined
212
+ ]);
213
+ });
214
+ globalThis.test("the blob fields are published, and marked hidden", () => {
215
+ let props = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(JSON.parse(Platform_Admin_Structure$ReventlessCore.pluginReadModel.schema)), o => o["properties"]), Stdlib_JSON.Decode.object), {});
216
+ let isHidden = key => Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(props[key], Stdlib_JSON.Decode.object), o => o["x-reventless-hidden"]), Stdlib_JSON.Decode.bool);
217
+ globalThis.expect([
218
+ isHidden("apiSchemaFragment"),
219
+ isHidden("structure")
220
+ ]).toEqual([
221
+ true,
222
+ true
223
+ ]);
224
+ });
225
+ globalThis.test("while the internal fields are not published at all", () => {
226
+ let props = Stdlib_Option.mapOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(JSON.parse(Platform_Admin_Structure$ReventlessCore.pluginReadModel.schema)), o => o["properties"]), Stdlib_JSON.Decode.object), [], prim => Object.keys(prim));
227
+ globalThis.expect([
228
+ props.includes("eventCollector"),
229
+ props.includes("extensionNames"),
230
+ props.includes("statusChange")
231
+ ]).toEqual([
232
+ false,
233
+ false,
234
+ true
235
+ ]);
236
+ });
237
+ });
238
+
239
+ export {
240
+ commandNamed,
241
+ activate,
242
+ }
24
243
  /* Not a pure module */
@@ -25,6 +25,7 @@ let queryable = (~name, ~visibility=?, ~ownerField=?, ()): queryableDef => {
25
25
  ownerField,
26
26
  retiredField: None,
27
27
  retiredValues: None,
28
+ namedWhenRetired: None,
28
29
  }
29
30
 
30
31
  let command = (~name, ~references=[], ~ownerField=?, ()): commandDef => {
@@ -22,6 +22,7 @@ function queryable(name, visibility, ownerField, param) {
22
22
  ownerField: ownerField,
23
23
  retiredField: undefined,
24
24
  retiredValues: undefined,
25
+ namedWhenRetired: undefined,
25
26
  visibility: visibility,
26
27
  chapter: undefined,
27
28
  singleQueryField: `Ordering_` + name + `Single`,
@@ -40,6 +40,7 @@ let qbl: queryableDef = {
40
40
  ownerField: None,
41
41
  retiredField: None,
42
42
  retiredValues: None,
43
+ namedWhenRetired: None,
43
44
  }
44
45
 
45
46
  let wbl: writableDef = {
@@ -268,6 +269,7 @@ describe("visibility filtering (deployed AutoUI hides Internal)", () => {
268
269
  ownerField: None,
269
270
  retiredField: None,
270
271
  retiredValues: None,
272
+ namedWhenRetired: None,
271
273
  }
272
274
  // A distinct name per source array: the complement has to be fed by both, and
273
275
  // reusing one def would hide a version that only walks `readModels`.
@@ -367,6 +369,7 @@ describe("allowedStates + lifecycleField populated", () => {
367
369
  ownerField: None,
368
370
  retiredField: None,
369
371
  retiredValues: None,
372
+ namedWhenRetired: None,
370
373
  }
371
374
 
372
375
  let wblWithStates: writableDef = {
@@ -55,6 +55,7 @@ let qbl = {
55
55
  ownerField: undefined,
56
56
  retiredField: undefined,
57
57
  retiredValues: undefined,
58
+ namedWhenRetired: undefined,
58
59
  visibility: undefined,
59
60
  chapter: undefined,
60
61
  singleQueryField: qbl_singleQueryField,
@@ -288,6 +289,7 @@ globalThis.describe("visibility filtering (deployed AutoUI hides Internal)", ()
288
289
  ownerField: undefined,
289
290
  retiredField: undefined,
290
291
  retiredValues: undefined,
292
+ namedWhenRetired: undefined,
291
293
  visibility: internalQbl_visibility,
292
294
  chapter: undefined,
293
295
  singleQueryField: internalQbl_singleQueryField,
@@ -316,6 +318,7 @@ globalThis.describe("visibility filtering (deployed AutoUI hides Internal)", ()
316
318
  ownerField: undefined,
317
319
  retiredField: undefined,
318
320
  retiredValues: undefined,
321
+ namedWhenRetired: undefined,
319
322
  visibility: internalSlice_visibility,
320
323
  chapter: undefined,
321
324
  singleQueryField: internalSlice_singleQueryField,
@@ -422,6 +425,7 @@ globalThis.describe("allowedStates + lifecycleField populated", () => {
422
425
  ownerField: undefined,
423
426
  retiredField: undefined,
424
427
  retiredValues: undefined,
428
+ namedWhenRetired: undefined,
425
429
  visibility: undefined,
426
430
  chapter: undefined,
427
431
  singleQueryField: qblWithStatus_singleQueryField,
@@ -503,6 +507,7 @@ globalThis.describe("singleQueryField reaches the caller", () => {
503
507
  ownerField: undefined,
504
508
  retiredField: undefined,
505
509
  retiredValues: undefined,
510
+ namedWhenRetired: undefined,
506
511
  visibility: undefined,
507
512
  chapter: undefined,
508
513
  singleQueryField: undefined,
@@ -550,6 +555,7 @@ globalThis.describe("idField / idFieldSource reach the caller", () => {
550
555
  ownerField: undefined,
551
556
  retiredField: undefined,
552
557
  retiredValues: undefined,
558
+ namedWhenRetired: undefined,
553
559
  visibility: undefined,
554
560
  chapter: undefined,
555
561
  singleQueryField: "Catalog_Product",
@@ -26,6 +26,7 @@ let publicRm: queryableDef = {
26
26
  ownerField: None,
27
27
  retiredField: None,
28
28
  retiredValues: None,
29
+ namedWhenRetired: None,
29
30
  }
30
31
 
31
32
  // The component `Platform_ComponentDefinitions` drops and this query must keep.
@@ -35,6 +35,7 @@ let publicRm = {
35
35
  ownerField: undefined,
36
36
  retiredField: undefined,
37
37
  retiredValues: undefined,
38
+ namedWhenRetired: undefined,
38
39
  visibility: undefined,
39
40
  chapter: publicRm_chapter,
40
41
  singleQueryField: publicRm_singleQueryField,
@@ -74,6 +75,7 @@ let internalRm = {
74
75
  ownerField: undefined,
75
76
  retiredField: undefined,
76
77
  retiredValues: undefined,
78
+ namedWhenRetired: undefined,
77
79
  visibility: internalRm_visibility,
78
80
  chapter: internalRm_chapter,
79
81
  singleQueryField: internalRm_singleQueryField,
@@ -12,7 +12,6 @@
12
12
 
13
13
  open JestGlobals
14
14
 
15
- S.enableJson()
16
15
 
17
16
  module AggSpec = {
18
17
  module Id = Reventless.Id.StringPure
@@ -1,37 +1,35 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
- import * as S from "sury/src/S.res.mjs";
3
+ import * as Sury from "sury";
4
4
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
5
5
  import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
6
6
  import * as Effect from "effect/Effect";
7
7
  import * as Stream from "effect/Stream";
8
8
  import * as Aggregate_Callback$ReventlessCore from "../../src/components/Aggregate/Aggregate_Callback.res.mjs";
9
9
 
10
- S.enableJson();
11
-
12
10
  let name = "CacheTestAggregate";
13
11
 
14
- let commandSchema = S.union([
15
- S.schema(s => ({
12
+ let commandSchema = Sury.union([
13
+ Sury.$schema(s => ({
16
14
  TAG: "Add",
17
- name: s.m(S.string)
15
+ name: s.m(Sury.string)
18
16
  })),
19
- S.literal("Checkpoint"),
20
- S.literal("Noop")
17
+ Sury.literal("Checkpoint"),
18
+ Sury.literal("Noop")
21
19
  ]);
22
20
 
23
- let eventSchema = S.union([
24
- S.schema(s => ({
21
+ let eventSchema = Sury.union([
22
+ Sury.$schema(s => ({
25
23
  TAG: "Added",
26
- name: s.m(S.string)
24
+ name: s.m(Sury.string)
27
25
  })),
28
- S.schema(s => ({
26
+ Sury.$schema(s => ({
29
27
  TAG: "CheckpointTaken",
30
- count: s.m(S.int)
28
+ count: s.m(Sury.int)
31
29
  }))
32
30
  ]);
33
31
 
34
- let errorSchema = S.literal("Never");
32
+ let errorSchema = Sury.literal("Never");
35
33
 
36
34
  let moduleUrl = import.meta.url;
37
35
 
@@ -430,4 +428,4 @@ export {
430
428
  makeTopicItem,
431
429
  run,
432
430
  }
433
- /* Not a pure module */
431
+ /* commandSchema Not a pure module */
@@ -3,7 +3,6 @@
3
3
 
4
4
  open JestGlobals
5
5
 
6
- S.enableJson()
7
6
 
8
7
  module AggSpec = {
9
8
  module Id = Reventless.Id.StringPure