@reventlessdev/reventless-aws 3.0.0-alpha.175 → 3.0.0-alpha.176
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 +13 -0
- package/package.json +10 -10
- package/src/Platform.res +115 -31
- package/src/Platform.res.mjs +217 -44
- package/src/adapter/DcbEventLog/DcbBackend.res +2 -2
- package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream.res +4 -1
- package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream.res.mjs +1 -1
- package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream_Runtime.res +14 -4
- package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream_Runtime.res.mjs +19 -14
- package/src/adapter/EventLog/EventLogBackend.res +82 -0
- package/src/adapter/EventLog/EventLogBackend.res.mjs +53 -0
- package/src/adapter/EventLog/EventLogStorage.res +15 -0
- package/src/adapter/EventLog/EventLogStorage.res.mjs +20 -1
- package/src/adapter/EventLog/EventLogStorage_Postgres.res +37 -0
- package/src/adapter/EventLog/EventLogStorage_Postgres.res.mjs +21 -0
- package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +26 -11
- package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +15 -6
- package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res +54 -0
- package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs +23 -0
- package/src/adapter/Postgres/PgProjectionFeed.res +100 -0
- package/src/adapter/Postgres/PgProjectionFeed.res.mjs +54 -0
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res +192 -0
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res.mjs +141 -0
- package/src/adapter/QueryDb/QueryDbBackend.res +52 -0
- package/src/adapter/QueryDb/QueryDbBackend.res.mjs +44 -0
- package/src/adapter/QueryDb/QueryDbResolvers.res +48 -0
- package/src/adapter/QueryDb/QueryDbResolvers.res.mjs +17 -1
- package/src/adapter/QueryDb/QueryDbStorage.res +44 -0
- package/src/adapter/QueryDb/QueryDbStorage.res.mjs +34 -1
- package/src/adapter/QueryDb/QueryDbStorage_Postgres.res +43 -0
- package/src/adapter/QueryDb/QueryDbStorage_Postgres.res.mjs +21 -0
- package/src/adapter/QueryDb/QueryDbStorage_Postgres_Runtime.res +19 -0
- package/src/adapter/QueryDb/QueryDbStorage_Postgres_Runtime.res.mjs +15 -0
- package/src/adapter/QueryEngine/QueryEngine_DynamoDb.res +12 -1
- package/src/adapter/QueryEngine/QueryEngine_DynamoDb.res.mjs +2 -1
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res +83 -3
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res.mjs +46 -3
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res +83 -3
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res.mjs +48 -3
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res +130 -5
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res.mjs +66 -6
- package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.mjs +15 -10
- package/src/adapter/Runtime/ReadModelEntryPoint.mjs +32 -15
- package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +26 -12
- package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res +138 -8
- package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res.mjs +76 -11
- package/src/components/Aggregate_Builder_Single.res +12 -5
- package/src/components/Aggregate_Builder_Single.res.mjs +12 -6
- package/src/components/Aggregate_Builder_Single_Async.res +12 -5
- package/src/components/Aggregate_Builder_Single_Async.res.mjs +12 -6
- package/src/components/ReadModel_Builder_NoResolver.res.mjs +1 -1
- package/src/components/ReadModel_Builder_NoResolver_Stream.res +13 -4
- package/src/components/ReadModel_Builder_NoResolver_Stream.res.mjs +14 -9
- package/src/components/ReadModel_Builder_Single.res +14 -5
- package/src/components/ReadModel_Builder_Single.res.mjs +15 -12
- package/src/components/ReadModel_Builder_Single_Stream.res +14 -5
- package/src/components/ReadModel_Builder_Single_Stream.res.mjs +15 -12
- package/src/components/StateViewSlice_Builder.res +10 -4
- package/src/components/StateViewSlice_Builder.res.mjs +8 -10
- package/src/components/StateViewSlice_Builder_Stream.res +10 -4
- package/src/components/StateViewSlice_Builder_Stream.res.mjs +8 -10
- package/src/plugin/runtime/PluginRuntime_Builder.res +12 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +4 -0
- package/tests/EventCollectorChannelStreamRuntimeTest.res +55 -0
- package/tests/EventCollectorChannelStreamRuntimeTest.res.mjs +78 -0
- package/tests/PgChangeFeedRelay_IntegrationTest.res +115 -4
- package/tests/PgChangeFeedRelay_IntegrationTest.res.mjs +84 -0
- package/tests/PgPipeline_IntegrationTest.res +208 -0
- package/tests/PgPipeline_IntegrationTest.res.mjs +260 -0
- package/tests/PgQueryResolver_LambdaTest.res +206 -0
- package/tests/PgQueryResolver_LambdaTest.res.mjs +259 -0
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Pulumi from "@pulumi/pulumi";
|
|
3
4
|
import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
|
|
4
5
|
import * as Util_Bundle$ReventlessAws from "../util/Util_Bundle.res.mjs";
|
|
5
|
-
import * as
|
|
6
|
+
import * as QueryDbBackend$ReventlessAws from "../adapter/QueryDb/QueryDbBackend.res.mjs";
|
|
7
|
+
import * as QueryDbStorage$ReventlessAws from "../adapter/QueryDb/QueryDbStorage.res.mjs";
|
|
8
|
+
import * as QueryDbResolvers$ReventlessAws from "../adapter/QueryDb/QueryDbResolvers.res.mjs";
|
|
6
9
|
import * as StateViewSlice_Builder$ReventlessCore from "@reventlessdev/reventless-core/src/components/StateViewSlice/StateViewSlice_Builder.res.mjs";
|
|
7
|
-
import * as QueryDbResolvers_AppSync$ReventlessAws from "../adapter/QueryDb/QueryDbResolvers_AppSync.res.mjs";
|
|
8
10
|
import * as RuntimeEnvironment_Lambda$ReventlessAws from "../adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs";
|
|
9
11
|
import * as EventCollectorChannel_DynamoDbStream$ReventlessAws from "../adapter/EventCollector/EventCollectorChannel_DynamoDbStream.res.mjs";
|
|
10
12
|
import * as StateViewSliceRuntime_Builder_Single$ReventlessAws from "../adapter/Runtime/StateViewSliceRuntime_Builder_Single.res.mjs";
|
|
@@ -16,11 +18,7 @@ function Make(Api) {
|
|
|
16
18
|
extractCorrelationId: RuntimeEnvironment_Lambda$ReventlessAws.extractCorrelationId,
|
|
17
19
|
asEventHandler: prim => prim,
|
|
18
20
|
asEffectHandler: prim => prim
|
|
19
|
-
})({
|
|
20
|
-
make: QueryDbStorage_DynamoDb$ReventlessAws.make
|
|
21
|
-
})({
|
|
22
|
-
make: QueryDbResolvers_AppSync$ReventlessAws.make
|
|
23
|
-
})({
|
|
21
|
+
})(QueryDbStorage$ReventlessAws.Selectable)(QueryDbResolvers$ReventlessAws.Selectable)({
|
|
24
22
|
make: EventCollectorChannel_DynamoDbStream$ReventlessAws.make,
|
|
25
23
|
connect: EventCollectorChannel_DynamoDbStream$ReventlessAws.connect
|
|
26
24
|
})({
|
|
@@ -36,8 +34,8 @@ function Make(Api) {
|
|
|
36
34
|
let make = (dcbEventLog, opts) => {
|
|
37
35
|
let sv = InnerMake.make(dcbEventLog, opts);
|
|
38
36
|
let queryDbOutputs = Component$ReventlessCore.outputs(sv).queryDb;
|
|
39
|
-
let
|
|
40
|
-
StateViewSliceRuntime_Builder_Single$ReventlessAws.registerStateViewSlice(Spec.name, Util_Bundle$ReventlessAws.getModuleSpecifier(Spec.moduleUrl), Util_Bundle$ReventlessAws.getModuleSpecifier(Projection.moduleUrl),
|
|
37
|
+
let queryDbTableName = QueryDbBackend$ReventlessAws.isPostgresFor(Spec.name) ? Pulumi.output(Spec.name) : queryDbOutputs.resources[0].name;
|
|
38
|
+
StateViewSliceRuntime_Builder_Single$ReventlessAws.registerStateViewSlice(Spec.name, Util_Bundle$ReventlessAws.getModuleSpecifier(Spec.moduleUrl), Util_Bundle$ReventlessAws.getModuleSpecifier(Projection.moduleUrl), queryDbTableName, queryDbOutputs.resources);
|
|
41
39
|
return sv;
|
|
42
40
|
};
|
|
43
41
|
return {
|
|
@@ -65,4 +63,4 @@ export {
|
|
|
65
63
|
EventCollectorRuntimeBuilder,
|
|
66
64
|
Make,
|
|
67
65
|
}
|
|
68
|
-
/*
|
|
66
|
+
/* @pulumi/pulumi Not a pure module */
|
|
@@ -15,8 +15,8 @@ module Make = (Api: {
|
|
|
15
15
|
}) => {
|
|
16
16
|
module Inner = ReventlessCore.StateViewSlice_Builder.Make(
|
|
17
17
|
RuntimeEnvironment,
|
|
18
|
-
|
|
19
|
-
QueryDbResolvers.
|
|
18
|
+
QueryDbStorage.SelectableStream,
|
|
19
|
+
QueryDbResolvers.Selectable,
|
|
20
20
|
EventCollectorChannel,
|
|
21
21
|
EventCollectorRuntimeBuilder,
|
|
22
22
|
Api,
|
|
@@ -36,12 +36,18 @@ module Make = (Api: {
|
|
|
36
36
|
let make = (~dcbEventLog, ~opts=?): component => {
|
|
37
37
|
let sv = InnerMake.make(~dcbEventLog, ~opts?)
|
|
38
38
|
let queryDbOutputs = (sv->ReventlessCore.Component.outputs).queryDb
|
|
39
|
-
|
|
39
|
+
// On the Postgres backend there is no DynamoDB table (resources is
|
|
40
|
+
// empty) — the slice spec name is the stable `qdb_<name>` discriminator.
|
|
41
|
+
let queryDbTableName = if QueryDbBackend.isPostgresFor(Spec.name) {
|
|
42
|
+
Pulumi.Output.make(Spec.name)
|
|
43
|
+
} else {
|
|
44
|
+
(queryDbOutputs.resources->Array.getUnsafe(0)).name
|
|
45
|
+
}
|
|
40
46
|
EventCollectorRuntimeBuilder.registerStateViewSlice(
|
|
41
47
|
~name=Spec.name,
|
|
42
48
|
~specModulePath=Util_Bundle.getModuleSpecifier(Spec.moduleUrl),
|
|
43
49
|
~projectionModulePath=Util_Bundle.getModuleSpecifier(Projection.moduleUrl),
|
|
44
|
-
~queryDbTableName
|
|
50
|
+
~queryDbTableName,
|
|
45
51
|
~queryDbResources=queryDbOutputs.resources,
|
|
46
52
|
)
|
|
47
53
|
sv
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Pulumi from "@pulumi/pulumi";
|
|
3
4
|
import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
|
|
4
5
|
import * as Util_Bundle$ReventlessAws from "../util/Util_Bundle.res.mjs";
|
|
6
|
+
import * as QueryDbBackend$ReventlessAws from "../adapter/QueryDb/QueryDbBackend.res.mjs";
|
|
7
|
+
import * as QueryDbStorage$ReventlessAws from "../adapter/QueryDb/QueryDbStorage.res.mjs";
|
|
8
|
+
import * as QueryDbResolvers$ReventlessAws from "../adapter/QueryDb/QueryDbResolvers.res.mjs";
|
|
5
9
|
import * as StateViewSlice_Builder$ReventlessCore from "@reventlessdev/reventless-core/src/components/StateViewSlice/StateViewSlice_Builder.res.mjs";
|
|
6
|
-
import * as QueryDbResolvers_AppSync$ReventlessAws from "../adapter/QueryDb/QueryDbResolvers_AppSync.res.mjs";
|
|
7
10
|
import * as RuntimeEnvironment_Lambda$ReventlessAws from "../adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs";
|
|
8
|
-
import * as QueryDbStorage_DynamoDbStream$ReventlessAws from "../adapter/QueryDb/QueryDbStorage_DynamoDbStream.res.mjs";
|
|
9
11
|
import * as EventCollectorChannel_DynamoDbStream$ReventlessAws from "../adapter/EventCollector/EventCollectorChannel_DynamoDbStream.res.mjs";
|
|
10
12
|
import * as StateViewSliceRuntime_Builder_Single$ReventlessAws from "../adapter/Runtime/StateViewSliceRuntime_Builder_Single.res.mjs";
|
|
11
13
|
|
|
@@ -16,11 +18,7 @@ function Make(Api) {
|
|
|
16
18
|
extractCorrelationId: RuntimeEnvironment_Lambda$ReventlessAws.extractCorrelationId,
|
|
17
19
|
asEventHandler: prim => prim,
|
|
18
20
|
asEffectHandler: prim => prim
|
|
19
|
-
})({
|
|
20
|
-
make: QueryDbStorage_DynamoDbStream$ReventlessAws.make
|
|
21
|
-
})({
|
|
22
|
-
make: QueryDbResolvers_AppSync$ReventlessAws.make
|
|
23
|
-
})({
|
|
21
|
+
})(QueryDbStorage$ReventlessAws.SelectableStream)(QueryDbResolvers$ReventlessAws.Selectable)({
|
|
24
22
|
make: EventCollectorChannel_DynamoDbStream$ReventlessAws.make,
|
|
25
23
|
connect: EventCollectorChannel_DynamoDbStream$ReventlessAws.connect
|
|
26
24
|
})({
|
|
@@ -36,8 +34,8 @@ function Make(Api) {
|
|
|
36
34
|
let make = (dcbEventLog, opts) => {
|
|
37
35
|
let sv = InnerMake.make(dcbEventLog, opts);
|
|
38
36
|
let queryDbOutputs = Component$ReventlessCore.outputs(sv).queryDb;
|
|
39
|
-
let
|
|
40
|
-
StateViewSliceRuntime_Builder_Single$ReventlessAws.registerStateViewSlice(Spec.name, Util_Bundle$ReventlessAws.getModuleSpecifier(Spec.moduleUrl), Util_Bundle$ReventlessAws.getModuleSpecifier(Projection.moduleUrl),
|
|
37
|
+
let queryDbTableName = QueryDbBackend$ReventlessAws.isPostgresFor(Spec.name) ? Pulumi.output(Spec.name) : queryDbOutputs.resources[0].name;
|
|
38
|
+
StateViewSliceRuntime_Builder_Single$ReventlessAws.registerStateViewSlice(Spec.name, Util_Bundle$ReventlessAws.getModuleSpecifier(Spec.moduleUrl), Util_Bundle$ReventlessAws.getModuleSpecifier(Projection.moduleUrl), queryDbTableName, queryDbOutputs.resources);
|
|
41
39
|
return sv;
|
|
42
40
|
};
|
|
43
41
|
return {
|
|
@@ -65,4 +63,4 @@ export {
|
|
|
65
63
|
EventCollectorRuntimeBuilder,
|
|
66
64
|
Make,
|
|
67
65
|
}
|
|
68
|
-
/*
|
|
66
|
+
/* @pulumi/pulumi Not a pure module */
|
|
@@ -216,6 +216,18 @@ module Make = (
|
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
// Classic Postgres logs: `~eventTopics` is keyed by aggregate name, so each key
|
|
220
|
+
// attaches this plugin's EventCollector queue to exactly the aggregates whose
|
|
221
|
+
// event-log DynamoDB stream the collector would have subscribed on the DynamoDB
|
|
222
|
+
// path (admin's extension-point filtering included).
|
|
223
|
+
if EventLogBackend.isPostgres() {
|
|
224
|
+
eventTopics
|
|
225
|
+
->Dict.keysToArray
|
|
226
|
+
->Array.forEach(aggregateName =>
|
|
227
|
+
EventLogBackend.attachCollectorQueue(~aggregateName, ~url=queue.id, ~arn=queue.arn)
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
219
231
|
// Pull the per-EventCollector context registered by Plugin_Helpers.connect
|
|
220
232
|
// (plugins) or synthesise an admin one if none registered.
|
|
221
233
|
let context: ReventlessCore.Plugin_Helpers.eventCollectorContext =
|
|
@@ -14,6 +14,7 @@ import * as PluginSpec$ReventlessCore from "@reventlessdev/reventless-core/src/p
|
|
|
14
14
|
import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
|
|
15
15
|
import * as ComponentType$ReventlessCore from "@reventlessdev/reventless-core/src/ComponentType.res.mjs";
|
|
16
16
|
import * as EventCollector$ReventlessCore from "@reventlessdev/reventless-core/src/components/EventCollector/EventCollector.res.mjs";
|
|
17
|
+
import * as EventLogBackend$ReventlessAws from "../../adapter/EventLog/EventLogBackend.res.mjs";
|
|
17
18
|
import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_Helpers.res.mjs";
|
|
18
19
|
import * as RuntimeEnvironment_Lambda$ReventlessAws from "../../adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs";
|
|
19
20
|
import * as StateChangeSliceRuntime_Builder_Single$ReventlessAws from "../../adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs";
|
|
@@ -174,6 +175,9 @@ function Make(EventCollectorChannel) {
|
|
|
174
175
|
DcbBackend$ReventlessAws.attachCollectorQueue(pluginName + "DcbEventLog", queue.id, queue.arn);
|
|
175
176
|
}
|
|
176
177
|
}
|
|
178
|
+
if (EventLogBackend$ReventlessAws.isPostgres()) {
|
|
179
|
+
Object.keys(eventTopics).forEach(aggregateName => EventLogBackend$ReventlessAws.attachCollectorQueue(aggregateName, queue.id, queue.arn));
|
|
180
|
+
}
|
|
177
181
|
let ctx = Plugin_Helpers$ReventlessCore.eventCollectorContextRef.contents[name];
|
|
178
182
|
let context = ctx !== undefined ? ctx : synthesizeAdminContext();
|
|
179
183
|
let envVars = {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// B3.0: the ReadModel/StateViewSlice record decoder must accept BOTH DynamoDB
|
|
2
|
+
// stream records (DynamoDB backend) and SQS records (Postgres backend — bodies
|
|
3
|
+
// injected by the PgChangeFeedRelay via the projection feed queue), in one batch.
|
|
4
|
+
|
|
5
|
+
open JestGlobals
|
|
6
|
+
|
|
7
|
+
let mkEvent = (records): PulumiAws.Lambda.CallbackFunction.event => Obj.magic({"Records": records})
|
|
8
|
+
|
|
9
|
+
let sqsRecord = body =>
|
|
10
|
+
{
|
|
11
|
+
"eventSource": "aws:sqs",
|
|
12
|
+
"eventSourceARN": "arn:aws:sqs:eu-west-1:1:AllReadModelsFeed",
|
|
13
|
+
"body": body,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let run = event => {
|
|
17
|
+
let seen: array<JSON.t> = []
|
|
18
|
+
let handler: ReventlessCore.EventCollector.jsonEventsHandler = stream =>
|
|
19
|
+
stream
|
|
20
|
+
->Stream.runForEach(json => Effect.succeed(seen->Array.push(json)->ignore))
|
|
21
|
+
->Effect.map(_ => ())
|
|
22
|
+
EventCollectorChannel_DynamoDbStream_Runtime.handleStreamEvent(handler, event, ())
|
|
23
|
+
->Effect.runPromise
|
|
24
|
+
->Promise.thenResolve(_ => seen)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("handleStreamEvent (ReadModel/SVS record decoder)", () => {
|
|
28
|
+
test("parses SQS feed-queue records into their JSON bodies", async () => {
|
|
29
|
+
let seen = await run(
|
|
30
|
+
mkEvent([
|
|
31
|
+
sqsRecord(`{"id":"agg-1","meta":{"service":"Product"},"event":{"type":"Added","data":{}}}`),
|
|
32
|
+
sqsRecord(`{"id":"agg-2","meta":{"service":"Product"},"event":{"type":"Added","data":{}}}`),
|
|
33
|
+
]),
|
|
34
|
+
)
|
|
35
|
+
expect(seen->Array.length)->toBe(2)
|
|
36
|
+
let id0 =
|
|
37
|
+
seen
|
|
38
|
+
->Array.getUnsafe(0)
|
|
39
|
+
->JSON.Decode.object
|
|
40
|
+
->Option.flatMap(o => o->Dict.get("id"))
|
|
41
|
+
->Option.flatMap(JSON.Decode.string)
|
|
42
|
+
expect(id0)->toEqual(Some("agg-1"))
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test("skips malformed SQS bodies and unknown sources without failing the batch", async () => {
|
|
46
|
+
let seen = await run(
|
|
47
|
+
mkEvent([
|
|
48
|
+
sqsRecord("not-json"),
|
|
49
|
+
{"eventSource": "aws:kinesis", "eventSourceARN": "arn:x", "body": "{}"},
|
|
50
|
+
sqsRecord(`{"ok":true}`),
|
|
51
|
+
]),
|
|
52
|
+
)
|
|
53
|
+
expect(seen->Array.length)->toBe(1)
|
|
54
|
+
})
|
|
55
|
+
})
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as Effect from "effect/Effect";
|
|
6
|
+
import * as Stream from "effect/Stream";
|
|
7
|
+
import * as EventCollectorChannel_DynamoDbStream_Runtime$ReventlessAws from "../src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream_Runtime.res.mjs";
|
|
8
|
+
|
|
9
|
+
function mkEvent(records) {
|
|
10
|
+
return {
|
|
11
|
+
Records: records
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function sqsRecord(body) {
|
|
16
|
+
return {
|
|
17
|
+
eventSource: "aws:sqs",
|
|
18
|
+
eventSourceARN: "arn:aws:sqs:eu-west-1:1:AllReadModelsFeed",
|
|
19
|
+
body: body
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function run(event) {
|
|
24
|
+
let seen = [];
|
|
25
|
+
let handler = stream => Effect.map(Stream.runForEach(stream, json => Effect.succeed((seen.push(json), undefined))), () => {});
|
|
26
|
+
return Effect.runPromise(EventCollectorChannel_DynamoDbStream_Runtime$ReventlessAws.handleStreamEvent(handler, event, undefined)).then(() => seen);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
globalThis.describe("handleStreamEvent (ReadModel/SVS record decoder)", () => {
|
|
30
|
+
globalThis.test("parses SQS feed-queue records into their JSON bodies", async () => {
|
|
31
|
+
let seen = await run({
|
|
32
|
+
Records: [
|
|
33
|
+
{
|
|
34
|
+
eventSource: "aws:sqs",
|
|
35
|
+
eventSourceARN: "arn:aws:sqs:eu-west-1:1:AllReadModelsFeed",
|
|
36
|
+
body: `{"id":"agg-1","meta":{"service":"Product"},"event":{"type":"Added","data":{}}}`
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
eventSource: "aws:sqs",
|
|
40
|
+
eventSourceARN: "arn:aws:sqs:eu-west-1:1:AllReadModelsFeed",
|
|
41
|
+
body: `{"id":"agg-2","meta":{"service":"Product"},"event":{"type":"Added","data":{}}}`
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
});
|
|
45
|
+
globalThis.expect(seen.length).toBe(2);
|
|
46
|
+
let id0 = Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(seen[0]), o => o["id"]), Stdlib_JSON.Decode.string);
|
|
47
|
+
globalThis.expect(id0).toEqual("agg-1");
|
|
48
|
+
});
|
|
49
|
+
globalThis.test("skips malformed SQS bodies and unknown sources without failing the batch", async () => {
|
|
50
|
+
let seen = await run({
|
|
51
|
+
Records: [
|
|
52
|
+
{
|
|
53
|
+
eventSource: "aws:sqs",
|
|
54
|
+
eventSourceARN: "arn:aws:sqs:eu-west-1:1:AllReadModelsFeed",
|
|
55
|
+
body: "not-json"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
eventSource: "aws:kinesis",
|
|
59
|
+
eventSourceARN: "arn:x",
|
|
60
|
+
body: "{}"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
eventSource: "aws:sqs",
|
|
64
|
+
eventSourceARN: "arn:aws:sqs:eu-west-1:1:AllReadModelsFeed",
|
|
65
|
+
body: `{"ok":true}`
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
});
|
|
69
|
+
globalThis.expect(seen.length).toBe(1);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
export {
|
|
74
|
+
mkEvent,
|
|
75
|
+
sqsRecord,
|
|
76
|
+
run,
|
|
77
|
+
}
|
|
78
|
+
/* Not a pure module */
|
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
// PG_URL=postgres://postgres:postgres@localhost:5432/postgres pnpm test
|
|
6
6
|
//
|
|
7
7
|
// Exercises the deployed propagation path end-to-end minus AWS: DCB events are
|
|
8
|
-
// appended to a real `dcb_event` log
|
|
9
|
-
//
|
|
10
|
-
//
|
|
8
|
+
// appended to a real `dcb_event` log (classic events to `event_log`), then
|
|
9
|
+
// `PgChangeFeedRelay_Runtime.relayWithPool` / `relayClassicWithPool` drains the
|
|
10
|
+
// feed, transforms each event into the EventCollector `{id, meta, event}` body,
|
|
11
|
+
// and hands the batch to an injected `sendBatch` (standing in for the SQS
|
|
11
12
|
// SendMessage the entry point wires). Asserts the emitted bodies, the partition-tag
|
|
12
|
-
// `id` derivation, and the
|
|
13
|
+
// `id` derivation, and the checkpoints (a second drain sees nothing new; per-log
|
|
14
|
+
// subscribers stay isolated).
|
|
13
15
|
|
|
14
16
|
open JestGlobals
|
|
15
17
|
open ReventlessCore
|
|
@@ -110,6 +112,115 @@ switch processEnv->Dict.get("PG_URL") {
|
|
|
110
112
|
expect(sink.contents->Array.length)->toBe(2)
|
|
111
113
|
})
|
|
112
114
|
|
|
115
|
+
testPromise("classic: relays each appended event_log event as an {id, meta, event} body", async () => {
|
|
116
|
+
let (_n, ops, _s) =
|
|
117
|
+
ReventlessPostgres.EventLogStorage_Postgres.makeStorage(
|
|
118
|
+
~pool,
|
|
119
|
+
~name="ClassicRelayBasicEventLog",
|
|
120
|
+
~opts=(),
|
|
121
|
+
)
|
|
122
|
+
// The flat on-disk item the classic append stores verbatim — same shape the
|
|
123
|
+
// DynamoDB path puts (id, position, event, data, decomposed meta).
|
|
124
|
+
let flatItem = (~id, ~seq, ~eventType, ~data) =>
|
|
125
|
+
[
|
|
126
|
+
("id", JSON.Encode.string(id)),
|
|
127
|
+
("position", JSON.Encode.string(seq->Int.toString->String.padStart(9, "0"))),
|
|
128
|
+
("event", JSON.Encode.string(eventType)),
|
|
129
|
+
("data", data),
|
|
130
|
+
]
|
|
131
|
+
->Array.concat(
|
|
132
|
+
ReventlessCore.Message.generateMeta(~service="relay-it")->ReventlessCore.Message.decomposeMeta,
|
|
133
|
+
)
|
|
134
|
+
->Dict.fromArray
|
|
135
|
+
->JSON.Encode.object
|
|
136
|
+
let _ = await ops.append(
|
|
137
|
+
0,
|
|
138
|
+
"agg-1",
|
|
139
|
+
[
|
|
140
|
+
flatItem(~id="agg-1", ~seq=0, ~eventType="NameUpdated", ~data=jsonObj([("n", JSON.Encode.int(0))])),
|
|
141
|
+
flatItem(~id="agg-1", ~seq=1, ~eventType="NameUpdated", ~data=jsonObj([("n", JSON.Encode.int(1))])),
|
|
142
|
+
],
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
let sink = ref([])
|
|
146
|
+
let processed = await PgChangeFeedRelay_Runtime.relayClassicWithPool(
|
|
147
|
+
~pool,
|
|
148
|
+
~logName="ClassicRelayBasicEventLog",
|
|
149
|
+
~subscriber="relay-it:ClassicRelayBasicEventLog",
|
|
150
|
+
~sendBatch=capturingSendBatch(sink),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
expect(processed)->toBe(2)
|
|
154
|
+
expect(sink.contents->Array.length)->toBe(2)
|
|
155
|
+
let first = sink.contents->Array.getUnsafe(0)
|
|
156
|
+
expect(idOf(first))->toEqual(Some("agg-1"))
|
|
157
|
+
let obj = first->JSON.Decode.object->Option.getOrThrow
|
|
158
|
+
expect(
|
|
159
|
+
obj->Dict.get("event")->Option.getOrThrow->JSON.stringify->String.includes("NameUpdated"),
|
|
160
|
+
)->toBe(true)
|
|
161
|
+
let metaObj = obj->Dict.get("meta")->Option.flatMap(JSON.Decode.object)->Option.getOrThrow
|
|
162
|
+
expect(metaObj->Dict.get("service"))->toEqual(Some(JSON.Encode.string("relay-it")))
|
|
163
|
+
|
|
164
|
+
// Checkpointed: a second drain from the same subscriber sees nothing new.
|
|
165
|
+
let second = await PgChangeFeedRelay_Runtime.relayClassicWithPool(
|
|
166
|
+
~pool,
|
|
167
|
+
~logName="ClassicRelayBasicEventLog",
|
|
168
|
+
~subscriber="relay-it:ClassicRelayBasicEventLog",
|
|
169
|
+
~sendBatch=capturingSendBatch(sink),
|
|
170
|
+
)
|
|
171
|
+
expect(second)->toBe(0)
|
|
172
|
+
expect(sink.contents->Array.length)->toBe(2)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
testPromise("classic: per-log subscribers keep checkpoints isolated across logs", async () => {
|
|
176
|
+
// event_log_subscription keys by subscriber alone. With a SHARED subscriber,
|
|
177
|
+
// draining log A after log B's events were appended would save a cursor past
|
|
178
|
+
// B's rows and silently skip them — the clobber the per-log subscriber fixes.
|
|
179
|
+
let mkOps = name => {
|
|
180
|
+
let (_n, ops, _s) =
|
|
181
|
+
ReventlessPostgres.EventLogStorage_Postgres.makeStorage(~pool, ~name, ~opts=())
|
|
182
|
+
ops
|
|
183
|
+
}
|
|
184
|
+
let opsB = mkOps("ClassicRelayIsoBEventLog")
|
|
185
|
+
let opsA = mkOps("ClassicRelayIsoAEventLog")
|
|
186
|
+
let meta = () =>
|
|
187
|
+
ReventlessCore.Message.generateMeta(~service="relay-it")->ReventlessCore.Message.decomposeMeta
|
|
188
|
+
let item = (id, eventType) =>
|
|
189
|
+
[
|
|
190
|
+
("id", JSON.Encode.string(id)),
|
|
191
|
+
("position", JSON.Encode.string("000000000")),
|
|
192
|
+
("event", JSON.Encode.string(eventType)),
|
|
193
|
+
("data", jsonObj([])),
|
|
194
|
+
]
|
|
195
|
+
->Array.concat(meta())
|
|
196
|
+
->Dict.fromArray
|
|
197
|
+
->JSON.Encode.object
|
|
198
|
+
// B's event commits FIRST (lower global_seq), then A's.
|
|
199
|
+
let _ = await opsB.append(0, "b-1", [item("b-1", "BHappened")])
|
|
200
|
+
let _ = await opsA.append(0, "a-1", [item("a-1", "AHappened")])
|
|
201
|
+
|
|
202
|
+
// Drain A first — its checkpoint lands PAST B's rows in global_seq order.
|
|
203
|
+
let sinkA = ref([])
|
|
204
|
+
let drainedA = await PgChangeFeedRelay_Runtime.relayClassicWithPool(
|
|
205
|
+
~pool,
|
|
206
|
+
~logName="ClassicRelayIsoAEventLog",
|
|
207
|
+
~subscriber="relay-it:ClassicRelayIsoAEventLog",
|
|
208
|
+
~sendBatch=capturingSendBatch(sinkA),
|
|
209
|
+
)
|
|
210
|
+
expect(drainedA)->toBe(1)
|
|
211
|
+
|
|
212
|
+
// B still sees its event because its subscriber has its own checkpoint.
|
|
213
|
+
let sinkB = ref([])
|
|
214
|
+
let drainedB = await PgChangeFeedRelay_Runtime.relayClassicWithPool(
|
|
215
|
+
~pool,
|
|
216
|
+
~logName="ClassicRelayIsoBEventLog",
|
|
217
|
+
~subscriber="relay-it:ClassicRelayIsoBEventLog",
|
|
218
|
+
~sendBatch=capturingSendBatch(sinkB),
|
|
219
|
+
)
|
|
220
|
+
expect(drainedB)->toBe(1)
|
|
221
|
+
expect(idOf(sinkB.contents->Array.getUnsafe(0)))->toEqual(Some("b-1"))
|
|
222
|
+
})
|
|
223
|
+
|
|
113
224
|
testPromise("partitionTag pins the id to its tag on a multi-tag event", async () => {
|
|
114
225
|
let (_n, ops, _s) =
|
|
115
226
|
ReventlessPostgres.DcbEventLogStorage_Postgres.makeStorage(
|
|
@@ -8,6 +8,7 @@ import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Mess
|
|
|
8
8
|
import * as PgDriver$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/PgDriver.res.mjs";
|
|
9
9
|
import * as PgSchema$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/PgSchema.res.mjs";
|
|
10
10
|
import * as PgChangeFeedRelay_Runtime$ReventlessAws from "../src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs";
|
|
11
|
+
import * as EventLogStorage_Postgres$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/EventLogStorage_Postgres.res.mjs";
|
|
11
12
|
import * as DcbEventLogStorage_Postgres$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/DcbEventLogStorage_Postgres.res.mjs";
|
|
12
13
|
|
|
13
14
|
let opts = {};
|
|
@@ -104,6 +105,89 @@ if (url !== undefined) {
|
|
|
104
105
|
globalThis.expect(second).toBe(0);
|
|
105
106
|
globalThis.expect(sink.contents.length).toBe(2);
|
|
106
107
|
});
|
|
108
|
+
globalThis.test("classic: relays each appended event_log event as an {id, meta, event} body", async () => {
|
|
109
|
+
let match = EventLogStorage_Postgres$ReventlessPostgres.makeStorage(pool, "ClassicRelayBasicEventLog", undefined, undefined);
|
|
110
|
+
let flatItem = (id, seq, eventType, data) => Object.fromEntries([
|
|
111
|
+
[
|
|
112
|
+
"id",
|
|
113
|
+
id
|
|
114
|
+
],
|
|
115
|
+
[
|
|
116
|
+
"position",
|
|
117
|
+
seq.toString().padStart(9, "0")
|
|
118
|
+
],
|
|
119
|
+
[
|
|
120
|
+
"event",
|
|
121
|
+
eventType
|
|
122
|
+
],
|
|
123
|
+
[
|
|
124
|
+
"data",
|
|
125
|
+
data
|
|
126
|
+
]
|
|
127
|
+
].concat(Message$ReventlessCore.decomposeMeta(Message$ReventlessCore.generateMeta("relay-it", undefined, undefined, undefined, undefined, undefined, undefined, undefined))));
|
|
128
|
+
await match[1].append(0, "agg-1", [
|
|
129
|
+
flatItem("agg-1", 0, "NameUpdated", Object.fromEntries([[
|
|
130
|
+
"n",
|
|
131
|
+
0
|
|
132
|
+
]])),
|
|
133
|
+
flatItem("agg-1", 1, "NameUpdated", Object.fromEntries([[
|
|
134
|
+
"n",
|
|
135
|
+
1
|
|
136
|
+
]]))
|
|
137
|
+
]);
|
|
138
|
+
let sink = {
|
|
139
|
+
contents: []
|
|
140
|
+
};
|
|
141
|
+
let processed = await PgChangeFeedRelay_Runtime$ReventlessAws.relayClassicWithPool(pool, "ClassicRelayBasicEventLog", "relay-it:ClassicRelayBasicEventLog", capturingSendBatch(sink));
|
|
142
|
+
globalThis.expect(processed).toBe(2);
|
|
143
|
+
globalThis.expect(sink.contents.length).toBe(2);
|
|
144
|
+
let first = sink.contents[0];
|
|
145
|
+
globalThis.expect(idOf(first)).toEqual("agg-1");
|
|
146
|
+
let obj = Stdlib_Option.getOrThrow(Stdlib_JSON.Decode.object(first), undefined);
|
|
147
|
+
globalThis.expect(JSON.stringify(Stdlib_Option.getOrThrow(obj["event"], undefined)).includes("NameUpdated")).toBe(true);
|
|
148
|
+
let metaObj = Stdlib_Option.getOrThrow(Stdlib_Option.flatMap(obj["meta"], Stdlib_JSON.Decode.object), undefined);
|
|
149
|
+
globalThis.expect(metaObj["service"]).toEqual("relay-it");
|
|
150
|
+
let second = await PgChangeFeedRelay_Runtime$ReventlessAws.relayClassicWithPool(pool, "ClassicRelayBasicEventLog", "relay-it:ClassicRelayBasicEventLog", capturingSendBatch(sink));
|
|
151
|
+
globalThis.expect(second).toBe(0);
|
|
152
|
+
globalThis.expect(sink.contents.length).toBe(2);
|
|
153
|
+
});
|
|
154
|
+
globalThis.test("classic: per-log subscribers keep checkpoints isolated across logs", async () => {
|
|
155
|
+
let mkOps = name => EventLogStorage_Postgres$ReventlessPostgres.makeStorage(pool, name, undefined, undefined)[1];
|
|
156
|
+
let opsB = mkOps("ClassicRelayIsoBEventLog");
|
|
157
|
+
let opsA = mkOps("ClassicRelayIsoAEventLog");
|
|
158
|
+
let meta = () => Message$ReventlessCore.decomposeMeta(Message$ReventlessCore.generateMeta("relay-it", undefined, undefined, undefined, undefined, undefined, undefined, undefined));
|
|
159
|
+
let item = (id, eventType) => Object.fromEntries([
|
|
160
|
+
[
|
|
161
|
+
"id",
|
|
162
|
+
id
|
|
163
|
+
],
|
|
164
|
+
[
|
|
165
|
+
"position",
|
|
166
|
+
"000000000"
|
|
167
|
+
],
|
|
168
|
+
[
|
|
169
|
+
"event",
|
|
170
|
+
eventType
|
|
171
|
+
],
|
|
172
|
+
[
|
|
173
|
+
"data",
|
|
174
|
+
Object.fromEntries([])
|
|
175
|
+
]
|
|
176
|
+
].concat(meta()));
|
|
177
|
+
await opsB.append(0, "b-1", [item("b-1", "BHappened")]);
|
|
178
|
+
await opsA.append(0, "a-1", [item("a-1", "AHappened")]);
|
|
179
|
+
let sinkA = {
|
|
180
|
+
contents: []
|
|
181
|
+
};
|
|
182
|
+
let drainedA = await PgChangeFeedRelay_Runtime$ReventlessAws.relayClassicWithPool(pool, "ClassicRelayIsoAEventLog", "relay-it:ClassicRelayIsoAEventLog", capturingSendBatch(sinkA));
|
|
183
|
+
globalThis.expect(drainedA).toBe(1);
|
|
184
|
+
let sinkB = {
|
|
185
|
+
contents: []
|
|
186
|
+
};
|
|
187
|
+
let drainedB = await PgChangeFeedRelay_Runtime$ReventlessAws.relayClassicWithPool(pool, "ClassicRelayIsoBEventLog", "relay-it:ClassicRelayIsoBEventLog", capturingSendBatch(sinkB));
|
|
188
|
+
globalThis.expect(drainedB).toBe(1);
|
|
189
|
+
globalThis.expect(idOf(sinkB.contents[0])).toEqual("b-1");
|
|
190
|
+
});
|
|
107
191
|
globalThis.test("partitionTag pins the id to its tag on a multi-tag event", async () => {
|
|
108
192
|
let match = DcbEventLogStorage_Postgres$ReventlessPostgres.makeStorage(pool, "relay-pt", [], {
|
|
109
193
|
TAG: "Simple",
|