@reventlessdev/reventless-aws 3.0.0-alpha.188 → 3.0.0-alpha.190

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.
@@ -0,0 +1,221 @@
1
+ // Typed cold-start core for the DCB CommandTopic Lambda entry point.
2
+ //
3
+ // The "typed core, thin shell" split (docs/plans/minimize-lambda-entrypoint-mjs-shell.md).
4
+ // The `.mjs` shell owns the one boundary that is inherently untyped: reading
5
+ // `HANDLER_CONFIG` and dynamically `import()`-ing user Spec/Behavior modules at
6
+ // cold start, whose types are unknowable here. Everything below is compiler-
7
+ // checked against the *real* framework signatures, so the invariants that used
8
+ // to live only in `.mjs` comments are now enforced by the build:
9
+ //
10
+ // * decision-read scope derivation — re-deriving from annotations alone once
11
+ // dropped inferred cross-partition reference reads and rejected every
12
+ // reference-guarded command in prod. See
13
+ // docs/analysis/dcb-runtime-scope-annotation-drift.md.
14
+ // * storage `partitionTag` derivation + threading — dropping it collapsed the
15
+ // composite fence and produced TransactionConflict bursts in prod. See
16
+ // docs/plans/done/dcb-composite-fence-residual-burst-contention.md.
17
+ // * the storage-ops calls take *labeled* args; the `.mjs` called them
18
+ // positionally and relied on a comment to stay aligned with the compiled
19
+ // signature. Here a signature change is a compile error, not a silent
20
+ // runtime regression.
21
+
22
+ // A user StateChangeSlice spec module, dynamically imported and id-patched by the
23
+ // `.mjs` shell. Opaque here; only the fields the derivations read are projected
24
+ // out through typed getters — the single sanctioned coercion point for specs.
25
+ type specModule
26
+
27
+ @get external specName: specModule => string = "name"
28
+ @get external specModuleUrl: specModule => Nullable.t<string> = "moduleUrl"
29
+ @get external specCommandSchema: specModule => S.t<unknown> = "commandSchema"
30
+ @get external specConsumedEventSchema: specModule => S.t<unknown> = "consumedEventSchema"
31
+ @get external specEventSchema: specModule => S.t<unknown> = "eventSchema"
32
+
33
+ // The `pgConnection` object as it arrives in `HANDLER_CONFIG` — the
34
+ // `PgConnection.connectionConfig` fields plus `lockStrategy`. Present iff this
35
+ // DcbEventLog is Postgres-backed.
36
+ type pgConnectionJson
37
+ @get external pgLockStrategy: pgConnectionJson => Nullable.t<string> = "lockStrategy"
38
+ external asConnectionConfig: pgConnectionJson => PgConnection.connectionConfig = "%identity"
39
+
40
+ // The decision-read scope + storage partition tag for one consistency boundary,
41
+ // derived exactly the way the deploy-time `Dcb_Builder` does (both call the same
42
+ // `Reventless.DcbTag` functions — the single source of truth), so the runtime
43
+ // query can't diverge from the storage/GSI scope.
44
+ type derivedScope = {
45
+ crossPartitionTagKeys: array<string>,
46
+ tagKeysByEventType: dict<array<string>>,
47
+ partitionTag: option<Reventless.DcbTag.derivedPartitionTag>,
48
+ }
49
+
50
+ let deriveScope = (specs: array<specModule>): derivedScope => {
51
+ let scope = Reventless.DcbTag.deriveEffectiveScope(
52
+ specs->Array.map(s => {
53
+ Reventless.DcbTag.name: specName(s),
54
+ commandSchema: specCommandSchema(s),
55
+ consumedEventSchema: specConsumedEventSchema(s),
56
+ eventSchema: specEventSchema(s),
57
+ }),
58
+ )
59
+
60
+ // `derivePartitionTag` throws only on a misconfigured spec, which the deploy
61
+ // would already have rejected; degrade to untagged fences rather than crash
62
+ // cold start (matches the shell's prior defensive behaviour).
63
+ let partitionTag = try Some(
64
+ Reventless.DcbTag.derivePartitionTag(
65
+ specs->Array.map(s => (
66
+ specName(s),
67
+ specModuleUrl(s)->Nullable.toOption->Option.getOr(specName(s)),
68
+ specEventSchema(s),
69
+ )),
70
+ ),
71
+ ) catch {
72
+ | _ => None
73
+ }
74
+
75
+ {
76
+ crossPartitionTagKeys: scope.crossPartitionTagKeys,
77
+ tagKeysByEventType: scope.tagKeysByEventType,
78
+ partitionTag,
79
+ }
80
+ }
81
+
82
+ // The command TAG names this slice handles — used by the shell to build its
83
+ // `handlersByType` routing map.
84
+ let commandTypeNames = (spec: specModule): array<string> =>
85
+ Reventless.DcbTag.extractVariantNames(specCommandSchema(spec))
86
+
87
+ // DynamoDB storage ops for the shared DcbEventLog, threading the derived scope.
88
+ // Mirrors `DcbEventLogStorage_DynamoDb.res`'s deploy-time wiring minus the
89
+ // Pulumi.Output layer (the table name is already resolved at runtime).
90
+ let makeDynamoStorageOps = (
91
+ ~tableName: string,
92
+ scope: derivedScope,
93
+ ): ReventlessCore.DcbEventLog_Adapter.operations => {
94
+ let table: Util_DynamoDb_Runtime.resolvedTable = {
95
+ id: "",
96
+ name: tableName,
97
+ arn: "",
98
+ hashKey: "id",
99
+ }
100
+ {
101
+ ReventlessCore.DcbEventLog_Adapter.read: DcbEventLogStorage_DynamoDb_Runtime.read(
102
+ table,
103
+ ~crossPartitionTagKeys=scope.crossPartitionTagKeys,
104
+ ),
105
+ append: DcbEventLogStorage_DynamoDb_Runtime.append(
106
+ table,
107
+ ~partitionTag=?scope.partitionTag,
108
+ ~crossPartitionTagKeys=scope.crossPartitionTagKeys,
109
+ ),
110
+ readStream: DcbEventLogStorage_DynamoDb_Runtime.readStream(
111
+ table,
112
+ ~crossPartitionTagKeys=scope.crossPartitionTagKeys,
113
+ ),
114
+ }
115
+ }
116
+
117
+ // Postgres storage ops. `logName` is the `dcbEventLogTableName` (Postgres
118
+ // evaluates the DCB query atomically, so scope/GSI routing doesn't apply).
119
+ let makePostgresStorageOps = (
120
+ ~pgConnection: pgConnectionJson,
121
+ ~logName: string,
122
+ ): ReventlessCore.DcbEventLog_Adapter.operations => {
123
+ let lockStrategy = switch pgConnection->pgLockStrategy->Nullable.toOption {
124
+ | Some("RowLocks") => #RowLocks
125
+ | _ => #AdvisoryLocks
126
+ }
127
+ DcbEventLogStorage_Postgres_Runtime.opsFor(
128
+ pgConnection->asConnectionConfig,
129
+ ~logName,
130
+ ~lockStrategy,
131
+ )
132
+ }
133
+
134
+ // Storage-backend selection: Postgres when `pgConnection` is present, else
135
+ // DynamoDB. One entry point for the shell.
136
+ let makeStorageOps = (
137
+ ~tableName: string,
138
+ ~pgConnection: option<pgConnectionJson>,
139
+ scope: derivedScope,
140
+ ): ReventlessCore.DcbEventLog_Adapter.operations =>
141
+ switch pgConnection {
142
+ | Some(pg) => makePostgresStorageOps(~pgConnection=pg, ~logName=tableName)
143
+ | None => makeDynamoStorageOps(~tableName, scope)
144
+ }
145
+
146
+ // ── Tier 2.5: typed slice-handler wiring ────────────────────────────────────
147
+ // The shell previously invoked the compiled `StateChangeSlice_Callback.Make`
148
+ // functor and its `handleCommands` result POSITIONALLY — the exact call the
149
+ // line-146 comment flagged, and the 2026-06-21 incident's regression class
150
+ // (a labelled arg added before the positional ones shifts `stream` to
151
+ // undefined, crashing inside `Stream.mapEffect`). Moving the functor call, the
152
+ // JSON→command decode, and the `handleCommands` invocation here makes all three
153
+ // compiler-checked. The loaded Spec/Behavior modules enter opaque (a clean
154
+ // pass-through to the functor — not poked at); `dcbEventLog` enters as a
155
+ // boundary-typed parameter (the shell still builds it).
156
+
157
+ // The loaded slice's `command` type — opaque; flows from the decode straight
158
+ // into `handleCommands`, so decode and consume share one abstract type.
159
+ type command
160
+ type behaviorModule
161
+
162
+ @get external specCommandSchemaTyped: specModule => S.t<command> = "commandSchema"
163
+
164
+ // `StateChangeSlice_Callback.Make(Spec)(Behavior)` — curried in compiled form.
165
+ type sliceCallback = {
166
+ handleCommands: (
167
+ ~tagKeysByEventType: dict<array<string>>=?,
168
+ ~crossPartitionTagKeys: array<string>=?,
169
+ ReventlessInfra.DcbEventLog.operations,
170
+ Stream.t<
171
+ ReventlessInfra.CommandTopic.topicItem<
172
+ ReventlessCore.Message.command'<Reventless.Id.String.t, command>,
173
+ >,
174
+ string,
175
+ unit,
176
+ >,
177
+ ) => Effect.t<array<result<string, string>>, string, unit>,
178
+ }
179
+ @module("@reventlessdev/reventless-core/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs")
180
+ external makeSliceCallback: specModule => behaviorModule => sliceCallback = "Make"
181
+
182
+ // Builds the per-slice JSON-command handler: decode each topic item's JSON
183
+ // command against the slice's schema (dropping undecodable ones), then dispatch
184
+ // the decoded stream through the slice callback. Returns the same
185
+ // `Stream.t<topicItem<JSON>> => Effect` the shell's routing map keys on.
186
+ let buildSliceHandler = (
187
+ spec: specModule,
188
+ behavior: behaviorModule,
189
+ ~tagKeysByEventType: dict<array<string>>,
190
+ ~crossPartitionTagKeys: array<string>,
191
+ dcbEventLog: ReventlessInfra.DcbEventLog.operations,
192
+ ) => {
193
+ let callback = makeSliceCallback(spec)(behavior)
194
+ let commandSchema = spec->specCommandSchemaTyped
195
+ (
196
+ jsonStream: Stream.t<ReventlessInfra.CommandTopic.topicItem<JSON.t>, string, unit>,
197
+ ): Effect.t<array<result<string, string>>, string, unit> => {
198
+ let decodedStream =
199
+ jsonStream
200
+ ->Stream.mapEffect(topicItem =>
201
+ Effect.sync(() =>
202
+ switch ReventlessCore.Message.decodeCommand'(
203
+ topicItem.command,
204
+ Reventless.Id.String.schema,
205
+ commandSchema,
206
+ ) {
207
+ | decoded =>
208
+ Some({ReventlessInfra.CommandTopic.command: decoded, reference: topicItem.reference})
209
+ | exception _ => None
210
+ }
211
+ )
212
+ )
213
+ ->Stream.flatMap(opt =>
214
+ switch opt {
215
+ | Some(item) => Stream.fromIterable([item])
216
+ | None => Stream.empty
217
+ }
218
+ )
219
+ callback.handleCommands(~tagKeysByEventType, ~crossPartitionTagKeys, dcbEventLog, decodedStream)
220
+ }
221
+ }
@@ -0,0 +1,104 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
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 Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
8
+ import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
9
+ import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
10
+ import * as DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws from "../DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res.mjs";
11
+ import * as DcbEventLogStorage_Postgres_Runtime$ReventlessAws from "../DcbEventLog/DcbEventLogStorage_Postgres_Runtime.res.mjs";
12
+ import * as StateChangeSlice_CallbackResMjs from "@reventlessdev/reventless-core/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs";
13
+
14
+ function deriveScope(specs) {
15
+ let scope = DcbTag$Reventless.deriveEffectiveScope(specs.map(s => ({
16
+ name: s.name,
17
+ commandSchema: s.commandSchema,
18
+ consumedEventSchema: s.consumedEventSchema,
19
+ eventSchema: s.eventSchema
20
+ })));
21
+ let partitionTag;
22
+ try {
23
+ partitionTag = DcbTag$Reventless.derivePartitionTag(specs.map(s => [
24
+ s.name,
25
+ Stdlib_Option.getOr(Primitive_option.fromNullable(s.moduleUrl), s.name),
26
+ s.eventSchema
27
+ ]));
28
+ } catch (exn) {
29
+ partitionTag = undefined;
30
+ }
31
+ return {
32
+ crossPartitionTagKeys: scope.crossPartitionTagKeys,
33
+ tagKeysByEventType: scope.tagKeysByEventType,
34
+ partitionTag: partitionTag
35
+ };
36
+ }
37
+
38
+ function commandTypeNames(spec) {
39
+ return DcbTag$Reventless.extractVariantNames(spec.commandSchema);
40
+ }
41
+
42
+ function makeDynamoStorageOps(tableName, scope) {
43
+ let table = {
44
+ id: "",
45
+ name: tableName,
46
+ arn: "",
47
+ hashKey: "id"
48
+ };
49
+ return {
50
+ read: DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.read(table, scope.crossPartitionTagKeys),
51
+ append: DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.append(table, scope.partitionTag, scope.crossPartitionTagKeys),
52
+ readStream: DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.readStream(table, scope.crossPartitionTagKeys)
53
+ };
54
+ }
55
+
56
+ function makePostgresStorageOps(pgConnection, logName) {
57
+ let match = pgConnection.lockStrategy;
58
+ let lockStrategy = (match == null) || match !== "RowLocks" ? "AdvisoryLocks" : "RowLocks";
59
+ return DcbEventLogStorage_Postgres_Runtime$ReventlessAws.opsFor(pgConnection, logName, lockStrategy);
60
+ }
61
+
62
+ function makeStorageOps(tableName, pgConnection, scope) {
63
+ if (pgConnection !== undefined) {
64
+ return makePostgresStorageOps(Primitive_option.valFromOption(pgConnection), tableName);
65
+ } else {
66
+ return makeDynamoStorageOps(tableName, scope);
67
+ }
68
+ }
69
+
70
+ function buildSliceHandler(spec, behavior, tagKeysByEventType, crossPartitionTagKeys, dcbEventLog) {
71
+ let callback = StateChangeSlice_CallbackResMjs.Make(spec)(behavior);
72
+ let commandSchema = spec.commandSchema;
73
+ return jsonStream => {
74
+ let decodedStream = Stream.flatMap(Stream.mapEffect(jsonStream, topicItem => Effect.sync(() => {
75
+ let decoded;
76
+ try {
77
+ decoded = Message$ReventlessCore.decodeCommand$p(topicItem.command, Id$Reventless.$$String.schema, commandSchema);
78
+ } catch (exn) {
79
+ return;
80
+ }
81
+ return {
82
+ command: decoded,
83
+ reference: topicItem.reference
84
+ };
85
+ })), opt => {
86
+ if (opt !== undefined) {
87
+ return Stream.fromIterable([opt]);
88
+ } else {
89
+ return Stream.empty;
90
+ }
91
+ });
92
+ return callback.handleCommands(tagKeysByEventType, crossPartitionTagKeys, dcbEventLog, decodedStream);
93
+ };
94
+ }
95
+
96
+ export {
97
+ deriveScope,
98
+ commandTypeNames,
99
+ makeDynamoStorageOps,
100
+ makePostgresStorageOps,
101
+ makeStorageOps,
102
+ buildSliceHandler,
103
+ }
104
+ /* Id-Reventless Not a pure module */
@@ -0,0 +1,33 @@
1
+ // Typed cold-start core shared by the QueryDb-backed entry-point shells
2
+ // (ReadModelEntryPoint.mjs, StateViewSliceEntryPoint.mjs).
3
+ //
4
+ // The "typed core, thin shell" split (docs/plans/minimize-lambda-entrypoint-mjs-shell.md).
5
+ // The DynamoDB QueryDb operations were built in each shell with seven positional
6
+ // `load(table)` / `save(table)` / … calls into the compiled runtime, assembled
7
+ // into an object the shell then passes to the ReadModel/StateViewSlice callback
8
+ // functor. Typing the return as `QueryDb_Adapter.operations` makes that assembly
9
+ // compiler-checked: a field rename/reorder or a `QueryDbStorage_DynamoDb_Runtime`
10
+ // signature change is a build error, not a silent runtime break.
11
+ //
12
+ // The Postgres branch (`pgQdbOpsFor` + env-gated `withLiveUpdates`) and the
13
+ // id-injection wrappers (`mkInjectIdSave`) stay in the shell: `pgQdbOpsFor` is
14
+ // already a single typed call, and the wrappers/live-update publishing are
15
+ // env-driven shell business logic, not framework-call drift.
16
+
17
+ let makeDynamoQueryDbOps = (~tableName: string): ReventlessCore.QueryDb_Adapter.operations => {
18
+ let table: Util_DynamoDb_Runtime.resolvedTable = {
19
+ id: "",
20
+ name: tableName,
21
+ arn: "",
22
+ hashKey: "id",
23
+ }
24
+ {
25
+ load: QueryDbStorage_DynamoDb_Runtime.load(table),
26
+ loadStream: QueryDbStorage_DynamoDb_Runtime.loadStream(table),
27
+ save: QueryDbStorage_DynamoDb_Runtime.save(table),
28
+ saveBatch: QueryDbStorage_DynamoDb_Runtime.saveBatch(table),
29
+ count: QueryDbStorage_DynamoDb_Runtime.count(table),
30
+ delete: QueryDbStorage_DynamoDb_Runtime.delete(table),
31
+ deleteBatch: QueryDbStorage_DynamoDb_Runtime.deleteBatch(table),
32
+ }
33
+ }
@@ -0,0 +1,26 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as QueryDbStorage_DynamoDb_Runtime$ReventlessAws from "../QueryDb/QueryDbStorage_DynamoDb_Runtime.res.mjs";
4
+
5
+ function makeDynamoQueryDbOps(tableName) {
6
+ let table = {
7
+ id: "",
8
+ name: tableName,
9
+ arn: "",
10
+ hashKey: "id"
11
+ };
12
+ return {
13
+ load: QueryDbStorage_DynamoDb_Runtime$ReventlessAws.load(table),
14
+ loadStream: QueryDbStorage_DynamoDb_Runtime$ReventlessAws.loadStream(table),
15
+ save: QueryDbStorage_DynamoDb_Runtime$ReventlessAws.save(table),
16
+ saveBatch: QueryDbStorage_DynamoDb_Runtime$ReventlessAws.saveBatch(table),
17
+ count: QueryDbStorage_DynamoDb_Runtime$ReventlessAws.count(table),
18
+ delete: QueryDbStorage_DynamoDb_Runtime$ReventlessAws.$$delete(table),
19
+ deleteBatch: QueryDbStorage_DynamoDb_Runtime$ReventlessAws.deleteBatch(table)
20
+ };
21
+ }
22
+
23
+ export {
24
+ makeDynamoQueryDbOps,
25
+ }
26
+ /* QueryDbStorage_DynamoDb_Runtime-ReventlessAws Not a pure module */
@@ -3,10 +3,13 @@
3
3
  // wires ReadModel_Callback.Make, builds handler map keyed by source URN.
