@reventlessdev/reventless-aws 3.0.0-alpha.220 → 3.0.0-alpha.221
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 +11 -0
- package/package.json +8 -8
- package/src/Platform.res.mjs +4 -0
- package/src/Platform_Stack.res +6 -0
- package/src/Platform_Stack.res.mjs +3 -1
- package/src/adapter/AWS_Tags.res +127 -12
- package/src/adapter/AWS_Tags.res.mjs +58 -17
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +13 -1
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +3 -4
- package/src/adapter/Api/Platform_UIFragments_Lambda.res +13 -1
- package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +3 -4
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_AppSync.res +12 -0
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_AppSync.res.mjs +4 -2
- package/src/adapter/CommandGenerator/InboundTranslationResolvers_AppSync.res +6 -0
- package/src/adapter/CommandGenerator/InboundTranslationResolvers_AppSync.res.mjs +2 -1
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS.res +2 -2
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS.res.mjs +2 -2
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Async.res +2 -2
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Async.res.mjs +2 -2
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_FIFO.res +2 -2
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_FIFO.res.mjs +2 -2
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Sync.res +2 -2
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Sync.res.mjs +2 -2
- package/src/adapter/Counter/CounterHandler_DynamoDbStream.res +1 -0
- package/src/adapter/Counter/CounterHandler_DynamoDbStream.res.mjs +1 -1
- package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb.res +1 -1
- package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb.res.mjs +1 -2
- package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream.res +1 -1
- package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream.res.mjs +1 -1
- package/src/adapter/EventCollector/EventCollectorChannel_SQS.res +2 -2
- package/src/adapter/EventCollector/EventCollectorChannel_SQS.res.mjs +2 -2
- package/src/adapter/EventCollector/EventCollectorChannel_SQS_FIFO.res +2 -2
- package/src/adapter/EventCollector/EventCollectorChannel_SQS_FIFO.res.mjs +2 -2
- package/src/adapter/EventLog/EventLogStorage.res +3 -3
- package/src/adapter/EventLog/EventLogStorage.res.mjs +3 -3
- package/src/adapter/EventLog/EventLogStorage_DynamoDb.res +2 -2
- package/src/adapter/EventLog/EventLogStorage_DynamoDb.res.mjs +2 -2
- package/src/adapter/EventLog/EventLogStorage_DynamoDbStream.res +2 -2
- package/src/adapter/EventLog/EventLogStorage_DynamoDbStream.res.mjs +2 -2
- package/src/adapter/EventLog/EventLogStorage_Postgres.res +1 -1
- package/src/adapter/EventLog/EventLogStorage_Postgres.res.mjs +1 -1
- package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res +7 -1
- package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res.mjs +2 -2
- package/src/adapter/EventTopic/EventTopicPublisher_DynamoDbStream.res +1 -1
- package/src/adapter/EventTopic/EventTopicPublisher_DynamoDbStream.res.mjs +1 -1
- package/src/adapter/EventTopic/EventTopicPublisher_SNS.res +2 -2
- package/src/adapter/EventTopic/EventTopicPublisher_SNS.res.mjs +2 -2
- package/src/adapter/EventTopic/EventTopicPublisher_SNS_FIFO.res +2 -2
- package/src/adapter/EventTopic/EventTopicPublisher_SNS_FIFO.res.mjs +2 -2
- package/src/adapter/Mcp/MCP_Lambda.res.mjs +1 -1
- package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +7 -0
- package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +4 -2
- package/src/adapter/Postgres/PgMigration_Builder.res +1 -0
- package/src/adapter/Postgres/PgMigration_Builder.res.mjs +1 -1
- package/src/adapter/QueryDb/PgQueryResolver_Builder.res +7 -0
- package/src/adapter/QueryDb/PgQueryResolver_Builder.res.mjs +4 -2
- package/src/adapter/QueryDb/QueryDbStorage.res +6 -6
- package/src/adapter/QueryDb/QueryDbStorage.res.mjs +6 -6
- package/src/adapter/QueryDb/QueryDbStorage_DynamoDb.res +2 -2
- package/src/adapter/QueryDb/QueryDbStorage_DynamoDb.res.mjs +2 -2
- package/src/adapter/QueryDb/QueryDbStorage_DynamoDbStream.res +2 -2
- package/src/adapter/QueryDb/QueryDbStorage_DynamoDbStream.res.mjs +2 -2
- package/src/adapter/QueryDb/QueryDbStorage_Postgres.res +1 -1
- package/src/adapter/QueryDb/QueryDbStorage_Postgres.res.mjs +1 -1
- package/src/adapter/Runtime/AdminEventCollectorEntryPoint.mjs +24 -41
- package/src/adapter/Runtime/AggregateEntryPoint.mjs +52 -45
- package/src/adapter/Runtime/AggregateRuntime_Builder_Micro.res +19 -2
- package/src/adapter/Runtime/AggregateRuntime_Builder_Micro.res.mjs +8 -5
- package/src/adapter/Runtime/AggregateRuntime_Builder_Micro_Async.res +19 -2
- package/src/adapter/Runtime/AggregateRuntime_Builder_Micro_Async.res.mjs +8 -5
- package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate.res +9 -1
- package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate.res.mjs +4 -2
- package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate_Async.res +9 -1
- package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate_Async.res.mjs +4 -2
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res +9 -1
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res.mjs +4 -2
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res +9 -1
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res.mjs +4 -2
- package/src/adapter/Runtime/AutomationSliceEntryPoint.mjs +36 -22
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +1 -0
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +1 -1
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +39 -42
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_PerEventCollector.res +7 -1
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_PerEventCollector.res.mjs +4 -2
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res +19 -1
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res.mjs +5 -2
- package/src/adapter/Runtime/EventMapperEntryPoint.mjs +25 -23
- package/src/adapter/Runtime/ExtensionPointEntryPoint.mjs +25 -41
- package/src/adapter/Runtime/ExtensionPointRuntime_Builder_PerExtensionPoint.res +1 -0
- package/src/adapter/Runtime/ExtensionPointRuntime_Builder_PerExtensionPoint.res.mjs +1 -1
- package/src/adapter/Runtime/HandlerFactoryHelpers.mjs +126 -0
- package/src/adapter/Runtime/PluginExtensionPointEntryPoint.mjs +24 -41
- package/src/adapter/Runtime/ReadModelEntryPoint.mjs +33 -24
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +52 -2
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +25 -7
- package/src/adapter/Runtime/SideEffectEntryPoint.mjs +25 -24
- package/src/adapter/Runtime/SideEffectHandlerRuntime_Builder_Single.res +9 -1
- package/src/adapter/Runtime/SideEffectHandlerRuntime_Builder_Single.res.mjs +4 -2
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +1 -0
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +1 -1
- package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +26 -21
- package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res +2 -1
- package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res.mjs +2 -2
- package/src/adapter/Runtime/TaskRuntime_Builder_PerBucket.res +1 -0
- package/src/adapter/Runtime/TaskRuntime_Builder_PerBucket.res.mjs +1 -1
- package/src/adapter/ScheduledPublisher/ScheduledPublisher_CloudWatchEvents.res +12 -0
- package/src/adapter/ScheduledPublisher/ScheduledPublisher_CloudWatchEvents.res.mjs +4 -2
- package/src/adapter/StateTopic/StateTopic_AppSync.res +7 -1
- package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +2 -2
- package/src/adapter/Task/TaskBucket_S3.res +1 -0
- package/src/adapter/Task/TaskBucket_S3.res.mjs +7 -1
- package/src/components/Aggregate_Builder_Micro.res.mjs +2 -0
- package/src/components/Aggregate_Builder_NoResolver.res.mjs +2 -0
- package/src/components/Aggregate_Builder_PerAggregate.res.mjs +2 -0
- package/src/components/Aggregate_Builder_Single.res.mjs +2 -0
- package/src/components/Aggregate_Builder_Single_Async.res.mjs +2 -0
- package/src/components/Api/AppSync_Adapter.res +12 -0
- package/src/components/Api/AppSync_Adapter.res.mjs +6 -2
- package/src/components/Api/AppSync_MergedApi.res +12 -0
- package/src/components/Api/AppSync_MergedApi.res.mjs +5 -2
- package/src/components/AutomationSlice_Builder.res.mjs +2 -0
- package/src/components/ExtensionPoint_Builder.res.mjs +2 -0
- package/src/components/ForeignReadModel_Builder.res.mjs +4 -0
- package/src/components/OutboundTranslationSlice_Builder.res.mjs +2 -0
- package/src/components/Plugin.res.mjs +2 -0
- package/src/components/ReadModel_Builder_NoResolver.res.mjs +2 -0
- package/src/components/ReadModel_Builder_NoResolver_Stream.res.mjs +2 -0
- package/src/components/ReadModel_Builder_PerReadModel.res.mjs +2 -0
- package/src/components/ReadModel_Builder_Single.res.mjs +2 -0
- package/src/components/ReadModel_Builder_Single_Stream.res.mjs +2 -0
- package/src/components/SideEffectHandler_PerSideEffectHandler.res.mjs +6 -0
- package/src/components/SideEffectHandler_Single.res.mjs +4 -0
- package/src/components/StateViewSlice_Builder.res.mjs +2 -0
- package/src/components/StateViewSlice_Builder_Stream.res.mjs +2 -0
- package/src/components/Task_Builder_PerBucket.res.mjs +4 -0
- package/src/components/Vpc.res +10 -1
- package/src/components/Vpc.res.mjs +3 -1
- package/src/plugin/cloner/ClonerRunner_Fargate.res +26 -1
- package/src/plugin/cloner/ClonerRunner_Fargate.res.mjs +9 -6
- package/src/plugin/heartbeat/HeartbeatRunner_CloudWatchEvents.res +6 -0
- package/src/plugin/heartbeat/HeartbeatRunner_CloudWatchEvents.res.mjs +4 -1
- package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res +1 -0
- package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res.mjs +1 -1
- package/src/plugin/runtime/PluginRuntime_Builder.res +2 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +2 -2
- package/src/plugin/stack/Plugin_ExtensionPoint_Builder.res.mjs +4 -0
- package/src/plugin/stack/Plugin_Stack.res +26 -1
- package/src/plugin/stack/Plugin_Stack.res.mjs +8 -3
- package/src/util/Util_CognitoGroupUser.res +6 -0
- package/src/util/Util_CognitoGroupUser.res.mjs +3 -1
- package/src/util/Util_DeadLetterQueue.res +8 -2
- package/src/util/Util_DeadLetterQueue.res.mjs +3 -4
- package/src/util/Util_LogAttribution.res +31 -0
- package/src/util/Util_LogAttribution.res.mjs +27 -0
- package/tests/AWS_TagsTest.res +236 -0
- package/tests/AWS_TagsTest.res.mjs +153 -0
- package/tests/HandlerFactoryDispatchTest.res +190 -0
- package/tests/HandlerFactoryDispatchTest.res.mjs +135 -0
- package/tests/integration/AggregateEntryPoint_IntegrationTest.res +5 -3
- package/tests/integration/AggregateEntryPoint_IntegrationTest.res.mjs +5 -3
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Deploy-time log attribution baked into HANDLER_CONFIG.
|
|
2
|
+
//
|
|
3
|
+
// The deployed entry points annotate each invocation with `comp` and `plugin`
|
|
4
|
+
// (HandlerFactoryHelpers.runEffect). `comp` a shell can usually derive itself
|
|
5
|
+
// from the spec name it loads; `plugin` it cannot — resolution goes through
|
|
6
|
+
// LogPrefix's component→plugin registry, which `Plugin_Builder.construct`
|
|
7
|
+
// populates at **deploy** time and which is empty inside a Lambda. And the
|
|
8
|
+
// Lambda-name fallback (`derivePluginFromLambdaName`) only works for Lambdas
|
|
9
|
+
// named `<Plugin>Plugin<Suffix>`: it yields nothing for a shared multi-plugin
|
|
10
|
+
// Lambda (`AllAggregatesCmdHandler`, `AllReadModels`) or a per-component one
|
|
11
|
+
// (`<Aggregate>Aggr`) — exactly the cases where the field matters most.
|
|
12
|
+
//
|
|
13
|
+
// So resolve it here, where the registry exists, and ship it in the config.
|
|
14
|
+
// See docs/plans/entrypoint-dispatch-parity-and-latency-fields.md.
|
|
15
|
+
|
|
16
|
+
/** `,"plugin":"<Name>"` for a HANDLER_CONFIG entry, or `""` when the comp
|
|
17
|
+
resolves to no plugin (the shell then falls back to the Lambda name). */
|
|
18
|
+
let pluginFragment = (~comp: string): string =>
|
|
19
|
+
switch ReventlessCore.Logger.resolvePlugin(~comp, ()) {
|
|
20
|
+
| Some(plugin) => `,"plugin":${plugin->JSON.Encode.string->JSON.stringify}`
|
|
21
|
+
| None => ""
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** `,"comp":"<Kind(Name)>"` for a HANDLER_CONFIG entry. For the shells that
|
|
25
|
+
cannot name the element themselves — a read model or side-effect handler is
|
|
26
|
+
known to its shell only by module path, while the aggregate/slice shells read
|
|
27
|
+
the name off the spec they load. */
|
|
28
|
+
let compFragment = (~comp: string): string => `,"comp":${comp->JSON.Encode.string->JSON.stringify}`
|
|
29
|
+
|
|
30
|
+
/** Both fragments, in the order the entry points expect. */
|
|
31
|
+
let fragments = (~comp: string): string => compFragment(~comp) ++ pluginFragment(~comp)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
4
|
+
|
|
5
|
+
function pluginFragment(comp) {
|
|
6
|
+
let plugin = Logger$ReventlessCore.resolvePlugin(comp, undefined);
|
|
7
|
+
if (plugin !== undefined) {
|
|
8
|
+
return `,"plugin":` + JSON.stringify(plugin);
|
|
9
|
+
} else {
|
|
10
|
+
return "";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function compFragment(comp) {
|
|
15
|
+
return `,"comp":` + JSON.stringify(comp);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function fragments(comp) {
|
|
19
|
+
return compFragment(comp) + pluginFragment(comp);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
pluginFragment,
|
|
24
|
+
compFragment,
|
|
25
|
+
fragments,
|
|
26
|
+
}
|
|
27
|
+
/* Logger-ReventlessCore Not a pure module */
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// Guards the resource-attribution tag schema. A downstream inventory answers
|
|
4
|
+
// "which model component owns this resource, in what role, at what scope" purely
|
|
5
|
+
// from these keys, so the key set and its namespacing are load-bearing.
|
|
6
|
+
// See docs/plans/resource-attribution-tag-schema.md.
|
|
7
|
+
|
|
8
|
+
module Attribution = ReventlessCore.ResourceAttribution
|
|
9
|
+
|
|
10
|
+
// Pulumi.Input.make is %identity, so the tags value is the underlying dict.
|
|
11
|
+
external toDict: Pulumi.Input.t<dict<string>> => dict<string> = "%identity"
|
|
12
|
+
|
|
13
|
+
let tagsFor = (
|
|
14
|
+
~name,
|
|
15
|
+
~kind,
|
|
16
|
+
~role,
|
|
17
|
+
~scope: Attribution.Scope.t=Component,
|
|
18
|
+
~component=?,
|
|
19
|
+
~plugin=?,
|
|
20
|
+
~platform=?,
|
|
21
|
+
) =>
|
|
22
|
+
AWS_Tags.make(~name, ~kind, ~role, ~scope, ~component?, ~plugin?, ~platform?)->toDict
|
|
23
|
+
|
|
24
|
+
describe("AWS_Tags — key set", () => {
|
|
25
|
+
testSync("emits every reventless: fact exactly once, plus the bare Name", () => {
|
|
26
|
+
let keys =
|
|
27
|
+
tagsFor(
|
|
28
|
+
~name="ProductsQueryDb",
|
|
29
|
+
~kind=ReventlessCore.ComponentType.ReadModel,
|
|
30
|
+
~role=QueryDb,
|
|
31
|
+
~plugin="Catalog",
|
|
32
|
+
~platform="online-shop",
|
|
33
|
+
)
|
|
34
|
+
->Dict.keysToArray
|
|
35
|
+
->Array.toSorted(String.compare)
|
|
36
|
+
expect(keys)->toEqual([
|
|
37
|
+
"Name",
|
|
38
|
+
"reventless:component",
|
|
39
|
+
"reventless:environment",
|
|
40
|
+
"reventless:kind",
|
|
41
|
+
"reventless:platform",
|
|
42
|
+
"reventless:plugin",
|
|
43
|
+
"reventless:role",
|
|
44
|
+
"reventless:scope",
|
|
45
|
+
])
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
testSync("the dropped bare duplicates stay dropped", () => {
|
|
49
|
+
// `Type` and `Plugin` were exact duplicates of reventless:kind / :plugin, and
|
|
50
|
+
// `Environment` was a framework fact wearing an un-namespaced name. One
|
|
51
|
+
// authoritative name per fact — `Name` is the sole bare key, because the AWS
|
|
52
|
+
// console reads that literal spelling for its resource-name column.
|
|
53
|
+
let tags = tagsFor(~name="Products", ~kind=ReventlessCore.ComponentType.ReadModel, ~role=QueryDb)
|
|
54
|
+
expect(tags->Dict.get("Type"))->toEqual(None)
|
|
55
|
+
expect(tags->Dict.get("Plugin"))->toEqual(None)
|
|
56
|
+
expect(tags->Dict.get("Environment"))->toEqual(None)
|
|
57
|
+
expect(tags->Dict.get("Name"))->toEqual(Some("Products"))
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe("AWS_Tags — role and kind are independent facts", () => {
|
|
62
|
+
testSync("one component kind spans several piece roles", () => {
|
|
63
|
+
let kind = ReventlessCore.ComponentType.Aggregate
|
|
64
|
+
let eventLog = tagsFor(~name="OrderEventLog", ~kind, ~role=EventLog)
|
|
65
|
+
let commandTopic = tagsFor(~name="OrderCmdTopic", ~kind, ~role=CommandTopic)
|
|
66
|
+
expect(eventLog->Dict.get("reventless:kind"))->toEqual(Some("Aggregate"))
|
|
67
|
+
expect(commandTopic->Dict.get("reventless:kind"))->toEqual(Some("Aggregate"))
|
|
68
|
+
expect(eventLog->Dict.get("reventless:role"))->toEqual(Some("EventLog"))
|
|
69
|
+
expect(commandTopic->Dict.get("reventless:role"))->toEqual(Some("CommandTopic"))
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
testSync("one piece role spans several component kinds — the Lambda mis-tag", () => {
|
|
73
|
+
// Regression guard for the defect this schema exists to fix: every runtime
|
|
74
|
+
// Lambda used to report kind=role=CommandTopic regardless of what it ran.
|
|
75
|
+
let aggregate = tagsFor(
|
|
76
|
+
~name="AllAggregatesCmdHandler",
|
|
77
|
+
~kind=ReventlessCore.ComponentType.Aggregate,
|
|
78
|
+
~role=Runtime,
|
|
79
|
+
)
|
|
80
|
+
let projection = tagsFor(
|
|
81
|
+
~name="ProductsProjection",
|
|
82
|
+
~kind=ReventlessCore.ComponentType.StateViewSlice,
|
|
83
|
+
~role=Runtime,
|
|
84
|
+
)
|
|
85
|
+
expect(aggregate->Dict.get("reventless:role"))->toEqual(Some("Runtime"))
|
|
86
|
+
expect(projection->Dict.get("reventless:role"))->toEqual(Some("Runtime"))
|
|
87
|
+
expect(aggregate->Dict.get("reventless:kind"))->toEqual(Some("Aggregate"))
|
|
88
|
+
expect(projection->Dict.get("reventless:kind"))->toEqual(Some("StateViewSlice"))
|
|
89
|
+
// …and neither reports the old hardcoded lie.
|
|
90
|
+
expect(aggregate->Dict.get("reventless:kind") == Some("CommandTopic"))->toBe(false)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe("AWS_Tags — owner overrides the piece adapter's own kind", () => {
|
|
95
|
+
// The piece adapters pass their OWN ComponentType as ~kind, which names the
|
|
96
|
+
// piece and so collapses onto ~role. A QueryDb is instantiated by six different
|
|
97
|
+
// component kinds, so without the owner a reader cannot tell a ReadModel's
|
|
98
|
+
// table from a StateViewSlice's. See docs/plans/resource-attribution-owner-kind.md.
|
|
99
|
+
let queryDbTagsFor = (~owner=?) =>
|
|
100
|
+
AWS_Tags.make(
|
|
101
|
+
~name="ProductsQueryDb",
|
|
102
|
+
~kind=ReventlessCore.ComponentType.QueryDb,
|
|
103
|
+
~role=QueryDb,
|
|
104
|
+
~owner?,
|
|
105
|
+
)->toDict
|
|
106
|
+
|
|
107
|
+
testSync("without an owner, kind still names the piece", () => {
|
|
108
|
+
let tags = queryDbTagsFor()
|
|
109
|
+
expect(tags->Dict.get("reventless:kind"))->toEqual(Some("QueryDB"))
|
|
110
|
+
expect(tags->Dict.get("reventless:role"))->toEqual(Some("QueryDb"))
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
testSync("two QueryDb tables under different owners differ in kind", () => {
|
|
114
|
+
let readModel = queryDbTagsFor(
|
|
115
|
+
~owner={kind: ReventlessCore.ComponentType.ReadModel, name: "Products"},
|
|
116
|
+
)
|
|
117
|
+
let stateView = queryDbTagsFor(
|
|
118
|
+
~owner={kind: ReventlessCore.ComponentType.StateViewSlice, name: "ProductAvailability"},
|
|
119
|
+
)
|
|
120
|
+
expect(readModel->Dict.get("reventless:kind"))->toEqual(Some("ReadModel"))
|
|
121
|
+
expect(stateView->Dict.get("reventless:kind"))->toEqual(Some("StateViewSlice"))
|
|
122
|
+
// …while both still report the same piece role.
|
|
123
|
+
expect(readModel->Dict.get("reventless:role"))->toEqual(Some("QueryDb"))
|
|
124
|
+
expect(stateView->Dict.get("reventless:role"))->toEqual(Some("QueryDb"))
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
testSync("the owner also supplies the component stem, not the resource name", () => {
|
|
128
|
+
let tags = queryDbTagsFor(
|
|
129
|
+
~owner={kind: ReventlessCore.ComponentType.ReadModel, name: "Products"},
|
|
130
|
+
)
|
|
131
|
+
expect(tags->Dict.get("Name"))->toEqual(Some("ProductsQueryDb"))
|
|
132
|
+
expect(tags->Dict.get("reventless:component"))->toEqual(Some("Products"))
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
testSync("a Plugin-kinded owner attributes substrate to the plugin", () => {
|
|
136
|
+
// A plugin IS a model element, so shared substrate is owned by it rather than
|
|
137
|
+
// by nothing. The owner implies plugin scope even though the piece adapter
|
|
138
|
+
// passed no ~scope, and it names the plugin.
|
|
139
|
+
let tags =
|
|
140
|
+
AWS_Tags.make(
|
|
141
|
+
~name="OrderingDcbEventTopic",
|
|
142
|
+
~kind=ReventlessCore.ComponentType.EventTopic,
|
|
143
|
+
~role=EventTopic,
|
|
144
|
+
~owner={kind: ReventlessCore.ComponentType.Plugin, name: "Ordering"},
|
|
145
|
+
)->toDict
|
|
146
|
+
expect(tags->Dict.get("reventless:kind"))->toEqual(Some("Plugin"))
|
|
147
|
+
expect(tags->Dict.get("reventless:scope"))->toEqual(Some("plugin"))
|
|
148
|
+
expect(tags->Dict.get("reventless:plugin"))->toEqual(Some("Ordering"))
|
|
149
|
+
expect(tags->Dict.get("reventless:component"))->toEqual(Some(""))
|
|
150
|
+
// …and the piece it plays is still legible.
|
|
151
|
+
expect(tags->Dict.get("reventless:role"))->toEqual(Some("EventTopic"))
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
testSync("an owner does not leak a component onto plugin substrate", () => {
|
|
155
|
+
let tags =
|
|
156
|
+
AWS_Tags.make(
|
|
157
|
+
~name="OrderingDcbEventLog",
|
|
158
|
+
~kind=ReventlessCore.ComponentType.DcbEventLog,
|
|
159
|
+
~role=DcbEventLog,
|
|
160
|
+
~scope=Plugin,
|
|
161
|
+
~owner={kind: ReventlessCore.ComponentType.Aggregate, name: "Order"},
|
|
162
|
+
)->toDict
|
|
163
|
+
expect(tags->Dict.get("reventless:component"))->toEqual(Some(""))
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
describe("AWS_Tags — scope", () => {
|
|
168
|
+
testSync("component scope names the component, defaulting to the resource name", () => {
|
|
169
|
+
let tags = tagsFor(~name="Products", ~kind=ReventlessCore.ComponentType.ReadModel, ~role=QueryDb)
|
|
170
|
+
expect(tags->Dict.get("reventless:scope"))->toEqual(Some("component"))
|
|
171
|
+
expect(tags->Dict.get("reventless:component"))->toEqual(Some("Products"))
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
testSync("an explicit component stem survives a suffixed resource name", () => {
|
|
175
|
+
let tags = tagsFor(
|
|
176
|
+
~name="ProductsStateTopic",
|
|
177
|
+
~kind=ReventlessCore.ComponentType.ReadModel,
|
|
178
|
+
~role=StateTopic,
|
|
179
|
+
~component="Products",
|
|
180
|
+
)
|
|
181
|
+
expect(tags->Dict.get("Name"))->toEqual(Some("ProductsStateTopic"))
|
|
182
|
+
expect(tags->Dict.get("reventless:component"))->toEqual(Some("Products"))
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
testSync("plugin substrate carries no component, but keeps its plugin", () => {
|
|
186
|
+
let tags = tagsFor(
|
|
187
|
+
~name="OrderingDcbEventLog",
|
|
188
|
+
~kind=ReventlessCore.ComponentType.DcbEventLog,
|
|
189
|
+
~role=DcbEventLog,
|
|
190
|
+
~scope=Plugin,
|
|
191
|
+
~plugin="Ordering",
|
|
192
|
+
)
|
|
193
|
+
expect(tags->Dict.get("reventless:scope"))->toEqual(Some("plugin"))
|
|
194
|
+
expect(tags->Dict.get("reventless:component"))->toEqual(Some(""))
|
|
195
|
+
expect(tags->Dict.get("reventless:plugin"))->toEqual(Some("Ordering"))
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
testSync("platform substrate carries neither component nor plugin", () => {
|
|
199
|
+
let tags = tagsFor(
|
|
200
|
+
~name="UIFragmentsLambda",
|
|
201
|
+
~kind=ReventlessCore.ComponentType.ReadModel,
|
|
202
|
+
~role=Runtime,
|
|
203
|
+
~scope=Platform,
|
|
204
|
+
~plugin="Ordering",
|
|
205
|
+
~platform="online-shop",
|
|
206
|
+
)
|
|
207
|
+
expect(tags->Dict.get("reventless:scope"))->toEqual(Some("platform"))
|
|
208
|
+
expect(tags->Dict.get("reventless:component"))->toEqual(Some(""))
|
|
209
|
+
expect(tags->Dict.get("reventless:plugin"))->toEqual(Some(""))
|
|
210
|
+
expect(tags->Dict.get("reventless:platform"))->toEqual(Some("online-shop"))
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
describe("AWS_Tags — plugin attribution", () => {
|
|
215
|
+
testSync("falls back to the ambient plugin published by Plugin_Builder", () => {
|
|
216
|
+
// Adapters sit far below the builder that knows the plugin name, so the
|
|
217
|
+
// ambient context is how a DynamoDB table learns it belongs to Ordering.
|
|
218
|
+
let previous = Attribution.enter(~platform="online-shop", ~plugin="Ordering")
|
|
219
|
+
let tags = tagsFor(~name="OrderEventLog", ~kind=ReventlessCore.ComponentType.Aggregate, ~role=EventLog)
|
|
220
|
+
Attribution.restore(previous)
|
|
221
|
+
expect(tags->Dict.get("reventless:plugin"))->toEqual(Some("Ordering"))
|
|
222
|
+
expect(tags->Dict.get("reventless:platform"))->toEqual(Some("online-shop"))
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
testSync("an explicit plugin beats the ambient one", () => {
|
|
226
|
+
let previous = Attribution.enter(~platform="online-shop", ~plugin="Ordering")
|
|
227
|
+
let tags = tagsFor(
|
|
228
|
+
~name="CatalogTable",
|
|
229
|
+
~kind=ReventlessCore.ComponentType.ReadModel,
|
|
230
|
+
~role=QueryDb,
|
|
231
|
+
~plugin="Catalog",
|
|
232
|
+
)
|
|
233
|
+
Attribution.restore(previous)
|
|
234
|
+
expect(tags->Dict.get("reventless:plugin"))->toEqual(Some("Catalog"))
|
|
235
|
+
})
|
|
236
|
+
})
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
4
|
+
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
5
|
+
import * as AWS_Tags$ReventlessAws from "../src/adapter/AWS_Tags.res.mjs";
|
|
6
|
+
import * as ResourceAttribution$ReventlessCore from "@reventlessdev/reventless-core/src/ResourceAttribution.res.mjs";
|
|
7
|
+
|
|
8
|
+
function tagsFor(name, kind, role, scopeOpt, component, plugin, platform) {
|
|
9
|
+
let scope = scopeOpt !== undefined ? scopeOpt : "Component";
|
|
10
|
+
return AWS_Tags$ReventlessAws.make(name, kind, role, scope, component, undefined, plugin, platform);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
globalThis.describe("AWS_Tags — key set", () => {
|
|
14
|
+
globalThis.test("emits every reventless: fact exactly once, plus the bare Name", () => {
|
|
15
|
+
let keys = Object.keys(tagsFor("ProductsQueryDb", "ReadModel", "QueryDb", undefined, undefined, "Catalog", "online-shop")).toSorted(Primitive_string.compare);
|
|
16
|
+
globalThis.expect(keys).toEqual([
|
|
17
|
+
"Name",
|
|
18
|
+
"reventless:component",
|
|
19
|
+
"reventless:environment",
|
|
20
|
+
"reventless:kind",
|
|
21
|
+
"reventless:platform",
|
|
22
|
+
"reventless:plugin",
|
|
23
|
+
"reventless:role",
|
|
24
|
+
"reventless:scope"
|
|
25
|
+
]);
|
|
26
|
+
});
|
|
27
|
+
globalThis.test("the dropped bare duplicates stay dropped", () => {
|
|
28
|
+
let tags = tagsFor("Products", "ReadModel", "QueryDb", undefined, undefined, undefined, undefined);
|
|
29
|
+
globalThis.expect(tags["Type"]).toEqual(undefined);
|
|
30
|
+
globalThis.expect(tags["Plugin"]).toEqual(undefined);
|
|
31
|
+
globalThis.expect(tags["Environment"]).toEqual(undefined);
|
|
32
|
+
globalThis.expect(tags["Name"]).toEqual("Products");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
globalThis.describe("AWS_Tags — role and kind are independent facts", () => {
|
|
37
|
+
globalThis.test("one component kind spans several piece roles", () => {
|
|
38
|
+
let eventLog = tagsFor("OrderEventLog", "Aggregate", "EventLog", undefined, undefined, undefined, undefined);
|
|
39
|
+
let commandTopic = tagsFor("OrderCmdTopic", "Aggregate", "CommandTopic", undefined, undefined, undefined, undefined);
|
|
40
|
+
globalThis.expect(eventLog["reventless:kind"]).toEqual("Aggregate");
|
|
41
|
+
globalThis.expect(commandTopic["reventless:kind"]).toEqual("Aggregate");
|
|
42
|
+
globalThis.expect(eventLog["reventless:role"]).toEqual("EventLog");
|
|
43
|
+
globalThis.expect(commandTopic["reventless:role"]).toEqual("CommandTopic");
|
|
44
|
+
});
|
|
45
|
+
globalThis.test("one piece role spans several component kinds — the Lambda mis-tag", () => {
|
|
46
|
+
let aggregate = tagsFor("AllAggregatesCmdHandler", "Aggregate", "Runtime", undefined, undefined, undefined, undefined);
|
|
47
|
+
let projection = tagsFor("ProductsProjection", "StateViewSlice", "Runtime", undefined, undefined, undefined, undefined);
|
|
48
|
+
globalThis.expect(aggregate["reventless:role"]).toEqual("Runtime");
|
|
49
|
+
globalThis.expect(projection["reventless:role"]).toEqual("Runtime");
|
|
50
|
+
globalThis.expect(aggregate["reventless:kind"]).toEqual("Aggregate");
|
|
51
|
+
globalThis.expect(projection["reventless:kind"]).toEqual("StateViewSlice");
|
|
52
|
+
globalThis.expect(Primitive_object.equal(aggregate["reventless:kind"], "CommandTopic")).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
globalThis.describe("AWS_Tags — owner overrides the piece adapter's own kind", () => {
|
|
57
|
+
let queryDbTagsFor = owner => AWS_Tags$ReventlessAws.make("ProductsQueryDb", "QueryDb", "QueryDb", undefined, undefined, owner, undefined, undefined);
|
|
58
|
+
globalThis.test("without an owner, kind still names the piece", () => {
|
|
59
|
+
let tags = queryDbTagsFor(undefined);
|
|
60
|
+
globalThis.expect(tags["reventless:kind"]).toEqual("QueryDB");
|
|
61
|
+
globalThis.expect(tags["reventless:role"]).toEqual("QueryDb");
|
|
62
|
+
});
|
|
63
|
+
globalThis.test("two QueryDb tables under different owners differ in kind", () => {
|
|
64
|
+
let readModel = queryDbTagsFor({
|
|
65
|
+
kind: "ReadModel",
|
|
66
|
+
name: "Products"
|
|
67
|
+
});
|
|
68
|
+
let stateView = queryDbTagsFor({
|
|
69
|
+
kind: "StateViewSlice",
|
|
70
|
+
name: "ProductAvailability"
|
|
71
|
+
});
|
|
72
|
+
globalThis.expect(readModel["reventless:kind"]).toEqual("ReadModel");
|
|
73
|
+
globalThis.expect(stateView["reventless:kind"]).toEqual("StateViewSlice");
|
|
74
|
+
globalThis.expect(readModel["reventless:role"]).toEqual("QueryDb");
|
|
75
|
+
globalThis.expect(stateView["reventless:role"]).toEqual("QueryDb");
|
|
76
|
+
});
|
|
77
|
+
globalThis.test("the owner also supplies the component stem, not the resource name", () => {
|
|
78
|
+
let tags = queryDbTagsFor({
|
|
79
|
+
kind: "ReadModel",
|
|
80
|
+
name: "Products"
|
|
81
|
+
});
|
|
82
|
+
globalThis.expect(tags["Name"]).toEqual("ProductsQueryDb");
|
|
83
|
+
globalThis.expect(tags["reventless:component"]).toEqual("Products");
|
|
84
|
+
});
|
|
85
|
+
globalThis.test("a Plugin-kinded owner attributes substrate to the plugin", () => {
|
|
86
|
+
let tags = AWS_Tags$ReventlessAws.make("OrderingDcbEventTopic", "EventTopic", "EventTopic", undefined, undefined, {
|
|
87
|
+
kind: "Plugin",
|
|
88
|
+
name: "Ordering"
|
|
89
|
+
}, undefined, undefined);
|
|
90
|
+
globalThis.expect(tags["reventless:kind"]).toEqual("Plugin");
|
|
91
|
+
globalThis.expect(tags["reventless:scope"]).toEqual("plugin");
|
|
92
|
+
globalThis.expect(tags["reventless:plugin"]).toEqual("Ordering");
|
|
93
|
+
globalThis.expect(tags["reventless:component"]).toEqual("");
|
|
94
|
+
globalThis.expect(tags["reventless:role"]).toEqual("EventTopic");
|
|
95
|
+
});
|
|
96
|
+
globalThis.test("an owner does not leak a component onto plugin substrate", () => {
|
|
97
|
+
let tags = AWS_Tags$ReventlessAws.make("OrderingDcbEventLog", "DcbEventLog", "DcbEventLog", "Plugin", undefined, {
|
|
98
|
+
kind: "Aggregate",
|
|
99
|
+
name: "Order"
|
|
100
|
+
}, undefined, undefined);
|
|
101
|
+
globalThis.expect(tags["reventless:component"]).toEqual("");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
globalThis.describe("AWS_Tags — scope", () => {
|
|
106
|
+
globalThis.test("component scope names the component, defaulting to the resource name", () => {
|
|
107
|
+
let tags = tagsFor("Products", "ReadModel", "QueryDb", undefined, undefined, undefined, undefined);
|
|
108
|
+
globalThis.expect(tags["reventless:scope"]).toEqual("component");
|
|
109
|
+
globalThis.expect(tags["reventless:component"]).toEqual("Products");
|
|
110
|
+
});
|
|
111
|
+
globalThis.test("an explicit component stem survives a suffixed resource name", () => {
|
|
112
|
+
let tags = tagsFor("ProductsStateTopic", "ReadModel", "StateTopic", undefined, "Products", undefined, undefined);
|
|
113
|
+
globalThis.expect(tags["Name"]).toEqual("ProductsStateTopic");
|
|
114
|
+
globalThis.expect(tags["reventless:component"]).toEqual("Products");
|
|
115
|
+
});
|
|
116
|
+
globalThis.test("plugin substrate carries no component, but keeps its plugin", () => {
|
|
117
|
+
let tags = tagsFor("OrderingDcbEventLog", "DcbEventLog", "DcbEventLog", "Plugin", undefined, "Ordering", undefined);
|
|
118
|
+
globalThis.expect(tags["reventless:scope"]).toEqual("plugin");
|
|
119
|
+
globalThis.expect(tags["reventless:component"]).toEqual("");
|
|
120
|
+
globalThis.expect(tags["reventless:plugin"]).toEqual("Ordering");
|
|
121
|
+
});
|
|
122
|
+
globalThis.test("platform substrate carries neither component nor plugin", () => {
|
|
123
|
+
let tags = tagsFor("UIFragmentsLambda", "ReadModel", "Runtime", "Platform", undefined, "Ordering", "online-shop");
|
|
124
|
+
globalThis.expect(tags["reventless:scope"]).toEqual("platform");
|
|
125
|
+
globalThis.expect(tags["reventless:component"]).toEqual("");
|
|
126
|
+
globalThis.expect(tags["reventless:plugin"]).toEqual("");
|
|
127
|
+
globalThis.expect(tags["reventless:platform"]).toEqual("online-shop");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
globalThis.describe("AWS_Tags — plugin attribution", () => {
|
|
132
|
+
globalThis.test("falls back to the ambient plugin published by Plugin_Builder", () => {
|
|
133
|
+
let previous = ResourceAttribution$ReventlessCore.enter("online-shop", "Ordering");
|
|
134
|
+
let tags = tagsFor("OrderEventLog", "Aggregate", "EventLog", undefined, undefined, undefined, undefined);
|
|
135
|
+
ResourceAttribution$ReventlessCore.restore(previous);
|
|
136
|
+
globalThis.expect(tags["reventless:plugin"]).toEqual("Ordering");
|
|
137
|
+
globalThis.expect(tags["reventless:platform"]).toEqual("online-shop");
|
|
138
|
+
});
|
|
139
|
+
globalThis.test("an explicit plugin beats the ambient one", () => {
|
|
140
|
+
let previous = ResourceAttribution$ReventlessCore.enter("online-shop", "Ordering");
|
|
141
|
+
let tags = tagsFor("CatalogTable", "ReadModel", "QueryDb", undefined, undefined, "Catalog", undefined);
|
|
142
|
+
ResourceAttribution$ReventlessCore.restore(previous);
|
|
143
|
+
globalThis.expect(tags["reventless:plugin"]).toEqual("Catalog");
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
let Attribution;
|
|
148
|
+
|
|
149
|
+
export {
|
|
150
|
+
Attribution,
|
|
151
|
+
tagsFor,
|
|
152
|
+
}
|
|
153
|
+
/* Not a pure module */
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// Regression guard for the deployed entry points' dispatch boundary.
|
|
2
|
+
//
|
|
3
|
+
// `HandlerFactoryHelpers.mjs` is the single place where every Lambda invocation
|
|
4
|
+
// gets its log annotations and its `RequestContext` — the AWS counterpart of
|
|
5
|
+
// ReventlessCore.Runtime.annotateInvocation, which does not run in a Lambda (the
|
|
6
|
+
// runtime builders take `~handler as _` and deploy the .mjs shell instead). Two
|
|
7
|
+
// things here are checkable only from JS and break silently otherwise:
|
|
8
|
+
//
|
|
9
|
+
// 1. `RequestContext.fromOptions` is called across the ReScript/JS seam. A
|
|
10
|
+
// labelled-argument constructor compiles to positional arguments, so an
|
|
11
|
+
// inserted field would silently shift every value; `fromOptions` takes one
|
|
12
|
+
// options object precisely to avoid that, and this test fails if the shell
|
|
13
|
+
// and the record ever disagree.
|
|
14
|
+
// 2. The transport extractors read two different record shapes — SQS (`body`
|
|
15
|
+
// envelope + `attributes`) and DynamoDB streams (flat top-level meta
|
|
16
|
+
// attributes on `NewImage` + `ApproximateCreationDateTime` in *seconds*).
|
|
17
|
+
//
|
|
18
|
+
// See docs/plans/entrypoint-dispatch-parity-and-latency-fields.md.
|
|
19
|
+
|
|
20
|
+
open JestGlobals
|
|
21
|
+
|
|
22
|
+
// ─── The shell under test ───────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
type dispatchOptions = {
|
|
25
|
+
correlationId?: string,
|
|
26
|
+
causationId?: string,
|
|
27
|
+
comp?: string,
|
|
28
|
+
plugin?: string,
|
|
29
|
+
timestamp?: float,
|
|
30
|
+
retryCount?: int,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@module("@reventlessdev/reventless-aws/src/adapter/Runtime/HandlerFactoryHelpers.mjs")
|
|
34
|
+
external runEffect: (Effect.t<'a, 'e, 'r>, dispatchOptions) => promise<'a> = "runEffect"
|
|
35
|
+
|
|
36
|
+
@module("@reventlessdev/reventless-aws/src/adapter/Runtime/HandlerFactoryHelpers.mjs")
|
|
37
|
+
external setRequestId: string => unit = "setRequestId"
|
|
38
|
+
|
|
39
|
+
@module("@reventlessdev/reventless-aws/src/adapter/Runtime/HandlerFactoryHelpers.mjs")
|
|
40
|
+
external extractMetaField: (array<'record>, string) => option<string> = "extractMetaField"
|
|
41
|
+
|
|
42
|
+
@module("@reventlessdev/reventless-aws/src/adapter/Runtime/HandlerFactoryHelpers.mjs")
|
|
43
|
+
external extractSentTimestamp: array<'record> => option<float> = "extractSentTimestamp"
|
|
44
|
+
|
|
45
|
+
@module("@reventlessdev/reventless-aws/src/adapter/Runtime/HandlerFactoryHelpers.mjs")
|
|
46
|
+
external extractRetryCount: array<'record> => int = "extractRetryCount"
|
|
47
|
+
|
|
48
|
+
// ─── Record fixtures, typed rather than cast ────────────────────────────────
|
|
49
|
+
|
|
50
|
+
type sqsAttributes = {
|
|
51
|
+
@as("SentTimestamp") sentTimestamp: string,
|
|
52
|
+
@as("ApproximateReceiveCount") approximateReceiveCount: string,
|
|
53
|
+
}
|
|
54
|
+
type sqsRecord = {body: string, attributes: sqsAttributes}
|
|
55
|
+
type bodyOnlyRecord = {body: string}
|
|
56
|
+
|
|
57
|
+
type streamAttribute = {@as("S") s: string}
|
|
58
|
+
type streamImage = {correlationId: streamAttribute, causationId: streamAttribute}
|
|
59
|
+
type streamPayload = {
|
|
60
|
+
@as("NewImage") newImage: streamImage,
|
|
61
|
+
@as("ApproximateCreationDateTime") approximateCreationDateTime: float,
|
|
62
|
+
}
|
|
63
|
+
type streamRecord = {dynamodb: streamPayload}
|
|
64
|
+
|
|
65
|
+
let sqsRecords = [
|
|
66
|
+
{
|
|
67
|
+
body: `{"meta":{"correlationId":"corr-1","causationId":"cause-1"}}`,
|
|
68
|
+
attributes: {sentTimestamp: "1750000000000", approximateReceiveCount: "3"},
|
|
69
|
+
},
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
let streamRecords = [
|
|
73
|
+
{
|
|
74
|
+
dynamodb: {
|
|
75
|
+
newImage: {correlationId: {s: "corr-2"}, causationId: {s: "cause-2"}},
|
|
76
|
+
approximateCreationDateTime: 1750000001.0,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
// Read back what the dispatch boundary provided, so the assertions see exactly
|
|
82
|
+
// what an application handler would.
|
|
83
|
+
let capture = (): Effect.t<ReventlessCore.RequestContext.t, string, _> =>
|
|
84
|
+
Effect.serviceWith(ReventlessCore.RequestContext.tag, ctx => ctx)
|
|
85
|
+
|
|
86
|
+
describe("entry-point dispatch boundary", () => {
|
|
87
|
+
describe("SQS records", () => {
|
|
88
|
+
testSync("reads the envelope out of the body", () => {
|
|
89
|
+
expect(extractMetaField(sqsRecords, "correlationId"))->Expect.toEqual(Some("corr-1"))
|
|
90
|
+
expect(extractMetaField(sqsRecords, "causationId"))->Expect.toEqual(Some("cause-1"))
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
testSync("reads SentTimestamp as ms since epoch", () =>
|
|
94
|
+
expect(extractSentTimestamp(sqsRecords))->Expect.toEqual(Some(1750000000000.0))
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
testSync("reads ApproximateReceiveCount as the delivery attempt", () =>
|
|
98
|
+
expect(extractRetryCount(sqsRecords))->Expect.toBe(3)
|
|
99
|
+
)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe("DynamoDB stream records", () => {
|
|
103
|
+
// EventLog rows store meta keys flat as top-level attributes
|
|
104
|
+
// (Message.composeMeta rebuilds `meta` from them) — not as a nested map.
|
|
105
|
+
testSync("reads the envelope off the flat NewImage attributes", () => {
|
|
106
|
+
expect(extractMetaField(streamRecords, "correlationId"))->Expect.toEqual(Some("corr-2"))
|
|
107
|
+
expect(extractMetaField(streamRecords, "causationId"))->Expect.toEqual(Some("cause-2"))
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
testSync("converts ApproximateCreationDateTime from seconds to ms", () =>
|
|
111
|
+
expect(extractSentTimestamp(streamRecords))->Expect.toEqual(Some(1750000001000.0))
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
testSync("reports a first delivery — streams have no receive count", () =>
|
|
115
|
+
expect(extractRetryCount(streamRecords))->Expect.toBe(1)
|
|
116
|
+
)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
describe("absent or malformed input", () => {
|
|
120
|
+
testSync("an empty batch has no send time", () =>
|
|
121
|
+
expect(extractSentTimestamp([]))->Expect.toEqual(None)
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
testSync("an unparseable body yields no envelope field", () =>
|
|
125
|
+
expect(extractMetaField([{body: "not json"}], "correlationId"))->Expect.toEqual(None)
|
|
126
|
+
)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
describe("RequestContext provided at dispatch", () => {
|
|
130
|
+
test("carries the full record, not a bare correlationId literal", async () => {
|
|
131
|
+
let ctx = await runEffect(
|
|
132
|
+
capture(),
|
|
133
|
+
{
|
|
134
|
+
correlationId: "corr-1",
|
|
135
|
+
causationId: "cause-1",
|
|
136
|
+
comp: "AggregateRuntime(Customer)",
|
|
137
|
+
plugin: "Catalog",
|
|
138
|
+
timestamp: 1750000000000.0,
|
|
139
|
+
retryCount: 3,
|
|
140
|
+
},
|
|
141
|
+
)
|
|
142
|
+
expect(ctx.correlationId)->Expect.toBe("corr-1")
|
|
143
|
+
expect(ctx.causationId)->Expect.toEqual(Some("cause-1"))
|
|
144
|
+
expect(ctx.component)->Expect.toEqual(Some("AggregateRuntime(Customer)"))
|
|
145
|
+
expect(ctx.pluginName)->Expect.toEqual(Some("Catalog"))
|
|
146
|
+
expect(ctx.timestamp)->Expect.toEqual(Some(1750000000000.0))
|
|
147
|
+
expect(ctx.retryCount)->Expect.toEqual(Some(3))
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test("fills identity and claims, which the record declares non-optional", async () => {
|
|
151
|
+
let ctx = await runEffect(capture(), {correlationId: "corr-1"})
|
|
152
|
+
expect(ctx.identity.userId)->Expect.toBe(Reventless.Identity.anonymous.userId)
|
|
153
|
+
expect(ctx.claims->Dict.keysToArray)->Expect.toEqual([])
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test("falls back to a placeholder when no correlationId is on the message", async () => {
|
|
157
|
+
let ctx = await runEffect(capture(), {comp: "X(Y)"})
|
|
158
|
+
expect(ctx.correlationId)->Expect.toBe("unknown")
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
describe("log annotations", () => {
|
|
163
|
+
// Effect keeps annotations in a HashMap; read it as a dict.
|
|
164
|
+
let annotations = (): Effect.t<Effect.logAnnotationMap, string, _> => Effect.logAnnotations
|
|
165
|
+
|
|
166
|
+
test("promotes comp, causationId and the request id onto every line", async () => {
|
|
167
|
+
setRequestId("req-abc")
|
|
168
|
+
let annotated = await runEffect(
|
|
169
|
+
annotations(),
|
|
170
|
+
{correlationId: "corr-1", causationId: "cause-1", comp: "EventCollector(Customers)"},
|
|
171
|
+
)
|
|
172
|
+
let annotated = annotated->Effect.annotationsToDict
|
|
173
|
+
expect(annotated->Dict.get("comp"))->Expect.toEqual(Some("EventCollector(Customers)"))
|
|
174
|
+
expect(annotated->Dict.get("causationId"))->Expect.toEqual(Some("cause-1"))
|
|
175
|
+
expect(annotated->Dict.get("correlationId"))->Expect.toEqual(Some("corr-1"))
|
|
176
|
+
expect(annotated->Dict.get("requestId"))->Expect.toEqual(Some("req-abc"))
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
// A constant "1" on every line is noise; its absence is what makes
|
|
180
|
+
// `filter retryCount > 1` a usable CloudWatch query.
|
|
181
|
+
test("annotates retryCount only on a redelivery", async () => {
|
|
182
|
+
let first = await runEffect(annotations(), {correlationId: "c", retryCount: 1})
|
|
183
|
+
expect(first->Effect.annotationsToDict->Dict.get("retryCount"))->Expect.toEqual(None)
|
|
184
|
+
let redelivered = await runEffect(annotations(), {correlationId: "c", retryCount: 2})
|
|
185
|
+
expect(redelivered->Effect.annotationsToDict->Dict.get("retryCount"))->Expect.toEqual(
|
|
186
|
+
Some("2"),
|
|
187
|
+
)
|
|
188
|
+
})
|
|
189
|
+
})
|
|
190
|
+
})
|