@reventlessdev/reventless-core 3.0.0-alpha.259 → 3.0.0-alpha.261
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 +15 -0
- package/package.json +11 -11
- package/src/Behavior.res +1 -4
- package/src/Message.res +17 -14
- package/src/Projection.res +51 -25
- package/src/RequestContext.res +8 -9
- package/src/ResourceAttribution.res +28 -14
- package/src/adapter/Adapter.res +48 -4
- package/src/adapter/EventLogProvisioning/EventLogProvisioning.res +5 -5
- package/src/adapter/Messaging/Messaging_Log_Backend.res +10 -7
- package/src/adapter/Messaging/Messaging_Log_Backend.res.mjs +2 -4
- package/src/adapter/Monitoring/Monitoring.res +16 -17
- package/src/adapter/QueryInterception/QueryInterception.res +0 -1
- package/src/adapter/Runtime/AggregateRuntime_Builder_Common.res +12 -5
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res +11 -7
- package/src/adapter/Runtime/PluginRuntime_Builder_Micro.res +1 -7
- package/src/adapter/Runtime/PluginRuntime_Builder_Single.res +1 -7
- package/src/adapter/RuntimeExtension/RuntimeExtension.res +0 -1
- package/src/admin/Platform_Admin.res +17 -6
- package/src/admin/Platform_AdminApi.res +14 -9
- package/src/admin/Platform_Admin_Callback.res +1 -1
- package/src/admin/Platform_BakedManifest.res +34 -34
- package/src/admin/Platform_ComponentDefinitionsApi.res +9 -21
- package/src/admin/Platform_PluginStructuresApi.res +15 -12
- package/src/admin/UiFragmentRegistry/StateChange/UiFragmentRegistry.res +1 -1
- package/src/components/Aggregate/Aggregate.res +13 -12
- package/src/components/Aggregate/Aggregate_Builder.res +22 -21
- package/src/components/Aggregate/Aggregate_Callback.res +91 -86
- package/src/components/Api/ApiNoApiHelpers.res +1 -3
- package/src/components/Api/Api_Ids.res +0 -1
- package/src/components/Api/Api_Naming.res +41 -18
- package/src/components/Api/GraphQL_FragmentGenerator.res +42 -52
- package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +11 -4
- package/src/components/Api/GraphQL_SchemaInspector.res +5 -3
- package/src/components/Api/GraphQL_Stitcher.res +9 -10
- package/src/components/Api/MCP_SchemaGenerator.res +8 -12
- package/src/components/Api/OwnerScopeDiagnostics.res +1 -2
- package/src/components/Api/QueryDbListQuery.res +13 -6
- package/src/components/Api/SchemaType.res +40 -20
- package/src/components/Api/SchemaType.res.mjs +16 -0
- package/src/components/Api/SuryToJsonSchema.res +18 -10
- package/src/components/Api/SuryToJsonSchema.res.mjs +135 -121
- package/src/components/AutomationSlice/AutomationSlice.res +4 -4
- package/src/components/AutomationSlice/AutomationSlice_Builder.res +15 -14
- package/src/components/AutomationSlice/AutomationSlice_Callback.res +3 -3
- package/src/components/Builder_Helpers.res +12 -23
- package/src/components/CommandGenerator/CommandGenerator.res +6 -1
- package/src/components/CommandGenerator/CommandGenerator_Builder.res +29 -21
- package/src/components/CommandGenerator/CommandGenerator_Callback.res +17 -21
- package/src/components/CommandTopic/CommandTopic.res +3 -3
- package/src/components/CommandTopic/CommandTopic_Builder.res +10 -10
- package/src/components/CommandTopic/CommandTopic_Callback.res +1 -4
- package/src/components/CommandTopic/CommandTopic_Helpers.res +1 -2
- package/src/components/Counter/Counter_Builder.res +1 -6
- package/src/components/Counter/Counter_Callback.res +3 -3
- package/src/components/Dcb/Dcb_Builder.res +970 -956
- package/src/components/DcbEventLog/DcbEventLog.res +4 -4
- package/src/components/DcbEventLog/DcbEventLog_Builder.res +0 -1
- package/src/components/DcbEventLog/DcbEventLog_Operations.res +17 -18
- package/src/components/EventCollector/EventCollector_Builder.res +2 -1
- package/src/components/EventLog/EventLog.res +5 -1
- package/src/components/EventLog/EventLog_Operations.res +12 -8
- package/src/components/EventMapper/EventMapper.res +8 -10
- package/src/components/EventMapper/EventMapper_Builder.res +4 -1
- package/src/components/EventMapper/EventMapper_Callback.res +46 -31
- package/src/components/EventTopic/EventTopic.res +3 -3
- package/src/components/EventTopic/EventTopic_Builder.res +1 -1
- package/src/components/Extension/Extension.res +4 -1
- package/src/components/Extension/Extension_Operations.res +11 -3
- package/src/components/ExtensionPoint/ExtensionPoint.res +3 -3
- package/src/components/ExtensionPoint/ExtensionPoint_Builder.res +9 -10
- package/src/components/ExtensionPoint/ExtensionPoint_Callback.res +2 -6
- package/src/components/ExtensionPoint/ExtensionPoint_Operations.res +13 -5
- package/src/components/Heartbeat/Heartbeat_Callback.res +3 -3
- package/src/components/InboundTranslationSlice/InboundTranslationSlice.res +4 -4
- package/src/components/InboundTranslationSlice/InboundTranslationSlice_Builder.res +7 -10
- package/src/components/OutboundTranslationSlice/OutboundTranslationSlice.res +4 -4
- package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Builder.res +15 -14
- package/src/components/QueryDb/QueryDb_Adapter.res +1 -3
- package/src/components/QueryDb/QueryDb_Operations.res +10 -10
- package/src/components/ReadModel/ReadModel.res +3 -3
- package/src/components/ReadModel/ReadModel_Builder.res +1 -2
- package/src/components/ReadModel/ReadModel_Callback.res +2 -1
- package/src/components/SideEffectHandler/SideEffectHandler_Callback.res +5 -2
- package/src/components/StateChangeSlice/StateChangeSlice.res +3 -3
- package/src/components/StateChangeSlice/StateChangeSlice_Builder.res +23 -4
- package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +22 -6
- package/src/components/StateTopic.res +1 -3
- package/src/components/StateViewSlice/StateViewSlice.res +4 -4
- package/src/components/StateViewSlice/StateViewSlice_Builder.res +79 -68
- package/src/components/StateViewSlice/StateViewSlice_Callback.res +3 -1
- package/src/components/Task/Task.res +3 -3
- package/src/components/Task/Task_Adapter.res +4 -1
- package/src/components/Task/Task_Builder.res +24 -21
- package/src/plugin/api/PluginBaseFragment.res +9 -3
- package/src/plugin/component/Capability_Inference.res +8 -10
- package/src/plugin/component/Plugin.res +6 -6
- package/src/plugin/component/Plugin_Builder.res +54 -50
- package/src/plugin/component/Plugin_Callback.res +10 -6
- package/src/plugin/component/Plugin_Helpers.res +264 -230
- package/src/plugin/component/Plugin_Structure.res +370 -324
- package/src/plugin/component/Plugin_SubscriptionSchema.res +8 -6
- package/src/plugin/component/SchemaWalker.res +11 -12
- package/src/plugin/connect/PluginConnectExtension_Mapping.res +45 -50
- package/src/plugin/lifecycle/PluginBehavior.res +3 -4
- package/src/plugin/lifecycle/PluginSpec.res +1 -2
- package/src/plugin/lifecycle/PluginsProjection.res +16 -5
- package/src/plugin/lifecycle/PluginsReadModelSpec.res +0 -2
- package/src/util/CommandPublisher.res +9 -2
- package/src/util/CsvStream.res +3 -3
- package/src/util/Interstack.res +7 -15
- package/src/util/Interstack.resi +3 -3
- package/src/util/LogFormat.res +5 -9
- package/src/util/Logger.res +1 -2
- package/src/util/Lru.res +2 -1
- package/src/util/Lru.resi +2 -3
- package/src/util/ScheduleOps.res +24 -27
- package/src/util/StorePrefixCollision.res +6 -9
- package/src/util/Util_Promise.res +4 -1
- package/src/util/Util_Promise.res.mjs +1 -1
- package/tests/IdentityTest.res +166 -113
- package/tests/ResourceAttributionTest.res +28 -25
- package/tests/StorePrefixCollisionTest.res +2 -1
- package/tests/TaskBucketNamingTest.res +3 -2
- package/tests/adapter/EventLogProvisioningTest.res +51 -48
- package/tests/adapter/MonitoringTest.res +81 -38
- package/tests/adapter/QueryInterceptionTest.res +2 -2
- package/tests/adapter/RefusalVocabularyTest.res +31 -23
- package/tests/adapter/RuntimeExtensionHookReachTest.res +9 -8
- package/tests/adapter/RuntimeExtensionTest.res +5 -3
- package/tests/admin/AdminApiSchemaDriftTest.res +22 -16
- package/tests/admin/Platform_Admin_StructureTest.res +10 -11
- package/tests/admin/Platform_BakedManifestTest.res +31 -35
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +18 -15
- package/tests/admin/Platform_PluginStructuresApiTest.res +5 -3
- package/tests/admin/Platform_UIFragmentsApiTest.res +6 -3
- package/tests/aggregate/AggregateCacheTest.res +10 -12
- package/tests/aggregate/AggregateCallbackTest.res +75 -46
- package/tests/aggregate/AggregateCausationTest.res +19 -20
- package/tests/aggregate/AggregateConflictTest.res +41 -19
- package/tests/aggregate/AggregateFixtures.res +15 -5
- package/tests/aggregate/AggregateRejectionTest.res +97 -80
- package/tests/aggregate/AggregateSnapshotTest.res +44 -16
- package/tests/api/Api_IdsTest.res +10 -4
- package/tests/api/BrandedMarkerCompositionTest.res +48 -36
- package/tests/api/GeolocationSemanticTest.res +3 -3
- package/tests/api/GraphQL_FragmentGeneratorTest.res +66 -54
- package/tests/api/OwnerScopeDiagnosticsTest.res +31 -27
- package/tests/api/StorageRefFieldsTest.res +30 -18
- package/tests/api/SuryToJsonSchemaTest.res +1338 -981
- package/tests/api/SuryToJsonSchemaTest.res.mjs +31 -0
- package/tests/api/TaggedUnionSchemaTest.res +17 -24
- package/tests/commandgenerator/CommandGeneratorCallbackTest.res +235 -179
- package/tests/commandgenerator/CommandGeneratorFixtures.res +8 -13
- package/tests/commandgenerator/OwnerStampingTest.res +91 -76
- package/tests/commandtopic/CommandOutcomeHookTest.res +198 -155
- package/tests/commandtopic/CommandTopicCallbackFixtures.res +5 -4
- package/tests/commandtopic/CommandTopicCallbackTest.res +61 -43
- package/tests/commandtopic/CommandTopicHelpersRejectionTest.res +70 -55
- package/tests/counter/CounterCallbackTest.res +40 -34
- package/tests/counter/CounterFixtures.res +4 -4
- package/tests/dcb/DcbDecodeTest.res +115 -84
- package/tests/dcb/DcbEventLogOperationsTest.res +194 -143
- package/tests/dcb/DcbEventLogSchemaTest.res +13 -13
- package/tests/dcb/DcbFixtures.res +4 -5
- package/tests/dcb/DcbScopeInferenceTest.res +173 -114
- package/tests/dcb/DcbStateChangeSliceTest.res +391 -262
- package/tests/dcb/DcbTagTest.res +22 -11
- package/tests/dcb/DcbValidationTest.res +288 -218
- package/tests/eventlog/EventLogFixtures.res +4 -4
- package/tests/eventlog/EventLogOperationsTest.res +102 -67
- package/tests/eventlog/EventLogRetryTest.res +104 -65
- package/tests/eventmapper/EventMapperCallbackTest.res +118 -83
- package/tests/eventmapper/EventMapperFixtures.res +15 -23
- package/tests/eventmapper/EventMapperTest.res +54 -38
- package/tests/eventtopic/EventTopicFixtures.res +3 -4
- package/tests/eventtopic/EventTopicOperationsTest.res +41 -29
- package/tests/extensionpoint/ExtensionPointCallbackTest.res +58 -45
- package/tests/extensionpoint/ExtensionPointFixtures.res +16 -20
- package/tests/extensionpoint/ExtensionPointOperationsTest.res +70 -49
- package/tests/extensionpoint/PluginDisconnectScheduleTest.res +2 -1
- package/tests/logger/LogFormatTest.res +104 -82
- package/tests/logger/LoggerLazyTest.res +21 -12
- package/tests/message/MessageTest.res +96 -93
- package/tests/message/MetaEnvelopeTest.res +26 -30
- package/tests/plugin/HeartbeatDisconnectGraceTest.res +31 -25
- package/tests/plugin/ManifestVisibilityTest.res +20 -12
- package/tests/plugin/PluginDefinitionScalars.res +1 -2
- package/tests/plugin/PluginLifecycleCorpusTest.res +15 -12
- package/tests/plugin/PluginSpecExperiment.res +0 -1
- package/tests/plugin/PluginStructureTest.res +1533 -1030
- package/tests/plugin/PluginStructureTest.res.mjs +6 -0
- package/tests/plugin/Plugin_EventQuerySchemaTest.res +21 -10
- package/tests/plugin/SchemaWalkerTest.res +116 -70
- package/tests/plugin/StateChange/PsReserveStock.res +4 -4
- package/tests/plugin/StateView/PsAnnotatedView.res +6 -4
- package/tests/plugin/StateView/PsAnnotatedView.res.mjs +7 -3
- package/tests/plugin/StateView/PsCustomersView.res +4 -4
- package/tests/plugin/StateView/PsShipmentsView.res +3 -2
- package/tests/projection/LifecycleTrailTest.res +1 -4
- package/tests/projection/ProjectionOptimizeTest.res +13 -9
- package/tests/querydb/QueryDbFixtures.res +0 -1
- package/tests/querydb/QueryDbOperationsTest.res +99 -64
- package/tests/querydb/TaggedUnionStampTest.res +10 -8
- package/tests/sideeffecthandler/SideEffectHandlerCallbackTest.res +50 -35
- package/tests/sideeffecthandler/SideEffectHandlerFixtures.res +4 -10
- package/tests/util/CommandPublisherTest.res +0 -1
- package/tests/util/CsvStreamTest.res +3 -3
- package/tests/util/LruTest.res +84 -56
|
@@ -170,91 +170,102 @@ module Make = (
|
|
|
170
170
|
// View/automation/translation slices are consumers and need produced events to exist.
|
|
171
171
|
let hasDcb = stateChangeSlices->Array.length > 0
|
|
172
172
|
if hasDcb {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
->
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
)
|
|
210
|
-
|
|
173
|
+
// Partition slices by channel mode (set by MakeAsync vs Make)
|
|
174
|
+
let syncSlices =
|
|
175
|
+
stateChangeSlices->Array.filter((module(M: StateChangeSlice.T)) => !M.isAsync)
|
|
176
|
+
let asyncSlices =
|
|
177
|
+
stateChangeSlices->Array.filter((module(M: StateChangeSlice.T)) => M.isAsync)
|
|
178
|
+
|
|
179
|
+
// Run validation: check produced vs consumed event compatibility
|
|
180
|
+
let produced =
|
|
181
|
+
stateChangeSlices->Array.map((module(Sc: StateChangeSlice.T)) => (
|
|
182
|
+
Sc.Spec.name,
|
|
183
|
+
Sc.Spec.eventSchema->S.castToUnknown,
|
|
184
|
+
))
|
|
185
|
+
let producedNamed =
|
|
186
|
+
stateChangeSlices->Array.map((module(Sc: StateChangeSlice.T)) => (
|
|
187
|
+
Sc.Spec.name,
|
|
188
|
+
toRelativePath(Sc.Spec.moduleUrl),
|
|
189
|
+
Sc.Spec.eventSchema->S.castToUnknown,
|
|
190
|
+
))
|
|
191
|
+
let consumed =
|
|
192
|
+
stateChangeSlices
|
|
193
|
+
->Array.map((module(Sc: StateChangeSlice.T)) => (
|
|
194
|
+
Sc.Spec.name,
|
|
195
|
+
Sc.Spec.consumedEventSchema->S.castToUnknown,
|
|
196
|
+
))
|
|
197
|
+
->Array.concat(
|
|
198
|
+
stateViewSlices->Array.map((module(V: StateViewSlice.T)) => (
|
|
199
|
+
V.Spec.name,
|
|
200
|
+
V.Spec.consumedEventSchema->S.castToUnknown,
|
|
201
|
+
)),
|
|
202
|
+
)
|
|
203
|
+
->Array.concat(
|
|
204
|
+
// Plan 04: AutomationSlice has per-source Mappings, each carrying
|
|
205
|
+
// its own `sourceEventSchema`. Validation contributes one entry per
|
|
206
|
+
// (slice, source) pair so unknown variants surface against the right
|
|
207
|
+
// mapping context.
|
|
208
|
+
automationSlices->Array.flatMap((module(A: AutomationSlice.T)) =>
|
|
209
|
+
A.Automation.mappings->Array.map((module(M: A.Automation.Mapping)) => (
|
|
210
|
+
A.Spec.name,
|
|
211
|
+
M.sourceEventSchema->S.castToUnknown,
|
|
212
|
+
))
|
|
213
|
+
),
|
|
214
|
+
)
|
|
215
|
+
->Array.concat(
|
|
216
|
+
outboundTranslationSlices->Array.map((module(O: OutboundTranslationSlice.T)) => (
|
|
217
|
+
O.Spec.name,
|
|
218
|
+
O.Spec.consumedEventSchema->S.castToUnknown,
|
|
219
|
+
)),
|
|
220
|
+
)
|
|
211
221
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
+
switch Reventless.DcbValidation.validateProducedAndConsumed(
|
|
223
|
+
~produced=produced->Array.concat(aggregateProducedEvents),
|
|
224
|
+
~consumed,
|
|
225
|
+
) {
|
|
226
|
+
| Error(errors) =>
|
|
227
|
+
errors->Array.forEach(err =>
|
|
228
|
+
log.error(~comp="Dcb_Builder", `DCB validation error (${err.sliceName}): ${err.message}`)
|
|
229
|
+
)
|
|
230
|
+
| Ok() => ()
|
|
231
|
+
}
|
|
222
232
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
})
|
|
233
|
+
// Extract tagged fields from all produced event schemas for DynamoDB GSI creation
|
|
234
|
+
let producedSchemas = produced->Array.map(((_, schema)) => schema)
|
|
235
|
+
let indexes =
|
|
236
|
+
producedSchemas
|
|
237
|
+
->Array.flatMap(schema => Reventless.DcbTag.extractTaggedFields(schema))
|
|
238
|
+
->(arr => {
|
|
239
|
+
let seen = Set.make()
|
|
240
|
+
arr->Array.filter(f => {
|
|
241
|
+
if seen->Set.has(f) {
|
|
242
|
+
false
|
|
243
|
+
} else {
|
|
244
|
+
seen->Set.add(f)
|
|
245
|
+
true
|
|
246
|
+
}
|
|
238
247
|
})
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
248
|
+
})
|
|
249
|
+
->Array.map(tagKey => `tag_${tagKey}`)
|
|
250
|
+
let indexes = if indexes->Array.length > 1 {
|
|
251
|
+
indexes->Array.concat(["tag_composite"])
|
|
252
|
+
} else {
|
|
253
|
+
indexes
|
|
254
|
+
}
|
|
245
255
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
256
|
+
let partitionTag = Reventless.DcbTag.derivePartitionTag(producedNamed)
|
|
257
|
+
|
|
258
|
+
// Tag keys declared `@crossPartition` across the produced event schemas.
|
|
259
|
+
// Derived once at build time (the scope is a property of the tag key, and
|
|
260
|
+
// must agree across every producer that carries it). Threaded to both the
|
|
261
|
+
// decision-model query builder (fan a cross-partition scalar tag into its
|
|
262
|
+
// own single-tag clause) and the storage adapter (read routing +
|
|
263
|
+
// fence scope), keeping read-scope = fence-scope per tag.
|
|
264
|
+
let crossPartitionTagKeys =
|
|
265
|
+
producedSchemas
|
|
266
|
+
->Array.flatMap(schema => Reventless.DcbTag.extractCrossPartitionTagKeys(schema))
|
|
267
|
+
->(
|
|
268
|
+
arr => {
|
|
258
269
|
let seen = Set.make()
|
|
259
270
|
arr->Array.filter(f => {
|
|
260
271
|
if seen->Set.has(f) {
|
|
@@ -264,892 +275,893 @@ module Make = (
|
|
|
264
275
|
true
|
|
265
276
|
}
|
|
266
277
|
})
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// A tag key's scope must agree across every producer that carries it,
|
|
270
|
-
// else the (writer-driven) fence is ambiguous. Non-fatal — surfaces a
|
|
271
|
-
// mismatch loudly without aborting the deploy.
|
|
272
|
-
Reventless.DcbValidation.validateCrossPartitionScope(
|
|
273
|
-
~producers=produced,
|
|
274
|
-
)->Array.forEach(err =>
|
|
275
|
-
log.error(
|
|
276
|
-
~comp="Dcb_Builder",
|
|
277
|
-
`DCB cross-partition scope error (${err.sliceName}): ${err.message}`,
|
|
278
|
-
)
|
|
278
|
+
}
|
|
279
279
|
)
|
|
280
280
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
->Reventless.DcbTag.mergeTagKeysByEventType
|
|
291
|
-
|
|
292
|
-
// Warn on composite (multi-tag) decision reads that would silently miss an
|
|
293
|
-
// event carrying extra tags (the tag_composite key is the event's full tag
|
|
294
|
-
// set, so composite reads are exact-match). Non-fatal — today's slices are
|
|
295
|
-
// aligned; the guard catches a tag added to a multi-tag event later.
|
|
296
|
-
Reventless.DcbValidation.validateCompositeReads(
|
|
297
|
-
~slices=stateChangeSlices->Array.map((module(Sc: StateChangeSlice.T)) => (
|
|
298
|
-
Sc.Spec.name,
|
|
299
|
-
Sc.Spec.commandSchema->S.castToUnknown,
|
|
300
|
-
Sc.Spec.consumedEventSchema->S.castToUnknown,
|
|
301
|
-
)),
|
|
302
|
-
~producedTagKeys=tagKeysByEventType,
|
|
303
|
-
)->Array.forEach(w =>
|
|
304
|
-
log.warn(~comp="Dcb_Builder", `DCB composite-read warning (${w.sliceName}): ${w.message}`)
|
|
281
|
+
// A tag key's scope must agree across every producer that carries it,
|
|
282
|
+
// else the (writer-driven) fence is ambiguous. Non-fatal — surfaces a
|
|
283
|
+
// mismatch loudly without aborting the deploy.
|
|
284
|
+
Reventless.DcbValidation.validateCrossPartitionScope(
|
|
285
|
+
~producers=produced,
|
|
286
|
+
)->Array.forEach(err =>
|
|
287
|
+
log.error(
|
|
288
|
+
~comp="Dcb_Builder",
|
|
289
|
+
`DCB cross-partition scope error (${err.sliceName}): ${err.message}`,
|
|
305
290
|
)
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
// Map each produced event type to its full produced tag-key set. Threaded
|
|
294
|
+
// into each StateChangeSlice so the decision-model query drops vacuous
|
|
295
|
+
// (type, tag) clause combinations (a type that can't carry the clause's
|
|
296
|
+
// tag) — pure dead-clause removal, results unchanged. Built from the
|
|
297
|
+
// producer schemas (not consumers' `consumedEventSchema`, which may
|
|
298
|
+
// under-declare tags a slice legitimately reads by).
|
|
299
|
+
let tagKeysByEventType =
|
|
300
|
+
producedSchemas
|
|
301
|
+
->Array.map(schema => Reventless.DcbTag.extractTagKeysByEventType(schema))
|
|
302
|
+
->Reventless.DcbTag.mergeTagKeysByEventType
|
|
303
|
+
|
|
304
|
+
// Warn on composite (multi-tag) decision reads that would silently miss an
|
|
305
|
+
// event carrying extra tags (the tag_composite key is the event's full tag
|
|
306
|
+
// set, so composite reads are exact-match). Non-fatal — today's slices are
|
|
307
|
+
// aligned; the guard catches a tag added to a multi-tag event later.
|
|
308
|
+
Reventless.DcbValidation.validateCompositeReads(
|
|
309
|
+
~slices=stateChangeSlices->Array.map((module(Sc: StateChangeSlice.T)) => (
|
|
310
|
+
Sc.Spec.name,
|
|
311
|
+
Sc.Spec.commandSchema->S.castToUnknown,
|
|
312
|
+
Sc.Spec.consumedEventSchema->S.castToUnknown,
|
|
313
|
+
)),
|
|
314
|
+
~producedTagKeys=tagKeysByEventType,
|
|
315
|
+
)->Array.forEach(w =>
|
|
316
|
+
log.warn(~comp="Dcb_Builder", `DCB composite-read warning (${w.sliceName}): ${w.message}`)
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
// --- Phase 2 (dcb-tag-scope-inference): derive scope from the global slice
|
|
320
|
+
// graph and THREAD it into the decision-query wiring, replacing the
|
|
321
|
+
// annotation-based extraction. The derived `tagKeysByEventType` is what
|
|
322
|
+
// fixes the sibling leak — a foreign reference key (e.g. `categoryId` on
|
|
323
|
+
// `ProductAdded`) is payload, so the cross-partition read clause no longer
|
|
324
|
+
// sweeps up sibling products. Cross-partition fan-out and capacity reads are
|
|
325
|
+
// preserved (generalised rule 3). Safety guard: if any slice's partition is
|
|
326
|
+
// ambiguous we keep the annotated values for the whole boundary rather than
|
|
327
|
+
// thread a partial inference. See docs/plans/done/dcb-tag-scope-inference.md.
|
|
328
|
+
let inferenceShapes =
|
|
329
|
+
stateChangeSlices->Array.map((module(Sc: StateChangeSlice.T)) =>
|
|
330
|
+
Reventless.DcbTag.sliceShapeFromSchemas(
|
|
331
|
+
~name=Sc.Spec.name,
|
|
332
|
+
~commandSchema=Sc.Spec.commandSchema->S.castToUnknown,
|
|
333
|
+
~consumedEventSchema=Sc.Spec.consumedEventSchema->S.castToUnknown,
|
|
334
|
+
~eventSchema=Sc.Spec.eventSchema->S.castToUnknown,
|
|
324
335
|
)
|
|
325
|
-
|
|
326
|
-
|
|
336
|
+
)
|
|
337
|
+
let inferred = Reventless.DcbScopeInference.infer(inferenceShapes)
|
|
338
|
+
if inferred.crossPartitionTagKeys != crossPartitionTagKeys {
|
|
339
|
+
log.info(
|
|
340
|
+
~comp="Dcb_Builder",
|
|
341
|
+
`DCB scope-inference diff: crossPartitionTagKeys annotated=[${crossPartitionTagKeys->Array.join(
|
|
342
|
+
", ",
|
|
343
|
+
)}] inferred=[${inferred.crossPartitionTagKeys->Array.join(", ")}]`,
|
|
344
|
+
)
|
|
345
|
+
}
|
|
346
|
+
inferred.tagKeysByEventType
|
|
347
|
+
->Dict.toArray
|
|
348
|
+
->Array.forEach(((eventType, inferredKeys)) => {
|
|
349
|
+
let annotatedKeys = tagKeysByEventType->Dict.get(eventType)->Option.getOr([])
|
|
350
|
+
let dropped = annotatedKeys->Array.filter(k => !(inferredKeys->Array.includes(k)))
|
|
351
|
+
if dropped->Array.length > 0 {
|
|
327
352
|
log.info(
|
|
328
353
|
~comp="Dcb_Builder",
|
|
329
|
-
`DCB scope-inference diff:
|
|
354
|
+
`DCB scope-inference diff: ${eventType} indexes [${inferredKeys->Array.join(
|
|
355
|
+
", ",
|
|
356
|
+
)}] (inferred) vs [${annotatedKeys->Array.join(
|
|
330
357
|
", ",
|
|
331
|
-
)}]
|
|
358
|
+
)}] (annotated) — payload now: [${dropped->Array.join(", ")}]`,
|
|
332
359
|
)
|
|
333
360
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
361
|
+
})
|
|
362
|
+
inferred.ambiguities->Array.forEach(((sliceName, reason)) =>
|
|
363
|
+
log.warn(~comp="Dcb_Builder", `DCB scope-inference ambiguity (${sliceName}): ${reason}`)
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
// Validate any remaining explicit @crossPartition against the derived scope:
|
|
367
|
+
// contradictions (a key marked cross-partition that is the slice's own
|
|
368
|
+
// partition) are likely bugs; redundant annotations can simply be dropped.
|
|
369
|
+
let scopeAnnotations =
|
|
370
|
+
stateChangeSlices->Array.map((module(Sc: StateChangeSlice.T)) => (
|
|
371
|
+
Sc.Spec.name,
|
|
372
|
+
Reventless.DcbTag.extractCrossPartitionTagKeys(Sc.Spec.eventSchema->S.castToUnknown),
|
|
373
|
+
))
|
|
374
|
+
let scopeIssues = Reventless.DcbValidation.validateScopeVsInference(
|
|
375
|
+
~annotations=scopeAnnotations,
|
|
376
|
+
~inferred,
|
|
377
|
+
)
|
|
378
|
+
scopeIssues.contradictions->Array.forEach(e =>
|
|
379
|
+
log.warn(~comp="Dcb_Builder", `DCB scope contradiction (${e.sliceName}): ${e.message}`)
|
|
380
|
+
)
|
|
381
|
+
scopeIssues.redundancies->Array.forEach(e =>
|
|
382
|
+
log.info(~comp="Dcb_Builder", `DCB scope (${e.sliceName}): ${e.message}`)
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
// All-or-nothing: thread the derived scope only when every slice resolved.
|
|
386
|
+
// An ambiguous boundary keeps the annotated values (no partial inference).
|
|
387
|
+
// Both this builder and the deployed command-handler entry point
|
|
388
|
+
// (DcbCommandTopicEntryPoint.mjs) route through `DcbTag.deriveEffectiveScope`
|
|
389
|
+
// so the runtime decision query cannot drift from this scope — see
|
|
390
|
+
// docs/analysis/dcb-runtime-scope-annotation-drift.md.
|
|
391
|
+
let effective = Reventless.DcbTag.deriveEffectiveScope(
|
|
392
|
+
stateChangeSlices->Array.map((module(Sc: StateChangeSlice.T)) => {
|
|
393
|
+
Reventless.DcbTag.name: Sc.Spec.name,
|
|
394
|
+
commandSchema: Sc.Spec.commandSchema->S.castToUnknown,
|
|
395
|
+
consumedEventSchema: Sc.Spec.consumedEventSchema->S.castToUnknown,
|
|
396
|
+
eventSchema: Sc.Spec.eventSchema->S.castToUnknown,
|
|
397
|
+
}),
|
|
398
|
+
)
|
|
399
|
+
let effectiveCrossPartitionTagKeys = effective.crossPartitionTagKeys
|
|
400
|
+
let effectiveTagKeysByEventType = effective.tagKeysByEventType
|
|
401
|
+
|
|
402
|
+
// An ambiguity is a warning above because a boundary can fall back and
|
|
403
|
+
// lose nothing. Losing a cross-partition key is not that: the decision
|
|
404
|
+
// read for every slice that references another entity silently narrows to
|
|
405
|
+
// its own partition, so it decides against an empty history and rejects a
|
|
406
|
+
// command whose facts are sitting in the log. Nothing downstream fails —
|
|
407
|
+
// the wrong answer *is* the failure — so this is the only place it can be
|
|
408
|
+
// said, and it is said at error level naming the slice to fix.
|
|
409
|
+
if effective.droppedCrossPartitionTagKeys->Array.length > 0 {
|
|
410
|
+
log.error(
|
|
411
|
+
~comp="Dcb_Builder",
|
|
412
|
+
`DCB scope degraded (${name}): cross-partition reads of [${effective.droppedCrossPartitionTagKeys->Array.join(
|
|
413
|
+
", ",
|
|
414
|
+
)}] were derived but dropped — an ambiguous slice forced the boundary onto @crossPartition annotations that do not carry them. Slices referencing another entity by these keys will decide against an empty history and reject valid commands. Resolve: ${effective.ambiguities
|
|
415
|
+
->Array.map(((slice, reason)) => `${slice} (${reason})`)
|
|
416
|
+
->Array.join(" | ")}`,
|
|
355
417
|
)
|
|
418
|
+
}
|
|
356
419
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
420
|
+
module DcbEventLog = DcbEventLog_Builder.Make(DcbEventLogStorage, DcbEventTopicPublisher)
|
|
421
|
+
let dcbEventLog = DcbEventLog.make(
|
|
422
|
+
~name,
|
|
423
|
+
~indexes,
|
|
424
|
+
~partitionTag,
|
|
425
|
+
~crossPartitionTagKeys=effectiveCrossPartitionTagKeys,
|
|
426
|
+
~opts,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
// Notify platform hook that DCB EventLog was created (AWS extracts table name)
|
|
430
|
+
HooksConfig.hooks.onDcbEventLogCreated->Option.forEach(hook => hook(dcbEventLog->Obj.magic))
|
|
431
|
+
|
|
432
|
+
// Create sync CommandTopic for sync StateChangeSlices (and all other DCB slice types)
|
|
433
|
+
module DcbCommandTopicSpec = {
|
|
434
|
+
module Id = Reventless.Id.String
|
|
435
|
+
let name = childName
|
|
436
|
+
@schema
|
|
437
|
+
type command = JSON.t
|
|
438
|
+
}
|
|
439
|
+
module DcbCommandTopic = CommandTopic_Builder.Make(
|
|
440
|
+
DcbCommandTopicSpec,
|
|
441
|
+
DcbCommandTopicChannel,
|
|
442
|
+
)
|
|
443
|
+
let dcbCommandTopic = DcbCommandTopic.make(
|
|
444
|
+
~name=`${name}Dcb`,
|
|
445
|
+
~owner={kind: ComponentType.Plugin, name},
|
|
446
|
+
~opts,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
// Notify platform hook that DCB CommandTopic was created (AWS extracts SQS queue URL)
|
|
450
|
+
HooksConfig.hooks.onDcbCommandTopicCreated->Option.forEach(hook =>
|
|
451
|
+
hook(dcbCommandTopic->Obj.magic)
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
let publishJsons =
|
|
455
|
+
dcbCommandTopic
|
|
456
|
+
->Component.operations
|
|
457
|
+
->Pulumi.Output.apply(ops => ops.publishJsons)
|
|
458
|
+
|
|
459
|
+
// Create FIFO CommandTopic for async StateChangeSlices (only if any are configured)
|
|
460
|
+
module DcbCommandTopicSpecAsync = {
|
|
461
|
+
module Id = Reventless.Id.String
|
|
462
|
+
let name = childName ++ "Async"
|
|
463
|
+
@schema
|
|
464
|
+
type command = JSON.t
|
|
465
|
+
}
|
|
466
|
+
module DcbAsyncCommandTopic = CommandTopic_Builder.Make(
|
|
467
|
+
DcbCommandTopicSpecAsync,
|
|
468
|
+
DcbCommandTopicChannelAsync,
|
|
469
|
+
)
|
|
470
|
+
let asyncDcbCommandTopicOpt = if asyncSlices->Array.length > 0 {
|
|
471
|
+
let t = DcbAsyncCommandTopic.make(
|
|
472
|
+
~name=`${name}DcbAsync`,
|
|
473
|
+
~owner={kind: ComponentType.Plugin, name},
|
|
474
|
+
~opts,
|
|
374
475
|
)
|
|
476
|
+
HooksConfig.hooks.onDcbCommandTopicCreated->Option.forEach(hook => hook(t->Obj.magic))
|
|
477
|
+
Some(t)
|
|
478
|
+
} else {
|
|
479
|
+
None
|
|
480
|
+
}
|
|
375
481
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
consumedEventSchema: Sc.Spec.consumedEventSchema->S.castToUnknown,
|
|
387
|
-
eventSchema: Sc.Spec.eventSchema->S.castToUnknown,
|
|
388
|
-
}),
|
|
389
|
-
)
|
|
390
|
-
let effectiveCrossPartitionTagKeys = effective.crossPartitionTagKeys
|
|
391
|
-
let effectiveTagKeysByEventType = effective.tagKeysByEventType
|
|
392
|
-
|
|
393
|
-
// An ambiguity is a warning above because a boundary can fall back and
|
|
394
|
-
// lose nothing. Losing a cross-partition key is not that: the decision
|
|
395
|
-
// read for every slice that references another entity silently narrows to
|
|
396
|
-
// its own partition, so it decides against an empty history and rejects a
|
|
397
|
-
// command whose facts are sitting in the log. Nothing downstream fails —
|
|
398
|
-
// the wrong answer *is* the failure — so this is the only place it can be
|
|
399
|
-
// said, and it is said at error level naming the slice to fix.
|
|
400
|
-
if effective.droppedCrossPartitionTagKeys->Array.length > 0 {
|
|
401
|
-
log.error(
|
|
402
|
-
~comp="Dcb_Builder",
|
|
403
|
-
`DCB scope degraded (${name}): cross-partition reads of [${effective.droppedCrossPartitionTagKeys->Array.join(
|
|
404
|
-
", ",
|
|
405
|
-
)}] were derived but dropped — an ambiguous slice forced the boundary onto @crossPartition annotations that do not carry them. Slices referencing another entity by these keys will decide against an empty history and reject valid commands. Resolve: ${effective.ambiguities
|
|
406
|
-
->Array.map(((slice, reason)) => `${slice} (${reason})`)
|
|
407
|
-
->Array.join(" | ")}`,
|
|
482
|
+
let stateChangeSlicesOutputs =
|
|
483
|
+
syncSlices
|
|
484
|
+
->Array.map((module(StateChangeSlice: StateChangeSlice.T)) => {
|
|
485
|
+
let ch = StateChangeSlice.make(
|
|
486
|
+
~dcbEventLog,
|
|
487
|
+
~publishJsons,
|
|
488
|
+
~tagKeysByEventType=effectiveTagKeysByEventType,
|
|
489
|
+
~crossPartitionTagKeys=effectiveCrossPartitionTagKeys,
|
|
490
|
+
~runtime=?componentRuntime->Dict.get(StateChangeSlice.Spec.name),
|
|
491
|
+
~opts,
|
|
408
492
|
)
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
493
|
+
(StateChangeSlice.Spec.name, ch->Component.outputs)
|
|
494
|
+
})
|
|
495
|
+
->Dict.fromArray
|
|
496
|
+
|
|
497
|
+
// Create async slice components using the FIFO CommandTopic's publishJsons
|
|
498
|
+
let asyncStateChangeSlicesOutputs = switch asyncDcbCommandTopicOpt {
|
|
499
|
+
| None => Dict.make()
|
|
500
|
+
| Some(asyncDcbCommandTopic) =>
|
|
501
|
+
let asyncPublishJsons =
|
|
502
|
+
asyncDcbCommandTopic->Component.operations->Pulumi.Output.apply(ops => ops.publishJsons)
|
|
503
|
+
asyncSlices
|
|
504
|
+
->Array.map((module(StateChangeSlice: StateChangeSlice.T)) => {
|
|
505
|
+
let ch = StateChangeSlice.make(
|
|
506
|
+
~dcbEventLog,
|
|
507
|
+
~publishJsons=asyncPublishJsons,
|
|
508
|
+
~tagKeysByEventType=effectiveTagKeysByEventType,
|
|
420
509
|
~crossPartitionTagKeys=effectiveCrossPartitionTagKeys,
|
|
510
|
+
~runtime=?componentRuntime->Dict.get(StateChangeSlice.Spec.name),
|
|
421
511
|
~opts,
|
|
422
512
|
)
|
|
513
|
+
(StateChangeSlice.Spec.name, ch->Component.outputs)
|
|
514
|
+
})
|
|
515
|
+
->Dict.fromArray
|
|
516
|
+
}
|
|
423
517
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
)
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
)
|
|
440
|
-
let dcbCommandTopic = DcbCommandTopic.make(~name=`${name}Dcb`, ~owner={kind: ComponentType.Plugin, name}, ~opts)
|
|
441
|
-
|
|
442
|
-
// Notify platform hook that DCB CommandTopic was created (AWS extracts SQS queue URL)
|
|
443
|
-
HooksConfig.hooks.onDcbCommandTopicCreated->Option.forEach(hook =>
|
|
444
|
-
hook(dcbCommandTopic->Obj.magic)
|
|
445
|
-
)
|
|
446
|
-
|
|
447
|
-
let publishJsons =
|
|
448
|
-
dcbCommandTopic
|
|
449
|
-
->Component.operations
|
|
450
|
-
->Pulumi.Output.apply(ops => ops.publishJsons)
|
|
451
|
-
|
|
452
|
-
// Create FIFO CommandTopic for async StateChangeSlices (only if any are configured)
|
|
453
|
-
module DcbCommandTopicSpecAsync = {
|
|
454
|
-
module Id = Reventless.Id.String
|
|
455
|
-
let name = childName ++ "Async"
|
|
456
|
-
@schema
|
|
457
|
-
type command = JSON.t
|
|
458
|
-
}
|
|
459
|
-
module DcbAsyncCommandTopic = CommandTopic_Builder.Make(
|
|
460
|
-
DcbCommandTopicSpecAsync,
|
|
461
|
-
DcbCommandTopicChannelAsync,
|
|
462
|
-
)
|
|
463
|
-
let asyncDcbCommandTopicOpt = if asyncSlices->Array.length > 0 {
|
|
464
|
-
let t = DcbAsyncCommandTopic.make(~name=`${name}DcbAsync`, ~owner={kind: ComponentType.Plugin, name}, ~opts)
|
|
465
|
-
HooksConfig.hooks.onDcbCommandTopicCreated->Option.forEach(hook => hook(t->Obj.magic))
|
|
466
|
-
Some(t)
|
|
467
|
-
} else {
|
|
468
|
-
None
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
let stateChangeSlicesOutputs =
|
|
472
|
-
syncSlices
|
|
473
|
-
->Array.map((module(StateChangeSlice: StateChangeSlice.T)) => {
|
|
474
|
-
let ch = StateChangeSlice.make(
|
|
475
|
-
~dcbEventLog,
|
|
476
|
-
~publishJsons,
|
|
477
|
-
~tagKeysByEventType=effectiveTagKeysByEventType,
|
|
478
|
-
~crossPartitionTagKeys=effectiveCrossPartitionTagKeys,
|
|
479
|
-
~runtime=?componentRuntime->Dict.get(StateChangeSlice.Spec.name),
|
|
480
|
-
~opts,
|
|
518
|
+
// Phase 1: Register DCB mutation SDL + resolver stubs via platform hook
|
|
519
|
+
switch HooksConfig.hooks.mutationResolverHook {
|
|
520
|
+
| Some(registerResolver) =>
|
|
521
|
+
syncSlices->Array.forEach((module(S: StateChangeSlice.T)) => {
|
|
522
|
+
let commandSchema = S.Spec.commandSchema->Reventless.DcbTag.toUnknownSchema
|
|
523
|
+
if !ApiNoApiHelpers.isNoApi(commandSchema) {
|
|
524
|
+
registerResolver(
|
|
525
|
+
~kind=Dcb,
|
|
526
|
+
~fields=Api_Naming.sliceMutationFields(
|
|
527
|
+
~plugin=apiNamePrefix,
|
|
528
|
+
~slice=S.Spec.name,
|
|
529
|
+
~commandSchema,
|
|
530
|
+
)->Array.map(((f, _)) => f),
|
|
531
|
+
~commandSchema,
|
|
532
|
+
~commandAuthorization=S.Spec.commandAuthorization->Obj.magic,
|
|
481
533
|
)
|
|
482
|
-
(StateChangeSlice.Spec.name, ch->Component.outputs)
|
|
483
|
-
})
|
|
484
|
-
->Dict.fromArray
|
|
485
|
-
|
|
486
|
-
// Create async slice components using the FIFO CommandTopic's publishJsons
|
|
487
|
-
let asyncStateChangeSlicesOutputs =
|
|
488
|
-
switch asyncDcbCommandTopicOpt {
|
|
489
|
-
| None => Dict.make()
|
|
490
|
-
| Some(asyncDcbCommandTopic) =>
|
|
491
|
-
let asyncPublishJsons =
|
|
492
|
-
asyncDcbCommandTopic->Component.operations->Pulumi.Output.apply(ops => ops.publishJsons)
|
|
493
|
-
asyncSlices
|
|
494
|
-
->Array.map((module(StateChangeSlice: StateChangeSlice.T)) => {
|
|
495
|
-
let ch = StateChangeSlice.make(
|
|
496
|
-
~dcbEventLog,
|
|
497
|
-
~publishJsons=asyncPublishJsons,
|
|
498
|
-
~tagKeysByEventType=effectiveTagKeysByEventType,
|
|
499
|
-
~crossPartitionTagKeys=effectiveCrossPartitionTagKeys,
|
|
500
|
-
~runtime=?componentRuntime->Dict.get(StateChangeSlice.Spec.name),
|
|
501
|
-
~opts,
|
|
502
|
-
)
|
|
503
|
-
(StateChangeSlice.Spec.name, ch->Component.outputs)
|
|
504
|
-
})
|
|
505
|
-
->Dict.fromArray
|
|
506
534
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
registerResolver(
|
|
517
|
-
~kind=Dcb,
|
|
518
|
-
~fields=Api_Naming.sliceMutationFields(
|
|
519
|
-
~plugin=apiNamePrefix,
|
|
520
|
-
~slice=S.Spec.name,
|
|
521
|
-
~commandSchema,
|
|
522
|
-
)->Array.map(((f, _)) => f),
|
|
535
|
+
})
|
|
536
|
+
asyncSlices->Array.forEach((module(S: StateChangeSlice.T)) => {
|
|
537
|
+
let commandSchema = S.Spec.commandSchema->Reventless.DcbTag.toUnknownSchema
|
|
538
|
+
if !ApiNoApiHelpers.isNoApi(commandSchema) {
|
|
539
|
+
registerResolver(
|
|
540
|
+
~kind=Dcb,
|
|
541
|
+
~fields=Api_Naming.sliceMutationFields(
|
|
542
|
+
~plugin=apiNamePrefix,
|
|
543
|
+
~slice=S.Spec.name,
|
|
523
544
|
~commandSchema,
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
545
|
+
)->Array.map(((f, _)) => f),
|
|
546
|
+
~commandSchema,
|
|
547
|
+
~commandAuthorization=S.Spec.commandAuthorization->Obj.magic,
|
|
548
|
+
)
|
|
549
|
+
}
|
|
550
|
+
})
|
|
551
|
+
| None => ()
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Phase 2: Bind generateCommand to resolver stubs when publishJsons resolves
|
|
555
|
+
// Sync slices use sync CommandTopic ops (publishJsonsAndWait → CommandAccepted/Rejected)
|
|
556
|
+
// Async slices use async CommandTopic ops (publishJsons only → CommandPending)
|
|
557
|
+
switch HooksConfig.hooks.mutationBindHook {
|
|
558
|
+
| Some(bindHandler) =>
|
|
559
|
+
let _ =
|
|
560
|
+
dcbCommandTopic
|
|
561
|
+
->Component.operations
|
|
562
|
+
->Pulumi.Output.apply(ops => {
|
|
563
|
+
syncSlices->Array.forEach((module(S: StateChangeSlice.T)) => {
|
|
564
|
+
let commandSchema = S.Spec.commandSchema->Reventless.DcbTag.toUnknownSchema
|
|
565
|
+
if !ApiNoApiHelpers.isNoApi(commandSchema) {
|
|
566
|
+
// One generateCommand backs every constructor's field — the TAG is
|
|
567
|
+
// injected by the resolver per field, so a single binding suffices.
|
|
568
|
+
let generateCommand = CommandGenerator_Callback.makeGenerateCommand(
|
|
569
|
+
~publishJsons=ops.publishJsons,
|
|
570
|
+
~publishJsonsAndWait=?ops.publishJsonsAndWait,
|
|
571
|
+
~serviceName=S.Spec.name,
|
|
572
|
+
~commandSchema=S.Spec.commandSchema->Obj.magic,
|
|
573
|
+
~componentKind=CommandGenerator_Callback.StateChangeSlice,
|
|
574
|
+
~stripIdFromParams=false,
|
|
575
|
+
)
|
|
576
|
+
Api_Naming.sliceMutationFields(
|
|
536
577
|
~plugin=apiNamePrefix,
|
|
537
578
|
~slice=S.Spec.name,
|
|
538
579
|
~commandSchema,
|
|
539
|
-
)->Array.
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
}
|
|
580
|
+
)->Array.forEach(
|
|
581
|
+
((fieldName, _)) => bindHandler(~field=fieldName, ~generateCommand),
|
|
582
|
+
)
|
|
583
|
+
}
|
|
584
|
+
})
|
|
544
585
|
})
|
|
586
|
+
switch asyncDcbCommandTopicOpt {
|
|
545
587
|
| None => ()
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
// Phase 2: Bind generateCommand to resolver stubs when publishJsons resolves
|
|
549
|
-
// Sync slices use sync CommandTopic ops (publishJsonsAndWait → CommandAccepted/Rejected)
|
|
550
|
-
// Async slices use async CommandTopic ops (publishJsons only → CommandPending)
|
|
551
|
-
switch HooksConfig.hooks.mutationBindHook {
|
|
552
|
-
| Some(bindHandler) =>
|
|
588
|
+
| Some(asyncDcbCommandTopic) =>
|
|
553
589
|
let _ =
|
|
554
|
-
|
|
590
|
+
asyncDcbCommandTopic
|
|
555
591
|
->Component.operations
|
|
556
|
-
->Pulumi.Output.apply(
|
|
557
|
-
|
|
558
|
-
module(S: StateChangeSlice.T),
|
|
559
|
-
) => {
|
|
592
|
+
->Pulumi.Output.apply(asyncOps => {
|
|
593
|
+
asyncSlices->Array.forEach((module(S: StateChangeSlice.T)) => {
|
|
560
594
|
let commandSchema = S.Spec.commandSchema->Reventless.DcbTag.toUnknownSchema
|
|
561
|
-
if !
|
|
595
|
+
if !ApiNoApiHelpers.isNoApi(commandSchema) {
|
|
562
596
|
// One generateCommand backs every constructor's field — the TAG is
|
|
563
597
|
// injected by the resolver per field, so a single binding suffices.
|
|
564
598
|
let generateCommand = CommandGenerator_Callback.makeGenerateCommand(
|
|
565
|
-
~publishJsons=
|
|
566
|
-
~publishJsonsAndWait=?
|
|
599
|
+
~publishJsons=asyncOps.publishJsons,
|
|
600
|
+
~publishJsonsAndWait=?asyncOps.publishJsonsAndWait,
|
|
567
601
|
~serviceName=S.Spec.name,
|
|
568
602
|
~commandSchema=S.Spec.commandSchema->Obj.magic,
|
|
569
603
|
~componentKind=CommandGenerator_Callback.StateChangeSlice,
|
|
570
604
|
~stripIdFromParams=false,
|
|
571
605
|
)
|
|
572
|
-
Api_Naming.sliceMutationFields(
|
|
573
|
-
|
|
606
|
+
Api_Naming.sliceMutationFields(
|
|
607
|
+
~plugin=apiNamePrefix,
|
|
608
|
+
~slice=S.Spec.name,
|
|
609
|
+
~commandSchema,
|
|
610
|
+
)->Array.forEach(
|
|
611
|
+
((fieldName, _)) => bindHandler(~field=fieldName, ~generateCommand),
|
|
612
|
+
)
|
|
574
613
|
}
|
|
575
614
|
})
|
|
576
615
|
})
|
|
577
|
-
switch asyncDcbCommandTopicOpt {
|
|
578
|
-
| None => ()
|
|
579
|
-
| Some(asyncDcbCommandTopic) =>
|
|
580
|
-
let _ =
|
|
581
|
-
asyncDcbCommandTopic
|
|
582
|
-
->Component.operations
|
|
583
|
-
->Pulumi.Output.apply(asyncOps => {
|
|
584
|
-
asyncSlices->Array.forEach((
|
|
585
|
-
module(S: StateChangeSlice.T),
|
|
586
|
-
) => {
|
|
587
|
-
let commandSchema = S.Spec.commandSchema->Reventless.DcbTag.toUnknownSchema
|
|
588
|
-
if !(ApiNoApiHelpers.isNoApi(commandSchema)) {
|
|
589
|
-
// One generateCommand backs every constructor's field — the TAG is
|
|
590
|
-
// injected by the resolver per field, so a single binding suffices.
|
|
591
|
-
let generateCommand = CommandGenerator_Callback.makeGenerateCommand(
|
|
592
|
-
~publishJsons=asyncOps.publishJsons,
|
|
593
|
-
~publishJsonsAndWait=?asyncOps.publishJsonsAndWait,
|
|
594
|
-
~serviceName=S.Spec.name,
|
|
595
|
-
~commandSchema=S.Spec.commandSchema->Obj.magic,
|
|
596
|
-
~componentKind=CommandGenerator_Callback.StateChangeSlice,
|
|
597
|
-
~stripIdFromParams=false,
|
|
598
|
-
)
|
|
599
|
-
Api_Naming.sliceMutationFields(~plugin=apiNamePrefix, ~slice=S.Spec.name, ~commandSchema)
|
|
600
|
-
->Array.forEach(((fieldName, _)) => bindHandler(~field=fieldName, ~generateCommand))
|
|
601
|
-
}
|
|
602
|
-
})
|
|
603
|
-
})
|
|
604
|
-
}
|
|
605
|
-
| None => ()
|
|
606
616
|
}
|
|
617
|
+
| None => ()
|
|
618
|
+
}
|
|
607
619
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
620
|
+
// Populate query field names registry for all slice types BEFORE creating them
|
|
621
|
+
stateViewSlices->Array.forEach((module(V: StateViewSlice.T)) => {
|
|
622
|
+
let qn = Api_Naming.queryFieldNamesForStateView(~plugin=name, ~viewName=V.Spec.name)
|
|
623
|
+
let label = Plugin_Structure.labelFieldsFromStateSchema(
|
|
624
|
+
~entityName=V.Spec.name,
|
|
625
|
+
V.Spec.stateSchema->S.castToUnknown,
|
|
626
|
+
)
|
|
627
|
+
let qn = {
|
|
628
|
+
...qn,
|
|
629
|
+
labelField: label.field,
|
|
630
|
+
connectionFilterTypeName: qn.returnTypeName ++ "Filter",
|
|
631
|
+
}
|
|
632
|
+
let qn = switch V.Spec.subIdConfig {
|
|
633
|
+
| Some(_) => {
|
|
618
634
|
...qn,
|
|
619
|
-
|
|
620
|
-
|
|
635
|
+
itemsFieldName: qn.singleFieldName ++ "Items",
|
|
636
|
+
itemsFilterTypeName: qn.returnTypeName ++ "ItemsFilter",
|
|
621
637
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
638
|
+
| None => qn
|
|
639
|
+
}
|
|
640
|
+
Plugin_Helpers.queryFieldNamesRegistry->Dict.set(V.Spec.name, qn)
|
|
641
|
+
Plugin_Helpers.stateSchemaRegistry->Dict.set(
|
|
642
|
+
V.Spec.name,
|
|
643
|
+
V.Spec.stateSchema->S.castToUnknown,
|
|
644
|
+
)
|
|
645
|
+
})
|
|
646
|
+
|
|
647
|
+
automationSlices->Array.forEach((module(A: AutomationSlice.T)) => {
|
|
648
|
+
let qn = Api_Naming.queryFieldNamesForSliceQueryDb(~plugin=name, ~queryDbName=A.queryDbName)
|
|
649
|
+
Plugin_Helpers.queryFieldNamesRegistry->Dict.set(A.queryDbName, qn)
|
|
650
|
+
})
|
|
651
|
+
|
|
652
|
+
outboundTranslationSlices->Array.forEach((module(O: OutboundTranslationSlice.T)) => {
|
|
653
|
+
let qn = Api_Naming.queryFieldNamesForSliceQueryDb(~plugin=name, ~queryDbName=O.queryDbName)
|
|
654
|
+
Plugin_Helpers.queryFieldNamesRegistry->Dict.set(O.queryDbName, qn)
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
inboundTranslationSlices->Array.forEach((module(I: InboundTranslationSlice.T)) => {
|
|
658
|
+
let qn = Api_Naming.queryFieldNamesForSliceQueryDb(~plugin=name, ~queryDbName=I.queryDbName)
|
|
659
|
+
Plugin_Helpers.queryFieldNamesRegistry->Dict.set(I.queryDbName, qn)
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
// Create StateViewSlices
|
|
663
|
+
let stateViewSlicesOutputs =
|
|
664
|
+
stateViewSlices
|
|
665
|
+
->Array.map((module(StateViewSlice: StateViewSlice.T)) => {
|
|
666
|
+
let sv = StateViewSlice.make(
|
|
667
|
+
~dcbEventLog,
|
|
668
|
+
~runtime=?componentRuntime->Dict.get(StateViewSlice.Spec.name),
|
|
669
|
+
~opts,
|
|
635
670
|
)
|
|
671
|
+
(StateViewSlice.Spec.name, sv->Component.outputs)
|
|
636
672
|
})
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
673
|
+
->Dict.fromArray
|
|
674
|
+
|
|
675
|
+
// Build the full topic dict: aggregate event topics + this plugin's
|
|
676
|
+
// own DCB event topic (keyed by `<pluginName>DcbEventLog` per Plan 03
|
|
677
|
+
// convention). AutomationSlice mappings reference topics by these keys.
|
|
678
|
+
let allEventTopics = aggregateEventTopics->Dict.copy
|
|
679
|
+
allEventTopics->Dict.set(name ++ "DcbEventLog", (dcbEventLog->Component.outputs).eventTopic)
|
|
680
|
+
|
|
681
|
+
// Create AutomationSlices
|
|
682
|
+
let automationSlicesOutputs =
|
|
683
|
+
automationSlices
|
|
684
|
+
->Array.map((module(AutoSlice: AutomationSlice.T)) => {
|
|
685
|
+
let context: Reventless.AutomationSlice.context = {
|
|
686
|
+
environment,
|
|
687
|
+
platformName,
|
|
688
|
+
pluginName: name,
|
|
689
|
+
sliceName: AutoSlice.Spec.name,
|
|
690
|
+
}
|
|
691
|
+
let as_ = AutoSlice.make(
|
|
692
|
+
~allEventTopics,
|
|
693
|
+
~publishJsons,
|
|
694
|
+
~context,
|
|
695
|
+
~runtime=?componentRuntime->Dict.get(AutoSlice.Spec.name),
|
|
696
|
+
~opts,
|
|
697
|
+
)
|
|
698
|
+
(AutoSlice.Spec.name, as_->Component.outputs)
|
|
650
699
|
})
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
700
|
+
->Dict.fromArray
|
|
701
|
+
|
|
702
|
+
// Create OutboundTranslationSlices
|
|
703
|
+
let outboundTranslationSlicesOutputs =
|
|
704
|
+
outboundTranslationSlices
|
|
705
|
+
->Array.map((module(OTS: OutboundTranslationSlice.T)) => {
|
|
706
|
+
// Where this slice's inbound command goes. `targetName` naming an
|
|
707
|
+
// Aggregate routes to that aggregate's CommandTopic; anything else —
|
|
708
|
+
// a DCB slice, or no target at all — keeps the plugin's DCB topic,
|
|
709
|
+
// which is what every slice wanted before aggregates were reachable.
|
|
710
|
+
let targetPublishJsons =
|
|
711
|
+
OTS.Spec.targetName
|
|
712
|
+
->Option.flatMap(target => publishToAggregates->Dict.get(target))
|
|
713
|
+
->Option.getOr(publishJsons)
|
|
714
|
+
|
|
715
|
+
let ots = OTS.make(
|
|
716
|
+
~dcbEventLog,
|
|
717
|
+
// Same dict the AutomationSlices get, so an outbound slice can
|
|
718
|
+
// name an Aggregate source by its Spec.name.
|
|
719
|
+
~allEventTopics,
|
|
720
|
+
~publishJsons=targetPublishJsons,
|
|
721
|
+
~runtime=?componentRuntime->Dict.get(OTS.Spec.name),
|
|
722
|
+
~opts,
|
|
723
|
+
)
|
|
724
|
+
(OTS.Spec.name, ots->Component.outputs)
|
|
657
725
|
})
|
|
726
|
+
->Dict.fromArray
|
|
727
|
+
|
|
728
|
+
// Create InboundTranslationSlices
|
|
729
|
+
let inboundTranslationSliceData =
|
|
730
|
+
inboundTranslationSlices->Array.map((module(ITS: InboundTranslationSlice.T)) => {
|
|
731
|
+
let its = ITS.make(
|
|
732
|
+
~publishJsons,
|
|
733
|
+
~runtime=?componentRuntime->Dict.get(ITS.Spec.name),
|
|
734
|
+
~opts,
|
|
735
|
+
)
|
|
736
|
+
let fieldName = Api_Naming.sliceMutationField(~plugin=name, ~slice=ITS.Spec.name)
|
|
658
737
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
~dcbEventLog,
|
|
665
|
-
~runtime=?componentRuntime->Dict.get(StateViewSlice.Spec.name),
|
|
666
|
-
~opts,
|
|
667
|
-
)
|
|
668
|
-
(StateViewSlice.Spec.name, sv->Component.outputs)
|
|
669
|
-
})
|
|
670
|
-
->Dict.fromArray
|
|
671
|
-
|
|
672
|
-
// Build the full topic dict: aggregate event topics + this plugin's
|
|
673
|
-
// own DCB event topic (keyed by `<pluginName>DcbEventLog` per Plan 03
|
|
674
|
-
// convention). AutomationSlice mappings reference topics by these keys.
|
|
675
|
-
let allEventTopics = aggregateEventTopics->Dict.copy
|
|
676
|
-
allEventTopics->Dict.set(
|
|
677
|
-
name ++ "DcbEventLog",
|
|
678
|
-
(dcbEventLog->Component.outputs).eventTopic,
|
|
679
|
-
)
|
|
680
|
-
|
|
681
|
-
// Create AutomationSlices
|
|
682
|
-
let automationSlicesOutputs =
|
|
683
|
-
automationSlices
|
|
684
|
-
->Array.map((module(AutoSlice: AutomationSlice.T)) => {
|
|
685
|
-
let context: Reventless.AutomationSlice.context = {
|
|
686
|
-
environment,
|
|
687
|
-
platformName,
|
|
688
|
-
pluginName: name,
|
|
689
|
-
sliceName: AutoSlice.Spec.name,
|
|
690
|
-
}
|
|
691
|
-
let as_ = AutoSlice.make(
|
|
692
|
-
~allEventTopics,
|
|
693
|
-
~publishJsons,
|
|
694
|
-
~context,
|
|
695
|
-
~runtime=?componentRuntime->Dict.get(AutoSlice.Spec.name),
|
|
696
|
-
~opts,
|
|
697
|
-
)
|
|
698
|
-
(AutoSlice.Spec.name, as_->Component.outputs)
|
|
699
|
-
})
|
|
700
|
-
->Dict.fromArray
|
|
701
|
-
|
|
702
|
-
// Create OutboundTranslationSlices
|
|
703
|
-
let outboundTranslationSlicesOutputs =
|
|
704
|
-
outboundTranslationSlices
|
|
705
|
-
->Array.map((module(OTS: OutboundTranslationSlice.T)) => {
|
|
706
|
-
// Where this slice's inbound command goes. `targetName` naming an
|
|
707
|
-
// Aggregate routes to that aggregate's CommandTopic; anything else —
|
|
708
|
-
// a DCB slice, or no target at all — keeps the plugin's DCB topic,
|
|
709
|
-
// which is what every slice wanted before aggregates were reachable.
|
|
710
|
-
let targetPublishJsons =
|
|
711
|
-
OTS.Spec.targetName
|
|
712
|
-
->Option.flatMap(target => publishToAggregates->Dict.get(target))
|
|
713
|
-
->Option.getOr(publishJsons)
|
|
714
|
-
|
|
715
|
-
let ots = OTS.make(
|
|
716
|
-
~dcbEventLog,
|
|
717
|
-
// Same dict the AutomationSlices get, so an outbound slice can
|
|
718
|
-
// name an Aggregate source by its Spec.name.
|
|
719
|
-
~allEventTopics,
|
|
720
|
-
~publishJsons=targetPublishJsons,
|
|
721
|
-
~runtime=?componentRuntime->Dict.get(OTS.Spec.name),
|
|
722
|
-
~opts,
|
|
738
|
+
switch HooksConfig.hooks.inboundMutationResolverHook {
|
|
739
|
+
| Some(registerResolver) =>
|
|
740
|
+
registerResolver(
|
|
741
|
+
~fieldName,
|
|
742
|
+
~externalInputSchema=ITS.Spec.externalInputSchema->S.castToUnknown,
|
|
723
743
|
)
|
|
724
|
-
|
|
725
|
-
}
|
|
726
|
-
->Dict.fromArray
|
|
727
|
-
|
|
728
|
-
// Create InboundTranslationSlices
|
|
729
|
-
let inboundTranslationSliceData =
|
|
730
|
-
inboundTranslationSlices->Array.map((
|
|
731
|
-
module(ITS: InboundTranslationSlice.T),
|
|
732
|
-
) => {
|
|
733
|
-
let its = ITS.make(
|
|
734
|
-
~publishJsons,
|
|
735
|
-
~runtime=?componentRuntime->Dict.get(ITS.Spec.name),
|
|
736
|
-
~opts,
|
|
737
|
-
)
|
|
738
|
-
let fieldName = Api_Naming.sliceMutationField(~plugin=name, ~slice=ITS.Spec.name)
|
|
744
|
+
| None => ()
|
|
745
|
+
}
|
|
739
746
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
)
|
|
746
|
-
|
|
747
|
-
|
|
747
|
+
switch HooksConfig.hooks.inboundMutationBindReceiveHook {
|
|
748
|
+
| Some(bindReceive) =>
|
|
749
|
+
let _ =
|
|
750
|
+
its
|
|
751
|
+
->Component.operations
|
|
752
|
+
->Pulumi.Output.apply(ops => bindReceive(~fieldName, ~receive=ops.receive))
|
|
753
|
+
| None => ()
|
|
754
|
+
}
|
|
748
755
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
let _ =
|
|
752
|
-
its
|
|
753
|
-
->Component.operations
|
|
754
|
-
->Pulumi.Output.apply(ops => bindReceive(~fieldName, ~receive=ops.receive))
|
|
755
|
-
| None => ()
|
|
756
|
-
}
|
|
756
|
+
(ITS.Spec.name, fieldName, its, ITS.Spec.externalInputSchema->S.castToUnknown)
|
|
757
|
+
})
|
|
757
758
|
|
|
758
|
-
|
|
759
|
-
|
|
759
|
+
let inboundTranslationSlicesOutputs =
|
|
760
|
+
inboundTranslationSliceData
|
|
761
|
+
->Array.map(((specName, _, its, _)) => (specName, its->Component.outputs))
|
|
762
|
+
->Dict.fromArray
|
|
760
763
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
764
|
+
// Collect InboundTranslationSlice receive functions for composite handler routing
|
|
765
|
+
let inboundReceiversOutput =
|
|
766
|
+
inboundTranslationSliceData
|
|
767
|
+
->Array.map(((_, fieldName, its, _)) =>
|
|
768
|
+
its
|
|
769
|
+
->Component.operations
|
|
770
|
+
->Pulumi.Output.apply(ops => (fieldName, ops.receive))
|
|
771
|
+
)
|
|
772
|
+
->Pulumi.Output.all
|
|
773
|
+
->Pulumi.Output.apply(pairs => pairs->Dict.fromArray)
|
|
774
|
+
|
|
775
|
+
// Composite handler: SQS commands (StateChangeSlice) + direct invocations (InboundTranslation)
|
|
776
|
+
// + AppSync direct invocations (CommandGenerator.payload format)
|
|
777
|
+
let dcbHandlerBase = DcbCommandTopic.makeFilteringHandler(dcbCommandTopic)
|
|
778
|
+
|
|
779
|
+
// generateCommand for AppSync direct invocations, one per command constructor.
|
|
780
|
+
//
|
|
781
|
+
// Every one of them shares this CommandTopic's publishJsons — the reason a
|
|
782
|
+
// single function used to suffice — and they differ in exactly one argument:
|
|
783
|
+
// the schema. That argument is not only a validator. `makeGenerateCommand`
|
|
784
|
+
// also reads it to find the fields a command marks as its owner, and stamps
|
|
785
|
+
// them with the caller. Handing it the permissive `S.json` (as this did, on
|
|
786
|
+
// the still-true grounds that AppSync has already validated against the SDL)
|
|
787
|
+
// makes that lookup answer "no owner fields" for every command there is — so
|
|
788
|
+
// the write publishes whatever owner the CLIENT sent, silently, and the row
|
|
789
|
+
// is wrong from then on.
|
|
790
|
+
//
|
|
791
|
+
// Per-slice decoding still happens inside `buildSliceHandler`; nothing here
|
|
792
|
+
// re-validates. The schema travels for what it says about ownership.
|
|
793
|
+
let dcbCommandSchemasByTag = {
|
|
794
|
+
let byTag = Dict.make()
|
|
795
|
+
stateChangeSlices->Array.forEach((module(S: StateChangeSlice.T)) => {
|
|
796
|
+
let commandSchema = S.Spec.commandSchema->Reventless.DcbTag.toUnknownSchema
|
|
797
|
+
if !ApiNoApiHelpers.isNoApi(commandSchema) {
|
|
798
|
+
Api_Naming.sliceMutationFields(
|
|
799
|
+
~plugin=apiNamePrefix,
|
|
800
|
+
~slice=S.Spec.name,
|
|
801
|
+
~commandSchema,
|
|
802
|
+
)->Array.forEach(((_, tag)) => {
|
|
803
|
+
// The resolver injects the TAG alone, so two slices claiming one
|
|
804
|
+
// constructor name would make the stamp ambiguous — and the wrong
|
|
805
|
+
// arm of that ambiguity writes a row owned by nobody's decision.
|
|
806
|
+
// Multi-command slices cannot reach here (their mutation fields
|
|
807
|
+
// collide first), which leaves single-command slices, where the
|
|
808
|
+
// field names differ and nothing else would notice.
|
|
809
|
+
switch byTag->Dict.get(tag) {
|
|
810
|
+
| Some(_) =>
|
|
811
|
+
log.error(
|
|
812
|
+
~comp="Dcb_Builder",
|
|
813
|
+
`Two DCB slices in "${name}" both declare the command "${tag}". ` ++
|
|
814
|
+
"Owner stamping resolves a command by that name alone, so it cannot " ++ "tell them apart. Rename one of the constructors.",
|
|
815
|
+
)
|
|
816
|
+
| None => byTag->Dict.set(tag, commandSchema)
|
|
817
|
+
}
|
|
818
|
+
})
|
|
819
|
+
}
|
|
820
|
+
})
|
|
821
|
+
byTag
|
|
822
|
+
}
|
|
765
823
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
let dcbHandlerBase = DcbCommandTopic.makeFilteringHandler(dcbCommandTopic)
|
|
780
|
-
|
|
781
|
-
// generateCommand for AppSync direct invocations, one per command constructor.
|
|
782
|
-
//
|
|
783
|
-
// Every one of them shares this CommandTopic's publishJsons — the reason a
|
|
784
|
-
// single function used to suffice — and they differ in exactly one argument:
|
|
785
|
-
// the schema. That argument is not only a validator. `makeGenerateCommand`
|
|
786
|
-
// also reads it to find the fields a command marks as its owner, and stamps
|
|
787
|
-
// them with the caller. Handing it the permissive `S.json` (as this did, on
|
|
788
|
-
// the still-true grounds that AppSync has already validated against the SDL)
|
|
789
|
-
// makes that lookup answer "no owner fields" for every command there is — so
|
|
790
|
-
// the write publishes whatever owner the CLIENT sent, silently, and the row
|
|
791
|
-
// is wrong from then on.
|
|
792
|
-
//
|
|
793
|
-
// Per-slice decoding still happens inside `buildSliceHandler`; nothing here
|
|
794
|
-
// re-validates. The schema travels for what it says about ownership.
|
|
795
|
-
let dcbCommandSchemasByTag = {
|
|
824
|
+
let dcbGenerateCommandOutput =
|
|
825
|
+
dcbCommandTopic
|
|
826
|
+
->Component.operations
|
|
827
|
+
->Pulumi.Output.apply(ops => {
|
|
828
|
+
let make = commandSchema =>
|
|
829
|
+
CommandGenerator_Callback.makeGenerateCommand(
|
|
830
|
+
~publishJsons=ops.publishJsons,
|
|
831
|
+
~publishJsonsAndWait=?ops.publishJsonsAndWait,
|
|
832
|
+
~serviceName=name,
|
|
833
|
+
~commandSchema,
|
|
834
|
+
~componentKind=CommandGenerator_Callback.StateChangeSlice,
|
|
835
|
+
~stripIdFromParams=false,
|
|
836
|
+
)
|
|
796
837
|
let byTag = Dict.make()
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
838
|
+
dcbCommandSchemasByTag
|
|
839
|
+
->Dict.toArray
|
|
840
|
+
->Array.forEach(((tag, schema)) => byTag->Dict.set(tag, make(schema)))
|
|
841
|
+
// A command no slice claims keeps the permissive generator — what every
|
|
842
|
+
// command got before this change — so nothing that reaches this handler
|
|
843
|
+
// by another route starts failing. It also cannot stamp, which is why
|
|
844
|
+
// the unknown case is logged rather than left to look like a success.
|
|
845
|
+
(byTag, make(S.json->S.castToUnknown))
|
|
846
|
+
})
|
|
847
|
+
|
|
848
|
+
let dcbHandler =
|
|
849
|
+
(dcbHandlerBase, inboundReceiversOutput, dcbGenerateCommandOutput)
|
|
850
|
+
->Pulumi.Output.all3
|
|
851
|
+
->Pulumi.Output.apply(((baseHandler, receivers, (generatorsByTag, generateFallback))) => {
|
|
852
|
+
let composite = (event, ctx) => {
|
|
853
|
+
let raw: dict<JSON.t> = event->Obj.magic
|
|
854
|
+
switch raw->Dict.get("__inboundTranslation") {
|
|
855
|
+
| Some(_) =>
|
|
856
|
+
let fieldName =
|
|
857
|
+
raw
|
|
858
|
+
->Dict.get("fieldName")
|
|
859
|
+
->Option.flatMap(JSON.Decode.string)
|
|
860
|
+
->Option.getOr("")
|
|
861
|
+
let args = raw->Dict.get("arguments")->Option.getOr(JSON.Encode.null)
|
|
862
|
+
switch receivers->Dict.get(fieldName) {
|
|
863
|
+
| Some(receiveFn) =>
|
|
864
|
+
Effect.promise(async () => {
|
|
865
|
+
let result = await receiveFn(args)
|
|
866
|
+
let response =
|
|
867
|
+
result
|
|
868
|
+
->InboundTranslationSlice_Callback.receiveResultToOutcome
|
|
869
|
+
->CommandTopic.commandOutcomeToJson
|
|
870
|
+
response->Obj.magic
|
|
871
|
+
})
|
|
872
|
+
| None => baseHandler(event, ctx)
|
|
873
|
+
}
|
|
874
|
+
| None =>
|
|
875
|
+
// Check for CommandGenerator.payload format (AppSync direct invocation)
|
|
876
|
+
switch (raw->Dict.get("command"), raw->Dict.get("arguments")) {
|
|
877
|
+
| (Some(JSON.String(_)), Some(_)) =>
|
|
878
|
+
let payload: CommandGenerator.payload = event->Obj.magic
|
|
879
|
+
let generateCommand = switch generatorsByTag->Dict.get(payload.command) {
|
|
880
|
+
| Some(g) => g
|
|
881
|
+
| None =>
|
|
882
|
+
log.warn(
|
|
814
883
|
~comp="Dcb_Builder",
|
|
815
|
-
`
|
|
816
|
-
"
|
|
817
|
-
"tell them apart. Rename one of the constructors.",
|
|
884
|
+
`No DCB slice in "${name}" declares the command "${payload.command}", ` ++
|
|
885
|
+
"so it is dispatched without an owner stamp. Any field the command " ++ "marks as its owner keeps the value the caller sent.",
|
|
818
886
|
)
|
|
819
|
-
|
|
820
|
-
}
|
|
821
|
-
})
|
|
822
|
-
}
|
|
823
|
-
})
|
|
824
|
-
byTag
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
let dcbGenerateCommandOutput =
|
|
828
|
-
dcbCommandTopic
|
|
829
|
-
->Component.operations
|
|
830
|
-
->Pulumi.Output.apply(ops => {
|
|
831
|
-
let make = commandSchema =>
|
|
832
|
-
CommandGenerator_Callback.makeGenerateCommand(
|
|
833
|
-
~publishJsons=ops.publishJsons,
|
|
834
|
-
~publishJsonsAndWait=?ops.publishJsonsAndWait,
|
|
835
|
-
~serviceName=name,
|
|
836
|
-
~commandSchema,
|
|
837
|
-
~componentKind=CommandGenerator_Callback.StateChangeSlice,
|
|
838
|
-
~stripIdFromParams=false,
|
|
839
|
-
)
|
|
840
|
-
let byTag = Dict.make()
|
|
841
|
-
dcbCommandSchemasByTag
|
|
842
|
-
->Dict.toArray
|
|
843
|
-
->Array.forEach(((tag, schema)) => byTag->Dict.set(tag, make(schema)))
|
|
844
|
-
// A command no slice claims keeps the permissive generator — what every
|
|
845
|
-
// command got before this change — so nothing that reaches this handler
|
|
846
|
-
// by another route starts failing. It also cannot stamp, which is why
|
|
847
|
-
// the unknown case is logged rather than left to look like a success.
|
|
848
|
-
(byTag, make(S.json->S.castToUnknown))
|
|
849
|
-
})
|
|
850
|
-
|
|
851
|
-
let dcbHandler =
|
|
852
|
-
(dcbHandlerBase, inboundReceiversOutput, dcbGenerateCommandOutput)
|
|
853
|
-
->Pulumi.Output.all3
|
|
854
|
-
->Pulumi.Output.apply(((baseHandler, receivers, (generatorsByTag, generateFallback))) => {
|
|
855
|
-
let composite = (event, ctx) => {
|
|
856
|
-
let raw: dict<JSON.t> = event->Obj.magic
|
|
857
|
-
switch raw->Dict.get("__inboundTranslation") {
|
|
858
|
-
| Some(_) =>
|
|
859
|
-
let fieldName =
|
|
860
|
-
raw
|
|
861
|
-
->Dict.get("fieldName")
|
|
862
|
-
->Option.flatMap(JSON.Decode.string)
|
|
863
|
-
->Option.getOr("")
|
|
864
|
-
let args = raw->Dict.get("arguments")->Option.getOr(JSON.Encode.null)
|
|
865
|
-
switch receivers->Dict.get(fieldName) {
|
|
866
|
-
| Some(receiveFn) =>
|
|
867
|
-
Effect.promise(async () => {
|
|
868
|
-
let result = await receiveFn(args)
|
|
869
|
-
let response =
|
|
870
|
-
result
|
|
871
|
-
->InboundTranslationSlice_Callback.receiveResultToOutcome
|
|
872
|
-
->CommandTopic.commandOutcomeToJson
|
|
873
|
-
response->Obj.magic
|
|
874
|
-
})
|
|
875
|
-
| None => baseHandler(event, ctx)
|
|
876
|
-
}
|
|
877
|
-
| None =>
|
|
878
|
-
// Check for CommandGenerator.payload format (AppSync direct invocation)
|
|
879
|
-
switch (raw->Dict.get("command"), raw->Dict.get("arguments")) {
|
|
880
|
-
| (Some(JSON.String(_)), Some(_)) =>
|
|
881
|
-
let payload: CommandGenerator.payload = event->Obj.magic
|
|
882
|
-
let generateCommand = switch generatorsByTag->Dict.get(payload.command) {
|
|
883
|
-
| Some(g) => g
|
|
884
|
-
| None =>
|
|
885
|
-
log.warn(
|
|
886
|
-
~comp="Dcb_Builder",
|
|
887
|
-
`No DCB slice in "${name}" declares the command "${payload.command}", ` ++
|
|
888
|
-
"so it is dispatched without an owner stamp. Any field the command " ++
|
|
889
|
-
"marks as its owner keeps the value the caller sent.",
|
|
890
|
-
)
|
|
891
|
-
generateFallback
|
|
892
|
-
}
|
|
893
|
-
(generateCommand(payload)->Effect.map(msgId => msgId->Obj.magic))->Obj.magic
|
|
894
|
-
| _ => baseHandler(event, ctx)
|
|
887
|
+
generateFallback
|
|
895
888
|
}
|
|
889
|
+
generateCommand(payload)->Effect.map(msgId => msgId->Obj.magic)->Obj.magic
|
|
890
|
+
| _ => baseHandler(event, ctx)
|
|
896
891
|
}
|
|
897
892
|
}
|
|
898
|
-
composite->Obj.magic
|
|
899
|
-
})
|
|
900
|
-
|
|
901
|
-
// Resources the sync Lambda needs access to. An InboundTranslationSlice
|
|
902
|
-
// exposes its audit table under `queryDb.resources` — its top-level
|
|
903
|
-
// `resources` is empty — and the sync DCB command Lambda is the Route 0
|
|
904
|
-
// target that writes those audit rows. Granting only `outputs.resources`
|
|
905
|
-
// left the Lambda role without `PutItem` on the audit table, so every
|
|
906
|
-
// audit write failed with AccessDenied (swallowed at the write site,
|
|
907
|
-
// surfacing only as a permanently empty audit view). Include the audit
|
|
908
|
-
// table's resources so the role can write it.
|
|
909
|
-
let dcbResources = Array.concat(
|
|
910
|
-
stateChangeSlicesOutputs->Dict.valuesToArray->Array.flatMap(outputs => outputs.resources),
|
|
911
|
-
inboundTranslationSlicesOutputs
|
|
912
|
-
->Dict.valuesToArray
|
|
913
|
-
->Array.flatMap(outputs => Array.concat(outputs.resources, outputs.queryDb.resources)),
|
|
914
|
-
)
|
|
915
|
-
|
|
916
|
-
// Resources the async Lambda needs access to
|
|
917
|
-
let asyncDcbResources =
|
|
918
|
-
asyncStateChangeSlicesOutputs->Dict.valuesToArray->Array.flatMap(outputs => outputs.resources)
|
|
919
|
-
|
|
920
|
-
let inboundFieldNames =
|
|
921
|
-
inboundTranslationSliceData->Array.map(((_, fieldName, _, _)) => fieldName)
|
|
922
|
-
let inboundSchemas = inboundTranslationSliceData->Array.map(((_, _, _, schema)) => schema)
|
|
923
|
-
|
|
924
|
-
// Collect DCB mutation field names + TAGs for AppSync resolver creation
|
|
925
|
-
// (excludes @noApi slices — includes both sync and async)
|
|
926
|
-
// One (fieldName, TAG) pair per API-exposed command constructor. The AWS
|
|
927
|
-
// resolver layer (CommandGeneratorResolvers_AppSync.makeDcb) zips these into
|
|
928
|
-
// one resolver per field, each injecting its own constructor TAG into the
|
|
929
|
-
// Lambda payload — so a multi-command slice's every command is dispatchable.
|
|
930
|
-
let dcbMutationData =
|
|
931
|
-
stateChangeSlices->Array.flatMap((
|
|
932
|
-
module(S: StateChangeSlice.T),
|
|
933
|
-
) => {
|
|
934
|
-
let commandSchema = S.Spec.commandSchema->Reventless.DcbTag.toUnknownSchema
|
|
935
|
-
if ApiNoApiHelpers.isNoApi(commandSchema) {
|
|
936
|
-
[]
|
|
937
|
-
} else {
|
|
938
|
-
Api_Naming.sliceMutationFields(~plugin=apiNamePrefix, ~slice=S.Spec.name, ~commandSchema)
|
|
939
|
-
}
|
|
940
|
-
})
|
|
941
|
-
let dcbFieldNames = dcbMutationData->Array.map(((f, _)) => f)
|
|
942
|
-
let dcbTags = dcbMutationData->Array.map(((_, t)) => t)
|
|
943
|
-
|
|
944
|
-
let dcbConnectFn = (~runtime) => {
|
|
945
|
-
DcbCommandTopic.connect(~runtime, ~resources=dcbResources, dcbCommandTopic)
|
|
946
|
-
|
|
947
|
-
if inboundFieldNames->Array.length > 0 {
|
|
948
|
-
switch HooksConfig.hooks.inboundAppSyncResolverHook {
|
|
949
|
-
| Some(hook) =>
|
|
950
|
-
hook({
|
|
951
|
-
runtime: runtime->Obj.magic,
|
|
952
|
-
fieldNames: inboundFieldNames,
|
|
953
|
-
externalInputSchemas: inboundSchemas,
|
|
954
|
-
opts,
|
|
955
|
-
})
|
|
956
|
-
| None => ()
|
|
957
|
-
}
|
|
958
893
|
}
|
|
894
|
+
composite->Obj.magic
|
|
895
|
+
})
|
|
959
896
|
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
897
|
+
// Resources the sync Lambda needs access to. An InboundTranslationSlice
|
|
898
|
+
// exposes its audit table under `queryDb.resources` — its top-level
|
|
899
|
+
// `resources` is empty — and the sync DCB command Lambda is the Route 0
|
|
900
|
+
// target that writes those audit rows. Granting only `outputs.resources`
|
|
901
|
+
// left the Lambda role without `PutItem` on the audit table, so every
|
|
902
|
+
// audit write failed with AccessDenied (swallowed at the write site,
|
|
903
|
+
// surfacing only as a permanently empty audit view). Include the audit
|
|
904
|
+
// table's resources so the role can write it.
|
|
905
|
+
let dcbResources = Array.concat(
|
|
906
|
+
stateChangeSlicesOutputs->Dict.valuesToArray->Array.flatMap(outputs => outputs.resources),
|
|
907
|
+
inboundTranslationSlicesOutputs
|
|
908
|
+
->Dict.valuesToArray
|
|
909
|
+
->Array.flatMap(outputs => Array.concat(outputs.resources, outputs.queryDb.resources)),
|
|
910
|
+
)
|
|
911
|
+
|
|
912
|
+
// Resources the async Lambda needs access to
|
|
913
|
+
let asyncDcbResources =
|
|
914
|
+
asyncStateChangeSlicesOutputs
|
|
915
|
+
->Dict.valuesToArray
|
|
916
|
+
->Array.flatMap(outputs => outputs.resources)
|
|
917
|
+
|
|
918
|
+
let inboundFieldNames =
|
|
919
|
+
inboundTranslationSliceData->Array.map(((_, fieldName, _, _)) => fieldName)
|
|
920
|
+
let inboundSchemas = inboundTranslationSliceData->Array.map(((_, _, _, schema)) => schema)
|
|
921
|
+
|
|
922
|
+
// Collect DCB mutation field names + TAGs for AppSync resolver creation
|
|
923
|
+
// (excludes @noApi slices — includes both sync and async)
|
|
924
|
+
// One (fieldName, TAG) pair per API-exposed command constructor. The AWS
|
|
925
|
+
// resolver layer (CommandGeneratorResolvers_AppSync.makeDcb) zips these into
|
|
926
|
+
// one resolver per field, each injecting its own constructor TAG into the
|
|
927
|
+
// Lambda payload — so a multi-command slice's every command is dispatchable.
|
|
928
|
+
let dcbMutationData = stateChangeSlices->Array.flatMap((module(S: StateChangeSlice.T)) => {
|
|
929
|
+
let commandSchema = S.Spec.commandSchema->Reventless.DcbTag.toUnknownSchema
|
|
930
|
+
if ApiNoApiHelpers.isNoApi(commandSchema) {
|
|
931
|
+
[]
|
|
932
|
+
} else {
|
|
933
|
+
Api_Naming.sliceMutationFields(~plugin=apiNamePrefix, ~slice=S.Spec.name, ~commandSchema)
|
|
934
|
+
}
|
|
935
|
+
})
|
|
936
|
+
let dcbFieldNames = dcbMutationData->Array.map(((f, _)) => f)
|
|
937
|
+
let dcbTags = dcbMutationData->Array.map(((_, t)) => t)
|
|
938
|
+
|
|
939
|
+
let dcbConnectFn = (~runtime) => {
|
|
940
|
+
DcbCommandTopic.connect(~runtime, ~resources=dcbResources, dcbCommandTopic)
|
|
941
|
+
|
|
942
|
+
if inboundFieldNames->Array.length > 0 {
|
|
943
|
+
switch HooksConfig.hooks.inboundAppSyncResolverHook {
|
|
944
|
+
| Some(hook) =>
|
|
945
|
+
hook({
|
|
946
|
+
runtime: runtime->Obj.magic,
|
|
947
|
+
fieldNames: inboundFieldNames,
|
|
948
|
+
externalInputSchemas: inboundSchemas,
|
|
949
|
+
opts,
|
|
950
|
+
})
|
|
951
|
+
| None => ()
|
|
972
952
|
}
|
|
973
953
|
}
|
|
974
954
|
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
955
|
+
if dcbFieldNames->Array.length > 0 {
|
|
956
|
+
switch HooksConfig.hooks.dcbAppSyncResolverHook {
|
|
957
|
+
| Some(hook) =>
|
|
958
|
+
hook({
|
|
959
|
+
runtime: runtime->Obj.magic,
|
|
960
|
+
fieldNames: dcbFieldNames,
|
|
961
|
+
tags: dcbTags,
|
|
962
|
+
onAdminApi,
|
|
963
|
+
opts,
|
|
964
|
+
})
|
|
965
|
+
| None => ()
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
980
969
|
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
970
|
+
// Notify platform hook that all DCB slices are created (AWS calls finish on bundled builders).
|
|
971
|
+
// Pass dcbEventLog so the platform can wait for its operations to resolve before calling finish().
|
|
972
|
+
HooksConfig.hooks.onDcbSlicesCreated->Option.forEach(hook => hook(dcbEventLog->Obj.magic))
|
|
973
|
+
|
|
974
|
+
// Per-component runtime overrides (plugin.json `runtime`) for the
|
|
975
|
+
// StateChangeSlices hosted on each shared DCB command Lambda. Only
|
|
976
|
+
// explicit overrides contribute (default 0); the platform folds them
|
|
977
|
+
// with the per-flavor commandHandlerConfig floor. Sync slices land on
|
|
978
|
+
// `<Plugin>DcbCmdHandler`, async on `<Plugin>DcbAsyncCmdHandler`.
|
|
979
|
+
let sliceMemoryFloor = slices =>
|
|
980
|
+
slices->Array.reduce(0, (acc, module(M: StateChangeSlice.T)) =>
|
|
981
|
+
Math.Int.max(
|
|
982
|
+
acc,
|
|
983
|
+
ReventlessInfra.RuntimeHints.resolveMemory(
|
|
984
|
+
componentRuntime->Dict.get(M.Spec.name),
|
|
985
|
+
~default=0,
|
|
986
|
+
),
|
|
989
987
|
)
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
988
|
+
)
|
|
989
|
+
let sliceTimeoutFloor = slices =>
|
|
990
|
+
slices->Array.reduce(0, (acc, module(M: StateChangeSlice.T)) =>
|
|
991
|
+
Math.Int.max(
|
|
992
|
+
acc,
|
|
993
|
+
ReventlessInfra.RuntimeHints.resolveTimeout(
|
|
994
|
+
componentRuntime->Dict.get(M.Spec.name),
|
|
995
|
+
~default=0,
|
|
996
|
+
),
|
|
993
997
|
)
|
|
998
|
+
)
|
|
994
999
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
)
|
|
1011
|
-
asyncDcbCommandTopic->RuntimeBuilder.forDcbCommandTopic(
|
|
1012
|
-
~handler=asyncDcbHandler->Obj.magic,
|
|
1013
|
-
~connect=asyncDcbConnectFn,
|
|
1014
|
-
~memorySize=sliceMemoryFloor(asyncSlices),
|
|
1015
|
-
~timeout=sliceTimeoutFloor(asyncSlices),
|
|
1000
|
+
let dcbRuntimeSetup = () => {
|
|
1001
|
+
dcbCommandTopic->RuntimeBuilder.forDcbCommandTopic(
|
|
1002
|
+
~handler=dcbHandler,
|
|
1003
|
+
~connect=dcbConnectFn,
|
|
1004
|
+
~memorySize=sliceMemoryFloor(syncSlices),
|
|
1005
|
+
~timeout=sliceTimeoutFloor(syncSlices),
|
|
1006
|
+
)
|
|
1007
|
+
// Set up async CommandTopic Lambda if any async slices are configured
|
|
1008
|
+
asyncDcbCommandTopicOpt->Option.forEach(asyncDcbCommandTopic => {
|
|
1009
|
+
let asyncDcbHandler = DcbAsyncCommandTopic.makeFilteringHandler(asyncDcbCommandTopic)
|
|
1010
|
+
let asyncDcbConnectFn = (~runtime) =>
|
|
1011
|
+
DcbAsyncCommandTopic.connect(
|
|
1012
|
+
~runtime,
|
|
1013
|
+
~resources=asyncDcbResources,
|
|
1014
|
+
asyncDcbCommandTopic,
|
|
1016
1015
|
)
|
|
1017
|
-
|
|
1018
|
-
|
|
1016
|
+
asyncDcbCommandTopic->RuntimeBuilder.forDcbCommandTopic(
|
|
1017
|
+
~handler=asyncDcbHandler->Obj.magic,
|
|
1018
|
+
~connect=asyncDcbConnectFn,
|
|
1019
|
+
~memorySize=sliceMemoryFloor(asyncSlices),
|
|
1020
|
+
~timeout=sliceTimeoutFloor(asyncSlices),
|
|
1021
|
+
)
|
|
1022
|
+
})
|
|
1023
|
+
}
|
|
1019
1024
|
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1025
|
+
// DCB-specific API schema entries
|
|
1026
|
+
//
|
|
1027
|
+
// Stage E2: a DCB StateChangeSlice has a single GraphQL field but its
|
|
1028
|
+
// command type may declare multiple constructors with different
|
|
1029
|
+
// authorization rules. The Cognito group directive operates at field
|
|
1030
|
+
// granularity, so we read the auth for the first constructor (matching
|
|
1031
|
+
// the existing dcbTags convention at line 549 above). When all
|
|
1032
|
+
// constructors share the file-level default, this is exact; when they
|
|
1033
|
+
// differ, resolver-level enforcement still fires inside the per-slice
|
|
1034
|
+
// handler (the file-level rule applied here is the least-restrictive
|
|
1035
|
+
// bound).
|
|
1036
|
+
let permissionForFirstConstructor = (
|
|
1037
|
+
~commandSchema: S.t<unknown>,
|
|
1038
|
+
~commandAuthorization: unknown => Reventless.Authorization.permission,
|
|
1039
|
+
): option<Reventless.Authorization.permission> => {
|
|
1040
|
+
let names = Reventless.DcbTag.extractAllVariantNames(commandSchema->Obj.magic)
|
|
1041
|
+
switch names->Array.get(0) {
|
|
1042
|
+
| None => None
|
|
1043
|
+
| Some(first) =>
|
|
1044
|
+
let hasPayload = Reventless.DcbTag.isVariantPayloadBearing(
|
|
1045
|
+
commandSchema->Obj.magic,
|
|
1046
|
+
first,
|
|
1047
|
+
)
|
|
1048
|
+
let syntheticCmd: unknown = hasPayload ? {"TAG": first}->Obj.magic : first->Obj.magic
|
|
1049
|
+
Some(commandAuthorization(syntheticCmd))
|
|
1045
1050
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
let hasPayload =
|
|
1067
|
-
Reventless.DcbTag.isVariantPayloadBearing(commandSchema->Obj.magic, ctor)
|
|
1068
|
-
let syntheticCmd: unknown =
|
|
1069
|
-
hasPayload ? {"TAG": ctor}->Obj.magic : ctor->Obj.magic
|
|
1070
|
-
fieldPermissions->Dict.set(fieldName, commandAuthorization(syntheticCmd))
|
|
1071
|
-
})
|
|
1072
|
-
Some({
|
|
1073
|
-
ReventlessInfra.Api.fieldNames: fieldSpecs->Array.map(((f, _)) => f),
|
|
1074
|
-
commandSchema,
|
|
1075
|
-
fieldPermissions,
|
|
1076
|
-
// Slice key fields (e.g. `orderId`) are payload args; there is no
|
|
1077
|
-
// separate aggregate `id`. A multi-variant slice command is a sury
|
|
1078
|
-
// `Union`, so without this the generator would inject `id: ID!`.
|
|
1079
|
-
injectIdArg: false,
|
|
1080
|
-
systemCallable: systemCallableComponents->Array.includes(S.Spec.name),
|
|
1081
|
-
linkedViews: ?sliceDef->Option.map(d => d.linkedViews),
|
|
1082
|
-
consistencyRead: ?sliceDef->Option.flatMap(d => d.consistencyRead),
|
|
1083
|
-
})
|
|
1084
|
-
}
|
|
1085
|
-
})
|
|
1086
|
-
|
|
1087
|
-
let mutationEntriesFromInboundSlices =
|
|
1088
|
-
inboundTranslationSlices->Array.map((
|
|
1089
|
-
module(ITS: InboundTranslationSlice.T),
|
|
1090
|
-
) => {
|
|
1091
|
-
let fieldName = Api_Naming.sliceMutationField(~plugin=name, ~slice=ITS.Spec.name)
|
|
1051
|
+
}
|
|
1052
|
+
let mutationEntriesFromSlices =
|
|
1053
|
+
stateChangeSlices->Array.filterMap((module(S: StateChangeSlice.T)) => {
|
|
1054
|
+
let commandSchema = S.Spec.commandSchema->Reventless.DcbTag.toUnknownSchema
|
|
1055
|
+
let fieldSpecs = Api_Naming.sliceMutationFields(
|
|
1056
|
+
~plugin=apiNamePrefix,
|
|
1057
|
+
~slice=S.Spec.name,
|
|
1058
|
+
~commandSchema,
|
|
1059
|
+
)
|
|
1060
|
+
if ApiNoApiHelpers.isNoApi(commandSchema) || fieldSpecs->Array.length == 0 {
|
|
1061
|
+
None
|
|
1062
|
+
} else {
|
|
1063
|
+
let sliceDef =
|
|
1064
|
+
pluginStructure->Option.flatMap(s =>
|
|
1065
|
+
s.stateChangeSlices->Array.find(d => d.name == S.Spec.name)
|
|
1066
|
+
)
|
|
1067
|
+
// One SDL field per constructor, each with its own constructor's args and
|
|
1068
|
+
// its own per-constructor authorization rule.
|
|
1069
|
+
let commandAuthorization: unknown => Reventless.Authorization.permission =
|
|
1070
|
+
S.Spec.commandAuthorization->Obj.magic
|
|
1092
1071
|
let fieldPermissions = Dict.make()
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1072
|
+
fieldSpecs->Array.forEach(((fieldName, ctor)) => {
|
|
1073
|
+
let hasPayload = Reventless.DcbTag.isVariantPayloadBearing(
|
|
1074
|
+
commandSchema->Obj.magic,
|
|
1075
|
+
ctor,
|
|
1076
|
+
)
|
|
1077
|
+
let syntheticCmd: unknown = hasPayload ? {"TAG": ctor}->Obj.magic : ctor->Obj.magic
|
|
1078
|
+
fieldPermissions->Dict.set(fieldName, commandAuthorization(syntheticCmd))
|
|
1079
|
+
})
|
|
1080
|
+
Some({
|
|
1081
|
+
ReventlessInfra.Api.fieldNames: fieldSpecs->Array.map(((f, _)) => f),
|
|
1082
|
+
commandSchema,
|
|
1103
1083
|
fieldPermissions,
|
|
1084
|
+
// Slice key fields (e.g. `orderId`) are payload args; there is no
|
|
1085
|
+
// separate aggregate `id`. A multi-variant slice command is a sury
|
|
1086
|
+
// `Union`, so without this the generator would inject `id: ID!`.
|
|
1104
1087
|
injectIdArg: false,
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
module(V: StateViewSlice.T),
|
|
1110
|
-
) => {
|
|
1111
|
-
let qn = Api_Naming.queryFieldNamesForStateView(~plugin=name, ~viewName=V.Spec.name)
|
|
1112
|
-
let subIdField = V.Spec.subIdConfig->Option.map(c => c.subIdField)
|
|
1113
|
-
let indexes = V.Spec.config.indexes
|
|
1114
|
-
let indexQueries = if indexes->Array.length > 0 {Some(indexes)} else {None}
|
|
1115
|
-
let resolved = Api_Naming.resolvedFieldsOfConfig(~plugin=name, V.Spec.config)
|
|
1116
|
-
let resolvedFields = if resolved->Array.length > 0 {Some(resolved)} else {None}
|
|
1117
|
-
{
|
|
1118
|
-
ReventlessInfra.Api.singleFieldName: qn.singleFieldName,
|
|
1119
|
-
listFieldName: qn.listFieldName,
|
|
1120
|
-
returnTypeName: qn.returnTypeName,
|
|
1121
|
-
stateSchema: V.Spec.stateSchema->Reventless.DcbTag.toUnknownSchema,
|
|
1122
|
-
authorization: None,
|
|
1123
|
-
permission: V.Spec.authorization,
|
|
1124
|
-
systemCallable: systemCallableComponents->Array.includes(V.Spec.name),
|
|
1125
|
-
includeIdParam: qn.includeIdParam,
|
|
1126
|
-
connectionSpec: true,
|
|
1127
|
-
subIdField: ?subIdField,
|
|
1128
|
-
indexQueries: ?indexQueries,
|
|
1129
|
-
resolvedFields: ?resolvedFields,
|
|
1088
|
+
systemCallable: systemCallableComponents->Array.includes(S.Spec.name),
|
|
1089
|
+
linkedViews: ?(sliceDef->Option.map(d => d.linkedViews)),
|
|
1090
|
+
consistencyRead: ?(sliceDef->Option.flatMap(d => d.consistencyRead)),
|
|
1091
|
+
})
|
|
1130
1092
|
}
|
|
1131
1093
|
})
|
|
1132
1094
|
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
let
|
|
1095
|
+
let mutationEntriesFromInboundSlices =
|
|
1096
|
+
inboundTranslationSlices->Array.map((module(ITS: InboundTranslationSlice.T)) => {
|
|
1097
|
+
let fieldName = Api_Naming.sliceMutationField(~plugin=name, ~slice=ITS.Spec.name)
|
|
1098
|
+
let fieldPermissions = Dict.make()
|
|
1099
|
+
switch permissionForFirstConstructor(
|
|
1100
|
+
~commandSchema=ITS.Spec.commandSchema->Reventless.DcbTag.toUnknownSchema,
|
|
1101
|
+
~commandAuthorization=ITS.Spec.commandAuthorization->Obj.magic,
|
|
1102
|
+
) {
|
|
1103
|
+
| Some(rule) => fieldPermissions->Dict.set(fieldName, rule)
|
|
1104
|
+
| None => ()
|
|
1105
|
+
}
|
|
1137
1106
|
{
|
|
1138
|
-
ReventlessInfra.Api.
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
A.Spec.todoItemSchema,
|
|
1143
|
-
)->S.castToUnknown,
|
|
1144
|
-
authorization: None,
|
|
1145
|
-
connectionSpec: true,
|
|
1107
|
+
ReventlessInfra.Api.fieldNames: [fieldName],
|
|
1108
|
+
commandSchema: ITS.Spec.externalInputSchema->S.castToUnknown,
|
|
1109
|
+
fieldPermissions,
|
|
1110
|
+
injectIdArg: false,
|
|
1146
1111
|
}
|
|
1147
1112
|
})
|
|
1148
1113
|
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1114
|
+
let stateViewEntries = stateViewSlices->Array.map((module(V: StateViewSlice.T)) => {
|
|
1115
|
+
let qn = Api_Naming.queryFieldNamesForStateView(~plugin=name, ~viewName=V.Spec.name)
|
|
1116
|
+
let subIdField = V.Spec.subIdConfig->Option.map(c => c.subIdField)
|
|
1117
|
+
let indexes = V.Spec.config.indexes
|
|
1118
|
+
let indexQueries = if indexes->Array.length > 0 {
|
|
1119
|
+
Some(indexes)
|
|
1120
|
+
} else {
|
|
1121
|
+
None
|
|
1122
|
+
}
|
|
1123
|
+
let resolved = Api_Naming.resolvedFieldsOfConfig(~plugin=name, V.Spec.config)
|
|
1124
|
+
let resolvedFields = if resolved->Array.length > 0 {
|
|
1125
|
+
Some(resolved)
|
|
1126
|
+
} else {
|
|
1127
|
+
None
|
|
1128
|
+
}
|
|
1129
|
+
{
|
|
1130
|
+
ReventlessInfra.Api.singleFieldName: qn.singleFieldName,
|
|
1131
|
+
listFieldName: qn.listFieldName,
|
|
1132
|
+
returnTypeName: qn.returnTypeName,
|
|
1133
|
+
stateSchema: V.Spec.stateSchema->Reventless.DcbTag.toUnknownSchema,
|
|
1134
|
+
authorization: None,
|
|
1135
|
+
permission: V.Spec.authorization,
|
|
1136
|
+
systemCallable: systemCallableComponents->Array.includes(V.Spec.name),
|
|
1137
|
+
includeIdParam: qn.includeIdParam,
|
|
1138
|
+
connectionSpec: true,
|
|
1139
|
+
?subIdField,
|
|
1140
|
+
?indexQueries,
|
|
1141
|
+
?resolvedFields,
|
|
1142
|
+
}
|
|
1143
|
+
})
|
|
1144
|
+
|
|
1145
|
+
let automationEntries = automationSlices->Array.map((module(A: AutomationSlice.T)) => {
|
|
1146
|
+
let qn = Api_Naming.queryFieldNamesForSliceQueryDb(~plugin=name, ~queryDbName=A.queryDbName)
|
|
1147
|
+
{
|
|
1148
|
+
ReventlessInfra.Api.singleFieldName: qn.singleFieldName,
|
|
1149
|
+
listFieldName: qn.listFieldName,
|
|
1150
|
+
returnTypeName: qn.returnTypeName,
|
|
1151
|
+
stateSchema: AutomationSlice_Callback.todoRowSchemaFor(
|
|
1152
|
+
A.Spec.todoItemSchema,
|
|
1153
|
+
)->S.castToUnknown,
|
|
1154
|
+
authorization: None,
|
|
1155
|
+
connectionSpec: true,
|
|
1156
|
+
}
|
|
1157
|
+
})
|
|
1158
|
+
|
|
1159
|
+
let outboundEntries =
|
|
1160
|
+
outboundTranslationSlices->Array.map((module(O: OutboundTranslationSlice.T)) => {
|
|
1161
|
+
let qn = Api_Naming.queryFieldNamesForSliceQueryDb(
|
|
1162
|
+
~plugin=name,
|
|
1163
|
+
~queryDbName=O.queryDbName,
|
|
1164
|
+
)
|
|
1153
1165
|
{
|
|
1154
1166
|
ReventlessInfra.Api.singleFieldName: qn.singleFieldName,
|
|
1155
1167
|
listFieldName: qn.listFieldName,
|
|
@@ -1162,10 +1174,12 @@ module Make = (
|
|
|
1162
1174
|
}
|
|
1163
1175
|
})
|
|
1164
1176
|
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1177
|
+
let inboundEntries =
|
|
1178
|
+
inboundTranslationSlices->Array.map((module(I: InboundTranslationSlice.T)) => {
|
|
1179
|
+
let qn = Api_Naming.queryFieldNamesForSliceQueryDb(
|
|
1180
|
+
~plugin=name,
|
|
1181
|
+
~queryDbName=I.queryDbName,
|
|
1182
|
+
)
|
|
1169
1183
|
{
|
|
1170
1184
|
ReventlessInfra.Api.singleFieldName: qn.singleFieldName,
|
|
1171
1185
|
listFieldName: qn.listFieldName,
|
|
@@ -1176,76 +1190,76 @@ module Make = (
|
|
|
1176
1190
|
}
|
|
1177
1191
|
})
|
|
1178
1192
|
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1193
|
+
// Every event this plugin's slices produce lands in the one DCB event log,
|
|
1194
|
+
// so the log's schema entry is the union of all of them. Taking the first
|
|
1195
|
+
// slice's schema as representative left the readers of that entry — the
|
|
1196
|
+
// upload claimer's `CLAIM_REF_FIELDS`, MCP's event-history resource —
|
|
1197
|
+
// describing one slice and silently blind to the rest.
|
|
1198
|
+
let allProducedSchemas =
|
|
1199
|
+
stateChangeSlices->Array.map((module(Sc: StateChangeSlice.T)) =>
|
|
1200
|
+
Sc.Spec.eventSchema->S.castToUnknown
|
|
1201
|
+
)
|
|
1188
1202
|
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
),
|
|
1203
|
+
// Derive the DCB command topic's queue URL *after* the component's
|
|
1204
|
+
// construct has stored its real resources. This reads `Component.outputs`
|
|
1205
|
+
// as a synchronous side channel inside a callback gated on a DIFFERENT
|
|
1206
|
+
// output (`Component.operations`, which resolves only post-construct) —
|
|
1207
|
+
// reading `resources[0].id` at build time instead captured the SQS
|
|
1208
|
+
// Queue's not-yet-resolved `.id`, which degenerated to the
|
|
1209
|
+
// `{BS_PRIVATE_NESTED_SOME_NONE: 0}` nested-option sentinel and silently
|
|
1210
|
+
// broke the Plugin EventCollector's sqs:SendMessage grant (cross-plugin
|
|
1211
|
+
// extension → DCB-slice publishes → IAM AccessDenied).
|
|
1212
|
+
//
|
|
1213
|
+
// CONSUMER CONSTRAINT: because the value comes from that gated
|
|
1214
|
+
// side-channel read (not from the `operations` payload itself), it must
|
|
1215
|
+
// be resolved with a direct `.apply`, NOT batched through
|
|
1216
|
+
// `Pulumi.Output.all`/`all2` — `all` invokes the callback at a different
|
|
1217
|
+
// point in the graph where the side-channel read still yields the
|
|
1218
|
+
// sentinel (verified on both preview and up). The EC grant builder
|
|
1219
|
+
// therefore resolves it per-target. See
|
|
1220
|
+
// docs/analysis/ec-publish-to-aggregates-grant-broken.md.
|
|
1221
|
+
let dcbCommandTopicQueueUrl =
|
|
1222
|
+
dcbCommandTopic
|
|
1223
|
+
->Component.operations
|
|
1224
|
+
->Pulumi.Output.flatMap(_ops => {
|
|
1225
|
+
let outputs: CommandTopic.outputs = dcbCommandTopic->Component.outputs->Obj.magic
|
|
1226
|
+
switch outputs.resources->Array.get(0) {
|
|
1227
|
+
| Some(r) => r.id
|
|
1228
|
+
| None => Pulumi.Output.make("")
|
|
1229
|
+
}
|
|
1230
|
+
})
|
|
1231
|
+
{
|
|
1232
|
+
dcbEventLogOutputs: Some(dcbEventLog->Component.outputs),
|
|
1233
|
+
stateChangeSlicesOutputs: Dict.fromArray(
|
|
1234
|
+
Array.concat(
|
|
1235
|
+
stateChangeSlicesOutputs->Dict.toArray,
|
|
1236
|
+
asyncStateChangeSlicesOutputs->Dict.toArray,
|
|
1224
1237
|
),
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
}
|
|
1238
|
+
),
|
|
1239
|
+
stateViewSlicesOutputs,
|
|
1240
|
+
automationSlicesOutputs,
|
|
1241
|
+
outboundTranslationSlicesOutputs,
|
|
1242
|
+
inboundTranslationSlicesOutputs,
|
|
1243
|
+
dcbRuntimeSetup: Some(dcbRuntimeSetup),
|
|
1244
|
+
dcbPublishJsons: Some(publishJsons),
|
|
1245
|
+
dcbCommandTopicQueueUrl,
|
|
1246
|
+
mutationEntries: Array.concat(mutationEntriesFromSlices, mutationEntriesFromInboundSlices),
|
|
1247
|
+
queryEntries: stateViewEntries
|
|
1248
|
+
->Array.concat(automationEntries)
|
|
1249
|
+
->Array.concat(outboundEntries)
|
|
1250
|
+
->Array.concat(inboundEntries),
|
|
1251
|
+
eventLogEntries: if allProducedSchemas->Array.length > 0 {
|
|
1252
|
+
[
|
|
1253
|
+
{
|
|
1254
|
+
ReventlessInfra.Api.busKey: name ++ "DcbEventLog",
|
|
1255
|
+
displayName: name,
|
|
1256
|
+
eventSchema: allProducedSchemas->mergedEventSchema,
|
|
1257
|
+
},
|
|
1258
|
+
]
|
|
1259
|
+
} else {
|
|
1260
|
+
[]
|
|
1261
|
+
},
|
|
1262
|
+
}
|
|
1249
1263
|
} else {
|
|
1250
1264
|
emptyResult
|
|
1251
1265
|
}
|