4
4
 
5
5
  import * as Effect from "effect/Effect";
6
- import { patchSpecId, makeTableRef, log, pluginName } from "./HandlerFactoryHelpers.mjs";
6
+ import { patchSpecId, log, pluginName } from "./HandlerFactoryHelpers.mjs";
7
7
  import { tag as requestContextTag } from "@reventlessdev/reventless-core/src/RequestContext.res.mjs";
8
8
  import { Make as readModelCallbackMake } from "@reventlessdev/reventless-core/src/components/ReadModel/ReadModel_Callback.res.mjs";
9
- import { load as qdbLoad, loadStream as qdbLoadStream, save as qdbSave, saveBatch as qdbSaveBatch, count as qdbCount, $$delete as qdbDelete, deleteBatch as qdbDeleteBatch } from "@reventlessdev/reventless-aws/src/adapter/QueryDb/QueryDbStorage_DynamoDb_Runtime.res.mjs";
9
+ // Typed cold-start core DynamoDB QueryDb ops, compiler-checked against the
10
+ // framework signatures (see the module header and
11
+ // docs/plans/minimize-lambda-entrypoint-mjs-shell.md).
12
+ import { makeDynamoQueryDbOps } from "./QueryDbEntryPoint_Ops.res.mjs";
10
13
  import { opsFor as pgQdbOpsFor } from "@reventlessdev/reventless-aws/src/adapter/QueryDb/QueryDbStorage_Postgres_Runtime.res.mjs";
