@reventlessdev/reventless-core 3.0.0-alpha.239 → 3.0.0-alpha.241
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/package.json +11 -11
- package/src/Message.res +11 -11
- package/src/Message.res.mjs +15 -14
- package/src/Projection.res +2 -2
- package/src/Projection.res.mjs +5 -4
- package/src/adapter/Monitoring/Monitoring.res +24 -2
- package/src/adapter/Monitoring/Monitoring.res.mjs +3 -3
- package/src/admin/Platform_Admin.res +1 -16
- package/src/admin/Platform_Admin.res.mjs +2 -30
- package/src/admin/Platform_AdminApi.res.mjs +3 -3
- package/src/admin/Platform_Admin_Structure.res +58 -113
- package/src/admin/Platform_Admin_Structure.res.mjs +24 -139
- package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res.mjs +12 -12
- package/src/admin/UiFragmentRegistry/StateViewSlice/UiFragments.res.mjs +14 -14
- package/src/components/Aggregate/Aggregate_Callback.res +2 -2
- package/src/components/Aggregate/Aggregate_Callback.res.mjs +4 -4
- package/src/components/Api/ApiAllowedStatesHelpers.res.mjs +3 -3
- package/src/components/Api/ApiNoApiHelpers.res.mjs +5 -5
- package/src/components/Api/ApiTargetStateHelpers.res.mjs +3 -3
- package/src/components/Api/GraphQL_FragmentGenerator.res +18 -12
- package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +7 -13
- package/src/components/Api/GraphQL_SchemaInspector.res +1 -1
- package/src/components/Api/GraphQL_SchemaInspector.res.mjs +1 -1
- package/src/components/Api/MCP_SchemaGenerator.res +1 -1
- package/src/components/Api/MCP_SchemaGenerator.res.mjs +1 -1
- package/src/components/Api/SchemaType.res +6 -10
- package/src/components/Api/SchemaType.res.mjs +4 -4
- package/src/components/Api/StorageRefFields.res +1 -1
- package/src/components/Api/StorageRefFields.res.mjs +1 -1
- package/src/components/Api/SuryToJsonSchema.res +11 -0
- package/src/components/Api/SuryToJsonSchema.res.mjs +2 -1
- package/src/components/AutomationSlice/AutomationSlice_Callback.res +3 -4
- package/src/components/AutomationSlice/AutomationSlice_Callback.res.mjs +22 -23
- package/src/components/CommandTopic/CommandTopic_Callback.res +1 -2
- package/src/components/CommandTopic/CommandTopic_Callback.res.mjs +3 -4
- package/src/components/CommandTopic/CommandTopic_Helpers.res +1 -1
- package/src/components/CommandTopic/CommandTopic_Helpers.res.mjs +3 -3
- package/src/components/Counter/Counter.res.mjs +2 -2
- package/src/components/Counter/Counter_Callback.res.mjs +4 -4
- package/src/components/Counter/Counter_Operations.res.mjs +4 -4
- package/src/components/Dcb/Dcb_Builder.res +1 -1
- package/src/components/Dcb/Dcb_Builder.res.mjs +8 -6
- package/src/components/DcbEventLog/DcbEventLog_Builder.res.mjs +4 -3
- package/src/components/EventCollector/EventCollector_Builder.res +1 -1
- package/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res +1 -2
- package/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res.mjs +11 -12
- package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Callback.res +3 -4
- package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Callback.res.mjs +24 -25
- package/src/components/SideEffectHandler/SideEffectHandler_Callback.res +1 -1
- package/src/components/SideEffectHandler/SideEffectHandler_Callback.res.mjs +3 -3
- package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +2 -2
- package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +4 -4
- package/src/components/StateViewSlice/StateViewSlice_Builder.res +1 -1
- package/src/components/StateViewSlice/StateViewSlice_Builder.res.mjs +3 -3
- package/src/plugin/api/PluginBaseFragment.res +12 -29
- package/src/plugin/api/PluginBaseFragment.res.mjs +3 -22
- package/src/plugin/component/Capability_Inference.res +118 -1
- package/src/plugin/component/Capability_Inference.res.mjs +128 -1
- package/src/plugin/component/Plugin_Builder.res +2 -2
- package/src/plugin/component/Plugin_Builder.res.mjs +2 -1
- package/src/plugin/component/Plugin_Helpers.res +11 -11
- package/src/plugin/component/Plugin_Helpers.res.mjs +11 -10
- package/src/plugin/component/Plugin_Structure.res +349 -274
- package/src/plugin/component/Plugin_Structure.res.mjs +212 -175
- package/src/plugin/component/Plugin_SubscriptionSchema.res +1 -1
- package/src/plugin/component/Plugin_SubscriptionSchema.res.mjs +1 -1
- package/src/plugin/component/SchemaWalker.res +11 -11
- package/src/plugin/component/SchemaWalker.res.mjs +8 -9
- package/src/plugin/connect/PluginExtensionPoint_Plugin.res.mjs +15 -15
- package/src/plugin/lifecycle/PluginBehavior.res.mjs +55 -55
- package/src/plugin/lifecycle/PluginSpec.res +22 -7
- package/src/plugin/lifecycle/PluginSpec.res.mjs +91 -36
- package/src/plugin/lifecycle/PluginsReadModelSpec.res +36 -9
- package/src/plugin/lifecycle/PluginsReadModelSpec.res.mjs +33 -22
- package/src/util/Interstack.res +3 -3
- package/src/util/LogFormat.res +1 -2
- package/src/util/LogFormat.res.mjs +2 -2
- package/tests/IdentityTest.res +6 -7
- package/tests/IdentityTest.res.mjs +7 -9
- package/tests/adapter/MonitoringTest.res +30 -3
- package/tests/adapter/MonitoringTest.res.mjs +31 -12
- package/tests/adapter/RuntimeExtensionHookReachTest.res +0 -1
- package/tests/adapter/RuntimeExtensionHookReachTest.res.mjs +5 -7
- package/tests/admin/Platform_Admin_StructureTest.res +254 -0
- package/tests/admin/Platform_Admin_StructureTest.res.mjs +219 -0
- package/tests/aggregate/AggregateCacheTest.res +0 -1
- package/tests/aggregate/AggregateCacheTest.res.mjs +13 -15
- package/tests/aggregate/AggregateConflictTest.res +0 -1
- package/tests/aggregate/AggregateConflictTest.res.mjs +7 -9
- package/tests/aggregate/AggregateFixtures.res +0 -1
- package/tests/aggregate/AggregateFixtures.res.mjs +19 -21
- package/tests/aggregate/AggregateSnapshotTest.res +1 -2
- package/tests/aggregate/AggregateSnapshotTest.res.mjs +15 -16
- package/tests/api/ApiNoApiTest.res.mjs +17 -9
- package/tests/api/GraphQL_FragmentGeneratorTest.res +62 -1
- package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +102 -58
- package/tests/api/PsInternalFieldsReadModel.res +21 -0
- package/tests/api/PsInternalFieldsReadModel.res.mjs +74 -0
- package/tests/api/StorageRefFieldsTest.res.mjs +13 -13
- package/tests/api/SuryToJsonSchemaTest.res +43 -0
- package/tests/api/SuryToJsonSchemaTest.res.mjs +352 -849
- package/tests/commandgenerator/CommandGeneratorFixtures.res +0 -1
- package/tests/commandgenerator/CommandGeneratorFixtures.res.mjs +19 -21
- package/tests/commandgenerator/OwnerStampingTest.res +0 -1
- package/tests/commandgenerator/OwnerStampingTest.res.mjs +9 -11
- package/tests/commandtopic/CommandTopicCallbackFixtures.res +0 -1
- package/tests/commandtopic/CommandTopicCallbackFixtures.res.mjs +7 -9
- package/tests/counter/CounterFixtures.res +0 -1
- package/tests/counter/CounterFixtures.res.mjs +1 -4
- package/tests/dcb/DcbDecodeTest.res.mjs +24 -24
- package/tests/dcb/DcbEventLogOperationsTest.res +2 -2
- package/tests/dcb/DcbEventLogOperationsTest.res.mjs +3 -3
- package/tests/dcb/DcbEventLogSchemaTest.res.mjs +7 -7
- package/tests/dcb/DcbFixtures.res +0 -1
- package/tests/dcb/DcbFixtures.res.mjs +87 -89
- package/tests/dcb/DcbTagTest.res.mjs +6 -6
- package/tests/dcb/DcbValidationTest.res.mjs +52 -52
- package/tests/eventlog/EventLogFixtures.res +0 -1
- package/tests/eventlog/EventLogFixtures.res.mjs +7 -9
- package/tests/eventmapper/EventMapperFixtures.res +0 -1
- package/tests/eventmapper/EventMapperFixtures.res.mjs +24 -26
- package/tests/eventtopic/EventTopicFixtures.res +0 -1
- package/tests/eventtopic/EventTopicFixtures.res.mjs +7 -9
- package/tests/extensionpoint/ExtensionPointFixtures.res +0 -1
- package/tests/extensionpoint/ExtensionPointFixtures.res.mjs +10 -12
- package/tests/extensionpoint/ExtensionPointOperationsTest.res +1 -2
- package/tests/extensionpoint/ExtensionPointOperationsTest.res.mjs +9 -10
- package/tests/logger/LogFormatTest.res +3 -2
- package/tests/logger/LogFormatTest.res.mjs +7 -9
- package/tests/message/MessageTest.res +2 -2
- package/tests/message/MessageTest.res.mjs +8 -7
- package/tests/message/MetaEnvelopeTest.res +5 -5
- package/tests/message/MetaEnvelopeTest.res.mjs +12 -11
- package/tests/plugin/ManifestVisibilityTest.res.mjs +7 -7
- package/tests/plugin/PluginDefinitionScalars.res +5 -3
- package/tests/plugin/PluginDefinitionScalars.res.mjs +2 -2
- package/tests/plugin/PluginLifecycleCorpusTest.res +4 -3
- package/tests/plugin/PluginLifecycleCorpusTest.res.mjs +3 -6
- package/tests/plugin/PluginSpecExperiment.res +0 -1
- package/tests/plugin/PluginSpecExperiment.res.mjs +12 -14
- package/tests/plugin/PluginStructureTest.res +188 -1
- package/tests/plugin/PluginStructureTest.res.mjs +293 -102
- package/tests/plugin/Plugin_EventQuerySchemaTest.res.mjs +3 -3
- package/tests/plugin/SchemaWalkerTest.res +0 -1
- package/tests/plugin/SchemaWalkerTest.res.mjs +17 -19
- package/tests/plugin/StateChangeSlice/PsAttachInvoice.res.mjs +5 -5
- package/tests/plugin/StateChangeSlice/PsChangePhoto.res.mjs +8 -8
- package/tests/plugin/StateChangeSlice/PsDispatchShipment.res.mjs +5 -5
- package/tests/plugin/StateChangeSlice/PsGatedCommands.res.mjs +9 -9
- package/tests/plugin/StateChangeSlice/PsPlaceOrder.res.mjs +5 -5
- package/tests/plugin/StateChangeSlice/PsReserveStock.res.mjs +8 -8
- package/tests/plugin/StateChangeSlice/PsShipOrder.res.mjs +15 -15
- package/tests/plugin/StateChangeSlice/PsTypoStore.res +31 -0
- package/tests/plugin/StateChangeSlice/PsTypoStore.res.mjs +62 -0
- package/tests/plugin/StateChangeSlice/PsUploadAvatar.res.mjs +8 -8
- package/tests/plugin/StateChangeSlice/PsUploadImages.res +40 -0
- package/tests/plugin/StateChangeSlice/PsUploadImages.res.mjs +66 -0
- package/tests/plugin/StateViewSlice/PsAnnotatedView.res.mjs +13 -12
- package/tests/plugin/StateViewSlice/PsAvailableProductsView.res.mjs +8 -8
- package/tests/plugin/StateViewSlice/PsCategoriesView.res.mjs +6 -6
- package/tests/plugin/StateViewSlice/PsCustomersView.res.mjs +10 -10
- package/tests/plugin/StateViewSlice/PsGatedView.res.mjs +6 -6
- package/tests/plugin/StateViewSlice/PsOrdersView.res.mjs +9 -9
- package/tests/plugin/StateViewSlice/PsShipmentsView.res.mjs +11 -10
- package/tests/plugin/pluginDefinitionRequiredScalars.txt +15 -0
- package/tests/querydb/QueryDbFixtures.res +0 -1
- package/tests/querydb/QueryDbFixtures.res.mjs +5 -7
- package/tests/sideeffecthandler/SideEffectHandlerFixtures.res +0 -1
- package/tests/sideeffecthandler/SideEffectHandlerFixtures.res.mjs +4 -6
- package/tests/util/CommandPublisherTest.res +0 -1
- package/tests/util/CommandPublisherTest.res.mjs +4 -6
|
@@ -41,8 +41,8 @@ let rec isLifecycleShape = (t: SchemaType.schemaType): bool =>
|
|
|
41
41
|
// and *exactly*: `customerName` holds a customer's name, not this record's.
|
|
42
42
|
let conventionalLabelNames = ["name", "title", "label", "displayname"]
|
|
43
43
|
|
|
44
|
-
let
|
|
45
|
-
SchemaType.fromSury(~parentName=entityName, ~fieldName=
|
|
44
|
+
let shapeOfField = (~entityName: string, ~name: string, schema: S.t<unknown>): SchemaType.schemaType =>
|
|
45
|
+
SchemaType.fromSury(~parentName=entityName, ~fieldName=name, schema)
|
|
46
46
|
|
|
47
47
|
// Resolve the field that holds the entity's lifecycle, used to filter a per-row
|
|
48
48
|
// command menu against each command's `allowedStates`. Resolution order:
|
|
@@ -69,12 +69,14 @@ let lifecycleFieldFromStateSchema = (
|
|
|
69
69
|
| Some(_) as some => some
|
|
70
70
|
| None =>
|
|
71
71
|
switch stateSchema {
|
|
72
|
-
| Object({
|
|
73
|
-
|
|
74
|
-
->
|
|
75
|
-
|
|
72
|
+
| Object({properties}) =>
|
|
73
|
+
properties
|
|
74
|
+
->Dict.get("lifecycle")
|
|
75
|
+
->Option.flatMap(schema =>
|
|
76
|
+
isLifecycleShape(shapeOfField(~entityName, ~name="lifecycle", schema))
|
|
77
|
+
? Some("lifecycle")
|
|
78
|
+
: None
|
|
76
79
|
)
|
|
77
|
-
->Option.map(item => item.location)
|
|
78
80
|
| _ => None
|
|
79
81
|
}
|
|
80
82
|
}
|
|
@@ -171,11 +173,11 @@ let checkRetiredValue = (~entityName: string, stateSchema: S.t<unknown>): retire
|
|
|
171
173
|
)
|
|
172
174
|
}
|
|
173
175
|
let declared = switch stateSchema {
|
|
174
|
-
| Object({
|
|
175
|
-
|
|
176
|
-
->
|
|
177
|
-
->Option.map(
|
|
178
|
-
switch
|
|
176
|
+
| Object({properties}) =>
|
|
177
|
+
properties
|
|
178
|
+
->Dict.get(field)
|
|
179
|
+
->Option.map(schema =>
|
|
180
|
+
switch shapeOfField(~entityName, ~name=field, schema) {
|
|
179
181
|
| Enum(_, values) => values
|
|
180
182
|
| Nullable(Enum(_, values)) => values
|
|
181
183
|
| _ => []
|
|
@@ -245,11 +247,11 @@ let lifecycleStatesFromStateSchema = (
|
|
|
245
247
|
): option<array<string>> =>
|
|
246
248
|
lifecycleFieldFromStateSchema(~entityName, stateSchema)->Option.flatMap(field =>
|
|
247
249
|
switch stateSchema {
|
|
248
|
-
| Object({
|
|
249
|
-
|
|
250
|
-
->
|
|
251
|
-
->Option.map(
|
|
252
|
-
switch
|
|
250
|
+
| Object({properties}) =>
|
|
251
|
+
properties
|
|
252
|
+
->Dict.get(field)
|
|
253
|
+
->Option.map(schema =>
|
|
254
|
+
switch shapeOfField(~entityName, ~name=field, schema) {
|
|
253
255
|
| Enum(_, values) => values
|
|
254
256
|
| Nullable(Enum(_, values)) => values
|
|
255
257
|
| _ => []
|
|
@@ -377,27 +379,21 @@ let lifecycleTopologyFindings = (
|
|
|
377
379
|
lifecycleStatesByView
|
|
378
380
|
->Dict.toArray
|
|
379
381
|
->Array.forEach(((view, states)) => {
|
|
380
|
-
// Every
|
|
381
|
-
|
|
382
|
+
// Every state some command declares a transition INTO. The question this
|
|
383
|
+
// check asks is only about arrival, so it reads the target and never the
|
|
384
|
+
// from-set — which is what lets a creating command (`@transition(() => X)`,
|
|
385
|
+
// a target and no from-set, because it runs from no row) count towards
|
|
386
|
+
// reachability the same way an edge out of another state does.
|
|
387
|
+
let reachable = writables->Array.reduce([], (acc, w) =>
|
|
382
388
|
w.linkedViews->Array.includes(view)
|
|
383
|
-
? Array.concat(
|
|
384
|
-
acc,
|
|
385
|
-
w.commands->Array.reduce([], (inner, cmd) =>
|
|
386
|
-
switch (cmd.allowedStates, cmd.targetState) {
|
|
387
|
-
| (Some(froms), Some(to)) =>
|
|
388
|
-
Array.concat(inner, froms->Array.map(from => (from, to)))
|
|
389
|
-
| _ => inner
|
|
390
|
-
}
|
|
391
|
-
),
|
|
392
|
-
)
|
|
389
|
+
? Array.concat(acc, w.commands->Array.filterMap(cmd => cmd.targetState))
|
|
393
390
|
: acc
|
|
394
391
|
)
|
|
395
|
-
if Array.length(
|
|
392
|
+
if Array.length(reachable) > 0 {
|
|
396
393
|
// Rows start in the first declared state — the same convention the
|
|
397
394
|
// lifecycle diagram uses — so nothing pointing at it is expected rather
|
|
398
395
|
// than suspicious.
|
|
399
396
|
let initial = states->Array.get(0)
|
|
400
|
-
let reachable = edges->Array.map(((_, to)) => to)
|
|
401
397
|
|
|
402
398
|
states->Array.forEach(state => {
|
|
403
399
|
if !(reachable->Array.includes(state)) && Some(state) != initial {
|
|
@@ -523,26 +519,26 @@ let labelFieldsFromStateSchema = (
|
|
|
523
519
|
| Some(spec) => {field: "displayName", searchableFields: spec.fields, source: Annotation}
|
|
524
520
|
| None =>
|
|
525
521
|
let candidates = switch stateSchema {
|
|
526
|
-
| Object({
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
isLabelShape(
|
|
522
|
+
| Object({properties}) =>
|
|
523
|
+
properties
|
|
524
|
+
->Dict.toArray
|
|
525
|
+
->Array.filter(((name, schema)) =>
|
|
526
|
+
name != "TAG" && name != "id" && isLabelShape(shapeOfField(~entityName, ~name, schema))
|
|
531
527
|
)
|
|
532
528
|
| _ => []
|
|
533
529
|
}
|
|
534
|
-
let conventional = candidates->Array.find(
|
|
535
|
-
let lower =
|
|
530
|
+
let conventional = candidates->Array.find(((name, _)) => {
|
|
531
|
+
let lower = name->String.toLowerCase
|
|
536
532
|
conventionalLabelNames->Array.some(n => n == lower)
|
|
537
533
|
})
|
|
538
534
|
let picked = switch conventional {
|
|
539
|
-
| Some(
|
|
540
|
-
| None => candidates->Array.get(0)->Option.map(
|
|
535
|
+
| Some((name, _)) => Some((name, Convention))
|
|
536
|
+
| None => candidates->Array.get(0)->Option.map(((name, _)) => (name, Position))
|
|
541
537
|
}
|
|
542
538
|
switch picked {
|
|
543
|
-
| Some((
|
|
544
|
-
field:
|
|
545
|
-
searchableFields: [
|
|
539
|
+
| Some((name, source)) => {
|
|
540
|
+
field: name,
|
|
541
|
+
searchableFields: [name],
|
|
546
542
|
source,
|
|
547
543
|
}
|
|
548
544
|
| None =>
|
|
@@ -613,7 +609,7 @@ let toEventDef = (v: S.t<unknown>): option<Reventless.Plugin.eventDef> => {
|
|
|
613
609
|
|
|
614
610
|
let extractEventDefs = (eventSchema: S.t<unknown>): array<Reventless.Plugin.eventDef> =>
|
|
615
611
|
switch eventSchema {
|
|
616
|
-
|
|
|
612
|
+
| AnyOf({anyOf}) => anyOf->Array.filterMap(toEventDef)
|
|
617
613
|
| _ => toEventDef(eventSchema)->Option.mapOr([], def => [def])
|
|
618
614
|
}
|
|
619
615
|
|
|
@@ -627,6 +623,302 @@ let extractErrorDefs = (errorSchema: S.t<unknown>): array<Reventless.Plugin.erro
|
|
|
627
623
|
{name, schema, references}: Reventless.Plugin.errorDef
|
|
628
624
|
))
|
|
629
625
|
|
|
626
|
+
// The command walk, module-level for the same reason as the event walk above:
|
|
627
|
+
// the synthetic Platform_Admin structure cannot reach `make` — it has no
|
|
628
|
+
// `module(Aggregate.T)` to hand it — and hand-writing a second copy of this walk
|
|
629
|
+
// is what let its command metadata drift from the SDL generated off the same
|
|
630
|
+
// schema.
|
|
631
|
+
|
|
632
|
+
// Aggregate commands that initialize a new aggregate instance are Collection-level
|
|
633
|
+
// (shown as table-top buttons); all others are Instance-level (shown per-row).
|
|
634
|
+
let isCreateCommandName = name =>
|
|
635
|
+
["Add", "Create", "Register", "Open", "Initialize", "Submit", "Start", "Place"]->Array.some(p =>
|
|
636
|
+
name->String.startsWith(p)
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
let commandLevelAndId = (~isAggregate, ~variantName, properties: dict<S.t<unknown>>) =>
|
|
640
|
+
if isAggregate {
|
|
641
|
+
if isCreateCommandName(variantName) {
|
|
642
|
+
(Reventless.Plugin.Collection, None)
|
|
643
|
+
} else {
|
|
644
|
+
(Reventless.Plugin.Instance, None)
|
|
645
|
+
}
|
|
646
|
+
} else {
|
|
647
|
+
let taggedFields =
|
|
648
|
+
properties
|
|
649
|
+
->Dict.toArray
|
|
650
|
+
->Array.filter(((fieldName, fieldSchema)) =>
|
|
651
|
+
fieldName != "TAG" &&
|
|
652
|
+
(Reventless.DcbTag.isTagged(fieldSchema) ||
|
|
653
|
+
Reventless.DcbTag.isTaggedArray(fieldSchema))
|
|
654
|
+
)
|
|
655
|
+
let taggedField =
|
|
656
|
+
taggedFields
|
|
657
|
+
->Array.find(((_, fieldSchema)) => Reventless.DcbTag.isPartitionTag(fieldSchema))
|
|
658
|
+
->Option.orElse(taggedFields->Array.get(0))
|
|
659
|
+
switch taggedField {
|
|
660
|
+
| Some((fieldName, _)) =>
|
|
661
|
+
if isCreateCommandName(variantName) {
|
|
662
|
+
// Creation command: Collection-level, but UUID is injected into the tagged ID field.
|
|
663
|
+
(Reventless.Plugin.Collection, Some(fieldName))
|
|
664
|
+
} else {
|
|
665
|
+
(Reventless.Plugin.Instance, Some(fieldName))
|
|
666
|
+
}
|
|
667
|
+
| None => (Reventless.Plugin.Collection, None)
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// A rule the server enforces, expressed as the keys a client checks against
|
|
672
|
+
// `identity.groups ++ config.accessTiers`. `AllowGroups` is satisfied by ANY of
|
|
673
|
+
// its groups (`Authorization.isAllowed` is `some`, not `every`), so the array is
|
|
674
|
+
// an any-of and a client must read it that way.
|
|
675
|
+
//
|
|
676
|
+
// `AllowAuthenticated` / `AllowAnonymous` ask for nothing a client can check —
|
|
677
|
+
// anyone holding a session already satisfies them — so they publish no keys
|
|
678
|
+
// rather than a key everyone holds.
|
|
679
|
+
//
|
|
680
|
+
// `DenyAll` also publishes none, deliberately. An unsatisfiable key would render
|
|
681
|
+
// as locked-with-upsell in a tiered shell: a surface advertised as purchasable
|
|
682
|
+
// that no purchase unlocks. A component nobody may call belongs in no menu, and
|
|
683
|
+
// that is an omission for the enumerating side to make, not a key to invent here.
|
|
684
|
+
let accessKeysFor = (rule: Reventless.Authorization.permission): option<array<string>> =>
|
|
685
|
+
switch rule {
|
|
686
|
+
| AllowGroups(groups) if groups->Array.length > 0 => Some(groups)
|
|
687
|
+
| AllowGroups(_) | AllowAuthenticated | AllowAnonymous | DenyAll => None
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Write each mutation argument's rendered GraphQL type onto the property it
|
|
691
|
+
// belongs to, so a consumer assembling its own mutation document declares the
|
|
692
|
+
// variable the server actually expects instead of guessing `String!`.
|
|
693
|
+
//
|
|
694
|
+
// Mutates the freshly derived schema in place — `deriveObjectSchema` has just
|
|
695
|
+
// built it and nothing else holds it yet. A property with no matching
|
|
696
|
+
// argument is left alone rather than annotated with a guess.
|
|
697
|
+
let annotateArgTypes = (schema: JSON.t, argTypes: dict<string>): JSON.t => {
|
|
698
|
+
schema
|
|
699
|
+
->JSON.Decode.object
|
|
700
|
+
->Option.flatMap(o => o->Dict.get("properties"))
|
|
701
|
+
->Option.flatMap(JSON.Decode.object)
|
|
702
|
+
->Option.forEach(props =>
|
|
703
|
+
props
|
|
704
|
+
->Dict.toArray
|
|
705
|
+
->Array.forEach(((key, prop)) =>
|
|
706
|
+
switch (argTypes->Dict.get(key), prop->JSON.Decode.object) {
|
|
707
|
+
| (Some(gqlType), Some(p)) =>
|
|
708
|
+
p->Dict.set("x-reventless-graphql-type", JSON.Encode.string(gqlType))
|
|
709
|
+
| _ => ()
|
|
710
|
+
}
|
|
711
|
+
)
|
|
712
|
+
)
|
|
713
|
+
schema
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
let toCommandDef = (
|
|
717
|
+
~isAggregate,
|
|
718
|
+
~mutationFieldFor: string => string,
|
|
719
|
+
~parentSchema: S.t<unknown>,
|
|
720
|
+
// The PPX-generated `command => permission`. Per VARIANT, not per component:
|
|
721
|
+
// one aggregate carries commands with very different audiences, and a
|
|
722
|
+
// component-level shortcut would gate `AddProduct` and `PlaceOrder` alike.
|
|
723
|
+
~commandAuthorization: unknown => Reventless.Authorization.permission,
|
|
724
|
+
v: S.t<unknown>,
|
|
725
|
+
): option<Reventless.Plugin.commandDef> => {
|
|
726
|
+
// Build a commandDef for one variant. `properties` is the variant's field dict —
|
|
727
|
+
// empty for a payload-less variant (e.g. `| Archive`), which compiles to a bare
|
|
728
|
+
// `S.literal("Archive")` string rather than an `{TAG, ...}` object.
|
|
729
|
+
let mkDef = (~variantName, ~properties) => {
|
|
730
|
+
let (level, aggregateIdField) = commandLevelAndId(~isAggregate, ~variantName, properties)
|
|
731
|
+
let references = extractReferences(properties)
|
|
732
|
+
// Per-variant `allowedStates` lives on the *parent* command schema
|
|
733
|
+
// (the PPX attaches a single dict<variantName, [|states|]> via
|
|
734
|
+
// markAllowedStates). Look it up by variant name; back-compat
|
|
735
|
+
// None when the variant lacks a @transition annotation.
|
|
736
|
+
let allowedStates = ApiAllowedStatesHelpers.getAllowedStates(parentSchema, ~variantName)
|
|
737
|
+
// The `@transition` target (the command's *to* status), read the same way
|
|
738
|
+
// as allowedStates. None ⇒ AutoUI's board resolver falls back to its
|
|
739
|
+
// name-stem heuristic.
|
|
740
|
+
let targetState = ApiTargetStateHelpers.getTargetState(parentSchema, ~variantName)
|
|
741
|
+
// API-exposed iff the whole command isn't @noApi and this variant
|
|
742
|
+
// isn't in its @noApi-variants set — mirrors the API-generation filter
|
|
743
|
+
// (Plugin_Helpers / PluginBaseFragment). Drives the event-graph API badge.
|
|
744
|
+
let apiExposed =
|
|
745
|
+
!ApiNoApiHelpers.isNoApi(parentSchema) &&
|
|
746
|
+
switch ApiNoApiHelpers.getExcludedVariants(parentSchema) {
|
|
747
|
+
| Some(excluded) => !(excluded->Set.has(variantName))
|
|
748
|
+
| None => true
|
|
749
|
+
}
|
|
750
|
+
// Evaluated against a synthetic value per constructor, the same shape the
|
|
751
|
+
// resolver builds at call time: a payload-bearing variant compiles to
|
|
752
|
+
// `{TAG, ...}`, a payload-less one to a bare string.
|
|
753
|
+
let syntheticCommand: unknown =
|
|
754
|
+
Reventless.DcbTag.isVariantPayloadBearing(parentSchema, variantName)
|
|
755
|
+
? {"TAG": variantName}->Obj.magic
|
|
756
|
+
: variantName->Obj.magic
|
|
757
|
+
let requiredAccess = accessKeysFor(commandAuthorization(syntheticCommand))
|
|
758
|
+
// See the note on the record's `mutationField` for why a non-exposed
|
|
759
|
+
// variant gets the empty sentinel. It has no callable field, and the
|
|
760
|
+
// argument type names are composed *from* that field name, so there is
|
|
761
|
+
// nothing to publish for it either.
|
|
762
|
+
let mutationField = apiExposed ? mutationFieldFor(variantName) : ""
|
|
763
|
+
let jsonSchema = v->SuryToJsonSchema.deriveObjectSchema
|
|
764
|
+
let annotatedSchema = if apiExposed {
|
|
765
|
+
GraphQL_FragmentGenerator.mutationArgTypes(~fieldName=mutationField, v)->Option.mapOr(
|
|
766
|
+
jsonSchema,
|
|
767
|
+
annotateArgTypes(jsonSchema, _),
|
|
768
|
+
)
|
|
769
|
+
} else {
|
|
770
|
+
jsonSchema
|
|
771
|
+
}
|
|
772
|
+
({
|
|
773
|
+
Reventless.Plugin.name: variantName,
|
|
774
|
+
// The derived schema, not sury's raw one: `S.toJSONSchema` carries the
|
|
775
|
+
// shape and drops every `x-reventless-*` marker the PPX put on the
|
|
776
|
+
// fields, so a command's `@storageRef`/`@semantic`/`@ref` reached the
|
|
777
|
+
// wire on the read side and nowhere on the write side. A reader that
|
|
778
|
+
// matches a field against its setter — or picks the upload endpoint of
|
|
779
|
+
// the store a command argument declares — then has nothing to match on.
|
|
780
|
+
// `MCP_SchemaGenerator` already derives these same variant schemas.
|
|
781
|
+
//
|
|
782
|
+
// Carries `x-reventless-graphql-type` per property — see
|
|
783
|
+
// `annotateArgTypes`.
|
|
784
|
+
schema: annotatedSchema->JSON.stringify,
|
|
785
|
+
level,
|
|
786
|
+
aggregateIdField,
|
|
787
|
+
// A non-exposed (`@noApi`) variant has no callable mutation field. For a
|
|
788
|
+
// single-exposed-command slice, `mutationFieldFor` resolves *every*
|
|
789
|
+
// variant — including the `@noApi` one — to the slice's one mutation
|
|
790
|
+
// field, so emitting it here would hand the non-exposed variant a
|
|
791
|
+
// sibling's callable-looking field (e.g. `ReopenOrder` →
|
|
792
|
+
// `Ordering_CancelOrder`). Emit an empty sentinel instead; the variant
|
|
793
|
+
// stays listed with `apiExposed: false` for the event-graph badge, but
|
|
794
|
+
// no consumer can mistake it for a callable field. Exposed variants are
|
|
795
|
+
// byte-identical.
|
|
796
|
+
mutationField,
|
|
797
|
+
references,
|
|
798
|
+
allowedStates,
|
|
799
|
+
targetState,
|
|
800
|
+
apiExposed: Some(apiExposed),
|
|
801
|
+
requiredAccess,
|
|
802
|
+
// Resolved from this constructor's own properties, not the union's: two
|
|
803
|
+
// commands in one slice can disagree about whether they record an owner,
|
|
804
|
+
// and the write path stamps per constructor for the same reason.
|
|
805
|
+
ownerField: Reventless.Owner.fieldNamesOfProperties(properties)->Array.get(0),
|
|
806
|
+
}: Reventless.Plugin.commandDef)
|
|
807
|
+
}
|
|
808
|
+
switch v {
|
|
809
|
+
| Object({properties}) =>
|
|
810
|
+
properties
|
|
811
|
+
->Dict.get("TAG")
|
|
812
|
+
->Option.flatMap(tagSchema =>
|
|
813
|
+
switch tagSchema {
|
|
814
|
+
| String({const: ?Some(variantName)}) => Some(mkDef(~variantName, ~properties))
|
|
815
|
+
| _ => None
|
|
816
|
+
}
|
|
817
|
+
)
|
|
818
|
+
// Payload-less command variants (`| Archive`) compile to a bare string literal,
|
|
819
|
+
// not an `{TAG, ...}` object. They still get a generated mutation (API generation
|
|
820
|
+
// walks the schema via extractAllVariantNames), so surface them here too —
|
|
821
|
+
// otherwise the event graph hides a command the API actually exposes.
|
|
822
|
+
| String({const: ?Some(variantName)}) => Some(mkDef(~variantName, ~properties=Dict.make()))
|
|
823
|
+
| _ => None
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
let extractCommandDefs = (
|
|
828
|
+
~isAggregate,
|
|
829
|
+
~mutationFieldFor: string => string,
|
|
830
|
+
~commandAuthorization: unknown => Reventless.Authorization.permission,
|
|
831
|
+
commandSchema: S.t<unknown>,
|
|
832
|
+
): array<Reventless.Plugin.commandDef> =>
|
|
833
|
+
switch commandSchema {
|
|
834
|
+
| AnyOf({anyOf}) =>
|
|
835
|
+
anyOf->Array.filterMap(v =>
|
|
836
|
+
toCommandDef(
|
|
837
|
+
~isAggregate,
|
|
838
|
+
~mutationFieldFor,
|
|
839
|
+
~parentSchema=commandSchema,
|
|
840
|
+
~commandAuthorization,
|
|
841
|
+
v,
|
|
842
|
+
)
|
|
843
|
+
)
|
|
844
|
+
| _ =>
|
|
845
|
+
// Single-variant command types compile to a bare Object schema, not a Union.
|
|
846
|
+
toCommandDef(
|
|
847
|
+
~isAggregate,
|
|
848
|
+
~mutationFieldFor,
|
|
849
|
+
~parentSchema=commandSchema,
|
|
850
|
+
~commandAuthorization,
|
|
851
|
+
commandSchema,
|
|
852
|
+
)->Option.mapOr([], def => [def])
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Omitted for the default `Public` so a published def stays compact: absent and
|
|
856
|
+
// "public" are the same answer, and writing one would make every ordinary view
|
|
857
|
+
// carry a word that says nothing.
|
|
858
|
+
let visibilityTag = (v: Reventless.Visibility.t): option<string> =>
|
|
859
|
+
switch v {
|
|
860
|
+
| Public => None
|
|
861
|
+
| Internal => Some("Internal")
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
A read model's published `queryableDef`, assembled from its **spec** rather than
|
|
866
|
+
from a built component.
|
|
867
|
+
|
|
868
|
+
`make` cannot serve the platform's own components: it takes
|
|
869
|
+
`module(ReadModel.T)` values, and the platform has no built module to hand
|
|
870
|
+
itself at structure-assembly time — the components it is describing are the ones
|
|
871
|
+
being assembled. That is a real constraint and this routes around it rather than
|
|
872
|
+
pretending it away, by taking the schemas the extractors actually need.
|
|
873
|
+
|
|
874
|
+
Everything here is the same helper `make` calls on the same schema, so the
|
|
875
|
+
platform's own view is described by the mechanism that describes everyone
|
|
876
|
+
else's. The alternative — a hand-written record — is what let the admin's
|
|
877
|
+
metadata drift from the SDL generated off the same spec, four times over.
|
|
878
|
+
|
|
879
|
+
`queryField` and `singleQueryField` come from `Api_Naming`, which is the only
|
|
880
|
+
place that decides how a name singularises. For the admin these are
|
|
881
|
+
byte-identical to the names it used to hand-write; `singularize` already handled
|
|
882
|
+
the plural-spec-name / singular-type shape that looked bespoke.
|
|
883
|
+
*/
|
|
884
|
+
let queryableDefFromSpec = (
|
|
885
|
+
~plugin: string,
|
|
886
|
+
~name: string,
|
|
887
|
+
~stateSchema: S.t<unknown>,
|
|
888
|
+
~authorization: Reventless.Authorization.permission,
|
|
889
|
+
~visibility: Reventless.Visibility.t=Public,
|
|
890
|
+
~consumedEventTypes: array<string>=[],
|
|
891
|
+
~linkedWriteSide: array<string>=[],
|
|
892
|
+
~chapter: option<string>=?,
|
|
893
|
+
): Reventless.Plugin.queryableDef => {
|
|
894
|
+
let qf = Api_Naming.queryFieldNamesForReadModel(~plugin, ~name)
|
|
895
|
+
let label = labelFieldsFromStateSchema(~entityName=name, stateSchema)
|
|
896
|
+
// The same call the capability deriver makes, so the published key and the key
|
|
897
|
+
// the generated filter/order-by is built from cannot disagree.
|
|
898
|
+
let keyField = GraphQL_FragmentGenerator.resolveKeyField(~entityName=name, stateSchema)
|
|
899
|
+
{
|
|
900
|
+
Reventless.Plugin.name: name,
|
|
901
|
+
queryField: qf.listFieldName,
|
|
902
|
+
schema: stateSchema->SuryToJsonSchema.deriveObjectSchema->JSON.stringify,
|
|
903
|
+
consumedEventTypes,
|
|
904
|
+
linkedWriteSide,
|
|
905
|
+
labelField: label.field,
|
|
906
|
+
searchableFields: label.searchableFields,
|
|
907
|
+
labelFieldSource: Some(labelFieldSourceToString(label.source)),
|
|
908
|
+
lifecycleField: lifecycleFieldFromStateSchema(~entityName=name, stateSchema),
|
|
909
|
+
ownerField: Reventless.Owner.fieldNames(stateSchema)->Array.get(0),
|
|
910
|
+
retiredField: retiredFieldFromStateSchema(stateSchema),
|
|
911
|
+
retiredValues: retiredValuesFromStateSchema(stateSchema),
|
|
912
|
+
namedWhenRetired: Some(namedWhenRetiredFromStateSchema(stateSchema)),
|
|
913
|
+
visibility: visibilityTag(visibility),
|
|
914
|
+
chapter,
|
|
915
|
+
singleQueryField: Some(qf.singleFieldName),
|
|
916
|
+
idField: keyField->Option.map(((f, _)) => f),
|
|
917
|
+
idFieldSource: keyField->Option.map(((_, rung)) => rung),
|
|
918
|
+
requiredAccess: accessKeysFor(authorization),
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
630
922
|
let make = (
|
|
631
923
|
type api role,
|
|
632
924
|
~name: string,
|
|
@@ -657,229 +949,6 @@ let make = (
|
|
|
657
949
|
let commandVariantNames = schema => Reventless.DcbTag.extractAllVariantNames(schema)
|
|
658
950
|
let qualify = (~prefix, names) => names->Array.map(n => prefix ++ "." ++ n)
|
|
659
951
|
|
|
660
|
-
// Aggregate commands that initialize a new aggregate instance are Collection-level
|
|
661
|
-
// (shown as table-top buttons); all others are Instance-level (shown per-row).
|
|
662
|
-
let isCreateCommandName = name =>
|
|
663
|
-
["Add", "Create", "Register", "Open", "Initialize", "Submit", "Start", "Place"]->Array.some(p =>
|
|
664
|
-
name->String.startsWith(p)
|
|
665
|
-
)
|
|
666
|
-
|
|
667
|
-
let commandLevelAndId = (~isAggregate, ~variantName, properties: dict<S.t<unknown>>) =>
|
|
668
|
-
if isAggregate {
|
|
669
|
-
if isCreateCommandName(variantName) {
|
|
670
|
-
(Reventless.Plugin.Collection, None)
|
|
671
|
-
} else {
|
|
672
|
-
(Reventless.Plugin.Instance, None)
|
|
673
|
-
}
|
|
674
|
-
} else {
|
|
675
|
-
let taggedFields =
|
|
676
|
-
properties
|
|
677
|
-
->Dict.toArray
|
|
678
|
-
->Array.filter(((fieldName, fieldSchema)) =>
|
|
679
|
-
fieldName != "TAG" &&
|
|
680
|
-
(Reventless.DcbTag.isTagged(fieldSchema) ||
|
|
681
|
-
Reventless.DcbTag.isTaggedArray(fieldSchema))
|
|
682
|
-
)
|
|
683
|
-
let taggedField =
|
|
684
|
-
taggedFields
|
|
685
|
-
->Array.find(((_, fieldSchema)) => Reventless.DcbTag.isPartitionTag(fieldSchema))
|
|
686
|
-
->Option.orElse(taggedFields->Array.get(0))
|
|
687
|
-
switch taggedField {
|
|
688
|
-
| Some((fieldName, _)) =>
|
|
689
|
-
if isCreateCommandName(variantName) {
|
|
690
|
-
// Creation command: Collection-level, but UUID is injected into the tagged ID field.
|
|
691
|
-
(Reventless.Plugin.Collection, Some(fieldName))
|
|
692
|
-
} else {
|
|
693
|
-
(Reventless.Plugin.Instance, Some(fieldName))
|
|
694
|
-
}
|
|
695
|
-
| None => (Reventless.Plugin.Collection, None)
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
// A rule the server enforces, expressed as the keys a client checks against
|
|
700
|
-
// `identity.groups ++ config.accessTiers`. `AllowGroups` is satisfied by ANY of
|
|
701
|
-
// its groups (`Authorization.isAllowed` is `some`, not `every`), so the array is
|
|
702
|
-
// an any-of and a client must read it that way.
|
|
703
|
-
//
|
|
704
|
-
// `AllowAuthenticated` / `AllowAnonymous` ask for nothing a client can check —
|
|
705
|
-
// anyone holding a session already satisfies them — so they publish no keys
|
|
706
|
-
// rather than a key everyone holds.
|
|
707
|
-
//
|
|
708
|
-
// `DenyAll` also publishes none, deliberately. An unsatisfiable key would render
|
|
709
|
-
// as locked-with-upsell in a tiered shell: a surface advertised as purchasable
|
|
710
|
-
// that no purchase unlocks. A component nobody may call belongs in no menu, and
|
|
711
|
-
// that is an omission for the enumerating side to make, not a key to invent here.
|
|
712
|
-
let accessKeysFor = (rule: Reventless.Authorization.permission): option<array<string>> =>
|
|
713
|
-
switch rule {
|
|
714
|
-
| AllowGroups(groups) if groups->Array.length > 0 => Some(groups)
|
|
715
|
-
| AllowGroups(_) | AllowAuthenticated | AllowAnonymous | DenyAll => None
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
// Write each mutation argument's rendered GraphQL type onto the property it
|
|
719
|
-
// belongs to, so a consumer assembling its own mutation document declares the
|
|
720
|
-
// variable the server actually expects instead of guessing `String!`.
|
|
721
|
-
//
|
|
722
|
-
// Mutates the freshly derived schema in place — `deriveObjectSchema` has just
|
|
723
|
-
// built it and nothing else holds it yet. A property with no matching
|
|
724
|
-
// argument is left alone rather than annotated with a guess.
|
|
725
|
-
let annotateArgTypes = (schema: JSON.t, argTypes: dict<string>): JSON.t => {
|
|
726
|
-
schema
|
|
727
|
-
->JSON.Decode.object
|
|
728
|
-
->Option.flatMap(o => o->Dict.get("properties"))
|
|
729
|
-
->Option.flatMap(JSON.Decode.object)
|
|
730
|
-
->Option.forEach(props =>
|
|
731
|
-
props
|
|
732
|
-
->Dict.toArray
|
|
733
|
-
->Array.forEach(((key, prop)) =>
|
|
734
|
-
switch (argTypes->Dict.get(key), prop->JSON.Decode.object) {
|
|
735
|
-
| (Some(gqlType), Some(p)) =>
|
|
736
|
-
p->Dict.set("x-reventless-graphql-type", JSON.Encode.string(gqlType))
|
|
737
|
-
| _ => ()
|
|
738
|
-
}
|
|
739
|
-
)
|
|
740
|
-
)
|
|
741
|
-
schema
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
let toCommandDef = (
|
|
745
|
-
~isAggregate,
|
|
746
|
-
~mutationFieldFor: string => string,
|
|
747
|
-
~parentSchema: S.t<unknown>,
|
|
748
|
-
// The PPX-generated `command => permission`. Per VARIANT, not per component:
|
|
749
|
-
// one aggregate carries commands with very different audiences, and a
|
|
750
|
-
// component-level shortcut would gate `AddProduct` and `PlaceOrder` alike.
|
|
751
|
-
~commandAuthorization: unknown => Reventless.Authorization.permission,
|
|
752
|
-
v: S.t<unknown>,
|
|
753
|
-
): option<Reventless.Plugin.commandDef> => {
|
|
754
|
-
// Build a commandDef for one variant. `properties` is the variant's field dict —
|
|
755
|
-
// empty for a payload-less variant (e.g. `| Archive`), which compiles to a bare
|
|
756
|
-
// `S.literal("Archive")` string rather than an `{TAG, ...}` object.
|
|
757
|
-
let mkDef = (~variantName, ~properties) => {
|
|
758
|
-
let (level, aggregateIdField) = commandLevelAndId(~isAggregate, ~variantName, properties)
|
|
759
|
-
let references = extractReferences(properties)
|
|
760
|
-
// Per-variant `allowedStates` lives on the *parent* command schema
|
|
761
|
-
// (the PPX attaches a single dict<variantName, [|states|]> via
|
|
762
|
-
// markAllowedStates). Look it up by variant name; back-compat
|
|
763
|
-
// None when the variant lacks a @transition annotation.
|
|
764
|
-
let allowedStates = ApiAllowedStatesHelpers.getAllowedStates(parentSchema, ~variantName)
|
|
765
|
-
// The `@transition` target (the command's *to* status), read the same way
|
|
766
|
-
// as allowedStates. None ⇒ AutoUI's board resolver falls back to its
|
|
767
|
-
// name-stem heuristic.
|
|
768
|
-
let targetState = ApiTargetStateHelpers.getTargetState(parentSchema, ~variantName)
|
|
769
|
-
// API-exposed iff the whole command isn't @noApi and this variant
|
|
770
|
-
// isn't in its @noApi-variants set — mirrors the API-generation filter
|
|
771
|
-
// (Plugin_Helpers / PluginBaseFragment). Drives the event-graph API badge.
|
|
772
|
-
let apiExposed =
|
|
773
|
-
!ApiNoApiHelpers.isNoApi(parentSchema) &&
|
|
774
|
-
switch ApiNoApiHelpers.getExcludedVariants(parentSchema) {
|
|
775
|
-
| Some(excluded) => !(excluded->Set.has(variantName))
|
|
776
|
-
| None => true
|
|
777
|
-
}
|
|
778
|
-
// Evaluated against a synthetic value per constructor, the same shape the
|
|
779
|
-
// resolver builds at call time: a payload-bearing variant compiles to
|
|
780
|
-
// `{TAG, ...}`, a payload-less one to a bare string.
|
|
781
|
-
let syntheticCommand: unknown =
|
|
782
|
-
Reventless.DcbTag.isVariantPayloadBearing(parentSchema, variantName)
|
|
783
|
-
? {"TAG": variantName}->Obj.magic
|
|
784
|
-
: variantName->Obj.magic
|
|
785
|
-
let requiredAccess = accessKeysFor(commandAuthorization(syntheticCommand))
|
|
786
|
-
// See the note on the record's `mutationField` for why a non-exposed
|
|
787
|
-
// variant gets the empty sentinel. It has no callable field, and the
|
|
788
|
-
// argument type names are composed *from* that field name, so there is
|
|
789
|
-
// nothing to publish for it either.
|
|
790
|
-
let mutationField = apiExposed ? mutationFieldFor(variantName) : ""
|
|
791
|
-
let jsonSchema = v->SuryToJsonSchema.deriveObjectSchema
|
|
792
|
-
let annotatedSchema = if apiExposed {
|
|
793
|
-
GraphQL_FragmentGenerator.mutationArgTypes(~fieldName=mutationField, v)->Option.mapOr(
|
|
794
|
-
jsonSchema,
|
|
795
|
-
annotateArgTypes(jsonSchema, _),
|
|
796
|
-
)
|
|
797
|
-
} else {
|
|
798
|
-
jsonSchema
|
|
799
|
-
}
|
|
800
|
-
({
|
|
801
|
-
Reventless.Plugin.name: variantName,
|
|
802
|
-
// The derived schema, not sury's raw one: `S.toJSONSchema` carries the
|
|
803
|
-
// shape and drops every `x-reventless-*` marker the PPX put on the
|
|
804
|
-
// fields, so a command's `@storageRef`/`@semantic`/`@ref` reached the
|
|
805
|
-
// wire on the read side and nowhere on the write side. A reader that
|
|
806
|
-
// matches a field against its setter — or picks the upload endpoint of
|
|
807
|
-
// the store a command argument declares — then has nothing to match on.
|
|
808
|
-
// `MCP_SchemaGenerator` already derives these same variant schemas.
|
|
809
|
-
//
|
|
810
|
-
// Carries `x-reventless-graphql-type` per property — see
|
|
811
|
-
// `annotateArgTypes`.
|
|
812
|
-
schema: annotatedSchema->JSON.stringify,
|
|
813
|
-
level,
|
|
814
|
-
aggregateIdField,
|
|
815
|
-
// A non-exposed (`@noApi`) variant has no callable mutation field. For a
|
|
816
|
-
// single-exposed-command slice, `mutationFieldFor` resolves *every*
|
|
817
|
-
// variant — including the `@noApi` one — to the slice's one mutation
|
|
818
|
-
// field, so emitting it here would hand the non-exposed variant a
|
|
819
|
-
// sibling's callable-looking field (e.g. `ReopenOrder` →
|
|
820
|
-
// `Ordering_CancelOrder`). Emit an empty sentinel instead; the variant
|
|
821
|
-
// stays listed with `apiExposed: false` for the event-graph badge, but
|
|
822
|
-
// no consumer can mistake it for a callable field. Exposed variants are
|
|
823
|
-
// byte-identical.
|
|
824
|
-
mutationField,
|
|
825
|
-
references,
|
|
826
|
-
allowedStates,
|
|
827
|
-
targetState,
|
|
828
|
-
apiExposed: Some(apiExposed),
|
|
829
|
-
requiredAccess,
|
|
830
|
-
// Resolved from this constructor's own properties, not the union's: two
|
|
831
|
-
// commands in one slice can disagree about whether they record an owner,
|
|
832
|
-
// and the write path stamps per constructor for the same reason.
|
|
833
|
-
ownerField: Reventless.Owner.fieldNamesOfProperties(properties)->Array.get(0),
|
|
834
|
-
}: Reventless.Plugin.commandDef)
|
|
835
|
-
}
|
|
836
|
-
switch v {
|
|
837
|
-
| Object({properties}) =>
|
|
838
|
-
properties
|
|
839
|
-
->Dict.get("TAG")
|
|
840
|
-
->Option.flatMap(tagSchema =>
|
|
841
|
-
switch tagSchema {
|
|
842
|
-
| String({const: ?Some(variantName)}) => Some(mkDef(~variantName, ~properties))
|
|
843
|
-
| _ => None
|
|
844
|
-
}
|
|
845
|
-
)
|
|
846
|
-
// Payload-less command variants (`| Archive`) compile to a bare string literal,
|
|
847
|
-
// not an `{TAG, ...}` object. They still get a generated mutation (API generation
|
|
848
|
-
// walks the schema via extractAllVariantNames), so surface them here too —
|
|
849
|
-
// otherwise the event graph hides a command the API actually exposes.
|
|
850
|
-
| String({const: ?Some(variantName)}) => Some(mkDef(~variantName, ~properties=Dict.make()))
|
|
851
|
-
| _ => None
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
let extractCommandDefs = (
|
|
856
|
-
~isAggregate,
|
|
857
|
-
~mutationFieldFor: string => string,
|
|
858
|
-
~commandAuthorization: unknown => Reventless.Authorization.permission,
|
|
859
|
-
commandSchema: S.t<unknown>,
|
|
860
|
-
): array<Reventless.Plugin.commandDef> =>
|
|
861
|
-
switch commandSchema {
|
|
862
|
-
| Union({anyOf}) =>
|
|
863
|
-
anyOf->Array.filterMap(v =>
|
|
864
|
-
toCommandDef(
|
|
865
|
-
~isAggregate,
|
|
866
|
-
~mutationFieldFor,
|
|
867
|
-
~parentSchema=commandSchema,
|
|
868
|
-
~commandAuthorization,
|
|
869
|
-
v,
|
|
870
|
-
)
|
|
871
|
-
)
|
|
872
|
-
| _ =>
|
|
873
|
-
// Single-variant command types compile to a bare Object schema, not a Union.
|
|
874
|
-
toCommandDef(
|
|
875
|
-
~isAggregate,
|
|
876
|
-
~mutationFieldFor,
|
|
877
|
-
~parentSchema=commandSchema,
|
|
878
|
-
~commandAuthorization,
|
|
879
|
-
commandSchema,
|
|
880
|
-
)->Option.mapOr([], def => [def])
|
|
881
|
-
}
|
|
882
|
-
|
|
883
952
|
// ── Per-component event type extraction ────────────────────────────────────
|
|
884
953
|
|
|
885
954
|
let scsProduced =
|
|
@@ -1035,7 +1104,7 @@ let make = (
|
|
|
1035
1104
|
| _ => []
|
|
1036
1105
|
}
|
|
1037
1106
|
switch schema {
|
|
1038
|
-
|
|
|
1107
|
+
| AnyOf({anyOf}) => anyOf->Array.flatMap(fromVariant)
|
|
1039
1108
|
| other => fromVariant(other)
|
|
1040
1109
|
}
|
|
1041
1110
|
}
|
|
@@ -1107,6 +1176,18 @@ let make = (
|
|
|
1107
1176
|
->Belt.Set.String.fromArray
|
|
1108
1177
|
->Belt.Set.String.toArray
|
|
1109
1178
|
|
|
1179
|
+
// Two stores one edit apart are a typo that would provision twice. A hard
|
|
1180
|
+
// failure, unlike the heuristic lint below: nothing downstream can see the
|
|
1181
|
+
// mistake, and by deploy time both buckets exist.
|
|
1182
|
+
switch Capability_Inference.collisions(requiredStoreDeclarations) {
|
|
1183
|
+
| [] => ()
|
|
1184
|
+
| found =>
|
|
1185
|
+
JsError.throwWithMessage(
|
|
1186
|
+
`${name}: two declared object stores look like one store misspelled.\n` ++
|
|
1187
|
+
found->Array.map(Capability_Inference.collisionMessage)->Array.join("\n"),
|
|
1188
|
+
)
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1110
1191
|
// Heuristic-only matches — a field *named* like a stored-object ref with no
|
|
1111
1192
|
// `@storageRef` — warn and provision nothing. Declaration outranks inference;
|
|
1112
1193
|
// the warning names the annotation that would settle it.
|
|
@@ -1124,12 +1205,6 @@ let make = (
|
|
|
1124
1205
|
// graph and dead-code analysis — read them so an Internal view still shows up there. The
|
|
1125
1206
|
// deployed AutoUI's consumers (Platform_ComponentDefinitionsApi menu/pages) re-filter on
|
|
1126
1207
|
// the tag so the live UI keeps hiding them — see Visibility.res, which documents this contract.
|
|
1127
|
-
let visibilityTag = (v: Reventless.Visibility.t): option<string> =>
|
|
1128
|
-
switch v {
|
|
1129
|
-
| Public => None
|
|
1130
|
-
| Internal => Some("Internal")
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
1208
|
// View name -> the states its lifecycle field can hold, collected as the view
|
|
1134
1209
|
// defs are built so the transition check below has both sides in one place.
|
|
1135
1210
|
let lifecycleStatesByView: dict<array<string>> = Dict.make()
|