@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.
Files changed (71) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/package.json +10 -10
  3. package/src/Platform.res +115 -31
  4. package/src/Platform.res.mjs +217 -44
  5. package/src/adapter/DcbEventLog/DcbBackend.res +2 -2
  6. package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream.res +4 -1
  7. package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream.res.mjs +1 -1
  8. package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream_Runtime.res +14 -4
  9. package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream_Runtime.res.mjs +19 -14
  10. package/src/adapter/EventLog/EventLogBackend.res +82 -0
  11. package/src/adapter/EventLog/EventLogBackend.res.mjs +53 -0
  12. package/src/adapter/EventLog/EventLogStorage.res +15 -0
  13. package/src/adapter/EventLog/EventLogStorage.res.mjs +20 -1
  14. package/src/adapter/EventLog/EventLogStorage_Postgres.res +37 -0
  15. package/src/adapter/EventLog/EventLogStorage_Postgres.res.mjs +21 -0
  16. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +26 -11
  17. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +15 -6
  18. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res +78 -7
  19. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs +30 -2
  20. package/src/adapter/Postgres/PgProjectionFeed.res +100 -0
  21. package/src/adapter/Postgres/PgProjectionFeed.res.mjs +54 -0
  22. package/src/adapter/QueryDb/PgQueryResolver_Lambda.res +192 -0
  23. package/src/adapter/QueryDb/PgQueryResolver_Lambda.res.mjs +141 -0
  24. package/src/adapter/QueryDb/QueryDbBackend.res +52 -0
  25. package/src/adapter/QueryDb/QueryDbBackend.res.mjs +44 -0
  26. package/src/adapter/QueryDb/QueryDbResolvers.res +48 -0
  27. package/src/adapter/QueryDb/QueryDbResolvers.res.mjs +17 -1
  28. package/src/adapter/QueryDb/QueryDbStorage.res +44 -0
  29. package/src/adapter/QueryDb/QueryDbStorage.res.mjs +34 -1
  30. package/src/adapter/QueryDb/QueryDbStorage_Postgres.res +43 -0
  31. package/src/adapter/QueryDb/QueryDbStorage_Postgres.res.mjs +21 -0
  32. package/src/adapter/QueryDb/QueryDbStorage_Postgres_Runtime.res +19 -0
  33. package/src/adapter/QueryDb/QueryDbStorage_Postgres_Runtime.res.mjs +15 -0
  34. package/src/adapter/QueryEngine/QueryEngine_DynamoDb.res +12 -1
  35. package/src/adapter/QueryEngine/QueryEngine_DynamoDb.res.mjs +2 -1
  36. package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res +83 -3
  37. package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res.mjs +46 -3
  38. package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res +83 -3
  39. package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res.mjs +48 -3
  40. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res +130 -5
  41. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res.mjs +66 -6
  42. package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.mjs +15 -10
  43. package/src/adapter/Runtime/ReadModelEntryPoint.mjs +32 -15
  44. package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +26 -12
  45. package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res +138 -8
  46. package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res.mjs +76 -11
  47. package/src/components/Aggregate_Builder_Single.res +12 -5
  48. package/src/components/Aggregate_Builder_Single.res.mjs +12 -6
  49. package/src/components/Aggregate_Builder_Single_Async.res +12 -5
  50. package/src/components/Aggregate_Builder_Single_Async.res.mjs +12 -6
  51. package/src/components/ReadModel_Builder_NoResolver.res.mjs +1 -1
  52. package/src/components/ReadModel_Builder_NoResolver_Stream.res +13 -4
  53. package/src/components/ReadModel_Builder_NoResolver_Stream.res.mjs +14 -9
  54. package/src/components/ReadModel_Builder_Single.res +14 -5
  55. package/src/components/ReadModel_Builder_Single.res.mjs +15 -12
  56. package/src/components/ReadModel_Builder_Single_Stream.res +14 -5
  57. package/src/components/ReadModel_Builder_Single_Stream.res.mjs +15 -12
  58. package/src/components/StateViewSlice_Builder.res +10 -4
  59. package/src/components/StateViewSlice_Builder.res.mjs +8 -10
  60. package/src/components/StateViewSlice_Builder_Stream.res +10 -4
  61. package/src/components/StateViewSlice_Builder_Stream.res.mjs +8 -10
  62. package/src/plugin/runtime/PluginRuntime_Builder.res +12 -0
  63. package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +4 -0
  64. package/tests/EventCollectorChannelStreamRuntimeTest.res +55 -0
  65. package/tests/EventCollectorChannelStreamRuntimeTest.res.mjs +78 -0
  66. package/tests/PgChangeFeedRelay_IntegrationTest.res +259 -0
  67. package/tests/PgChangeFeedRelay_IntegrationTest.res.mjs +238 -0
  68. package/tests/PgPipeline_IntegrationTest.res +208 -0
  69. package/tests/PgPipeline_IntegrationTest.res.mjs +260 -0
  70. package/tests/PgQueryResolver_LambdaTest.res +206 -0
  71. 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
- /* No side effect */
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
- /* No side effect */
51
+ /* QueryDbStorage_DynamoDb-ReventlessAws Not a pure module */