11
14
  import { withLiveUpdates } from "./StateTopicPublish.mjs";
12
15
  import { handleStreamEvent } from "@reventlessdev/reventless-aws/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream_Runtime.res.mjs";
@@ -99,18 +102,13 @@ function buildReadModelHandler(specModule, mappingsModule, queryDbTableName, pgC
99
102
  saveBatch: mkInjectIdSaveBatch(livePgOps.saveBatch),
100
103
  };
101
104
  } else {
102
- const table = makeTableRef(queryDbTableName);
103
- const rawSave = qdbSave(table);
104
- const rawSaveBatch = qdbSaveBatch(table);
105
-
105
+ // Typed core builds the 7 DynamoDB QueryDb ops; the shell keeps the id-
106
+ // injection wrap on save/saveBatch (business logic, not framework-call drift).
107
+ const base = makeDynamoQueryDbOps(queryDbTableName);
106
108
  operations = {
107
- load: qdbLoad(table),
108
- loadStream: qdbLoadStream(table),
109
- save: mkInjectIdSave(rawSave),
110
- saveBatch: mkInjectIdSaveBatch(rawSaveBatch),
111
- count: qdbCount(table),
112
- delete: qdbDelete(table),
113
- deleteBatch: qdbDeleteBatch(table),
109
+ ...base,
110
+ save: mkInjectIdSave(base.save),
111
+ saveBatch: mkInjectIdSaveBatch(base.saveBatch),
114
112
  };
115
113
  }
