@reventlessdev/reventless-local 3.0.0-alpha.249 → 3.0.0-alpha.250
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 +8 -0
- package/package.json +13 -13
- package/src/BakedManifest.res +5 -6
- package/src/EmitCapabilities.res +3 -1
- package/src/LocalPlatformRegistry.res +3 -1
- package/src/LocalPlatformStart.res +7 -6
- package/src/LocalSeedTarget.res +3 -1
- package/src/Platform.res +291 -215
- package/src/ShellConfig.res +10 -17
- package/src/UiHints.res +8 -14
- package/src/UiSlots.res +10 -17
- package/src/adapter/Api/LocalEvents_Server.res +5 -3
- package/src/adapter/Api/LocalGraphQL_Adapter.res +4 -4
- package/src/adapter/Auth/LocalAuth.res +11 -15
- package/src/adapter/Auth/UserStore.res +5 -7
- package/src/adapter/Backend.res +1 -2
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +42 -26
- package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res +13 -3
- package/src/adapter/CommandTopic/LocalCommandTopicChannel.res +15 -17
- package/src/adapter/CommandTopic/LocalCommandTopicRemoteChannel.res +18 -22
- package/src/adapter/Counter/LocalCounterHandler.res +1 -1
- package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +33 -22
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +57 -57
- package/src/adapter/DomainGraphQL_Server.res +35 -34
- package/src/adapter/DomainMCP_Server.res +45 -41
- package/src/adapter/EventCollector/LocalEventCollectorChannel.res +47 -36
- package/src/adapter/EventHistory/EventHistoryResolvers_GraphQL.res +15 -12
- package/src/adapter/EventLog/EventLogStorage_InMemory.res +9 -4
- package/src/adapter/EventLog/EventLogStorage_Sqlite.res +36 -24
- package/src/adapter/EventTopic/LocalEventTopicPublisher.res +9 -3
- package/src/adapter/Heartbeat/LocalHeartbeatRunner.res +9 -14
- package/src/adapter/LocalBus.res +12 -12
- package/src/adapter/LocalCapabilities.res.mjs +1 -1
- package/src/adapter/LocalGeocodeResolvers.res +1 -4
- package/src/adapter/LocalSideEffectHandler.res +1 -1
- package/src/adapter/LocalStateChangeDescriptor.res +2 -1
- package/src/adapter/MCP_ServerInstance.res +51 -39
- package/src/adapter/ObjectStore/LocalObjectStore.res +5 -2
- package/src/adapter/ObjectStore/ObjectStoreStorage_FileSystem.res +1 -2
- package/src/adapter/PlatformGraphQL_Server.res +9 -5
- package/src/adapter/ProjectionCheckpoint.res +8 -1
- package/src/adapter/ProjectionPending.res +1 -3
- package/src/adapter/QueryDb/LocalQueryDbStorage.res +2 -1
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +367 -269
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +5 -3
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +63 -40
- package/src/adapter/QueryDb/SortKey_Filter.res +14 -6
- package/src/adapter/Runtime/LocalAggregateRuntime_Builder.res +12 -10
- package/src/adapter/Runtime/LocalEventCollectorRuntime_Builder.res +8 -2
- package/src/adapter/Runtime/LocalPluginRuntime_Builder.res +1 -4
- package/src/adapter/Scheduler/LocalScheduledPublisher.res +12 -12
- package/src/adapter/SqliteDriver.res +16 -4
- package/src/adapter/Task/TaskBucket_Sqlite.res +5 -6
- package/src/components/Aggregate_Builder.res +6 -1
- package/src/components/Counter_Builder.res +1 -5
- package/src/components/DcbEventLog_Builder.res +5 -3
- package/src/components/ExtensionPoint_Builder.res +13 -15
- package/src/components/InboundTranslationSlice_Builder.res +3 -2
- package/src/components/OutboundTranslationSlice_Builder.res +3 -2
- package/src/components/ReadModel_Builder.res +18 -20
- package/src/components/StateViewSlice_Builder.res +3 -2
- package/src/components/Task_Builder.res +12 -13
- package/src/reset/LocalSeedReset.res +2 -5
- package/src/test/Mocks/MockDcbEventLogStorage.res +5 -1
- package/src/test/Mocks/MockEventLogStorage.res +8 -3
- package/src/test/Mocks/MockQueryDbStorage.res +3 -2
- package/src/test/TestRunner.res +1 -1
- package/tests/BakedManifestFilesTest.res +7 -10
- package/tests/LocalEventTapTest.res +26 -21
- package/tests/LocalPlatformStartTest.res +33 -28
- package/tests/PluginEventDecodeTest.res +3 -3
- package/tests/ShellConfigTest.res +30 -21
- package/tests/SplitApiFixtures.res +34 -10
- package/tests/SplitApiTest.res +92 -62
- package/tests/adapter/BackendParityTest.res +180 -130
- package/tests/adapter/CollectNEventsTest.res +6 -4
- package/tests/adapter/CommandAuthorizationTest.res +47 -41
- package/tests/adapter/CommandTopicChannelTest.res +163 -139
- package/tests/adapter/CounterHandlerTest.res +95 -77
- package/tests/adapter/DcbEventLogStorageSqliteTest.res +171 -168
- package/tests/adapter/DcbEventLogStorageTest.res +264 -207
- package/tests/adapter/DomainGraphQL_ServerTest.res +53 -43
- package/tests/adapter/EventCollectorChannelTest.res +153 -124
- package/tests/adapter/EventHistoryResolverTest.res +46 -18
- package/tests/adapter/EventLogSnapshotParityTest.res +57 -45
- package/tests/adapter/EventLogStorageSqliteTest.res +9 -10
- package/tests/adapter/EventTapTest.res +37 -20
- package/tests/adapter/EventTopicPublisherTest.res +74 -50
- package/tests/adapter/GraphQLSubscriptionBridgeTest.res +4 -3
- package/tests/adapter/GraphQL_SchemaInspectorTest.res +666 -568
- package/tests/adapter/GraphQL_SubscriptionResolversTest.res +105 -96
- package/tests/adapter/HeartbeatRunnerTest.res +99 -81
- package/tests/adapter/InboundTranslationMutationTest.res +4 -1
- package/tests/adapter/LocalAuthHttpTest.res +79 -85
- package/tests/adapter/LocalAuthLoginTest.res +42 -50
- package/tests/adapter/LocalAuthTest.res +8 -16
- package/tests/adapter/LocalAuthUserStoreTest.res +2 -7
- package/tests/adapter/LocalBusBoundedTest.res +187 -117
- package/tests/adapter/LocalBusPubSubTest.res +118 -76
- package/tests/adapter/LocalDispatchContextTest.res +1 -2
- package/tests/adapter/LocalEvents_ServerTest.res +242 -187
- package/tests/adapter/PgProjectionCatchupTest.res +44 -10
- package/tests/adapter/ProjectionCheckpointTest.res +261 -206
- package/tests/adapter/QueryDbGsiTtlTest.res +123 -83
- package/tests/adapter/QueryDbListPushdownParityTest.res +107 -67
- package/tests/adapter/QueryDbListResolverTest.res +225 -208
- package/tests/adapter/QueryDbResolvedFieldsTest.res +17 -14
- package/tests/adapter/QueryDbStorageSqliteTest.res +33 -49
- package/tests/adapter/QueryDbStorageTest.res +163 -117
- package/tests/adapter/QueryDbUnionRoundTripTest.res +8 -10
- package/tests/adapter/QueryEngineTest.res +179 -126
- package/tests/adapter/ScheduledPublisherTest.res +171 -135
- package/tests/adapter/ServedBucketHttpTest.res +65 -59
- package/tests/adapter/SortKey_FilterTest.res +19 -3
- package/tests/adapter/SqliteDriverTest.res +13 -10
- package/tests/adapter/TaskBucketTest.res +94 -80
- package/tests/components/aggregate/AggregateTest.res +31 -18
- package/tests/components/automationslice/AutomationSliceCallbackTest.res +259 -222
- package/tests/components/automationslice/AutomationSliceFixtures.res +4 -2
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +38 -38
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +12 -14
- package/tests/components/automationslice/MixedSourceAutomationSliceTest.res +8 -32
- package/tests/components/commandgenerator/CommandGeneratorFixtures.res +7 -5
- package/tests/components/commandgenerator/CommandGeneratorTest.res +40 -31
- package/tests/components/commandtopic/CommandTopicFixtures.res +3 -4
- package/tests/components/commandtopic/CommandTopicStreamFixtures.res +7 -5
- package/tests/components/commandtopic/CommandTopicTest.res +24 -15
- package/tests/components/counter/CounterFixtures.res +6 -8
- package/tests/components/dcb/DcbCrossPartitionFixtures.res +8 -2
- package/tests/components/dcb/DcbCrossPartitionReadTest.res +20 -13
- package/tests/components/dcb/DcbEventLogAppendStreamTest.res +12 -8
- package/tests/components/dcb/DcbEventLogStreamTest.res +88 -61
- package/tests/components/dcb/DcbFixtures.res +4 -1
- package/tests/components/dcb/DcbTest.res +9 -6
- package/tests/components/eventcollector/EventCollectorFixtures.res +0 -1
- package/tests/components/eventcollector/EventCollectorTest.res +6 -3
- package/tests/components/eventlog/EventLogAppendStreamTest.res +88 -68
- package/tests/components/eventlog/EventLogConflictTest.res +8 -4
- package/tests/components/eventlog/EventLogFixtures.res +3 -3
- package/tests/components/eventlog/EventLogProvisioningSeamTest.res +26 -13
- package/tests/components/eventlog/EventLogStreamTest.res +119 -56
- package/tests/components/eventlog/EventLogTest.res +20 -4
- package/tests/components/eventtopic/EventTopicFixtures.res +3 -3
- package/tests/components/eventtopic/EventTopicStreamFixtures.res +1 -2
- package/tests/components/eventtopic/EventTopicStreamTest.res +7 -1
- package/tests/components/extensionpoint/ExtensionPointFixtures.res +14 -9
- package/tests/components/extensionpoint/ExtensionPointTest.res +3 -2
- package/tests/components/heartbeat/HeartbeatFixtures.res +5 -6
- package/tests/components/heartbeat/HeartbeatTest.res +56 -47
- package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res +271 -249
- package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res +3 -2
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +361 -276
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res +8 -10
- package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res +37 -43
- package/tests/components/querydb/QueryDbFixtures.res +1 -5
- package/tests/components/querydb/QueryDbTest.res +2 -4
- package/tests/components/readmodel/DcbReadModelE2EFixtures.res +15 -18
- package/tests/components/readmodel/MixedSourceReadModelFixtures.res +5 -7
- package/tests/components/readmodel/ReadModelFixtures.res +8 -8
- package/tests/components/scheduler/SchedulerTest.res +76 -58
- package/tests/components/sideeffecthandler/SideEffectHandlerFixtures.res +4 -4
- package/tests/components/stateviewslice/StateViewSliceDisplayNameFixtures.res +5 -7
- package/tests/components/stateviewslice/StateViewSliceFixtures.res +8 -6
- package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res +17 -11
- package/tests/components/stateviewslice/StateViewSliceSubIdTest.res +12 -2
- package/tests/components/task/TaskFixtures.res +5 -5
- package/tests/components/task/TaskTest.res +95 -80
- package/tests/plugin/PluginsProjection_GWT.res +3 -1
- package/tests/plugin/UiFragments_GWT.res +6 -1
- package/tests/reset/LocalSeedResetTest.res +1 -0
package/src/ShellConfig.res
CHANGED
|
@@ -41,11 +41,7 @@ let manifestUrlOf = (config: ReventlessInfra.Platform.bakedManifest): string =>
|
|
|
41
41
|
// caller matching no declared group gets and what every existing deployment has.
|
|
42
42
|
let journeyManifestsKey = "journeyManifestUrls"
|
|
43
43
|
|
|
44
|
-
let computedKeys = [
|
|
45
|
-
"manifestUrl",
|
|
46
|
-
journeyManifestsKey,
|
|
47
|
-
ReventlessCore.Platform_UiSlots.configKey,
|
|
48
|
-
]
|
|
44
|
+
let computedKeys = ["manifestUrl", journeyManifestsKey, ReventlessCore.Platform_UiSlots.configKey]
|
|
49
45
|
|
|
50
46
|
/**
|
|
51
47
|
The overlay this platform puts on top of the shipped `config.json`.
|
|
@@ -96,8 +92,9 @@ let overlay = (
|
|
|
96
92
|
if collisions->Array.length > 0 {
|
|
97
93
|
JsError.throwWithMessage(
|
|
98
94
|
"host UI config.json: shellConfig sets key(s) the platform already computes — " ++
|
|
99
|
-
collisions->Array.join(
|
|
100
|
-
|
|
95
|
+
collisions->Array.join(
|
|
96
|
+
", ",
|
|
97
|
+
) ++ ". Remove them from shellConfig; a passthrough cannot redirect a computed key.",
|
|
101
98
|
)
|
|
102
99
|
}
|
|
103
100
|
extra->Dict.forEachWithKey((v, k) => out->Dict.set(k, v))
|
|
@@ -134,12 +131,10 @@ let emit = (
|
|
|
134
131
|
) => {
|
|
135
132
|
let overlay = overlay(~bakedManifest, ~uiSlotsFile?, ~shellConfig)
|
|
136
133
|
|
|
137
|
-
switch
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
) {
|
|
134
|
+
switch switch dir {
|
|
135
|
+
| Some(_) as given => given
|
|
136
|
+
| None => HostShellDist.dir()
|
|
137
|
+
} {
|
|
143
138
|
| None =>
|
|
144
139
|
// Nothing declared and no shell installed is the ordinary case for a
|
|
145
140
|
// platform nobody points a browser at; only a declaration makes the missing
|
|
@@ -148,8 +143,7 @@ let emit = (
|
|
|
148
143
|
JsError.throwWithMessage(
|
|
149
144
|
`host UI config.json: cannot resolve ${HostShellDist.package} from ${NodeProcess.cwd()} — ` ++
|
|
150
145
|
`the local shell reads its config from that package's dist/, so declaring shell ` ++
|
|
151
|
-
`config without the package installed would write nothing and leave the shell ` ++
|
|
152
|
-
`configured as it shipped.`,
|
|
146
|
+
`config without the package installed would write nothing and leave the shell ` ++ `configured as it shipped.`,
|
|
153
147
|
)
|
|
154
148
|
}
|
|
155
149
|
| Some(dir) =>
|
|
@@ -163,8 +157,7 @@ let emit = (
|
|
|
163
157
|
if !NodeFs.existsSync(path) {
|
|
164
158
|
JsError.throwWithMessage(
|
|
165
159
|
`host UI config.json: ${HostShellDist.package} ships no ${fileName} at ${dir} — ` ++
|
|
166
|
-
`there is no baseline to overlay, so the shell would boot with only the keys ` ++
|
|
167
|
-
`declared here and none of the ones it expects.`,
|
|
160
|
+
`there is no baseline to overlay, so the shell would boot with only the keys ` ++ `declared here and none of the ones it expects.`,
|
|
168
161
|
)
|
|
169
162
|
}
|
|
170
163
|
NodeFs.writeFileSync(baselinePath, NodeFs.readFileSync(path))
|
package/src/UiHints.res
CHANGED
|
@@ -50,27 +50,23 @@ let emit = (
|
|
|
50
50
|
| exception _ =>
|
|
51
51
|
JsError.throwWithMessage(
|
|
52
52
|
`host UI ${fileName}: cannot read the declared uiHintsFile at ${path} — ` ++
|
|
53
|
-
`the shell fetches this file at boot, so a declaration pointing nowhere ` ++
|
|
54
|
-
`applies no hints and says nothing about why.`,
|
|
53
|
+
`the shell fetches this file at boot, so a declaration pointing nowhere ` ++ `applies no hints and says nothing about why.`,
|
|
55
54
|
)
|
|
56
55
|
}
|
|
57
56
|
switch source->JSON.parseOrThrow {
|
|
58
57
|
| _ => ()
|
|
59
58
|
| exception _ =>
|
|
60
59
|
JsError.throwWithMessage(
|
|
61
|
-
`host UI ${fileName}: the declared uiHintsFile at ${path} is not JSON — ` ++
|
|
62
|
-
`the shell warns once and applies no hints, which reads as hints that do nothing.`,
|
|
60
|
+
`host UI ${fileName}: the declared uiHintsFile at ${path} is not JSON — ` ++ `the shell warns once and applies no hints, which reads as hints that do nothing.`,
|
|
63
61
|
)
|
|
64
62
|
}
|
|
65
63
|
source
|
|
66
64
|
})
|
|
67
65
|
|
|
68
|
-
switch
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
) {
|
|
66
|
+
switch switch dir {
|
|
67
|
+
| Some(_) as given => given
|
|
68
|
+
| None => HostShellDist.dir()
|
|
69
|
+
} {
|
|
74
70
|
| None =>
|
|
75
71
|
// No shell installed is the ordinary case for a platform nobody points a
|
|
76
72
|
// browser at; only a declaration makes the missing package an error.
|
|
@@ -78,8 +74,7 @@ let emit = (
|
|
|
78
74
|
JsError.throwWithMessage(
|
|
79
75
|
`host UI ${fileName}: cannot resolve ${HostShellDist.package} from ${NodeProcess.cwd()} — ` ++
|
|
80
76
|
`the local shell reads its hints from that package's dist/, so declaring a ` ++
|
|
81
|
-
`uiHintsFile without the package installed would write nothing and leave the ` ++
|
|
82
|
-
`shell applying whatever the package shipped.`,
|
|
77
|
+
`uiHintsFile without the package installed would write nothing and leave the ` ++ `shell applying whatever the package shipped.`,
|
|
83
78
|
)
|
|
84
79
|
}
|
|
85
80
|
| Some(dir) =>
|
|
@@ -148,8 +143,7 @@ let watch = (
|
|
|
148
143
|
// away between the two — worth a line, not worth a throw.
|
|
149
144
|
log.warn(
|
|
150
145
|
~comp="UiHints",
|
|
151
|
-
`not watching ${base}: ${sourceDir} does not exist, so changes to the declared ` ++
|
|
152
|
-
`uiHintsFile will need a restart`,
|
|
146
|
+
`not watching ${base}: ${sourceDir} does not exist, so changes to the declared ` ++ `uiHintsFile will need a restart`,
|
|
153
147
|
)
|
|
154
148
|
None
|
|
155
149
|
} else {
|
package/src/UiSlots.res
CHANGED
|
@@ -66,18 +66,15 @@ let emit = (
|
|
|
66
66
|
| exception _ =>
|
|
67
67
|
JsError.throwWithMessage(
|
|
68
68
|
`host UI ${fileName}: cannot read the declared uiSlotsFile at ${path} — ` ++
|
|
69
|
-
`the shell imports this file at boot, so a declaration pointing nowhere ` ++
|
|
70
|
-
`registers no renderers and says nothing about why.`,
|
|
69
|
+
`the shell imports this file at boot, so a declaration pointing nowhere ` ++ `registers no renderers and says nothing about why.`,
|
|
71
70
|
)
|
|
72
71
|
}
|
|
73
72
|
)
|
|
74
73
|
|
|
75
|
-
switch
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
) {
|
|
74
|
+
switch switch dir {
|
|
75
|
+
| Some(_) as given => given
|
|
76
|
+
| None => HostShellDist.dir()
|
|
77
|
+
} {
|
|
81
78
|
| None =>
|
|
82
79
|
// No shell installed is the ordinary case for a platform nobody points a
|
|
83
80
|
// browser at; only a declaration makes the missing package an error.
|
|
@@ -85,8 +82,7 @@ let emit = (
|
|
|
85
82
|
JsError.throwWithMessage(
|
|
86
83
|
`host UI ${fileName}: cannot resolve ${HostShellDist.package} from ${NodeProcess.cwd()} — ` ++
|
|
87
84
|
`the local shell imports its slot renderers from that package's dist/, so ` ++
|
|
88
|
-
`declaring a uiSlotsFile without the package installed would write nothing ` ++
|
|
89
|
-
`and leave every mode drawing its own regions.`,
|
|
85
|
+
`declaring a uiSlotsFile without the package installed would write nothing ` ++ `and leave every mode drawing its own regions.`,
|
|
90
86
|
)
|
|
91
87
|
}
|
|
92
88
|
| Some(dir) =>
|
|
@@ -132,11 +128,9 @@ let emit = (
|
|
|
132
128
|
`onReload` runs only after a re-copy actually succeeded, so a subscriber cannot
|
|
133
129
|
be told to re-import a file that did not change.
|
|
134
130
|
*/
|
|
135
|
-
let watch = (
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
~onReload: unit => unit,
|
|
139
|
-
): option<NodeFs.watcher> =>
|
|
131
|
+
let watch = (~uiSlotsFile: option<string>, ~dir: option<string>=?, ~onReload: unit => unit): option<
|
|
132
|
+
NodeFs.watcher,
|
|
133
|
+
> =>
|
|
140
134
|
uiSlotsFile->Option.flatMap(path => {
|
|
141
135
|
// Named apart from the `~dir` above, which is where the file is SERVED. This
|
|
142
136
|
// is where it is AUTHORED, and the two are never the same place — letting
|
|
@@ -150,8 +144,7 @@ let watch = (
|
|
|
150
144
|
// away between the two — worth a line, not worth a throw.
|
|
151
145
|
log.warn(
|
|
152
146
|
~comp="UiSlots",
|
|
153
|
-
`not watching ${base}: ${sourceDir} does not exist, so changes to the declared ` ++
|
|
154
|
-
`uiSlotsFile will need a restart`,
|
|
147
|
+
`not watching ${base}: ${sourceDir} does not exist, so changes to the declared ` ++ `uiSlotsFile will need a restart`,
|
|
155
148
|
)
|
|
156
149
|
None
|
|
157
150
|
} else {
|
|
@@ -27,8 +27,7 @@ let log = ReventlessCore.Logger.fromEnv()
|
|
|
27
27
|
|
|
28
28
|
// Mirrors AppSyncEventsSigner_Ops.pathSegment (aws) and the client's channel
|
|
29
29
|
// normalizer: any char outside [A-Za-z0-9-] becomes `-`.
|
|
30
|
-
let pathSegment = (s: string): string =>
|
|
31
|
-
s->String.replaceRegExp(/[^A-Za-z0-9-]/g, "-")
|
|
30
|
+
let pathSegment = (s: string): string => s->String.replaceRegExp(/[^A-Za-z0-9-]/g, "-")
|
|
32
31
|
|
|
33
32
|
let clientChannelPrefix = "/client/"
|
|
34
33
|
|
|
@@ -172,7 +171,10 @@ let broadcastStateChange = (~name: string, ~descriptor: JSON.t): unit => {
|
|
|
172
171
|
// -- Inbound frames (subscribe side) -----------------------------------------
|
|
173
172
|
|
|
174
173
|
let decodeStringField = (json: JSON.t, field: string): option<string> =>
|
|
175
|
-
json
|
|
174
|
+
json
|
|
175
|
+
->JSON.Decode.object
|
|
176
|
+
->Option.flatMap(o => o->Dict.get(field))
|
|
177
|
+
->Option.flatMap(JSON.Decode.string)
|
|
176
178
|
|
|
177
179
|
/** Handle one client→server text frame on an established connection. */
|
|
178
180
|
let handleFrame = (conn: connection, text: string): unit => {
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
type api = unit
|
|
8
8
|
type role = unit
|
|
9
9
|
|
|
10
|
-
let makeApiResource = (
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
)
|
|
10
|
+
let makeApiResource = (~name as _: string, ~opts as _: Pulumi.ComponentResource.options): (
|
|
11
|
+
Pulumi.Output.t<api>,
|
|
12
|
+
Pulumi.Output.t<role>,
|
|
13
|
+
) => (Pulumi.Output.make(), Pulumi.Output.make())
|
|
14
14
|
|
|
15
15
|
let generateFragment = (
|
|
16
16
|
~mutationEntries: array<ReventlessInfra.Api.mutationSchemaEntry>,
|
|
@@ -33,8 +33,7 @@ let adminUser: Identity.t = {
|
|
|
33
33
|
// A `dict<Identity.t>` keyed by `username`. A4 hydrates this from
|
|
34
34
|
// `.reventless/users.yaml`; tests inject via `registerUser`.
|
|
35
35
|
|
|
36
|
-
let initialUsers = () =>
|
|
37
|
-
Dict.fromArray([("user", defaultUser), ("admin", adminUser)])
|
|
36
|
+
let initialUsers = () => Dict.fromArray([("user", defaultUser), ("admin", adminUser)])
|
|
38
37
|
|
|
39
38
|
let users: ref<dict<Identity.t>> = ref(initialUsers())
|
|
40
39
|
|
|
@@ -43,8 +42,7 @@ let registerUser = (~username: string, ~identity: Identity.t): unit =>
|
|
|
43
42
|
|
|
44
43
|
let resetUsers = (): unit => users := initialUsers()
|
|
45
44
|
|
|
46
|
-
let lookupUser = (username: string): option<Identity.t> =>
|
|
47
|
-
users.contents->Dict.get(username)
|
|
45
|
+
let lookupUser = (username: string): option<Identity.t> => users.contents->Dict.get(username)
|
|
48
46
|
|
|
49
47
|
// ── Header parsing ────────────────────────────────────────────────────────
|
|
50
48
|
|
|
@@ -240,11 +238,10 @@ module Login = {
|
|
|
240
238
|
`activeRole` narrows the minted token to one of the caller's own roles; unset
|
|
241
239
|
mints exactly what it has always minted.
|
|
242
240
|
*/
|
|
243
|
-
let issue = async (
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
): result<string, string> =>
|
|
241
|
+
let issue = async (~username: string, ~password: string, ~activeRole: option<string>=?): result<
|
|
242
|
+
string,
|
|
243
|
+
string,
|
|
244
|
+
> =>
|
|
248
245
|
switch store.contents->Dict.get(username) {
|
|
249
246
|
| Some({password: stored, identity}) if stored === password =>
|
|
250
247
|
switch switch activeRole {
|
|
@@ -340,7 +337,6 @@ module Login = {
|
|
|
340
337
|
}
|
|
341
338
|
}
|
|
342
339
|
}
|
|
343
|
-
|
|
344
340
|
}
|
|
345
341
|
|
|
346
342
|
// ── Provider implementation ───────────────────────────────────────────────
|
|
@@ -354,9 +350,7 @@ let _bearerToken = (header: string): option<string> =>
|
|
|
354
350
|
None
|
|
355
351
|
}
|
|
356
352
|
|
|
357
|
-
let authenticate = async (
|
|
358
|
-
ctx: ReventlessCore.Auth_Adapter.requestContext,
|
|
359
|
-
): Identity.authResult => {
|
|
353
|
+
let authenticate = async (ctx: ReventlessCore.Auth_Adapter.requestContext): Identity.authResult => {
|
|
360
354
|
// Decoding order: Bearer (when signature valid) → X-User/X-Groups → default.
|
|
361
355
|
// A *present* Bearer that fails verification is rejected (AuthError) rather
|
|
362
356
|
// than falling through to defaultUser — otherwise a client whose token was
|
|
@@ -402,5 +396,7 @@ let authenticate = async (
|
|
|
402
396
|
}
|
|
403
397
|
}
|
|
404
398
|
|
|
405
|
-
let make = (
|
|
406
|
-
|
|
399
|
+
let make = (
|
|
400
|
+
~name as _: string,
|
|
401
|
+
~opts as _: option<Pulumi.ComponentResource.options>=?,
|
|
402
|
+
): Pulumi.Output.t<authConfig> => Pulumi.Output.make()
|
|
@@ -83,11 +83,10 @@ let resolved: ref<bool> = ref(false)
|
|
|
83
83
|
|
|
84
84
|
let resetResolution = (): unit => resolved := false
|
|
85
85
|
|
|
86
|
-
let load = (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
): result<resolution, string> => {
|
|
86
|
+
let load = (~users: option<array<entry>>=?, ~usersFile: option<string>=?, ()): result<
|
|
87
|
+
resolution,
|
|
88
|
+
string,
|
|
89
|
+
> => {
|
|
91
90
|
resolved := true
|
|
92
91
|
switch users {
|
|
93
92
|
| Some(entries) =>
|
|
@@ -109,8 +108,7 @@ let load = (
|
|
|
109
108
|
})
|
|
110
109
|
} else {
|
|
111
110
|
Console.log(
|
|
112
|
-
"[LocalAuth] no users configured — POST /__inmemory/login will reject all. " ++
|
|
113
|
-
"Provide ~users / ~usersFile or create .reventless/users.yaml relative to cwd.",
|
|
111
|
+
"[LocalAuth] no users configured — POST /__inmemory/login will reject all. " ++ "Provide ~users / ~usersFile or create .reventless/users.yaml relative to cwd.",
|
|
114
112
|
)
|
|
115
113
|
Ok(Empty)
|
|
116
114
|
}
|
package/src/adapter/Backend.res
CHANGED
|
@@ -30,11 +30,10 @@ let postgres = async (~connection, ~resetOnStart=false) => {
|
|
|
30
30
|
}
|
|
31
31
|
let initialCount =
|
|
32
32
|
(await ReventlessPostgres.EventLogStorage_Postgres.countAll(pool)) +
|
|
33
|
-
|
|
33
|
+
(await ReventlessPostgres.DcbEventLogStorage_Postgres.countAll(pool))
|
|
34
34
|
Postgres({pool, initialCount, connection})
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
38
37
|
// Deletes a file if it exists. No-op if the path is missing.
|
|
39
38
|
|
|
40
39
|
let removeFileIfExists = (path: string) =>
|
|
@@ -35,15 +35,14 @@ let extractIdentity = (ctx: JSON.t): Reventless.Identity.t => {
|
|
|
35
35
|
// Returns a `CommandRejected` outcome with a fresh msgId so the GraphQL union
|
|
36
36
|
// resolves consistently — matches the existing CommandResult contract.
|
|
37
37
|
|
|
38
|
-
let rejectForbidden = (~field: string): JSON.t =>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
)
|
|
38
|
+
let rejectForbidden = (~field: string): JSON.t => JSON.Object(
|
|
39
|
+
Dict.fromArray([
|
|
40
|
+
("__typename", JSON.String("CommandRejected")),
|
|
41
|
+
("msgId", JSON.String(ReventlessCore.Message.uuid())),
|
|
42
|
+
("errorCode", JSON.String("Forbidden")),
|
|
43
|
+
("errorDetail", JSON.String(`Mutation.${field}: identity is not authorized`)),
|
|
44
|
+
]),
|
|
45
|
+
)
|
|
47
46
|
|
|
48
47
|
// -- Plugin status gate -------------------------------------------------------
|
|
49
48
|
// Set by the platform after Admin.construct to wire a per-mutation lookup against
|
|
@@ -61,15 +60,18 @@ let pluginStatusGate: ref<option<string => option<(string, string)>>> = ref(None
|
|
|
61
60
|
let setPluginStatusGate = fn => pluginStatusGate := Some(fn)
|
|
62
61
|
let resetPluginStatusGate = () => pluginStatusGate := None
|
|
63
62
|
|
|
64
|
-
let rejectPluginStatus = (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
63
|
+
let rejectPluginStatus = (
|
|
64
|
+
~field: string,
|
|
65
|
+
~errorCode: string,
|
|
66
|
+
~detail: string,
|
|
67
|
+
): JSON.t => JSON.Object(
|
|
68
|
+
Dict.fromArray([
|
|
69
|
+
("__typename", JSON.String("CommandRejected")),
|
|
70
|
+
("msgId", JSON.String(ReventlessCore.Message.uuid())),
|
|
71
|
+
("errorCode", JSON.String(errorCode)),
|
|
72
|
+
("errorDetail", JSON.String(`Mutation.${field}: ${detail}`)),
|
|
73
|
+
]),
|
|
74
|
+
)
|
|
73
75
|
|
|
74
76
|
let checkPluginStatus = (~field: string): option<JSON.t> =>
|
|
75
77
|
switch pluginStatusGate.contents {
|
|
@@ -92,8 +94,7 @@ let checkPluginStatus = (~field: string): option<JSON.t> =>
|
|
|
92
94
|
let syntheticCommand = (cname: string, ~hasPayload: bool): unknown =>
|
|
93
95
|
hasPayload ? {"TAG": cname}->Obj.magic : cname->Obj.magic
|
|
94
96
|
|
|
95
|
-
let capitalize = s =>
|
|
96
|
-
s->String.charAt(0)->String.toUpperCase ++ s->String.slice(~start=1)
|
|
97
|
+
let capitalize = s => s->String.charAt(0)->String.toUpperCase ++ s->String.slice(~start=1)
|
|
97
98
|
|
|
98
99
|
// -- CommandResult SDL types --------------------------------------------------
|
|
99
100
|
// Source of truth lives in GraphQL_FragmentGenerator.commandResultSdlTypes so
|
|
@@ -194,7 +195,10 @@ let register = (
|
|
|
194
195
|
// Aggregate commands target a specific instance — prepend id: ID!
|
|
195
196
|
let sdlFields = fields->Array.map(field => {
|
|
196
197
|
let variantIndex = variantIndexForField(commandSchema, ~field)
|
|
197
|
-
let sdl = deriveSdlField(
|
|
198
|
+
let sdl = deriveSdlField(
|
|
199
|
+
~fieldName=field,
|
|
200
|
+
extractVariantSchema(commandSchema, ~index=variantIndex),
|
|
201
|
+
)
|
|
198
202
|
if sdl->String.includes("(") {
|
|
199
203
|
sdl->String.replace(`${field}(`, `${field}(id: ID!, `)
|
|
200
204
|
} else {
|
|
@@ -214,7 +218,11 @@ let register = (
|
|
|
214
218
|
| _ => false
|
|
215
219
|
}
|
|
216
220
|
let commandName = extractCommandName(field)
|
|
217
|
-
let resolver: ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn = async (
|
|
221
|
+
let resolver: ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn = async (
|
|
222
|
+
_root,
|
|
223
|
+
args,
|
|
224
|
+
ctx,
|
|
225
|
+
) => {
|
|
218
226
|
switch handlerRef.contents {
|
|
219
227
|
| Some(generateCommand) =>
|
|
220
228
|
switch checkPluginStatus(~field) {
|
|
@@ -265,19 +273,27 @@ let registerDcb = (
|
|
|
265
273
|
// non-first constructor of a multi-command slice.
|
|
266
274
|
let constructorNames = Reventless.DcbTag.extractAllVariantNames(commandSchema->Obj.magic)
|
|
267
275
|
let variantIndex = variantIndexForField(commandSchema, ~field=fieldName)
|
|
268
|
-
let variantSchema = extractVariantSchema(
|
|
276
|
+
let variantSchema = extractVariantSchema(
|
|
277
|
+
commandSchema,
|
|
278
|
+
~index=variantIndex >= 0 ? variantIndex : 0,
|
|
279
|
+
)
|
|
269
280
|
let sdlFields = [deriveSdlField(~fieldName, variantSchema)]
|
|
270
281
|
|
|
271
282
|
// Extract TAG (variant constructor name) for routing
|
|
272
283
|
let tag =
|
|
273
|
-
(
|
|
274
|
-
|
|
284
|
+
(
|
|
285
|
+
variantIndex >= 0 ? constructorNames->Array.get(variantIndex) : constructorNames->Array.get(0)
|
|
286
|
+
)->Option.getOr(fieldName)
|
|
275
287
|
let hasPayload = Reventless.DcbTag.isVariantPayloadBearing(commandSchema->Obj.magic, tag)
|
|
276
288
|
|
|
277
289
|
let handlerRef = ref(None)
|
|
278
290
|
handlerRefs->Dict.set(fieldName, handlerRef)
|
|
279
291
|
|
|
280
|
-
let resolver: ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn = async (
|
|
292
|
+
let resolver: ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn = async (
|
|
293
|
+
_root,
|
|
294
|
+
args,
|
|
295
|
+
ctx,
|
|
296
|
+
) => {
|
|
281
297
|
switch handlerRef.contents {
|
|
282
298
|
| Some(generateCommand) =>
|
|
283
299
|
switch checkPluginStatus(~field=fieldName) {
|
|
@@ -25,7 +25,11 @@ let pendingQueueRegistry: dict<ref<array<pendingCall>>> = Dict.make()
|
|
|
25
25
|
// Phase 1: Register SDL + resolver stub synchronously (before server starts).
|
|
26
26
|
// Also pre-registers a queuing forwarder in receiveRegistry so callers can
|
|
27
27
|
// invoke receive immediately; calls are parked until bindReceive drains them.
|
|
28
|
-
let register = (
|
|
28
|
+
let register = (
|
|
29
|
+
~fieldName: string,
|
|
30
|
+
~externalInputSchema: S.t<unknown>,
|
|
31
|
+
~server: ReventlessGraphqlServer.GraphQL_ServerInstance.t,
|
|
32
|
+
) => {
|
|
29
33
|
// The mutation field returns CommandResult, so its union members must exist in
|
|
30
34
|
// this scope's document. Registered here rather than relying on a plugin's
|
|
31
35
|
// command handlers having registered them first — a plugin whose only mutation
|
|
@@ -53,11 +57,17 @@ let register = (~fieldName: string, ~externalInputSchema: S.t<unknown>, ~server:
|
|
|
53
57
|
})
|
|
54
58
|
receiveRegistry->Dict.set(fieldName, queuingReceive)
|
|
55
59
|
|
|
56
|
-
let resolver: ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn = async (
|
|
60
|
+
let resolver: ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn = async (
|
|
61
|
+
_root,
|
|
62
|
+
args,
|
|
63
|
+
_ctx,
|
|
64
|
+
) => {
|
|
57
65
|
let inputJson: JSON.t = args->Obj.magic
|
|
58
66
|
let receive = receiveRegistry->Dict.getUnsafe(fieldName)
|
|
59
67
|
let result = await receive(inputJson)
|
|
60
|
-
result
|
|
68
|
+
result
|
|
69
|
+
->InboundTranslationSlice_Callback.receiveResultToOutcome
|
|
70
|
+
->CommandTopic.commandOutcomeToJson
|
|
61
71
|
}
|
|
62
72
|
|
|
63
73
|
let resolvers = Dict.make()
|
|
@@ -32,17 +32,14 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
32
32
|
> = (~name, ~owner as _, ~opts as _=?) => {
|
|
33
33
|
// Captured when handleChannelEvent is called; used by publishJsonsAndWait to
|
|
34
34
|
// run the handler inline and collect typed outcomes without going through the bus.
|
|
35
|
-
let handleCmdsRef: ref<
|
|
36
|
-
option<ReventlessCore.CommandTopic.jsonCommandsHandler>,
|
|
37
|
-
> = ref(None)
|
|
35
|
+
let handleCmdsRef: ref<option<ReventlessCore.CommandTopic.jsonCommandsHandler>> = ref(None)
|
|
38
36
|
|
|
39
37
|
let publishJsons: ReventlessInfra.CommandTopic.publishJsons = async jsons => {
|
|
40
|
-
let _ =
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
->Promise.all
|
|
38
|
+
let _ = await jsons
|
|
39
|
+
->Array.map(async (cmdJson: Reventless.Message.commandJson) => {
|
|
40
|
+
await Bus.dispatchCommand(name, ReventlessCore.CommandTopic.encodeCommandJson(cmdJson))
|
|
41
|
+
})
|
|
42
|
+
->Promise.all
|
|
46
43
|
}
|
|
47
44
|
|
|
48
45
|
let publishJsonsStream: ReventlessInfra.CommandTopic.publishJsonsStream = stream =>
|
|
@@ -55,18 +52,20 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
55
52
|
| None =>
|
|
56
53
|
// Handler not yet registered — fall back to fire-and-forget, return Pending
|
|
57
54
|
let _ = await publishJsons(jsons)
|
|
58
|
-
jsons->Array.map(cmdJson =>
|
|
59
|
-
|
|
60
|
-
)
|
|
61
|
-
| Some(handleCmds) =>
|
|
62
|
-
await ReventlessCore.CommandTopic.runInlineAndCollect(jsons, handleCmds)
|
|
55
|
+
jsons->Array.map(cmdJson => ReventlessCore.CommandTopic.Pending({
|
|
56
|
+
msgId: cmdJson.meta.msgId,
|
|
57
|
+
}))
|
|
58
|
+
| Some(handleCmds) => await ReventlessCore.CommandTopic.runInlineAndCollect(jsons, handleCmds)
|
|
63
59
|
}
|
|
64
60
|
}
|
|
65
61
|
|
|
66
62
|
let handleChannelEvent = (
|
|
67
63
|
handleCmds: ReventlessCore.CommandTopic.jsonCommandsHandler,
|
|
68
|
-
): Pulumi.Output.t<
|
|
64
|
+
): Pulumi.Output.t<
|
|
65
|
+
ReventlessCore.Runtime.effectHandler<callbackEvent, 'context, unit, string>,
|
|
66
|
+
> => {
|
|
69
67
|
handleCmdsRef.contents = Some(handleCmds)
|
|
68
|
+
|
|
70
69
|
(
|
|
71
70
|
(fullBody: JSON.t, _ctx) => {
|
|
72
71
|
// Pass the full body as `command` — that's what handleJsonCommands decodes
|
|
@@ -84,8 +83,7 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
84
83
|
runtimeParts,
|
|
85
84
|
> = (~name as _, ~channel, ~runtime, ~resources as _, ~opts as _) => {
|
|
86
85
|
Bus.registerCommandHandler(channel.parts.name, async (json, ctx) => {
|
|
87
|
-
let handler =
|
|
88
|
-
await runtime.parts.handlerDeferred->Deferred.await_->Effect.runPromise
|
|
86
|
+
let handler = await runtime.parts.handlerDeferred->Deferred.await_->Effect.runPromise
|
|
89
87
|
await handler(json, ctx)
|
|
90
88
|
})
|
|
91
89
|
[]
|
|
@@ -5,28 +5,24 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
5
5
|
let make: ReventlessCore.CommandTopic_Adapter.remoteChannelMaker = resources => {
|
|
6
6
|
resources,
|
|
7
7
|
remotePublish: async jsons => {
|
|
8
|
-
let _ =
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
await Bus.dispatchCommand(channelName, body)
|
|
28
|
-
})
|
|
29
|
-
->Promise.all
|
|
8
|
+
let _ = await jsons
|
|
9
|
+
->Array.map(async (cmdJson: Reventless.Message.commandJson) => {
|
|
10
|
+
// Encode the same way LocalCommandTopicChannel does
|
|
11
|
+
let body = JSON.Encode.object(
|
|
12
|
+
Dict.fromArray([
|
|
13
|
+
("id", JSON.Encode.string(cmdJson.id)),
|
|
14
|
+
("meta", cmdJson.meta->Reventless.Util_Sury.toJson(Reventless.Message.metaSchema)),
|
|
15
|
+
("command", cmdJson.commandJson),
|
|
16
|
+
]),
|
|
17
|
+
)
|
|
18
|
+
// Dispatch to the first resource's name (the channel name)
|
|
19
|
+
let channelName = switch resources {
|
|
20
|
+
| [] => ""
|
|
21
|
+
| resources => (resources->Array.getUnsafe(0)).name
|
|
22
|
+
}
|
|
23
|
+
await Bus.dispatchCommand(channelName, body)
|
|
24
|
+
})
|
|
25
|
+
->Promise.all
|
|
30
26
|
},
|
|
31
27
|
}
|
|
32
28
|
}
|
|
@@ -20,7 +20,7 @@ let make: Counter_Adapter.handlerMaker = (
|
|
|
20
20
|
~publishChannelId as _,
|
|
21
21
|
~opts as _,
|
|
22
22
|
) => {
|
|
23
|
-
addToCounterTarget: async ({ReventlessInfra.Counter.counterId, target, targetRef}) => {
|
|
23
|
+
addToCounterTarget: async ({ReventlessInfra.Counter.counterId: counterId, target, targetRef}) => {
|
|
24
24
|
let refKey = counterId ++ ":" ++ targetRef
|
|
25
25
|
switch targetRefStore.contents->Dict.get(refKey) {
|
|
26
26
|
| Some(_) => () // Already counted this (counterId, targetRef) pair — skip
|