@reventlessdev/reventless-aws 3.0.0-alpha.174 → 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 +21 -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 +78 -7
- package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs +30 -2
- 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 +259 -0
- package/tests/PgChangeFeedRelay_IntegrationTest.res.mjs +238 -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
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Aws from "@pulumi/aws";
|
|
4
|
+
import * as SQS_Queue$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/SQS/SQS_Queue.res.mjs";
|
|
5
|
+
import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
|
|
6
|
+
import * as Util_DeadLetterQueue$ReventlessAws from "../../util/Util_DeadLetterQueue.res.mjs";
|
|
7
|
+
import * as Util_EventSourceMapping$ReventlessAws from "../../util/Util_EventSourceMapping.res.mjs";
|
|
8
|
+
|
|
9
|
+
let feedQueues = [];
|
|
10
|
+
|
|
11
|
+
function getFeedQueues() {
|
|
12
|
+
return feedQueues;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function makeQueue(name, scope, includeClassic, includeDcb, opts) {
|
|
16
|
+
let queue = new (Aws.sqs.Queue)(name, {
|
|
17
|
+
redrivePolicy: Util_DeadLetterQueue$ReventlessAws.queue.arn.apply(dlqArn => SQS_Queue$PulumiAws.RedrivePolicy.make(dlqArn, 5)),
|
|
18
|
+
visibilityTimeoutSeconds: 120,
|
|
19
|
+
sqsManagedSseEnabled: false
|
|
20
|
+
}, opts);
|
|
21
|
+
feedQueues.push({
|
|
22
|
+
scope: scope,
|
|
23
|
+
includeClassic: includeClassic,
|
|
24
|
+
includeDcb: includeDcb,
|
|
25
|
+
url: queue.id,
|
|
26
|
+
arn: queue.arn
|
|
27
|
+
});
|
|
28
|
+
return queue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function connect(name, queue, lambda, lambdaRole, opts) {
|
|
32
|
+
Util_EventSourceMapping$ReventlessAws.subscribeSqs(lambda, name, queue, undefined, opts);
|
|
33
|
+
queue.arn.apply(queueArn => new (Aws.iam.RolePolicy)(name + `Receive`, {
|
|
34
|
+
policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + `ReceivePolicy`, [{
|
|
35
|
+
Sid: "AllowReceiveFeedQueue",
|
|
36
|
+
Effect: "Allow",
|
|
37
|
+
Action: [
|
|
38
|
+
"sqs:ReceiveMessage",
|
|
39
|
+
"sqs:DeleteMessage",
|
|
40
|
+
"sqs:GetQueueAttributes"
|
|
41
|
+
],
|
|
42
|
+
Resource: queueArn
|
|
43
|
+
}])),
|
|
44
|
+
role: lambdaRole.id
|
|
45
|
+
}, opts));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export {
|
|
49
|
+
feedQueues,
|
|
50
|
+
getFeedQueues,
|
|
51
|
+
makeQueue,
|
|
52
|
+
connect,
|
|
53
|
+
}
|
|
54
|
+
/* @pulumi/aws Not a pure module */
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// Runtime handler for the shared Postgres GraphQL-read Lambda (B3.2).
|
|
2
|
+
//
|
|
3
|
+
// Postgres-backed read models have no per-table AppSync data source, so their
|
|
4
|
+
// GraphQL Query fields route through one in-VPC "PgQueryResolver" Lambda
|
|
5
|
+
// registered as an AppSync Lambda data source (B3.2b). Each resolver field's
|
|
6
|
+
// APPSYNC_JS template invokes this Lambda with
|
|
7
|
+
// { readModelName, kind, index?, arguments, identity }
|
|
8
|
+
// and this handler dispatches to the Postgres query push-downs
|
|
9
|
+
// (QueryEnginePostgres) + the shared connection spec (QueryDbListQuery).
|
|
10
|
+
//
|
|
11
|
+
// This module is the provider-agnostic *dispatch* — the headless twin of
|
|
12
|
+
// reventless-local's `QueryDbResolvers_GraphQL` (which registers graphql-yoga
|
|
13
|
+
// resolvers). It returns JSON straight to AppSync instead. The per-read-model
|
|
14
|
+
// context (ops set, push-downs, indexes, capability, …) is bound at Lambda init
|
|
15
|
+
// by the entry point (PgQueryResolverEntryPoint.mjs, B3.2b) and registered here;
|
|
16
|
+
// `dispatch` itself is pure over a `binding`, so it is unit-tested with an
|
|
17
|
+
// in-memory mock (no Postgres) — the SQL correctness lives in
|
|
18
|
+
// QueryEnginePostgres and is covered by its PG_URL-gated parity test.
|
|
19
|
+
|
|
20
|
+
let log = ReventlessCore.Logger.fromEnv()
|
|
21
|
+
|
|
22
|
+
// Wire payload from the resolver template. `arguments` mirrors the field name the
|
|
23
|
+
// other Invoke templates use (QueryInterceptor_Lambda, invokeCommandGenerator).
|
|
24
|
+
type payload = {
|
|
25
|
+
readModelName: string,
|
|
26
|
+
kind: string,
|
|
27
|
+
index?: string,
|
|
28
|
+
arguments: JSON.t,
|
|
29
|
+
identity: Reventless.Identity.t,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The Postgres query push-downs a binding needs (QueryEnginePostgres.Make
|
|
33
|
+
// provides these; the mock in tests provides in-memory equivalents).
|
|
34
|
+
type pushdowns = {
|
|
35
|
+
indexLookup: (~readModelName: string, string, string) => promise<array<JSON.t>>,
|
|
36
|
+
byIds: (~readModelName: string, array<string>) => promise<array<JSON.t>>,
|
|
37
|
+
listPage: (
|
|
38
|
+
~readModelName: string,
|
|
39
|
+
~argsDict: dict<JSON.t>,
|
|
40
|
+
~capability: ReventlessCore.GraphQL_FragmentGenerator.serverCapability,
|
|
41
|
+
~labelField: string,
|
|
42
|
+
) => promise<option<JSON.t>>,
|
|
43
|
+
// Full materialisation for the list fallback (shapes listPage declines).
|
|
44
|
+
scanAll: (~readModelName: string) => promise<array<JSON.t>>,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Per-read-model dispatch context, bound once at Lambda init.
|
|
48
|
+
type binding = {
|
|
49
|
+
ops: ReventlessCore.QueryDb_Adapter.operations,
|
|
50
|
+
pushdowns: pushdowns,
|
|
51
|
+
indexes: array<Reventless.ReadModel.indexConfig>,
|
|
52
|
+
subIdField: option<string>,
|
|
53
|
+
capability: ReventlessCore.GraphQL_FragmentGenerator.serverCapability,
|
|
54
|
+
labelField: string,
|
|
55
|
+
includeIdParam: bool,
|
|
56
|
+
authorization: Reventless.Authorization.permission,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// -- arg helpers -------------------------------------------------------------
|
|
60
|
+
let argObj = (args: JSON.t): dict<JSON.t> =>
|
|
61
|
+
args->JSON.Decode.object->Option.getOr(Dict.make())
|
|
62
|
+
let argStr = (args: JSON.t, key: string): option<string> =>
|
|
63
|
+
args->argObj->Dict.get(key)->Option.flatMap(JSON.Decode.string)
|
|
64
|
+
let argStrs = (args: JSON.t, key: string): array<string> =>
|
|
65
|
+
args
|
|
66
|
+
->argObj
|
|
67
|
+
->Dict.get(key)
|
|
68
|
+
->Option.flatMap(JSON.Decode.array)
|
|
69
|
+
->Option.getOr([])
|
|
70
|
+
->Array.filterMap(JSON.Decode.string)
|
|
71
|
+
|
|
72
|
+
// Add `id` (= partition key) to an item when absent — matches the shape the
|
|
73
|
+
// DynamoDB resolvers and the shared spec return.
|
|
74
|
+
let withId = (item: JSON.t, id: string): JSON.t =>
|
|
75
|
+
switch item->JSON.Decode.object {
|
|
76
|
+
| Some(obj) if obj->Dict.get("id")->Option.isNone =>
|
|
77
|
+
let copy = Dict.copy(obj)
|
|
78
|
+
copy->Dict.set("id", JSON.Encode.string(id))
|
|
79
|
+
JSON.Encode.object(copy)
|
|
80
|
+
| _ => item
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let emptyConnection = (): JSON.t =>
|
|
84
|
+
ReventlessCore.QueryDbListQuery.buildConnection(
|
|
85
|
+
~pageItems=[],
|
|
86
|
+
~hasNextPage=false,
|
|
87
|
+
~hasPreviousPage=false,
|
|
88
|
+
~cursorValueOf=_ => "",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
// Spec-level authorization then the (optional) user interceptor — mirrors
|
|
92
|
+
// QueryDbResolvers_GraphQL.runInterceptor. Denials return the kind's empty shape
|
|
93
|
+
// (the provider-agnostic resolver behaviour), not an error.
|
|
94
|
+
let runInterceptor = async (~binding, ~payload): ReventlessCore.QueryDb_Callback.interceptResult => {
|
|
95
|
+
if !Reventless.Authorization.isAllowed(binding.authorization, payload.identity) {
|
|
96
|
+
Deny("Forbidden")
|
|
97
|
+
} else {
|
|
98
|
+
switch ReventlessCore.QueryDb_Callback.queryInterceptorHook.contents {
|
|
99
|
+
| None => Allow
|
|
100
|
+
| Some(interceptor) =>
|
|
101
|
+
await interceptor(
|
|
102
|
+
~identity=payload.identity,
|
|
103
|
+
~readModelName=payload.readModelName,
|
|
104
|
+
~args=payload.arguments,
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let dispatch = async (~binding: binding, ~payload: payload): JSON.t => {
|
|
111
|
+
let rm = payload.readModelName
|
|
112
|
+
switch await runInterceptor(~binding, ~payload) {
|
|
113
|
+
| Deny(_) =>
|
|
114
|
+
// Empty shape per kind (null for single, [] for lists, empty connection).
|
|
115
|
+
switch payload.kind {
|
|
116
|
+
| "getById" => JSON.Encode.null
|
|
117
|
+
| "list" => emptyConnection()
|
|
118
|
+
| _ => JSON.Encode.array([])
|
|
119
|
+
}
|
|
120
|
+
| Allow =>
|
|
121
|
+
switch payload.kind {
|
|
122
|
+
| "getById" =>
|
|
123
|
+
let id = payload.arguments->argStr("id")->Option.getOr("")
|
|
124
|
+
switch await binding.ops.load(id) {
|
|
125
|
+
| Ok(items) =>
|
|
126
|
+
switch items->Array.get(0) {
|
|
127
|
+
| Some(item) => binding.includeIdParam ? withId(item, id) : item
|
|
128
|
+
| None => JSON.Encode.null
|
|
129
|
+
}
|
|
130
|
+
| Error(_) => JSON.Encode.null
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
| "byIds" =>
|
|
134
|
+
let ids = payload.arguments->argStrs("ids")
|
|
135
|
+
JSON.Encode.array(await binding.pushdowns.byIds(~readModelName=rm, ids))
|
|
136
|
+
|
|
137
|
+
| "index" =>
|
|
138
|
+
// The index arg name is the index; its stored field is idField ?? index.
|
|
139
|
+
let indexName = payload.index->Option.getOr("")
|
|
140
|
+
let field =
|
|
141
|
+
binding.indexes
|
|
142
|
+
->Array.find(ic => ic.index === indexName)
|
|
143
|
+
->Option.flatMap(ic => ic.idField)
|
|
144
|
+
->Option.getOr(indexName)
|
|
145
|
+
let value = payload.arguments->argStr(indexName)->Option.getOr("")
|
|
146
|
+
JSON.Encode.array(await binding.pushdowns.indexLookup(~readModelName=rm, field, value))
|
|
147
|
+
|
|
148
|
+
| "list" =>
|
|
149
|
+
let argsDict = payload.arguments->argObj
|
|
150
|
+
switch await binding.pushdowns.listPage(
|
|
151
|
+
~readModelName=rm,
|
|
152
|
+
~argsDict,
|
|
153
|
+
~capability=binding.capability,
|
|
154
|
+
~labelField=binding.labelField,
|
|
155
|
+
) {
|
|
156
|
+
| Some(conn) => conn
|
|
157
|
+
| None =>
|
|
158
|
+
// Shapes listPage declines (search/searchPrefix/ids/backward) → run the
|
|
159
|
+
// shared spec over the materialised model. No Relay global-id decoding
|
|
160
|
+
// in the Lambda, so `decodeLocalId` is a no-op (the `ids` filter still
|
|
161
|
+
// matches raw item ids).
|
|
162
|
+
let items = await binding.pushdowns.scanAll(~readModelName=rm)
|
|
163
|
+
ReventlessCore.QueryDbListQuery.run(
|
|
164
|
+
~items,
|
|
165
|
+
~argsDict,
|
|
166
|
+
~capability=binding.capability,
|
|
167
|
+
~labelField=binding.labelField,
|
|
168
|
+
~decodeLocalId=_ => None,
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
| other =>
|
|
173
|
+
// Fail loudly on unmapped kinds (feature-parity guard, per the B3.2 plan).
|
|
174
|
+
log.error(~comp="PgQueryResolver_Lambda", `unmapped resolver kind '${other}' for ${rm}`)
|
|
175
|
+
JsError.throwWithMessage(`PgQueryResolver: unmapped resolver kind '${other}' for ${rm}`)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Per-read-model binding registry, populated by the entry point at Lambda init.
|
|
181
|
+
let bindings: dict<binding> = Dict.make()
|
|
182
|
+
let register = (~readModelName: string, binding: binding): unit =>
|
|
183
|
+
bindings->Dict.set(readModelName, binding)
|
|
184
|
+
|
|
185
|
+
let handler = async (payload: payload, _context) =>
|
|
186
|
+
switch bindings->Dict.get(payload.readModelName) {
|
|
187
|
+
| Some(binding) => await dispatch(~binding, ~payload)
|
|
188
|
+
| None =>
|
|
189
|
+
JsError.throwWithMessage(
|
|
190
|
+
"PgQueryResolver: no binding registered for read model " ++ payload.readModelName,
|
|
191
|
+
)
|
|
192
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
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_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
5
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
7
|
+
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
8
|
+
import * as Authorization$Reventless from "@reventlessdev/reventless-spec/src/types/Authorization.res.mjs";
|
|
9
|
+
import * as QueryDbListQuery$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/QueryDbListQuery.res.mjs";
|
|
10
|
+
import * as QueryDb_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/QueryDb/QueryDb_Callback.res.mjs";
|
|
11
|
+
|
|
12
|
+
let log = Logger$ReventlessCore.fromEnv();
|
|
13
|
+
|
|
14
|
+
function argObj(args) {
|
|
15
|
+
return Stdlib_Option.getOr(Stdlib_JSON.Decode.object(args), {});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function argStr(args, key) {
|
|
19
|
+
return Stdlib_Option.flatMap(argObj(args)[key], Stdlib_JSON.Decode.string);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function argStrs(args, key) {
|
|
23
|
+
return Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_Option.flatMap(argObj(args)[key], Stdlib_JSON.Decode.array), []), Stdlib_JSON.Decode.string);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function withId(item, id) {
|
|
27
|
+
let obj = Stdlib_JSON.Decode.object(item);
|
|
28
|
+
if (obj === undefined) {
|
|
29
|
+
return item;
|
|
30
|
+
}
|
|
31
|
+
if (!Stdlib_Option.isNone(obj["id"])) {
|
|
32
|
+
return item;
|
|
33
|
+
}
|
|
34
|
+
let copy = Object.assign({}, obj);
|
|
35
|
+
copy["id"] = id;
|
|
36
|
+
return copy;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function emptyConnection() {
|
|
40
|
+
return QueryDbListQuery$ReventlessCore.buildConnection([], false, false, param => "");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function runInterceptor(binding, payload) {
|
|
44
|
+
if (!Authorization$Reventless.isAllowed(binding.authorization, payload.identity)) {
|
|
45
|
+
return {
|
|
46
|
+
TAG: "Deny",
|
|
47
|
+
_0: "Forbidden"
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
let interceptor = QueryDb_Callback$ReventlessCore.queryInterceptorHook.contents;
|
|
51
|
+
if (interceptor !== undefined) {
|
|
52
|
+
return await interceptor(payload.identity, payload.readModelName, payload.arguments);
|
|
53
|
+
} else {
|
|
54
|
+
return "Allow";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function dispatch(binding, payload) {
|
|
59
|
+
let rm = payload.readModelName;
|
|
60
|
+
let match = await runInterceptor(binding, payload);
|
|
61
|
+
if (typeof match !== "object") {
|
|
62
|
+
let other = payload.kind;
|
|
63
|
+
switch (other) {
|
|
64
|
+
case "byIds" :
|
|
65
|
+
let ids = argStrs(payload.arguments, "ids");
|
|
66
|
+
return await binding.pushdowns.byIds(rm, ids);
|
|
67
|
+
case "getById" :
|
|
68
|
+
let id = Stdlib_Option.getOr(argStr(payload.arguments, "id"), "");
|
|
69
|
+
let items = await binding.ops.load(id);
|
|
70
|
+
if (items.TAG !== "Ok") {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
let item = items._0[0];
|
|
74
|
+
if (item !== undefined) {
|
|
75
|
+
if (binding.includeIdParam) {
|
|
76
|
+
return withId(item, id);
|
|
77
|
+
} else {
|
|
78
|
+
return item;
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
case "index" :
|
|
84
|
+
let indexName = Stdlib_Option.getOr(payload.index, "");
|
|
85
|
+
let field = Stdlib_Option.getOr(Stdlib_Option.flatMap(binding.indexes.find(ic => ic.index === indexName), ic => ic.idField), indexName);
|
|
86
|
+
let value = Stdlib_Option.getOr(argStr(payload.arguments, indexName), "");
|
|
87
|
+
return await binding.pushdowns.indexLookup(rm, field, value);
|
|
88
|
+
case "list" :
|
|
89
|
+
let argsDict = argObj(payload.arguments);
|
|
90
|
+
let conn = await binding.pushdowns.listPage(rm, argsDict, binding.capability, binding.labelField);
|
|
91
|
+
if (conn !== undefined) {
|
|
92
|
+
return conn;
|
|
93
|
+
}
|
|
94
|
+
let items$1 = await binding.pushdowns.scanAll(rm);
|
|
95
|
+
return QueryDbListQuery$ReventlessCore.run(items$1, argsDict, binding.capability, binding.labelField, param => {});
|
|
96
|
+
default:
|
|
97
|
+
log.error("PgQueryResolver_Lambda", undefined, `unmapped resolver kind '` + other + `' for ` + rm);
|
|
98
|
+
return Stdlib_JsError.throwWithMessage(`PgQueryResolver: unmapped resolver kind '` + other + `' for ` + rm);
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
let match$1 = payload.kind;
|
|
102
|
+
switch (match$1) {
|
|
103
|
+
case "getById" :
|
|
104
|
+
return null;
|
|
105
|
+
case "list" :
|
|
106
|
+
return emptyConnection();
|
|
107
|
+
default:
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let bindings = {};
|
|
114
|
+
|
|
115
|
+
function register(readModelName, binding) {
|
|
116
|
+
bindings[readModelName] = binding;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function handler(payload, _context) {
|
|
120
|
+
let binding = bindings[payload.readModelName];
|
|
121
|
+
if (binding !== undefined) {
|
|
122
|
+
return await dispatch(binding, payload);
|
|
123
|
+
} else {
|
|
124
|
+
return Stdlib_JsError.throwWithMessage("PgQueryResolver: no binding registered for read model " + payload.readModelName);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export {
|
|
129
|
+
log,
|
|
130
|
+
argObj,
|
|
131
|
+
argStr,
|
|
132
|
+
argStrs,
|
|
133
|
+
withId,
|
|
134
|
+
emptyConnection,
|
|
135
|
+
runInterceptor,
|
|
136
|
+
dispatch,
|
|
137
|
+
bindings,
|
|
138
|
+
register,
|
|
139
|
+
handler,
|
|
140
|
+
}
|
|
141
|
+
/* log Not a pure module */
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// AWS-side selection for the QueryDb (read model) storage backend — the QueryDb
|
|
2
|
+
// analogue of `EventLogBackend` / `DcbBackend` (B3.1,
|
|
3
|
+
// docs/plans/aws-postgres-querydb-adapter.md).
|
|
4
|
+
//
|
|
5
|
+
// When `Platform.MakeWithConfig` is given a `~pgConnection`, it records the
|
|
6
|
+
// resolved selection here BEFORE any admin/plugin `construct` runs. Read by:
|
|
7
|
+
// - `QueryDbStorage.Selectable(Stream)` — route app read-model storage to
|
|
8
|
+
// Postgres (no DynamoDB table) and bind the Postgres operation set;
|
|
9
|
+
// - `QueryDbResolvers.Selectable` — suppress AppSync resolvers for
|
|
10
|
+
// Postgres-backed read models (GraphQL reads land with B3.2's Lambda data
|
|
11
|
+
// source; until then those fields are unresolved);
|
|
12
|
+
// - the ReadModel/StateViewSlice runtime builders — inject `pgConnection` into
|
|
13
|
+
// Postgres-backed handlers' HANDLER_CONFIG entries and put the projection
|
|
14
|
+
// Lambdas in-VPC with secret access.
|
|
15
|
+
//
|
|
16
|
+
// ADMIN EXEMPTION: platform/admin read models (Plugins, UIFragmentRegistry, …)
|
|
17
|
+
// stay on DynamoDB even when Postgres is selected. Deploy-time consumers (the
|
|
18
|
+
// AppSync schema-clobber guard's Plugin-RM scan, `PLUGIN_RM_TABLE_NAME` gates,
|
|
19
|
+
// retire hooks) query these tables during `pulumi up` — from outside the VPC —
|
|
20
|
+
// and the whole platform lifecycle depends on them. The platform registers the
|
|
21
|
+
// exempt names before Admin.construct.
|
|
22
|
+
|
|
23
|
+
type selection = {
|
|
24
|
+
connectionConfig: Pulumi.Output.t<PgConnection.connectionConfig>,
|
|
25
|
+
/** DB-access security group every projection Lambda attaches (PgConnection.securityGroupId). */
|
|
26
|
+
securityGroupId: Pulumi.Output.t<string>,
|
|
27
|
+
/** Private subnets for the in-VPC projection Lambdas (PgConnection.subnetIds). */
|
|
28
|
+
subnetIds: array<Pulumi.Input.t<string>>,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let selectionRef: ref<option<selection>> = ref(None)
|
|
32
|
+
|
|
33
|
+
/** Record the QueryDb Postgres selection. Called once by the platform before construct. */
|
|
34
|
+
let set = (sel: selection) => selectionRef := Some(sel)
|
|
35
|
+
|
|
36
|
+
/** The active QueryDb Postgres selection, if the platform was given a `PgConnection`. */
|
|
37
|
+
let get = (): option<selection> => selectionRef.contents
|
|
38
|
+
|
|
39
|
+
// Read-model names that stay on DynamoDB regardless of the selection.
|
|
40
|
+
let exemptNames: array<string> = []
|
|
41
|
+
|
|
42
|
+
/** Exempt an (admin) read model from the Postgres selection. Called by the
|
|
43
|
+
platform for every admin read model before Admin.construct. */
|
|
44
|
+
let exempt = (name: string) => exemptNames->Array.push(name)->ignore
|
|
45
|
+
|
|
46
|
+
/** Whether this read model's QueryDb is Postgres-backed on this platform. */
|
|
47
|
+
let isPostgresFor = (name: string): bool =>
|
|
48
|
+
selectionRef.contents->Option.isSome && !(exemptNames->Array.includes(name))
|
|
49
|
+
|
|
50
|
+
/** Whether ANY read model on this platform is Postgres-backed (used to decide
|
|
51
|
+
whether the projection Lambdas need VPC/secret access). */
|
|
52
|
+
let isPostgres = (): bool => selectionRef.contents->Option.isSome
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
|
+
|
|
5
|
+
let selectionRef = {
|
|
6
|
+
contents: undefined
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function set(sel) {
|
|
10
|
+
selectionRef.contents = sel;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function get() {
|
|
14
|
+
return selectionRef.contents;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let exemptNames = [];
|
|
18
|
+
|
|
19
|
+
function exempt(name) {
|
|
20
|
+
exemptNames.push(name);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isPostgresFor(name) {
|
|
24
|
+
if (Stdlib_Option.isSome(selectionRef.contents)) {
|
|
25
|
+
return !exemptNames.includes(name);
|
|
26
|
+
} else {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isPostgres() {
|
|
32
|
+
return Stdlib_Option.isSome(selectionRef.contents);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export {
|
|
36
|
+
selectionRef,
|
|
37
|
+
set,
|
|
38
|
+
get,
|
|
39
|
+
exemptNames,
|
|
40
|
+
exempt,
|
|
41
|
+
isPostgresFor,
|
|
42
|
+
isPostgres,
|
|
43
|
+
}
|
|
44
|
+
/* No side effect */
|
|
@@ -1,2 +1,50 @@
|
|
|
1
1
|
module AppSync = QueryDbResolvers_AppSync
|
|
2
2
|
module NoOp = QueryDbResolvers_NoOp
|
|
3
|
+
|
|
4
|
+
// B3.1: Postgres-backed read models have no DynamoDB data source — suppress the
|
|
5
|
+
// direct AppSync resolvers (their GraphQL fields stay unresolved until B3.2's
|
|
6
|
+
// Lambda data source). DynamoDB-backed (incl. admin-exempt) read models keep the
|
|
7
|
+
// full resolver set.
|
|
8
|
+
module Selectable = {
|
|
9
|
+
type api = QueryDbResolvers_AppSync_NoOp.api
|
|
10
|
+
type role = QueryDbResolvers_AppSync_NoOp.role
|
|
11
|
+
let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
12
|
+
~name,
|
|
13
|
+
~api,
|
|
14
|
+
~apiRole,
|
|
15
|
+
~dataSourceName,
|
|
16
|
+
~indexes,
|
|
17
|
+
~subIdField,
|
|
18
|
+
~idResolverConfigs,
|
|
19
|
+
~idsResolverConfigs,
|
|
20
|
+
~authorization,
|
|
21
|
+
~opts,
|
|
22
|
+
) =>
|
|
23
|
+
if QueryDbBackend.isPostgresFor(name) {
|
|
24
|
+
QueryDbResolvers_AppSync_NoOp.make(
|
|
25
|
+
~name,
|
|
26
|
+
~api,
|
|
27
|
+
~apiRole,
|
|
28
|
+
~dataSourceName,
|
|
29
|
+
~indexes,
|
|
30
|
+
~subIdField,
|
|
31
|
+
~idResolverConfigs,
|
|
32
|
+
~idsResolverConfigs,
|
|
33
|
+
~authorization,
|
|
34
|
+
~opts,
|
|
35
|
+
)
|
|
36
|
+
} else {
|
|
37
|
+
QueryDbResolvers_AppSync.make(
|
|
38
|
+
~name,
|
|
39
|
+
~api,
|
|
40
|
+
~apiRole,
|
|
41
|
+
~dataSourceName,
|
|
42
|
+
~indexes,
|
|
43
|
+
~subIdField,
|
|
44
|
+
~idResolverConfigs,
|
|
45
|
+
~idsResolverConfigs,
|
|
46
|
+
~authorization,
|
|
47
|
+
~opts,
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as QueryDbBackend$ReventlessAws from "./QueryDbBackend.res.mjs";
|
|
4
|
+
import * as QueryDbResolvers_AppSync$ReventlessAws from "./QueryDbResolvers_AppSync.res.mjs";
|
|
5
|
+
import * as QueryDbResolvers_AppSync_NoOp$ReventlessAws from "./QueryDbResolvers_AppSync_NoOp.res.mjs";
|
|
6
|
+
|
|
7
|
+
function make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolverConfigs, idsResolverConfigs, authorization, opts) {
|
|
8
|
+
if (QueryDbBackend$ReventlessAws.isPostgresFor(name)) {
|
|
9
|
+
return QueryDbResolvers_AppSync_NoOp$ReventlessAws.make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolverConfigs, idsResolverConfigs, authorization, opts);
|
|
10
|
+
} else {
|
|
11
|
+
return QueryDbResolvers_AppSync$ReventlessAws.make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolverConfigs, idsResolverConfigs, authorization, opts);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let Selectable = {
|
|
16
|
+
make: make
|
|
17
|
+
};
|
|
3
18
|
|
|
4
19
|
let AppSync;
|
|
5
20
|
|
|
@@ -8,5 +23,6 @@ let NoOp;
|
|
|
8
23
|
export {
|
|
9
24
|
AppSync,
|
|
10
25
|
NoOp,
|
|
26
|
+
Selectable,
|
|
11
27
|
}
|
|
12
|
-
/*
|
|
28
|
+
/* QueryDbResolvers_AppSync-ReventlessAws Not a pure module */
|
|
@@ -3,3 +3,47 @@ let name = "QueryDbStorage"
|
|
|
3
3
|
module DynamoDb = QueryDbStorage_DynamoDb
|
|
4
4
|
module DynamoDbStream = QueryDbStorage_DynamoDbStream
|
|
5
5
|
module DynamoDb_Runtime = QueryDbStorage_DynamoDb_Runtime
|
|
6
|
+
module Postgres = QueryDbStorage_Postgres
|
|
7
|
+
|
|
8
|
+
// Build-time QueryDb storage selectors (B3.1). Read `QueryDbBackend` — set by the
|
|
9
|
+
// platform when a `PgConnection` is supplied — and dispatch per read model:
|
|
10
|
+
// Postgres (no table, no data source) unless the name is admin-exempt. The Stream
|
|
11
|
+
// selector falls back to the streamed DynamoDB maker; on Postgres there is no
|
|
12
|
+
// stream — live updates for Postgres-backed read models are deferred (B3.3).
|
|
13
|
+
module Selectable = {
|
|
14
|
+
type api = QueryDbStorage_Postgres.api
|
|
15
|
+
type role = QueryDbStorage_Postgres.role
|
|
16
|
+
let make: ReventlessCore.QueryDb_Adapter.storageMaker<api, role> = (
|
|
17
|
+
~name,
|
|
18
|
+
~indexes,
|
|
19
|
+
~subIdField=?,
|
|
20
|
+
~ttl=?,
|
|
21
|
+
~api,
|
|
22
|
+
~apiRole,
|
|
23
|
+
~opts,
|
|
24
|
+
) =>
|
|
25
|
+
if QueryDbBackend.isPostgresFor(name) {
|
|
26
|
+
Postgres.make(~name, ~indexes, ~subIdField?, ~ttl?, ~api, ~apiRole, ~opts)
|
|
27
|
+
} else {
|
|
28
|
+
DynamoDb.make(~name, ~indexes, ~subIdField?, ~ttl?, ~api, ~apiRole, ~opts)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module SelectableStream = {
|
|
33
|
+
type api = QueryDbStorage_Postgres.api
|
|
34
|
+
type role = QueryDbStorage_Postgres.role
|
|
35
|
+
let make: ReventlessCore.QueryDb_Adapter.storageMaker<api, role> = (
|
|
36
|
+
~name,
|
|
37
|
+
~indexes,
|
|
38
|
+
~subIdField=?,
|
|
39
|
+
~ttl=?,
|
|
40
|
+
~api,
|
|
41
|
+
~apiRole,
|
|
42
|
+
~opts,
|
|
43
|
+
) =>
|
|
44
|
+
if QueryDbBackend.isPostgresFor(name) {
|
|
45
|
+
Postgres.make(~name, ~indexes, ~subIdField?, ~ttl?, ~api, ~apiRole, ~opts)
|
|
46
|
+
} else {
|
|
47
|
+
DynamoDbStream.make(~name, ~indexes, ~subIdField?, ~ttl?, ~api, ~apiRole, ~opts)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as QueryDbBackend$ReventlessAws from "./QueryDbBackend.res.mjs";
|
|
4
|
+
import * as QueryDbStorage_DynamoDb$ReventlessAws from "./QueryDbStorage_DynamoDb.res.mjs";
|
|
5
|
+
import * as QueryDbStorage_Postgres$ReventlessAws from "./QueryDbStorage_Postgres.res.mjs";
|
|
6
|
+
import * as QueryDbStorage_DynamoDbStream$ReventlessAws from "./QueryDbStorage_DynamoDbStream.res.mjs";
|
|
7
|
+
|
|
8
|
+
function make(name, indexes, subIdField, ttl, api, apiRole, opts) {
|
|
9
|
+
if (QueryDbBackend$ReventlessAws.isPostgresFor(name)) {
|
|
10
|
+
return QueryDbStorage_Postgres$ReventlessAws.make(name, indexes, subIdField, ttl, api, apiRole, opts);
|
|
11
|
+
} else {
|
|
12
|
+
return QueryDbStorage_DynamoDb$ReventlessAws.make(name, indexes, subIdField, ttl, api, apiRole, opts);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let Selectable = {
|
|
17
|
+
make: make
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function make$1(name, indexes, subIdField, ttl, api, apiRole, opts) {
|
|
21
|
+
if (QueryDbBackend$ReventlessAws.isPostgresFor(name)) {
|
|
22
|
+
return QueryDbStorage_Postgres$ReventlessAws.make(name, indexes, subIdField, ttl, api, apiRole, opts);
|
|
23
|
+
} else {
|
|
24
|
+
return QueryDbStorage_DynamoDbStream$ReventlessAws.make(name, indexes, subIdField, ttl, api, apiRole, opts);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let SelectableStream = {
|
|
29
|
+
make: make$1
|
|
30
|
+
};
|
|
3
31
|
|
|
4
32
|
let name = "QueryDbStorage";
|
|
5
33
|
|
|
@@ -9,10 +37,15 @@ let DynamoDbStream;
|
|
|
9
37
|
|
|
10
38
|
let DynamoDb_Runtime;
|
|
11
39
|
|
|
40
|
+
let Postgres;
|
|
41
|
+
|
|
12
42
|
export {
|
|
13
43
|
name,
|
|
14
44
|
DynamoDb,
|
|
15
45
|
DynamoDbStream,
|
|
16
46
|
DynamoDb_Runtime,
|
|
47
|
+
Postgres,
|
|
48
|
+
Selectable,
|
|
49
|
+
SelectableStream,
|
|
17
50
|
}
|
|
18
|
-
/*
|
|
51
|
+
/* QueryDbStorage_DynamoDb-ReventlessAws Not a pure module */
|