116
114
 
@@ -6,10 +6,13 @@
6
6
  import * as Effect from "effect/Effect";
7
7
  import * as Stream from "effect/Stream";
8
8
  import { parseJsonOrThrow } from "sury/src/S.res.mjs";
9
- import { makeTableRef, log, pluginName } from "./HandlerFactoryHelpers.mjs";
9
+ import { log, pluginName } from "./HandlerFactoryHelpers.mjs";
10
10
  import { tag as requestContextTag } from "@reventlessdev/reventless-core/src/RequestContext.res.mjs";
11
11
  import { handleAction } from "@reventlessdev/reventless-core/src/Projection.res.mjs";
12
- import { load, loadStream, save, saveBatch, count, $$delete, deleteBatch } from "@reventlessdev/reventless-aws/src/adapter/QueryDb/QueryDbStorage_DynamoDb_Runtime.res.mjs";
12
+ // Typed cold-start core DynamoDB QueryDb ops, compiler-checked against the
13
+ // framework signatures (see the module header and
14
+ // docs/plans/minimize-lambda-entrypoint-mjs-shell.md).
15
+ import { makeDynamoQueryDbOps } from "./QueryDbEntryPoint_Ops.res.mjs";
13
16
  import { opsFor as pgQdbOpsFor } from "@reventlessdev/reventless-aws/src/adapter/QueryDb/QueryDbStorage_Postgres_Runtime.res.mjs";
