@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
|
@@ -82,16 +82,18 @@ let makeStorage = (~name as _name, ~indexes as _, ~partitionTag as _, ~opts as _
|
|
|
82
82
|
| Some(tags) if tags->Array.length > 0 =>
|
|
83
83
|
// Events carrying ALL of the clause's tags = intersection of the tags'
|
|
84
84
|
// posting lists; a missing tag ⇒ empty ⇒ this clause adds nothing.
|
|
85
|
-
let lists =
|
|
86
|
-
byTag->Dict.get(tagPostingKey(t.key, t.value))->Option.getOr([])
|
|
87
|
-
)
|
|
85
|
+
let lists =
|
|
86
|
+
tags->Array.map(t => byTag->Dict.get(tagPostingKey(t.key, t.value))->Option.getOr([]))
|
|
88
87
|
if lists->Array.some(l => l->Array.length == 0) {
|
|
89
88
|
()
|
|
90
89
|
} else {
|
|
91
|
-
let sorted =
|
|
90
|
+
let sorted =
|
|
91
|
+
lists->Array.toSorted((a, b) => Int.toFloat(a->Array.length - b->Array.length))
|
|
92
92
|
let acc0 = sorted->Array.getUnsafe(0)
|
|
93
93
|
let folded =
|
|
94
|
-
sorted
|
|
94
|
+
sorted
|
|
95
|
+
->Array.slice(~start=1, ~end=sorted->Array.length)
|
|
96
|
+
->Array.reduce(acc0, intersectSorted)
|
|
95
97
|
addAll(folded)
|
|
96
98
|
}
|
|
97
99
|
| _ =>
|
|
@@ -100,7 +102,8 @@ let makeStorage = (~name as _name, ~indexes as _, ~partitionTag as _, ~opts as _
|
|
|
100
102
|
// clause is unconstrained and matches everything.
|
|
101
103
|
switch clause.eventTypes {
|
|
102
104
|
| Some(types) => types->Array.forEach(t => addAll(byType->Dict.get(t)->Option.getOr([])))
|
|
103
|
-
| None =>
|
|
105
|
+
| None =>
|
|
106
|
+
events.contents->Array.forEachWithIndex((_, i) => acc->Dict.set(Int.toString(i), i))
|
|
104
107
|
}
|
|
105
108
|
}
|
|
106
109
|
})
|
|
@@ -147,21 +150,23 @@ let makeStorage = (~name as _name, ~indexes as _, ~partitionTag as _, ~opts as _
|
|
|
147
150
|
Error(ReventlessInfra.DcbEventLog.Conflict)
|
|
148
151
|
} else {
|
|
149
152
|
let base = events.contents->Array.length
|
|
150
|
-
let storedEvents =
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
153
|
+
let storedEvents = newEvents->Array.mapWithIndex((
|
|
154
|
+
event: DcbEventLog_Adapter.rawStoredEvent,
|
|
155
|
+
j,
|
|
156
|
+
) => {
|
|
157
|
+
position := position.contents + 1
|
|
158
|
+
let pos = position.contents->Int.toString
|
|
159
|
+
let stored: DcbEventLog_Adapter.rawSequencedEvent = {
|
|
160
|
+
position: pos,
|
|
161
|
+
eventType: event.eventType,
|
|
162
|
+
data: event.data,
|
|
163
|
+
tags: event.tags,
|
|
164
|
+
meta: event.meta,
|
|
165
|
+
recordedAt: Message.nowAsISOString(),
|
|
166
|
+
}
|
|
167
|
+
indexEvent(base + j, stored)
|
|
168
|
+
stored
|
|
169
|
+
})
|
|
165
170
|
events := events.contents->Array.concat(storedEvents)
|
|
166
171
|
Ok(position.contents->Int.toString)
|
|
167
172
|
}
|
|
@@ -191,7 +196,13 @@ let makeStorage = (~name as _name, ~indexes as _, ~partitionTag as _, ~opts as _
|
|
|
191
196
|
// routing. The flag is accepted for interface parity and ignored.
|
|
192
197
|
// Pure in-memory storageMaker (no BackendState dispatch, no Bus). Backend
|
|
193
198
|
// selection lives in LocalDcbEventLogStorage.Make.
|
|
194
|
-
let make: DcbEventLog_Adapter.storageMaker = (
|
|
199
|
+
let make: DcbEventLog_Adapter.storageMaker = (
|
|
200
|
+
~name,
|
|
201
|
+
~indexes,
|
|
202
|
+
~partitionTag,
|
|
203
|
+
~crossPartitionTagKeys as _=?,
|
|
204
|
+
~opts,
|
|
205
|
+
) => {
|
|
195
206
|
let (_, _, storage) = makeStorage(~name, ~indexes, ~partitionTag, ~opts)
|
|
196
207
|
storage
|
|
197
208
|
}
|
|
@@ -37,9 +37,7 @@ let ensureSchema = (db: SqliteDriver.t) => {
|
|
|
37
37
|
// Serves the per-position tag hydration (the `WHERE log_name = ? AND position
|
|
38
38
|
// IN (...)` batch). The `_by_kv` index leads with tag_key/tag_value, so it
|
|
39
39
|
// cannot answer a position lookup — without this the batch scanned dcb_tag.
|
|
40
|
-
db->SqliteDriver.exec(
|
|
41
|
-
"CREATE INDEX IF NOT EXISTS dcb_tag_by_pos ON dcb_tag(log_name, position)",
|
|
42
|
-
)
|
|
40
|
+
db->SqliteDriver.exec("CREATE INDEX IF NOT EXISTS dcb_tag_by_pos ON dcb_tag(log_name, position)")
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
let posToInt = (pos: string) => pos->Int.fromString->Option.getOr(0)
|
|
@@ -81,11 +79,10 @@ let sqlEscape = (s: string) => s->String.replaceAll("'", "''")
|
|
|
81
79
|
// Build the WHERE clause and bound params for one query clause.
|
|
82
80
|
// Returns (whereSql, params) where whereSql includes a leading `(`/trailing `)`
|
|
83
81
|
// and uses `dcb_event` table aliases throughout.
|
|
84
|
-
let buildClauseSql = (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
): (string, array<JSON.t>) => {
|
|
82
|
+
let buildClauseSql = (~logName: string, ~clause: DcbTag.queryItem, ~after: option<int>): (
|
|
83
|
+
string,
|
|
84
|
+
array<JSON.t>,
|
|
85
|
+
) => {
|
|
89
86
|
let parts = ref(["log_name = ?"])
|
|
90
87
|
let params = ref([JSON.Encode.string(logName)])
|
|
91
88
|
|
|
@@ -126,11 +123,10 @@ let buildClauseSql = (
|
|
|
126
123
|
// The WHERE expression (+ bound params) for a query — shared by the row-fetching
|
|
127
124
|
// read path and the LIMIT-1 existence check the append-condition uses, so both
|
|
128
125
|
// match the exact same rows.
|
|
129
|
-
let buildQueryWhere = (
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
): (string, array<JSON.t>) => {
|
|
126
|
+
let buildQueryWhere = (~logName: string, ~query: DcbTag.query, ~after: option<int>): (
|
|
127
|
+
string,
|
|
128
|
+
array<JSON.t>,
|
|
129
|
+
) => {
|
|
134
130
|
if query->Array.length == 0 {
|
|
135
131
|
let baseWhere = switch after {
|
|
136
132
|
| Some(_) => "log_name = ? AND position > ?"
|
|
@@ -150,11 +146,10 @@ let buildQueryWhere = (
|
|
|
150
146
|
}
|
|
151
147
|
}
|
|
152
148
|
|
|
153
|
-
let buildQuerySql = (
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
): (string, array<JSON.t>) => {
|
|
149
|
+
let buildQuerySql = (~logName: string, ~query: DcbTag.query, ~after: option<int>): (
|
|
150
|
+
string,
|
|
151
|
+
array<JSON.t>,
|
|
152
|
+
) => {
|
|
158
153
|
let (where, params) = buildQueryWhere(~logName, ~query, ~after)
|
|
159
154
|
(
|
|
160
155
|
`SELECT position, event_type, data, meta, recorded_at FROM dcb_event WHERE ${where} ORDER BY position ASC`,
|
|
@@ -171,19 +166,21 @@ let makeStorage = (
|
|
|
171
166
|
) => {
|
|
172
167
|
ensureSchema(db)
|
|
173
168
|
|
|
174
|
-
let insertEventStmt =
|
|
175
|
-
|
|
176
|
-
|
|
169
|
+
let insertEventStmt =
|
|
170
|
+
db->SqliteDriver.prepare(
|
|
171
|
+
"INSERT INTO dcb_event(log_name, position, event_type, data, meta, recorded_at) VALUES(?,?,?,?,?,?)",
|
|
172
|
+
)
|
|
177
173
|
let lastRowidStmt = db->SqliteDriver.prepare("SELECT last_insert_rowid() AS r")
|
|
178
|
-
let insertTagStmt =
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
174
|
+
let insertTagStmt =
|
|
175
|
+
db->SqliteDriver.prepare(
|
|
176
|
+
"INSERT INTO dcb_tag(log_name, position, tag_key, tag_value) VALUES(?,?,?,?)",
|
|
177
|
+
)
|
|
178
|
+
let maxPositionStmt =
|
|
179
|
+
db->SqliteDriver.prepare(
|
|
180
|
+
"SELECT COALESCE(MAX(position), 0) AS m FROM dcb_event WHERE log_name = ?",
|
|
181
|
+
)
|
|
182
|
+
let headPositionStmt =
|
|
183
|
+
db->SqliteDriver.prepare("SELECT MAX(position) AS m FROM dcb_event WHERE log_name = ?")
|
|
187
184
|
|
|
188
185
|
// Prepared-statement cache keyed on SQL text. `runQuery`, the batched tag
|
|
189
186
|
// hydration, and the conflict check all build SQL whose shape varies with the
|
|
@@ -218,8 +215,7 @@ let makeStorage = (
|
|
|
218
215
|
if positions->Array.length > 0 {
|
|
219
216
|
let placeholders = positions->Array.map(_ => "?")->Array.join(",")
|
|
220
217
|
let sql = `SELECT position, tag_key, tag_value FROM dcb_tag WHERE log_name = ? AND position IN (${placeholders}) ORDER BY position ASC, rowid ASC`
|
|
221
|
-
let params =
|
|
222
|
-
[JSON.Encode.string(name)]->Array.concat(positions->Array.map(JSON.Encode.int))
|
|
218
|
+
let params = [JSON.Encode.string(name)]->Array.concat(positions->Array.map(JSON.Encode.int))
|
|
223
219
|
preparedFor(sql)
|
|
224
220
|
->SqliteDriver.all(params)
|
|
225
221
|
->Array.forEach(row => {
|
|
@@ -249,7 +245,9 @@ let makeStorage = (
|
|
|
249
245
|
byPos
|
|
250
246
|
}
|
|
251
247
|
|
|
252
|
-
let runQuery = (~query: DcbTag.query, ~after: option<int>): array<
|
|
248
|
+
let runQuery = (~query: DcbTag.query, ~after: option<int>): array<
|
|
249
|
+
DcbEventLog_Adapter.rawSequencedEvent,
|
|
250
|
+
> => {
|
|
253
251
|
let (sql, params) = buildQuerySql(~logName=name, ~query, ~after)
|
|
254
252
|
let rows = preparedFor(sql)->SqliteDriver.all(params)
|
|
255
253
|
let positions = rows->Array.map(row =>
|
|
@@ -284,14 +282,17 @@ let makeStorage = (
|
|
|
284
282
|
| Some(JSON.String(s)) => s
|
|
285
283
|
| _ => ""
|
|
286
284
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
285
|
+
|
|
286
|
+
(
|
|
287
|
+
{
|
|
288
|
+
DcbEventLog_Adapter.position: Int.toString(position),
|
|
289
|
+
eventType,
|
|
290
|
+
data,
|
|
291
|
+
tags: tagsByPos->Dict.get(Int.toString(position))->Option.getOr([]),
|
|
292
|
+
meta,
|
|
293
|
+
recordedAt,
|
|
294
|
+
}: DcbEventLog_Adapter.rawSequencedEvent
|
|
295
|
+
)
|
|
295
296
|
})
|
|
296
297
|
}
|
|
297
298
|
|
|
@@ -305,10 +306,7 @@ let makeStorage = (
|
|
|
305
306
|
->Option.isSome
|
|
306
307
|
}
|
|
307
308
|
|
|
308
|
-
let read = async (
|
|
309
|
-
~query: DcbTag.query,
|
|
310
|
-
~after=?,
|
|
311
|
-
): DcbEventLog_Adapter.rawReadResult => {
|
|
309
|
+
let read = async (~query: DcbTag.query, ~after=?): DcbEventLog_Adapter.rawReadResult => {
|
|
312
310
|
let afterInt = after->Option.map(posToInt)
|
|
313
311
|
let events = runQuery(~query, ~after=afterInt)
|
|
314
312
|
let head = currentMaxPosition()
|
|
@@ -319,10 +317,10 @@ let makeStorage = (
|
|
|
319
317
|
}
|
|
320
318
|
}
|
|
321
319
|
|
|
322
|
-
let append = async (
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
320
|
+
let append = async (newEvents: array<DcbEventLog_Adapter.rawStoredEvent>, ~condition=?): result<
|
|
321
|
+
DcbTag.sequencePosition,
|
|
322
|
+
ReventlessInfra.DcbEventLog.appendError,
|
|
323
|
+
> => {
|
|
326
324
|
let result = ref(Ok(""))
|
|
327
325
|
// (msgId, dcb_event rowid) per inserted event, for the projection
|
|
328
326
|
// checkpoint's DCB-axis pending set. The rowid is read immediately after
|
|
@@ -363,14 +361,16 @@ let makeStorage = (
|
|
|
363
361
|
}
|
|
364
362
|
| None => ()
|
|
365
363
|
}
|
|
366
|
-
event.tags->Array.forEach(
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
364
|
+
event.tags->Array.forEach(
|
|
365
|
+
tag => {
|
|
366
|
+
insertTagStmt->SqliteDriver.run([
|
|
367
|
+
JSON.Encode.string(name),
|
|
368
|
+
JSON.Encode.int(pos),
|
|
369
|
+
JSON.Encode.string(tag.key),
|
|
370
|
+
JSON.Encode.string(tag.value),
|
|
371
|
+
])
|
|
372
|
+
},
|
|
373
|
+
)
|
|
374
374
|
lastPos := pos
|
|
375
375
|
})
|
|
376
376
|
|
|
@@ -37,7 +37,6 @@ type nodeResponse
|
|
|
37
37
|
// mangled by utf8 decoding (setEncoding is deliberately NOT called).
|
|
38
38
|
@send external _onDataBuf: (nodeRequest, @as("data") _, NodeBuffer.t => unit) => unit = "on"
|
|
39
39
|
|
|
40
|
-
|
|
41
40
|
let readBody = (req: nodeRequest, onBody: string => unit): unit => {
|
|
42
41
|
let buf = ref("")
|
|
43
42
|
req->_setEncoding("utf8")
|
|
@@ -65,10 +64,7 @@ type requestHandler = (nodeRequest, nodeResponse) => unit
|
|
|
65
64
|
// -- /__inmemory/login + /logout handlers -----------------------------------
|
|
66
65
|
|
|
67
66
|
let _writeJson = (res: nodeResponse, ~status: int, body: JSON.t): unit => {
|
|
68
|
-
res->writeHead(
|
|
69
|
-
status,
|
|
70
|
-
{"Content-Type": "application/json", "Access-Control-Allow-Origin": "*"},
|
|
71
|
-
)
|
|
67
|
+
res->writeHead(status, {"Content-Type": "application/json", "Access-Control-Allow-Origin": "*"})
|
|
72
68
|
res->end_(body->JSON.stringify)
|
|
73
69
|
}
|
|
74
70
|
|
|
@@ -109,16 +105,12 @@ let handleLogin = (req: nodeRequest, res: nodeResponse): unit =>
|
|
|
109
105
|
| Some(i) => i
|
|
110
106
|
| None => Reventless.Identity.anonymous
|
|
111
107
|
}
|
|
112
|
-
let identityJson =
|
|
113
|
-
identity->Reventless.Util_Sury.toJson(Reventless.Identity.schema)
|
|
108
|
+
let identityJson = identity->Reventless.Util_Sury.toJson(Reventless.Identity.schema)
|
|
114
109
|
_writeJson(
|
|
115
110
|
res,
|
|
116
111
|
~status=200,
|
|
117
112
|
JSON.Encode.object(
|
|
118
|
-
Dict.fromArray([
|
|
119
|
-
("token", JSON.Encode.string(token)),
|
|
120
|
-
("identity", identityJson),
|
|
121
|
-
]),
|
|
113
|
+
Dict.fromArray([("token", JSON.Encode.string(token)), ("identity", identityJson)]),
|
|
122
114
|
),
|
|
123
115
|
)
|
|
124
116
|
}
|
|
@@ -169,10 +161,7 @@ let handleSwitchRole = (req: nodeRequest, res: nodeResponse): unit =>
|
|
|
169
161
|
JSON.Encode.object(
|
|
170
162
|
Dict.fromArray([
|
|
171
163
|
("token", JSON.Encode.string(newToken)),
|
|
172
|
-
(
|
|
173
|
-
"identity",
|
|
174
|
-
identity->Reventless.Util_Sury.toJson(Reventless.Identity.schema),
|
|
175
|
-
),
|
|
164
|
+
("identity", identity->Reventless.Util_Sury.toJson(Reventless.Identity.schema)),
|
|
176
165
|
]),
|
|
177
166
|
),
|
|
178
167
|
)
|
|
@@ -205,8 +194,7 @@ let _corsWriteHeaders = {
|
|
|
205
194
|
}
|
|
206
195
|
|
|
207
196
|
let handleObjectPut = (req: nodeRequest, res: nodeResponse, ~key: string): unit => {
|
|
208
|
-
let contentType =
|
|
209
|
-
req.headers->Dict.get("content-type")->Option.getOr("application/octet-stream")
|
|
197
|
+
let contentType = req.headers->Dict.get("content-type")->Option.getOr("application/octet-stream")
|
|
210
198
|
readBodyBuf(req, bytes => {
|
|
211
199
|
LocalObjectStore.put(~key, ~bytes, ~contentType)
|
|
212
200
|
res->writeHead(200, _corsWriteHeaders)
|
|
@@ -239,17 +227,22 @@ let handleObjectGet = (res: nodeResponse, ~key: string): unit =>
|
|
|
239
227
|
let _isInvalidBearer = (req: nodeRequest): bool =>
|
|
240
228
|
switch req.headers->Dict.get("authorization") {
|
|
241
229
|
| Some(h) if String.startsWith(h, "Bearer ") =>
|
|
242
|
-
let token =
|
|
243
|
-
String.slice(h, ~start=7, ~end=String.length(h))->String.trim
|
|
230
|
+
let token = String.slice(h, ~start=7, ~end=String.length(h))->String.trim
|
|
244
231
|
LocalAuth.Login.verifyAndDecode(token)->Option.isNone
|
|
245
232
|
| _ => false
|
|
246
233
|
}
|
|
247
234
|
|
|
248
235
|
// Shared dispatch for start(). Order matters: built-in
|
|
249
236
|
// endpoints win over the yoga catch-all so they aren't shadowed by graphql.
|
|
250
|
-
let _dispatch = (
|
|
237
|
+
let _dispatch = (
|
|
238
|
+
req: nodeRequest,
|
|
239
|
+
res: nodeResponse,
|
|
240
|
+
yoga: YG.yoga,
|
|
241
|
+
getSdl: unit => string,
|
|
242
|
+
): unit => {
|
|
251
243
|
// Strip query string before path matching.
|
|
252
244
|
let path = req.url->String.split("?")->Array.get(0)->Option.getOr(req.url)
|
|
245
|
+
|
|
253
246
|
// /__inmemory/login is the path that *issues* tokens — it never carries
|
|
254
247
|
// one. Every other path must reject an unverifiable Bearer with 401 so
|
|
255
248
|
// the host-shell's `on401` logout path can fire on stale tokens.
|
|
@@ -257,9 +250,7 @@ let _dispatch = (req: nodeRequest, res: nodeResponse, yoga: YG.yoga, getSdl: uni
|
|
|
257
250
|
_writeJson(
|
|
258
251
|
res,
|
|
259
252
|
~status=401,
|
|
260
|
-
JSON.Encode.object(
|
|
261
|
-
Dict.fromArray([("error", JSON.Encode.string("Invalid bearer token"))]),
|
|
262
|
-
),
|
|
253
|
+
JSON.Encode.object(Dict.fromArray([("error", JSON.Encode.string("Invalid bearer token"))])),
|
|
263
254
|
)
|
|
264
255
|
} else if path == "/sdl" {
|
|
265
256
|
res->writeHead(200, {"Content-Type": "text/plain", "Access-Control-Allow-Origin": "*"})
|
|
@@ -429,8 +420,7 @@ let relabelScope = (~from: string, ~to_: string) =>
|
|
|
429
420
|
->Dict.toArray
|
|
430
421
|
->Array.forEach(((typeName, byField)) =>
|
|
431
422
|
switch existing.fieldResolvers->Dict.get(typeName) {
|
|
432
|
-
| Some(target) =>
|
|
433
|
-
byField->Dict.toArray->Array.forEach(((k, v)) => target->Dict.set(k, v))
|
|
423
|
+
| Some(target) => byField->Dict.toArray->Array.forEach(((k, v)) => target->Dict.set(k, v))
|
|
434
424
|
| None => existing.fieldResolvers->Dict.set(typeName, byField)
|
|
435
425
|
}
|
|
436
426
|
)
|
|
@@ -483,7 +473,9 @@ let registerFieldResolvers = (~typeName: string, ~resolvers: dict<resolverFn>) =
|
|
|
483
473
|
let getFieldResolver = (~typeName: string, fieldName: string): option<resolverFn> =>
|
|
484
474
|
buckets.contents
|
|
485
475
|
->Dict.valuesToArray
|
|
486
|
-
->Array.findMap(b =>
|
|
476
|
+
->Array.findMap(b =>
|
|
477
|
+
b.fieldResolvers->Dict.get(typeName)->Option.flatMap(d => d->Dict.get(fieldName))
|
|
478
|
+
)
|
|
487
479
|
|
|
488
480
|
/** Look up a registered mutation resolver by field name (used by MCP_Server).
|
|
489
481
|
Searches all scope buckets. */
|
|
@@ -575,8 +567,7 @@ let assembleBucketSdl = (
|
|
|
575
567
|
sections->Array.push(typeDefinitions->Array.join("\n\n"))
|
|
576
568
|
}
|
|
577
569
|
if withRootDefaults || queryFields->Array.length > 0 {
|
|
578
|
-
let queries =
|
|
579
|
-
queryFields->Array.length > 0 ? queryFields->Array.join("\n") : " _noop: String"
|
|
570
|
+
let queries = queryFields->Array.length > 0 ? queryFields->Array.join("\n") : " _noop: String"
|
|
580
571
|
sections->Array.push(`type Query {\n${queries}\n}`)
|
|
581
572
|
}
|
|
582
573
|
if withRootDefaults || mutationFields->Array.length > 0 {
|
|
@@ -737,8 +728,14 @@ let start = (~port: int=4000, ~contextFactory as _: option<YG.contextFactory>=?,
|
|
|
737
728
|
// original Error even when the client response is masked — is never swallowed
|
|
738
729
|
// again), while verbose debug/info stay gated on GRAPHQL_DEBUG.
|
|
739
730
|
let yogaLogging: YG.yogaLogger = {
|
|
740
|
-
debug: a =>
|
|
741
|
-
|
|
731
|
+
debug: a =>
|
|
732
|
+
if debug {
|
|
733
|
+
log.debug(~comp="GraphQL:Domain", YG.logArgToString(a))
|
|
734
|
+
},
|
|
735
|
+
info: a =>
|
|
736
|
+
if debug {
|
|
737
|
+
log.info(~comp="GraphQL:Domain", YG.logArgToString(a))
|
|
738
|
+
},
|
|
742
739
|
warn: a => log.warn(~comp="GraphQL:Domain", YG.logArgToString(a)),
|
|
743
740
|
error: a => log.error(~comp="GraphQL:Domain", YG.logArgToString(a)),
|
|
744
741
|
}
|
|
@@ -760,15 +757,19 @@ let start = (~port: int=4000, ~contextFactory as _: option<YG.contextFactory>=?,
|
|
|
760
757
|
let detail = err->JsExn.message->Option.getOr("unknown error")
|
|
761
758
|
log.error(
|
|
762
759
|
~comp="GraphQL:Domain",
|
|
763
|
-
`failed to bind port ${port->Int.toString}: ${detail} — ` ++
|
|
764
|
-
`is another server (dev server, prior test) already listening there?`,
|
|
760
|
+
`failed to bind port ${port->Int.toString}: ${detail} — ` ++ `is another server (dev server, prior test) already listening there?`,
|
|
765
761
|
)
|
|
766
762
|
// Re-raise so the failure is loud instead of a silently-unbound server whose
|
|
767
763
|
// requests fall through to whatever else holds the port.
|
|
768
|
-
JsError.throwWithMessage(
|
|
764
|
+
JsError.throwWithMessage(
|
|
765
|
+
`DomainGraphQL_Server could not bind port ${port->Int.toString}: ${detail}`,
|
|
766
|
+
)
|
|
769
767
|
})
|
|
770
768
|
server->YG.listen(port, () =>
|
|
771
|
-
log.info(
|
|
769
|
+
log.info(
|
|
770
|
+
~comp="GraphQL:Domain",
|
|
771
|
+
`listening on http://localhost:${port->Int.toString}/graphql (SDL: /sdl)`,
|
|
772
|
+
)
|
|
772
773
|
)
|
|
773
774
|
// Local AppSync Events transport (subscribe WS). Attached here so every
|
|
774
775
|
// start mode (split, unified, replay) carries it.
|
|
@@ -91,6 +91,7 @@ let registerResourcesFromEntries = (
|
|
|
91
91
|
],
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
|
+
|
|
94
95
|
// URIs with template parameters ({id}) go into resource templates;
|
|
95
96
|
// fixed URIs (list endpoints) go into regular resources.
|
|
96
97
|
if def.uriTemplate->String.includes("{") {
|
|
@@ -237,9 +238,7 @@ let createServerInstance = (identity: Reventless.Identity.t) => {
|
|
|
237
238
|
}
|
|
238
239
|
await handler(uri)
|
|
239
240
|
| None => {
|
|
240
|
-
McpSdk.contents: [
|
|
241
|
-
{McpSdk.uri, text: `{"error": "Resource not found: ${uri}"}`},
|
|
242
|
-
],
|
|
241
|
+
McpSdk.contents: [{McpSdk.uri, text: `{"error": "Resource not found: ${uri}"}`}],
|
|
243
242
|
}
|
|
244
243
|
}
|
|
245
244
|
}
|
|
@@ -250,45 +249,47 @@ let createServerInstance = (identity: Reventless.Identity.t) => {
|
|
|
250
249
|
|
|
251
250
|
let start = (~port: int=3001, ()) => {
|
|
252
251
|
let httpServer = McpSdk.createHttpServer((req, res) => {
|
|
253
|
-
let _ = (
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
252
|
+
let _ = (
|
|
253
|
+
async () => {
|
|
254
|
+
let reqMethod = req->McpSdk.method
|
|
255
|
+
let reqUrl = req->McpSdk.url
|
|
256
|
+
|
|
257
|
+
// CORS headers for browser-based clients (e.g. MCP Inspector)
|
|
258
|
+
res->McpSdk.setHeader("Access-Control-Allow-Origin", "*")
|
|
259
|
+
res->McpSdk.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS")
|
|
260
|
+
res->McpSdk.setHeader("Access-Control-Allow-Headers", "Content-Type")
|
|
261
|
+
|
|
262
|
+
if reqUrl == "/mcp" || reqUrl->String.startsWith("/mcp?") {
|
|
263
|
+
switch reqMethod {
|
|
264
|
+
| "OPTIONS" =>
|
|
265
|
+
res->McpSdk.setStatusCode(204)
|
|
266
|
+
res->McpSdk.endResponseNoBody
|
|
267
|
+
| "POST" =>
|
|
268
|
+
let body = await McpSdk_Helpers.parseJsonBody(req)
|
|
269
|
+
let identity = extractIdentity(req)
|
|
270
|
+
// Stateless mode: fresh server + transport per request
|
|
271
|
+
let server = createServerInstance(identity)
|
|
272
|
+
let transport = McpSdk.newStreamableHTTPTransport({
|
|
273
|
+
enableJsonResponse: true,
|
|
274
|
+
})
|
|
275
|
+
let _ = await server->McpSdk.connect(transport)
|
|
276
|
+
let _ = await transport->McpSdk.handleRequest(req, res, body)
|
|
277
|
+
| "GET" =>
|
|
278
|
+
res->McpSdk.setHeader("Content-Type", "text/plain")
|
|
279
|
+
res->McpSdk.endResponse("MCP server running")
|
|
280
|
+
| "DELETE" =>
|
|
281
|
+
res->McpSdk.setStatusCode(200)
|
|
282
|
+
res->McpSdk.endResponseNoBody
|
|
283
|
+
| _ =>
|
|
284
|
+
res->McpSdk.setStatusCode(405)
|
|
285
|
+
res->McpSdk.endResponse("Method not allowed")
|
|
286
|
+
}
|
|
287
|
+
} else {
|
|
288
|
+
res->McpSdk.setStatusCode(404)
|
|
289
|
+
res->McpSdk.endResponse("Not found")
|
|
286
290
|
}
|
|
287
|
-
} else {
|
|
288
|
-
res->McpSdk.setStatusCode(404)
|
|
289
|
-
res->McpSdk.endResponse("Not found")
|
|
290
291
|
}
|
|
291
|
-
|
|
292
|
+
)()
|
|
292
293
|
})
|
|
293
294
|
|
|
294
295
|
httpServer->McpSdk.listen(port, () => {
|
|
@@ -301,7 +302,10 @@ let start = (~port: int=3001, ()) => {
|
|
|
301
302
|
toolNames->Array.forEach(t => log.info(~comp="MCP:Domain", ` - ${t}`))
|
|
302
303
|
log.info(~comp="MCP:Domain", `resources (${resourceNames->Array.length->Int.toString}):`)
|
|
303
304
|
resourceNames->Array.forEach(r => log.info(~comp="MCP:Domain", ` - ${r}`))
|
|
304
|
-
log.info(
|
|
305
|
+
log.info(
|
|
306
|
+
~comp="MCP:Domain",
|
|
307
|
+
`resource templates (${templateNames->Array.length->Int.toString}):`,
|
|
308
|
+
)
|
|
305
309
|
templateNames->Array.forEach(r => log.info(~comp="MCP:Domain", ` - ${r}`))
|
|
306
310
|
}
|
|
307
311
|
})
|