@reventlessdev/reventless-local 3.0.0-alpha.249 → 3.0.0-alpha.251
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 +30 -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 +12 -16
- package/src/adapter/Auth/LocalAuth.res.mjs +2 -1
- package/src/adapter/Auth/UserStore.res +13 -40
- package/src/adapter/Auth/UserStore.res.mjs +11 -67
- 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 +6 -0
- package/src/adapter/LocalCapabilities.res.mjs +5 -2
- 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 +10 -6
- package/src/adapter/PlatformGraphQL_Server.res.mjs +2 -1
- 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
|
@@ -15,7 +15,11 @@ let makeStorage = () =>
|
|
|
15
15
|
~opts,
|
|
16
16
|
)
|
|
17
17
|
|
|
18
|
-
let makeEvent = (
|
|
18
|
+
let makeEvent = (
|
|
19
|
+
~eventType,
|
|
20
|
+
~data,
|
|
21
|
+
~tags=[],
|
|
22
|
+
): ReventlessCore.DcbEventLog_Adapter.rawStoredEvent => {
|
|
19
23
|
eventType,
|
|
20
24
|
data,
|
|
21
25
|
tags,
|
|
@@ -24,236 +28,289 @@ let makeEvent = (~eventType, ~data, ~tags=[]): ReventlessCore.DcbEventLog_Adapte
|
|
|
24
28
|
|
|
25
29
|
describe("DcbEventLogStorage_InMemory", () => {
|
|
26
30
|
describe("append", () => {
|
|
27
|
-
testPromise(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
testPromise(
|
|
32
|
+
"stores events and returns incrementing position",
|
|
33
|
+
async () => {
|
|
34
|
+
let storage = makeStorage()
|
|
35
|
+
let ops = await storage.operations->TestRunner.resolve
|
|
36
|
+
let result1 = await ops.append([makeEvent(~eventType="ItemCreated", ~data=JSON.Null)])
|
|
37
|
+
let result2 = await ops.append([makeEvent(~eventType="ItemUpdated", ~data=JSON.Null)])
|
|
38
|
+
expect(result1)->toEqual(Ok("1"))
|
|
39
|
+
expect(result2)->toEqual(Ok("2"))
|
|
40
|
+
},
|
|
41
|
+
)
|
|
35
42
|
|
|
36
|
-
testPromise(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
testPromise(
|
|
44
|
+
"appending multiple events at once returns position of last event",
|
|
45
|
+
async () => {
|
|
46
|
+
let storage = makeStorage()
|
|
47
|
+
let ops = await storage.operations->TestRunner.resolve
|
|
48
|
+
let result = await ops.append([
|
|
49
|
+
makeEvent(~eventType="A", ~data=JSON.Null),
|
|
50
|
+
makeEvent(~eventType="B", ~data=JSON.Null),
|
|
51
|
+
])
|
|
52
|
+
expect(result)->toEqual(Ok("2"))
|
|
53
|
+
},
|
|
54
|
+
)
|
|
45
55
|
})
|
|
46
56
|
|
|
47
57
|
describe("read", () => {
|
|
48
|
-
testPromise(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
testPromise(
|
|
59
|
+
"empty query returns all events",
|
|
60
|
+
async () => {
|
|
61
|
+
let storage = makeStorage()
|
|
62
|
+
let ops = await storage.operations->TestRunner.resolve
|
|
63
|
+
let _ = await ops.append([
|
|
64
|
+
makeEvent(~eventType="Evt1", ~data=JSON.Null),
|
|
65
|
+
makeEvent(~eventType="Evt2", ~data=JSON.Null),
|
|
66
|
+
])
|
|
67
|
+
let result = await ops.read(~query=[])
|
|
68
|
+
expect(result.events->Array.length)->toBe(2)
|
|
69
|
+
},
|
|
70
|
+
)
|
|
58
71
|
|
|
59
|
-
testPromise(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
testPromise(
|
|
73
|
+
"filters by eventType",
|
|
74
|
+
async () => {
|
|
75
|
+
let storage = makeStorage()
|
|
76
|
+
let ops = await storage.operations->TestRunner.resolve
|
|
77
|
+
let _ = await ops.append([
|
|
78
|
+
makeEvent(~eventType="Created", ~data=JSON.Null),
|
|
79
|
+
makeEvent(~eventType="Updated", ~data=JSON.Null),
|
|
80
|
+
makeEvent(~eventType="Created", ~data=JSON.Null),
|
|
81
|
+
])
|
|
82
|
+
// Optional record fields use direct value syntax (not Some(...))
|
|
83
|
+
let result = await ops.read(~query=[{Reventless.DcbTag.eventTypes: ["Created"]}])
|
|
84
|
+
expect(result.events->Array.length)->toBe(2)
|
|
85
|
+
let firstEvent = result.events->Array.getUnsafe(0)
|
|
86
|
+
expect(firstEvent.eventType)->toBe("Created")
|
|
87
|
+
},
|
|
88
|
+
)
|
|
75
89
|
|
|
76
|
-
testPromise(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
90
|
+
testPromise(
|
|
91
|
+
"filters by tags",
|
|
92
|
+
async () => {
|
|
93
|
+
let storage = makeStorage()
|
|
94
|
+
let ops = await storage.operations->TestRunner.resolve
|
|
95
|
+
let _ = await ops.append([
|
|
96
|
+
makeEvent(
|
|
97
|
+
~eventType="Evt",
|
|
98
|
+
~data=JSON.Null,
|
|
99
|
+
~tags=[{Reventless.DcbTag.key: "tenant", value: "acme"}],
|
|
100
|
+
),
|
|
101
|
+
makeEvent(~eventType="Evt", ~data=JSON.Null, ~tags=[]),
|
|
102
|
+
])
|
|
103
|
+
let result = await ops.read(
|
|
104
|
+
~query=[
|
|
105
|
+
{
|
|
106
|
+
Reventless.DcbTag.tags: [{Reventless.DcbTag.key: "tenant", value: "acme"}],
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
)
|
|
110
|
+
expect(result.events->Array.length)->toBe(1)
|
|
111
|
+
let firstEvent = result.events->Array.getUnsafe(0)
|
|
112
|
+
expect(firstEvent.tags->Array.length)->toBe(1)
|
|
113
|
+
},
|
|
114
|
+
)
|
|
98
115
|
|
|
99
|
-
testPromise(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
],
|
|
119
|
-
|
|
120
|
-
]
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
116
|
+
testPromise(
|
|
117
|
+
"multi-tag clause matches only events carrying ALL tags",
|
|
118
|
+
async () => {
|
|
119
|
+
let storage = makeStorage()
|
|
120
|
+
let ops = await storage.operations->TestRunner.resolve
|
|
121
|
+
let _ = await ops.append([
|
|
122
|
+
makeEvent(
|
|
123
|
+
~eventType="Evt",
|
|
124
|
+
~data=JSON.Null,
|
|
125
|
+
~tags=[{Reventless.DcbTag.key: "tenant", value: "acme"}, {key: "region", value: "eu"}],
|
|
126
|
+
),
|
|
127
|
+
makeEvent(
|
|
128
|
+
~eventType="Evt",
|
|
129
|
+
~data=JSON.Null,
|
|
130
|
+
~tags=[{Reventless.DcbTag.key: "tenant", value: "acme"}],
|
|
131
|
+
),
|
|
132
|
+
makeEvent(
|
|
133
|
+
~eventType="Evt",
|
|
134
|
+
~data=JSON.Null,
|
|
135
|
+
~tags=[{Reventless.DcbTag.key: "region", value: "eu"}],
|
|
136
|
+
),
|
|
137
|
+
])
|
|
138
|
+
// Intersection of the two tags' posting lists → only the first event.
|
|
139
|
+
let result = await ops.read(
|
|
140
|
+
~query=[
|
|
141
|
+
{
|
|
142
|
+
Reventless.DcbTag.tags: [
|
|
143
|
+
{Reventless.DcbTag.key: "tenant", value: "acme"},
|
|
144
|
+
{key: "region", value: "eu"},
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
)
|
|
149
|
+
expect(result.events->Array.length)->toBe(1)
|
|
150
|
+
},
|
|
151
|
+
)
|
|
124
152
|
|
|
125
|
-
testPromise(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
]
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
153
|
+
testPromise(
|
|
154
|
+
"OR of clauses unions matches across clauses in position order",
|
|
155
|
+
async () => {
|
|
156
|
+
let storage = makeStorage()
|
|
157
|
+
let ops = await storage.operations->TestRunner.resolve
|
|
158
|
+
let _ = await ops.append([
|
|
159
|
+
makeEvent(~eventType="A", ~data=JSON.Null),
|
|
160
|
+
makeEvent(
|
|
161
|
+
~eventType="B",
|
|
162
|
+
~data=JSON.Null,
|
|
163
|
+
~tags=[{Reventless.DcbTag.key: "x", value: "1"}],
|
|
164
|
+
),
|
|
165
|
+
makeEvent(~eventType="C", ~data=JSON.Null),
|
|
166
|
+
])
|
|
167
|
+
// Clause 1 (type A) ∪ clause 2 (tag x=1) → events at positions 1 and 2.
|
|
168
|
+
let result = await ops.read(
|
|
169
|
+
~query=[
|
|
170
|
+
{Reventless.DcbTag.eventTypes: ["A"]},
|
|
171
|
+
{Reventless.DcbTag.tags: [{Reventless.DcbTag.key: "x", value: "1"}]},
|
|
172
|
+
],
|
|
173
|
+
)
|
|
174
|
+
expect(result.events->Array.length)->toBe(2)
|
|
175
|
+
expect((result.events->Array.getUnsafe(0)).eventType)->toBe("A")
|
|
176
|
+
expect((result.events->Array.getUnsafe(1)).eventType)->toBe("B")
|
|
177
|
+
},
|
|
178
|
+
)
|
|
144
179
|
|
|
145
180
|
// An empty tag value is a legitimate model state — an absent member of a
|
|
146
181
|
// composite partition key. Every backend must accept it, record it verbatim,
|
|
147
182
|
// and keep matching the event through its partition and composite reads. See
|
|
148
183
|
// docs/plans/done/dcb-empty-tag-values-break-append.md.
|
|
149
|
-
testPromise(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
184
|
+
testPromise(
|
|
185
|
+
"an empty tag value appends and stays readable",
|
|
186
|
+
async () => {
|
|
187
|
+
let storage = makeStorage()
|
|
188
|
+
let ops = await storage.operations->TestRunner.resolve
|
|
189
|
+
let tags = [
|
|
190
|
+
{Reventless.DcbTag.key: "itemId", value: "i1"},
|
|
191
|
+
{Reventless.DcbTag.key: "variantId", value: ""},
|
|
192
|
+
]
|
|
193
|
+
let appended = await ops.append([makeEvent(~eventType="ItemAdded", ~data=JSON.Null, ~tags)])
|
|
194
|
+
expect(appended)->toEqual(Ok("1"))
|
|
158
195
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
196
|
+
let byPartition = await ops.read(
|
|
197
|
+
~query=[{Reventless.DcbTag.tags: [{Reventless.DcbTag.key: "itemId", value: "i1"}]}],
|
|
198
|
+
)
|
|
199
|
+
expect(byPartition.events->Array.length)->toBe(1)
|
|
200
|
+
// The empty value is recorded verbatim, not dropped or substituted.
|
|
201
|
+
expect(
|
|
202
|
+
(byPartition.events->Array.getUnsafe(0)).tags->Array.map(t => (t.key, t.value)),
|
|
203
|
+
)->toEqual([("itemId", "i1"), ("variantId", "")])
|
|
166
204
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
205
|
+
let byComposite = await ops.read(~query=[{Reventless.DcbTag.tags: tags}])
|
|
206
|
+
expect(byComposite.events->Array.length)->toBe(1)
|
|
207
|
+
},
|
|
208
|
+
)
|
|
170
209
|
|
|
171
|
-
testPromise(
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
210
|
+
testPromise(
|
|
211
|
+
"after parameter skips events at or before that position",
|
|
212
|
+
async () => {
|
|
213
|
+
let storage = makeStorage()
|
|
214
|
+
let ops = await storage.operations->TestRunner.resolve
|
|
215
|
+
let _ = await ops.append([
|
|
216
|
+
makeEvent(~eventType="E1", ~data=JSON.Null),
|
|
217
|
+
makeEvent(~eventType="E2", ~data=JSON.Null),
|
|
218
|
+
makeEvent(~eventType="E3", ~data=JSON.Null),
|
|
219
|
+
])
|
|
220
|
+
let result = await ops.read(~query=[], ~after="1")
|
|
221
|
+
// Events at position > "1" — positions 2 and 3
|
|
222
|
+
expect(result.events->Array.length)->toBe(2)
|
|
223
|
+
let firstEvent = result.events->Array.getUnsafe(0)
|
|
224
|
+
expect(firstEvent.eventType)->toBe("E2")
|
|
225
|
+
},
|
|
226
|
+
)
|
|
185
227
|
})
|
|
186
228
|
|
|
187
229
|
describe("headPosition", () => {
|
|
188
|
-
testPromise(
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
230
|
+
testPromise(
|
|
231
|
+
"equals the position of the last appended event",
|
|
232
|
+
async () => {
|
|
233
|
+
let storage = makeStorage()
|
|
234
|
+
let ops = await storage.operations->TestRunner.resolve
|
|
235
|
+
let _ = await ops.append([
|
|
236
|
+
makeEvent(~eventType="E1", ~data=JSON.Null),
|
|
237
|
+
makeEvent(~eventType="E2", ~data=JSON.Null),
|
|
238
|
+
])
|
|
239
|
+
let result = await ops.read(~query=[])
|
|
240
|
+
expect(result.headPosition)->toEqual(Some("2"))
|
|
241
|
+
},
|
|
242
|
+
)
|
|
198
243
|
|
|
199
|
-
testPromise(
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
244
|
+
testPromise(
|
|
245
|
+
"headPosition is absent when no events have been stored",
|
|
246
|
+
async () => {
|
|
247
|
+
let storage = makeStorage()
|
|
248
|
+
let ops = await storage.operations->TestRunner.resolve
|
|
249
|
+
let result = await ops.read(~query=[])
|
|
250
|
+
expect(result.headPosition)->toEqual(None)
|
|
251
|
+
},
|
|
252
|
+
)
|
|
205
253
|
})
|
|
206
254
|
|
|
207
255
|
describe("conditional append", () => {
|
|
208
|
-
testPromise(
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
256
|
+
testPromise(
|
|
257
|
+
"append with matching condition query returns Error (conflict)",
|
|
258
|
+
async () => {
|
|
259
|
+
let storage = makeStorage()
|
|
260
|
+
let ops = await storage.operations->TestRunner.resolve
|
|
261
|
+
let _ = await ops.append([makeEvent(~eventType="ItemCreated", ~data=JSON.Null)])
|
|
262
|
+
let condition: Reventless.DcbTag.appendCondition = {
|
|
263
|
+
query: [{Reventless.DcbTag.eventTypes: ["ItemCreated"]}],
|
|
264
|
+
}
|
|
265
|
+
let result = await ops.append(
|
|
266
|
+
[makeEvent(~eventType="ItemUpdated", ~data=JSON.Null)],
|
|
267
|
+
~condition,
|
|
268
|
+
)
|
|
269
|
+
expect(
|
|
270
|
+
switch result {
|
|
271
|
+
| Error(_) => true
|
|
272
|
+
| Ok(_) => false
|
|
273
|
+
},
|
|
274
|
+
)->toBe(true)
|
|
275
|
+
},
|
|
276
|
+
)
|
|
226
277
|
|
|
227
|
-
testPromise(
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
278
|
+
testPromise(
|
|
279
|
+
"append with condition that matches no events succeeds",
|
|
280
|
+
async () => {
|
|
281
|
+
let storage = makeStorage()
|
|
282
|
+
let ops = await storage.operations->TestRunner.resolve
|
|
283
|
+
let _ = await ops.append([makeEvent(~eventType="OtherEvent", ~data=JSON.Null)])
|
|
284
|
+
let condition: Reventless.DcbTag.appendCondition = {
|
|
285
|
+
query: [{Reventless.DcbTag.eventTypes: ["ItemCreated"]}],
|
|
286
|
+
}
|
|
287
|
+
let result = await ops.append(
|
|
288
|
+
[makeEvent(~eventType="ItemCreated", ~data=JSON.Null)],
|
|
289
|
+
~condition,
|
|
290
|
+
)
|
|
291
|
+
expect(result)->toEqual(Ok("2"))
|
|
292
|
+
},
|
|
293
|
+
)
|
|
240
294
|
|
|
241
|
-
testPromise(
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
295
|
+
testPromise(
|
|
296
|
+
"condition with after only checks events after that position",
|
|
297
|
+
async () => {
|
|
298
|
+
let storage = makeStorage()
|
|
299
|
+
let ops = await storage.operations->TestRunner.resolve
|
|
300
|
+
// Position 1: ItemCreated
|
|
301
|
+
let _ = await ops.append([makeEvent(~eventType="ItemCreated", ~data=JSON.Null)])
|
|
302
|
+
// Condition checks for ItemCreated only AFTER position "1" — no such events
|
|
303
|
+
let condition: Reventless.DcbTag.appendCondition = {
|
|
304
|
+
query: [{Reventless.DcbTag.eventTypes: ["ItemCreated"]}],
|
|
305
|
+
after: "1",
|
|
306
|
+
}
|
|
307
|
+
let result = await ops.append(
|
|
308
|
+
[makeEvent(~eventType="ItemUpdated", ~data=JSON.Null)],
|
|
309
|
+
~condition,
|
|
310
|
+
)
|
|
311
|
+
// No conflict because ItemCreated is at position 1, not > 1
|
|
312
|
+
expect(result)->toEqual(Ok("2"))
|
|
313
|
+
},
|
|
314
|
+
)
|
|
258
315
|
})
|
|
259
316
|
})
|
|
@@ -11,12 +11,12 @@ module Server = DomainGraphQL_Server
|
|
|
11
11
|
// Execute a query directly against a composed schema (no HTTP server needed).
|
|
12
12
|
type executionResult = {data: Nullable.t<JSON.t>, errors: Nullable.t<array<JSON.t>>}
|
|
13
13
|
@module("graphql")
|
|
14
|
-
external graphqlExecute: {"schema": GraphqlYoga.schema, "source": string} => promise<
|
|
15
|
-
|
|
14
|
+
external graphqlExecute: {"schema": GraphqlYoga.schema, "source": string} => promise<
|
|
15
|
+
executionResult,
|
|
16
|
+
> = "graphql"
|
|
16
17
|
|
|
17
18
|
let intResolver = (value: int): Server.resolverFn =>
|
|
18
|
-
async (_root, _args, _ctx) =>
|
|
19
|
-
JSON.Encode.object(Dict.fromArray([("x", JSON.Encode.int(value))]))
|
|
19
|
+
async (_root, _args, _ctx) => JSON.Encode.object(Dict.fromArray([("x", JSON.Encode.int(value))]))
|
|
20
20
|
|
|
21
21
|
let resolversOf = (entries: array<(string, Server.resolverFn)>) => Dict.fromArray(entries)
|
|
22
22
|
|
|
@@ -67,27 +67,30 @@ test("two scopes with identical shared-type copies compose and both fields resol
|
|
|
67
67
|
|
|
68
68
|
// ── (b) standalone-invalid scope fails with plugin-name attribution ─────────
|
|
69
69
|
|
|
70
|
-
testSync(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
70
|
+
testSync(
|
|
71
|
+
"scope referencing an undefined type fails standalone validation naming the plugin",
|
|
72
|
+
() => {
|
|
73
|
+
// Register under a construction token, then relabel to the plugin name —
|
|
74
|
+
// exercising the Platform.res flow (name only known after construction).
|
|
75
|
+
Server.setScope("plugin-token-1")
|
|
76
|
+
Server.registerQueries(
|
|
77
|
+
~sdlFields=[" broken: MissingType"],
|
|
78
|
+
~resolvers=resolversOf([("broken", intResolver(0))]),
|
|
79
|
+
)
|
|
80
|
+
Server.relabelScope(~from="plugin-token-1", ~to_="BrokenPlugin")
|
|
81
|
+
Server.resetScope()
|
|
82
|
+
|
|
83
|
+
switch Server.composeSchema() {
|
|
84
|
+
| _ => Runner.fail("expected composeSchema to throw")
|
|
85
|
+
| exception JsExn(err) => {
|
|
86
|
+
expect(
|
|
87
|
+
exnMessage(err),
|
|
88
|
+
)->toContain(`Plugin "BrokenPlugin" subgraph document is not valid standalone`)
|
|
89
|
+
expect(exnMessage(err))->toContain("MissingType")
|
|
90
|
+
}
|
|
88
91
|
}
|
|
89
|
-
}
|
|
90
|
-
|
|
92
|
+
},
|
|
93
|
+
)
|
|
91
94
|
|
|
92
95
|
// ── (c) conflicting same-named type across scopes fails the merge ────────────
|
|
93
96
|
|
|
@@ -109,7 +112,9 @@ testSync("two scopes defining the same type name with conflicting fields fail co
|
|
|
109
112
|
switch Server.composeSchema() {
|
|
110
113
|
| _ => Runner.fail("expected composeSchema to throw")
|
|
111
114
|
| exception JsExn(err) =>
|
|
112
|
-
expect(exnMessage(err))->toContain(
|
|
115
|
+
expect(exnMessage(err))->toContain(
|
|
116
|
+
"Cross-plugin schema merge failed (mirrors AWS MERGE_FAILED)",
|
|
117
|
+
)
|
|
113
118
|
}
|
|
114
119
|
})
|
|
115
120
|
|
|
@@ -141,21 +146,26 @@ testSync("reset clears all scope buckets and restores the platform scope", () =>
|
|
|
141
146
|
|
|
142
147
|
// ── per-scope shared types keep plugin subgraphs standalone-valid ────────────
|
|
143
148
|
|
|
144
|
-
test(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
149
|
+
test(
|
|
150
|
+
"scope missing its own shared-type copy fails standalone even when platform defines it",
|
|
151
|
+
async () => {
|
|
152
|
+
// The platform bucket defines SharedThing; the plugin bucket references it
|
|
153
|
+
// without carrying its own copy — standalone-invalid, mirroring an AWS
|
|
154
|
+
// source API that leans on another API's types.
|
|
155
|
+
Server.registerTypes(~sdlTypes=[sharedTypeSdl]) // platform scope
|
|
156
|
+
Server.setScope("Leaner")
|
|
157
|
+
Server.registerQueries(
|
|
158
|
+
~sdlFields=[" leanItem: SharedThing"],
|
|
159
|
+
~resolvers=resolversOf([("leanItem", intResolver(3))]),
|
|
160
|
+
)
|
|
161
|
+
Server.resetScope()
|
|
162
|
+
|
|
163
|
+
switch Server.composeSchema() {
|
|
164
|
+
| _ => Runner.fail("expected composeSchema to throw")
|
|
165
|
+
| exception JsExn(err) =>
|
|
166
|
+
expect(
|
|
167
|
+
exnMessage(err),
|
|
168
|
+
)->toContain(`Plugin "Leaner" subgraph document is not valid standalone`)
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
)
|