@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.
- package/CHANGELOG.md +16 -0
- package/package.json +7 -7
- package/src/adapter/Runtime/AggregateEntryPoint.mjs +32 -35
- package/src/adapter/Runtime/AggregateEntryPoint_Ops.res +48 -0
- package/src/adapter/Runtime/AggregateEntryPoint_Ops.res.mjs +30 -0
- package/src/adapter/Runtime/CommandGeneratorEntryPoint_Ops.res +33 -0
- package/src/adapter/Runtime/CommandGeneratorEntryPoint_Ops.res.mjs +12 -0
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +44 -130
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +221 -0
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +104 -0
- package/src/adapter/Runtime/QueryDbEntryPoint_Ops.res +33 -0
- package/src/adapter/Runtime/QueryDbEntryPoint_Ops.res.mjs +26 -0
- package/src/adapter/Runtime/ReadModelEntryPoint.mjs +11 -13
- package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +6 -12
- package/tests/integration/AggIntegrationHarness.res +53 -0
- package/tests/integration/AggIntegrationHarness.res.mjs +92 -0
- package/tests/integration/AggTestAggregate.res +21 -0
- package/tests/integration/AggTestAggregate.res.mjs +33 -0
- package/tests/integration/AggTestAggregateBehavior.res +18 -0
- package/tests/integration/AggTestAggregateBehavior.res.mjs +35 -0
- package/tests/integration/AggregateEntryPoint_IntegrationTest.res +103 -0
- package/tests/integration/AggregateEntryPoint_IntegrationTest.res.mjs +97 -0
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 */
|