14
17
  import { withLiveUpdates } from "./StateTopicPublish.mjs";
15
18
  import { handleStreamEvent } from "@reventlessdev/reventless-aws/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream_Runtime.res.mjs";
@@ -62,16 +65,7 @@ export function buildJsonEventsHandler(specModule, projectionModule, queryDbTabl
62
65
  subIdField,
63
66
  });
64
67
  } else {
65
- const table = makeTableRef(queryDbTableName);
66
- queryDbOps = {
67
- load: load(table),
68
- loadStream: loadStream(table),
69
- save: save(table),
70
- saveBatch: saveBatch(table),
71
- count: count(table),
72
- delete: $$delete(table),
73
- deleteBatch: deleteBatch(table),
74
- };
68
+ queryDbOps = makeDynamoQueryDbOps(queryDbTableName);
75
69
  }
76
70
  // Spec module exports `consumedEventSchema`; the `project` function lives in
77
71
  // the sibling projection module (`<Name>_Projection.res`).
@@ -0,0 +1,53 @@
1
+ // Test-only harness for the Aggregate EventLog integration suite.
2
+ //
3
+ // Boots against a DynamoDB Local instance (see
4
+ // `scripts/run-integration-tests.sh` + `docker-compose.dynamodb-local.yml`); the
5
+ // production adapter's singleton DynamoDB client resolves its endpoint from
6
+ // `AWS_ENDPOINT_URL_DYNAMODB` (set by `jest.integration.setup.cjs`).
7
+ //
8
+ // The aggregate EventLog table is a plain `id` (HASH) + `position` (RANGE) table
9
+ // — no tag GSIs (that is the DCB store). Mirrors the deploy-time schema in
10
+ // `EventLogStorage_DynamoDb.res` (`~attributes=[id, position], ~rangeKey=position`).
11
+
12
+ type command
13
+
14
+ @new @module("@aws-sdk/client-dynamodb")
15
+ external createTableCommand: JSON.t => command = "CreateTableCommand"
16
+
17
+ @new @module("@aws-sdk/client-dynamodb")
18
+ external deleteTableCommand: JSON.t => command = "DeleteTableCommand"
19
+
20
+ @send
21
+ external sendRaw: (AwsSdk.DynamoDb.DynamoDb.client, command) => promise<JSON.t> = "send"
22
+
23
+ let send = cmd => sendRaw(AwsSdk.DynamoDb.DynamoDb.client(), cmd)
24
+
25
+ let s = JSON.Encode.string
26
+
27
+ let attrDef = name =>
28
+ Dict.fromArray([("AttributeName", s(name)), ("AttributeType", s("S"))])->JSON.Encode.object
29
+
30
+ let keyEl = (name, keyType) =>
31
+ Dict.fromArray([("AttributeName", s(name)), ("KeyType", s(keyType))])->JSON.Encode.object
32
+
33
+ let createEventLogTable = async (tableName): Util_DynamoDb_Runtime.resolvedTable => {
34
+ let input =
35
+ Dict.fromArray([
36
+ ("TableName", s(tableName)),
37
+ ("AttributeDefinitions", [attrDef("id"), attrDef("position")]->JSON.Encode.array),
38
+ ("KeySchema", [keyEl("id", "HASH"), keyEl("position", "RANGE")]->JSON.Encode.array),
39
+ ("BillingMode", s("PAY_PER_REQUEST")),
40
+ ])->JSON.Encode.object
41
+ let _ = await send(createTableCommand(input))
42
+ {
43
+ Util_DynamoDb_Runtime.id: tableName,
44
+ name: tableName,
45
+ arn: `arn:aws:dynamodb:local:000000000000:table/${tableName}`,
46
+ hashKey: "id",
47
+ }
48
+ }
49
+
50
+ let deleteTable = async (table: Util_DynamoDb_Runtime.resolvedTable) => {
51
+ let input = Dict.fromArray([("TableName", s(table.name))])->JSON.Encode.object
52
+ let _ = await send(deleteTableCommand(input))
53
+ }
@@ -0,0 +1,92 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as ClientDynamodb from "@aws-sdk/client-dynamodb";
4
+ import * as DynamoDb_DynamoDb$AwsSdk from "@reventlessdev/rescript-aws-sdk/src/DynamoDb_DynamoDb.res.mjs";
5
+
6
+ function send(cmd) {
7
+ return DynamoDb_DynamoDb$AwsSdk.client().send(cmd);
8
+ }
9
+
10
+ function s(prim) {
11
+ return prim;
12
+ }
13
+
14
+ function attrDef(name) {
15
+ return Object.fromEntries([
16
+ [
17
+ "AttributeName",
18
+ name
19
+ ],
20
+ [
21
+ "AttributeType",
22
+ "S"
23
+ ]
24
+ ]);
25
+ }
26
+
27
+ function keyEl(name, keyType) {
28
+ return Object.fromEntries([
29
+ [
30
+ "AttributeName",
31
+ name
32
+ ],
33
+ [
34
+ "KeyType",
35
+ keyType
36
+ ]
37
+ ]);
38
+ }
39
+
40
+ async function createEventLogTable(tableName) {
41
+ let input = Object.fromEntries([
42
+ [
43
+ "TableName",
44
+ tableName
45
+ ],
46
+ [
47
+ "AttributeDefinitions",
48
+ [
49
+ attrDef("id"),
50
+ attrDef("position")
51
+ ]
52
+ ],
53
+ [
54
+ "KeySchema",
55
+ [
56
+ keyEl("id", "HASH"),
57
+ keyEl("position", "RANGE")
58
+ ]
59
+ ],
60
+ [
61
+ "BillingMode",
62
+ "PAY_PER_REQUEST"
63
+ ]
64
+ ]);
65
+ let cmd = new ClientDynamodb.CreateTableCommand(input);
66
+ await DynamoDb_DynamoDb$AwsSdk.client().send(cmd);
67
+ return {
68
+ id: tableName,
69
+ name: tableName,
70
+ arn: `arn:aws:dynamodb:local:000000000000:table/` + tableName,
71
+ hashKey: "id"
72
+ };
73
+ }
74
+
75
+ async function deleteTable(table) {
76
+ let input = Object.fromEntries([[
77
+ "TableName",
78
+ table.name
79
+ ]]);
80
+ let cmd = new ClientDynamodb.DeleteTableCommand(input);
81
+ await DynamoDb_DynamoDb$AwsSdk.client().send(cmd);
82
+ }
83
+
84
+ export {
85
+ send,
86
+ s,
87
+ attrDef,
88
+ keyEl,
89
+ createEventLogTable,
90
+ deleteTable,
91
+ }
92
+ /* @aws-sdk/client-dynamodb Not a pure module */
@@ -0,0 +1,21 @@
1
+ // Tiny aggregate spec exercised by `AggregateEntryPoint_IntegrationTest`.
2
+ // Standalone schema — does not depend on the example apps — so the test is
3
+ // self-contained and the fixture's shape can drift independently of any shipped
4
+ // aggregate. Hand-written (no `@@reventless.spec` PPX: reventless-ppx is only
5
+ // wired into reventless-core's rescript.json, not reventless-aws's), exposing
6
+ // exactly the fields the compiled `@@reventless.spec` output would: name, Id
7
+ // (patched in at runtime by patchSpecId), commandSchema/eventSchema/errorSchema
8
+ // (via sury-ppx), moduleUrl, commandAuthorization.
9
+
10
+ @schema
11
+ type command = Add({name: string})
12
+
13
+ @schema
14
+ type event = Added({name: string})
15
+
16
+ @schema
17
+ type error = AlreadyExists
18
+
19
+ let name = "AggTestAggregate"
20
+ let moduleUrl = "agg-test://AggTestAggregate"
21
+ let commandAuthorization = (_: command): Reventless.Authorization.permission => AllowAnonymous
@@ -0,0 +1,33 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+
5
+ let commandSchema = S.schema(s => ({
6
+ TAG: "Add",
7
+ name: s.m(S.string)
8
+ }));
9
+
10
+ let eventSchema = S.schema(s => ({
11
+ TAG: "Added",
12
+ name: s.m(S.string)
13
+ }));
14
+
15
+ let errorSchema = S.literal("AlreadyExists");
16
+
17
+ function commandAuthorization(param) {
18
+ return "AllowAnonymous";
19
+ }
20
+
21
+ let name = "AggTestAggregate";
22
+
23
+ let moduleUrl = "agg-test://AggTestAggregate";
24
+
25
+ export {
26
+ commandSchema,
27
+ eventSchema,
28
+ errorSchema,
29
+ name,
30
+ moduleUrl,
31
+ commandAuthorization,
32
+ }
33
+ /* commandSchema Not a pure module */