@reventlessdev/reventless-aws 3.0.0-alpha.175 → 3.0.0-alpha.177

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 (76) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/package.json +10 -10
  3. package/src/Platform.res +127 -31
  4. package/src/Platform.res.mjs +226 -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 +54 -0
  19. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs +23 -0
  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_Builder.res +281 -0
  23. package/src/adapter/QueryDb/PgQueryResolver_Builder.res.mjs +189 -0
  24. package/src/adapter/QueryDb/PgQueryResolver_Lambda.res +335 -0
  25. package/src/adapter/QueryDb/PgQueryResolver_Lambda.res.mjs +252 -0
  26. package/src/adapter/QueryDb/QueryDbBackend.res +52 -0
  27. package/src/adapter/QueryDb/QueryDbBackend.res.mjs +44 -0
  28. package/src/adapter/QueryDb/QueryDbResolvers.res +49 -0
  29. package/src/adapter/QueryDb/QueryDbResolvers.res.mjs +20 -1
  30. package/src/adapter/QueryDb/QueryDbResolvers_Lambda.res +264 -0
  31. package/src/adapter/QueryDb/QueryDbResolvers_Lambda.res.mjs +164 -0
  32. package/src/adapter/QueryDb/QueryDbStorage.res +44 -0
  33. package/src/adapter/QueryDb/QueryDbStorage.res.mjs +34 -1
  34. package/src/adapter/QueryDb/QueryDbStorage_Postgres.res +47 -0
  35. package/src/adapter/QueryDb/QueryDbStorage_Postgres.res.mjs +21 -0
  36. package/src/adapter/QueryDb/QueryDbStorage_Postgres_Runtime.res +19 -0
  37. package/src/adapter/QueryDb/QueryDbStorage_Postgres_Runtime.res.mjs +15 -0
  38. package/src/adapter/QueryEngine/QueryEngine_DynamoDb.res +12 -1
  39. package/src/adapter/QueryEngine/QueryEngine_DynamoDb.res.mjs +2 -1
  40. package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res +83 -3
  41. package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res.mjs +46 -3
  42. package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res +83 -3
  43. package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res.mjs +48 -3
  44. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res +130 -5
  45. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res.mjs +66 -6
  46. package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.mjs +15 -10
  47. package/src/adapter/Runtime/PgQueryResolverEntryPoint.mjs +88 -0
  48. package/src/adapter/Runtime/ReadModelEntryPoint.mjs +32 -15
  49. package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +26 -12
  50. package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res +138 -8
  51. package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res.mjs +76 -11
  52. package/src/components/Aggregate_Builder_Single.res +12 -5
  53. package/src/components/Aggregate_Builder_Single.res.mjs +12 -6
  54. package/src/components/Aggregate_Builder_Single_Async.res +12 -5
  55. package/src/components/Aggregate_Builder_Single_Async.res.mjs +12 -6
  56. package/src/components/ReadModel_Builder_NoResolver.res.mjs +1 -1
  57. package/src/components/ReadModel_Builder_NoResolver_Stream.res +13 -4
  58. package/src/components/ReadModel_Builder_NoResolver_Stream.res.mjs +14 -9
  59. package/src/components/ReadModel_Builder_Single.res +14 -5
  60. package/src/components/ReadModel_Builder_Single.res.mjs +15 -12
  61. package/src/components/ReadModel_Builder_Single_Stream.res +14 -5
  62. package/src/components/ReadModel_Builder_Single_Stream.res.mjs +15 -12
  63. package/src/components/StateViewSlice_Builder.res +10 -4
  64. package/src/components/StateViewSlice_Builder.res.mjs +8 -10
  65. package/src/components/StateViewSlice_Builder_Stream.res +10 -4
  66. package/src/components/StateViewSlice_Builder_Stream.res.mjs +8 -10
  67. package/src/plugin/runtime/PluginRuntime_Builder.res +12 -0
  68. package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +4 -0
  69. package/tests/EventCollectorChannelStreamRuntimeTest.res +55 -0
  70. package/tests/EventCollectorChannelStreamRuntimeTest.res.mjs +78 -0
  71. package/tests/PgChangeFeedRelay_IntegrationTest.res +115 -4
  72. package/tests/PgChangeFeedRelay_IntegrationTest.res.mjs +84 -0
  73. package/tests/PgPipeline_IntegrationTest.res +208 -0
  74. package/tests/PgPipeline_IntegrationTest.res.mjs +260 -0
  75. package/tests/PgQueryResolver_LambdaTest.res +308 -0
  76. package/tests/PgQueryResolver_LambdaTest.res.mjs +392 -0
@@ -0,0 +1,335 @@
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
+ @val external atob: string => string = "atob"
23
+
24
+ // Where a cross-table resolver reads the target's sort-key value from.
25
+ // `kind`: "field" (the parent object) | "arg" (the GraphQL arguments).
26
+ type subIdSource = {kind: string, name: string}
27
+
28
+ // Wire payload from the resolver template. `arguments` mirrors the field name the
29
+ // other Invoke templates use (QueryInterceptor_Lambda, invokeCommandGenerator).
30
+ // The cross-table fields (B3.2c) are present only for resolveOne/resolveMany:
31
+ type payload = {
32
+ readModelName: string,
33
+ kind: string,
34
+ index?: string,
35
+ // Cross-table (@resolves/@resolvesMany) — see B3.2c dispatch below.
36
+ target?: string,
37
+ source?: JSON.t,
38
+ sourceIdField?: string,
39
+ sourceIdsField?: string,
40
+ sourceSubId?: subIdSource,
41
+ targetIndex?: string,
42
+ targetIndexIdField?: string,
43
+ multi?: bool,
44
+ arguments: JSON.t,
45
+ identity: Reventless.Identity.t,
46
+ }
47
+
48
+ // The Postgres query push-downs a binding needs (QueryEnginePostgres.Make
49
+ // provides these; the mock in tests provides in-memory equivalents).
50
+ type pushdowns = {
51
+ indexLookup: (~readModelName: string, string, string) => promise<array<JSON.t>>,
52
+ byIds: (~readModelName: string, array<string>) => promise<array<JSON.t>>,
53
+ listPage: (
54
+ ~readModelName: string,
55
+ ~argsDict: dict<JSON.t>,
56
+ ~capability: ReventlessCore.GraphQL_FragmentGenerator.serverCapability,
57
+ ~labelField: string,
58
+ ) => promise<option<JSON.t>>,
59
+ // Sub-id connection ({single}Items) — keyset over sub_key within a partition.
60
+ itemsPage: (
61
+ ~readModelName: string,
62
+ ~subIdField: string,
63
+ ~id: string,
64
+ ~argsDict: dict<JSON.t>,
65
+ ) => promise<JSON.t>,
66
+ // Full materialisation for the list fallback (shapes listPage declines).
67
+ scanAll: (~readModelName: string) => promise<array<JSON.t>>,
68
+ }
69
+
70
+ // Per-read-model dispatch context, bound once at Lambda init.
71
+ type binding = {
72
+ ops: ReventlessCore.QueryDb_Adapter.operations,
73
+ pushdowns: pushdowns,
74
+ indexes: array<Reventless.ReadModel.indexConfig>,
75
+ subIdField: option<string>,
76
+ capability: ReventlessCore.GraphQL_FragmentGenerator.serverCapability,
77
+ labelField: string,
78
+ includeIdParam: bool,
79
+ authorization: Reventless.Authorization.permission,
80
+ }
81
+
82
+ // -- arg helpers -------------------------------------------------------------
83
+ let argObj = (args: JSON.t): dict<JSON.t> =>
84
+ args->JSON.Decode.object->Option.getOr(Dict.make())
85
+ let argStr = (args: JSON.t, key: string): option<string> =>
86
+ args->argObj->Dict.get(key)->Option.flatMap(JSON.Decode.string)
87
+ let argStrs = (args: JSON.t, key: string): array<string> =>
88
+ args
89
+ ->argObj
90
+ ->Dict.get(key)
91
+ ->Option.flatMap(JSON.Decode.array)
92
+ ->Option.getOr([])
93
+ ->Array.filterMap(JSON.Decode.string)
94
+
95
+ // Add `id` (= partition key) to an item when absent — matches the shape the
96
+ // DynamoDB resolvers and the shared spec return.
97
+ let withId = (item: JSON.t, id: string): JSON.t =>
98
+ switch item->JSON.Decode.object {
99
+ | Some(obj) if obj->Dict.get("id")->Option.isNone =>
100
+ let copy = Dict.copy(obj)
101
+ copy->Dict.set("id", JSON.Encode.string(id))
102
+ JSON.Encode.object(copy)
103
+ | _ => item
104
+ }
105
+
106
+ let emptyConnection = (): JSON.t =>
107
+ ReventlessCore.QueryDbListQuery.buildConnection(
108
+ ~pageItems=[],
109
+ ~hasNextPage=false,
110
+ ~hasPreviousPage=false,
111
+ ~cursorValueOf=_ => "",
112
+ )
113
+
114
+ // Spec-level authorization then the (optional) user interceptor — mirrors
115
+ // QueryDbResolvers_GraphQL.runInterceptor. Denials return the kind's empty shape
116
+ // (the provider-agnostic resolver behaviour), not an error.
117
+ let runInterceptor = async (~binding, ~payload): ReventlessCore.QueryDb_Callback.interceptResult => {
118
+ if !Reventless.Authorization.isAllowed(binding.authorization, payload.identity) {
119
+ Deny("Forbidden")
120
+ } else {
121
+ switch ReventlessCore.QueryDb_Callback.queryInterceptorHook.contents {
122
+ | None => Allow
123
+ | Some(interceptor) =>
124
+ await interceptor(
125
+ ~identity=payload.identity,
126
+ ~readModelName=payload.readModelName,
127
+ ~args=payload.arguments,
128
+ )
129
+ }
130
+ }
131
+ }
132
+
133
+ let dispatch = async (~binding: binding, ~payload: payload): JSON.t => {
134
+ let rm = payload.readModelName
135
+ switch await runInterceptor(~binding, ~payload) {
136
+ | Deny(_) =>
137
+ // Empty shape per kind (null for single, connection for list/items, [] else).
138
+ let isMulti = payload.multi->Option.getOr(false)
139
+ switch payload.kind {
140
+ | "getById" => JSON.Encode.null
141
+ | "resolveOne" if !isMulti => JSON.Encode.null
142
+ | "list" | "items" => emptyConnection()
143
+ | _ => JSON.Encode.array([])
144
+ }
145
+ | Allow =>
146
+ switch payload.kind {
147
+ | "getById" =>
148
+ let id = payload.arguments->argStr("id")->Option.getOr("")
149
+ switch await binding.ops.load(id) {
150
+ | Ok(items) =>
151
+ switch items->Array.get(0) {
152
+ | Some(item) => binding.includeIdParam ? withId(item, id) : item
153
+ | None => JSON.Encode.null
154
+ }
155
+ | Error(_) => JSON.Encode.null
156
+ }
157
+
158
+ | "byIds" =>
159
+ let ids = payload.arguments->argStrs("ids")
160
+ JSON.Encode.array(await binding.pushdowns.byIds(~readModelName=rm, ids))
161
+
162
+ | "items" =>
163
+ // Sub-id connection: {single}Items(id, filter, first/after/last/before).
164
+ // Requires a subIdField; without one it's an empty connection.
165
+ switch binding.subIdField {
166
+ | Some(subIdField) =>
167
+ let id = payload.arguments->argStr("id")->Option.getOr("")
168
+ await binding.pushdowns.itemsPage(
169
+ ~readModelName=rm,
170
+ ~subIdField,
171
+ ~id,
172
+ ~argsDict=payload.arguments->argObj,
173
+ )
174
+ | None => emptyConnection()
175
+ }
176
+
177
+ | "index" =>
178
+ // The index arg name is the index; its stored field is idField ?? index.
179
+ let indexName = payload.index->Option.getOr("")
180
+ let field =
181
+ binding.indexes
182
+ ->Array.find(ic => ic.index === indexName)
183
+ ->Option.flatMap(ic => ic.idField)
184
+ ->Option.getOr(indexName)
185
+ let value = payload.arguments->argStr(indexName)->Option.getOr("")
186
+ JSON.Encode.array(await binding.pushdowns.indexLookup(~readModelName=rm, field, value))
187
+
188
+ | "list" =>
189
+ let argsDict = payload.arguments->argObj
190
+ switch await binding.pushdowns.listPage(
191
+ ~readModelName=rm,
192
+ ~argsDict,
193
+ ~capability=binding.capability,
194
+ ~labelField=binding.labelField,
195
+ ) {
196
+ | Some(conn) => conn
197
+ | None =>
198
+ // Shapes listPage declines (search/searchPrefix/ids/backward) → run the
199
+ // shared spec over the materialised model. No Relay global-id decoding
200
+ // in the Lambda, so `decodeLocalId` is a no-op (the `ids` filter still
201
+ // matches raw item ids).
202
+ let items = await binding.pushdowns.scanAll(~readModelName=rm)
203
+ ReventlessCore.QueryDbListQuery.run(
204
+ ~items,
205
+ ~argsDict,
206
+ ~capability=binding.capability,
207
+ ~labelField=binding.labelField,
208
+ ~decodeLocalId=_ => None,
209
+ )
210
+ }
211
+
212
+ // Cross-table single-ID field resolver (@resolves). `binding` is the TARGET
213
+ // binding (the handler looks it up by payload.target); the key comes from the
214
+ // parent object (payload.source[sourceIdField]).
215
+ | "resolveOne" =>
216
+ let target = payload.target->Option.getOr(rm)
217
+ let source = payload.source->Option.getOr(JSON.Encode.null)
218
+ let key = source->argStr(payload.sourceIdField->Option.getOr(""))->Option.getOr("")
219
+ let items = switch payload.targetIndex {
220
+ | Some(ix) =>
221
+ await binding.pushdowns.indexLookup(
222
+ ~readModelName=target,
223
+ payload.targetIndexIdField->Option.getOr(ix),
224
+ key,
225
+ )
226
+ | None =>
227
+ switch await binding.ops.load(key) {
228
+ | Ok(items) => items
229
+ | Error(_) => []
230
+ }
231
+ }
232
+ // Optional target sort-key filter (source field or GraphQL arg).
233
+ let filtered = switch (payload.sourceSubId, binding.subIdField) {
234
+ | (Some({kind, name}), Some(subField)) =>
235
+ let subVal =
236
+ switch kind {
237
+ | "arg" => payload.arguments->argStr(name)
238
+ | _ => source->argStr(name)
239
+ }->Option.getOr("")
240
+ items->Array.filter(it => it->argStr(subField)->Option.getOr("") == subVal)
241
+ | _ => items
242
+ }
243
+ if payload.multi->Option.getOr(false) {
244
+ JSON.Encode.array(filtered)
245
+ } else {
246
+ switch filtered->Array.get(0) {
247
+ | Some(item) => item
248
+ | None => JSON.Encode.null
249
+ }
250
+ }
251
+
252
+ // Cross-table batch field resolver (@resolvesMany). ids come from the parent
253
+ // object (payload.source[sourceIdsField]); BatchGet the target by partition
254
+ // key (missing ids drop out).
255
+ | "resolveMany" =>
256
+ let target = payload.target->Option.getOr(rm)
257
+ let source = payload.source->Option.getOr(JSON.Encode.null)
258
+ let ids = source->argStrs(payload.sourceIdsField->Option.getOr(""))
259
+ JSON.Encode.array(await binding.pushdowns.byIds(~readModelName=target, ids))
260
+
261
+ | other =>
262
+ // Fail loudly on unmapped kinds (feature-parity guard, per the B3.2 plan).
263
+ log.error(~comp="PgQueryResolver_Lambda", `unmapped resolver kind '${other}' for ${rm}`)
264
+ JsError.throwWithMessage(`PgQueryResolver: unmapped resolver kind '${other}' for ${rm}`)
265
+ }
266
+ }
267
+ }
268
+
269
+ // Per-read-model binding registry, populated by the entry point at Lambda init.
270
+ let bindings: dict<binding> = Dict.make()
271
+ let register = (~readModelName: string, binding: binding): unit =>
272
+ bindings->Dict.set(readModelName, binding)
273
+
274
+ // Relay node type → read-model-name map (B3.2c), populated at Lambda init from
275
+ // the deploy-time registerNodeType calls.
276
+ let nodeTypeMap: dict<string> = Dict.make()
277
+ let registerNodeType = (~typeName: string, ~readModelName: string): unit =>
278
+ nodeTypeMap->Dict.set(typeName, readModelName)
279
+
280
+ // node(id: ID!) — decode the global id (base64 of `typeName:localId`, matching
281
+ // AppSync's nodeDecodeGlobalId), map typeName → read model, load by localId,
282
+ // return the item tagged with __typename and the original global id. Runs the
283
+ // target's authorization/interceptor. Self-contained (getById returns raw ids,
284
+ // so no encoding elsewhere is affected).
285
+ let handleNode = async (~payload: payload): JSON.t => {
286
+ let globalId = payload.arguments->argStr("id")->Option.getOr("")
287
+ let decoded = try atob(globalId) catch {
288
+ | _ => ""
289
+ }
290
+ let colonIdx = decoded->String.indexOf(":")
291
+ if colonIdx <= 0 {
292
+ JSON.Encode.null
293
+ } else {
294
+ let typeName = decoded->String.slice(~start=0, ~end=colonIdx)
295
+ let localId = decoded->String.slice(~start=colonIdx + 1, ~end=decoded->String.length)
296
+ switch nodeTypeMap->Dict.get(typeName)->Option.flatMap(rm => bindings->Dict.get(rm)) {
297
+ | Some(binding) =>
298
+ switch await runInterceptor(~binding, ~payload) {
299
+ | Deny(_) => JSON.Encode.null
300
+ | Allow =>
301
+ switch await binding.ops.load(localId) {
302
+ | Ok(items) =>
303
+ switch items->Array.get(0) {
304
+ | Some(item) =>
305
+ let obj = item->JSON.Decode.object->Option.getOr(Dict.make())->Dict.copy
306
+ obj->Dict.set("__typename", JSON.Encode.string(typeName))
307
+ obj->Dict.set("id", JSON.Encode.string(globalId))
308
+ JSON.Encode.object(obj)
309
+ | None => JSON.Encode.null
310
+ }
311
+ | Error(_) => JSON.Encode.null
312
+ }
313
+ }
314
+ | None => JSON.Encode.null
315
+ }
316
+ }
317
+ }
318
+
319
+ let handler = async (payload: payload, _context) => {
320
+ // node decodes its own target; the cross-table field resolvers dispatch against
321
+ // the TARGET binding; everything else against the payload's own read model.
322
+ let bindingKey = switch payload.kind {
323
+ | "resolveOne" | "resolveMany" => payload.target->Option.getOr(payload.readModelName)
324
+ | _ => payload.readModelName
325
+ }
326
+ switch payload.kind {
327
+ | "node" => await handleNode(~payload)
328
+ | _ =>
329
+ switch bindings->Dict.get(bindingKey) {
330
+ | Some(binding) => await dispatch(~binding, ~payload)
331
+ | None =>
332
+ JsError.throwWithMessage("PgQueryResolver: no binding registered for read model " ++ bindingKey)
333
+ }
334
+ }
335
+ }
@@ -0,0 +1,252 @@
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 "items" :
89
+ let subIdField = binding.subIdField;
90
+ if (subIdField === undefined) {
91
+ return emptyConnection();
92
+ }
93
+ let id$1 = Stdlib_Option.getOr(argStr(payload.arguments, "id"), "");
94
+ return await binding.pushdowns.itemsPage(rm, subIdField, id$1, argObj(payload.arguments));
95
+ case "list" :
96
+ let argsDict = argObj(payload.arguments);
97
+ let conn = await binding.pushdowns.listPage(rm, argsDict, binding.capability, binding.labelField);
98
+ if (conn !== undefined) {
99
+ return conn;
100
+ }
101
+ let items$1 = await binding.pushdowns.scanAll(rm);
102
+ return QueryDbListQuery$ReventlessCore.run(items$1, argsDict, binding.capability, binding.labelField, param => {});
103
+ case "resolveMany" :
104
+ let target = Stdlib_Option.getOr(payload.target, rm);
105
+ let source = Stdlib_Option.getOr(payload.source, null);
106
+ let ids$1 = argStrs(source, Stdlib_Option.getOr(payload.sourceIdsField, ""));
107
+ return await binding.pushdowns.byIds(target, ids$1);
108
+ case "resolveOne" :
109
+ let target$1 = Stdlib_Option.getOr(payload.target, rm);
110
+ let source$1 = Stdlib_Option.getOr(payload.source, null);
111
+ let key = Stdlib_Option.getOr(argStr(source$1, Stdlib_Option.getOr(payload.sourceIdField, "")), "");
112
+ let ix = payload.targetIndex;
113
+ let items$2;
114
+ if (ix !== undefined) {
115
+ items$2 = await binding.pushdowns.indexLookup(target$1, Stdlib_Option.getOr(payload.targetIndexIdField, ix), key);
116
+ } else {
117
+ let items$3 = await binding.ops.load(key);
118
+ items$2 = items$3.TAG === "Ok" ? items$3._0 : [];
119
+ }
120
+ let match$1 = payload.sourceSubId;
121
+ let match$2 = binding.subIdField;
122
+ let filtered;
123
+ if (match$1 !== undefined && match$2 !== undefined) {
124
+ let name = match$1.name;
125
+ let tmp = match$1.kind === "arg" ? argStr(payload.arguments, name) : argStr(source$1, name);
126
+ let subVal = Stdlib_Option.getOr(tmp, "");
127
+ filtered = items$2.filter(it => Stdlib_Option.getOr(argStr(it, match$2), "") === subVal);
128
+ } else {
129
+ filtered = items$2;
130
+ }
131
+ if (Stdlib_Option.getOr(payload.multi, false)) {
132
+ return filtered;
133
+ }
134
+ let item$1 = filtered[0];
135
+ if (item$1 !== undefined) {
136
+ return item$1;
137
+ } else {
138
+ return null;
139
+ }
140
+ default:
141
+ log.error("PgQueryResolver_Lambda", undefined, `unmapped resolver kind '` + other + `' for ` + rm);
142
+ return Stdlib_JsError.throwWithMessage(`PgQueryResolver: unmapped resolver kind '` + other + `' for ` + rm);
143
+ }
144
+ } else {
145
+ let isMulti = Stdlib_Option.getOr(payload.multi, false);
146
+ let match$3 = payload.kind;
147
+ switch (match$3) {
148
+ case "getById" :
149
+ return null;
150
+ case "items" :
151
+ case "list" :
152
+ return emptyConnection();
153
+ case "resolveOne" :
154
+ if (isMulti) {
155
+ return [];
156
+ } else {
157
+ return null;
158
+ }
159
+ default:
160
+ return [];
161
+ }
162
+ }
163
+ }
164
+
165
+ let bindings = {};
166
+
167
+ function register(readModelName, binding) {
168
+ bindings[readModelName] = binding;
169
+ }
170
+
171
+ let nodeTypeMap = {};
172
+
173
+ function registerNodeType(typeName, readModelName) {
174
+ nodeTypeMap[typeName] = readModelName;
175
+ }
176
+
177
+ async function handleNode(payload) {
178
+ let globalId = Stdlib_Option.getOr(argStr(payload.arguments, "id"), "");
179
+ let decoded;
180
+ try {
181
+ decoded = atob(globalId);
182
+ } catch (exn) {
183
+ decoded = "";
184
+ }
185
+ let colonIdx = decoded.indexOf(":");
186
+ if (colonIdx <= 0) {
187
+ return null;
188
+ }
189
+ let typeName = decoded.slice(0, colonIdx);
190
+ let localId = decoded.slice(colonIdx + 1 | 0, decoded.length);
191
+ let binding = Stdlib_Option.flatMap(nodeTypeMap[typeName], rm => bindings[rm]);
192
+ if (binding === undefined) {
193
+ return null;
194
+ }
195
+ let match = await runInterceptor(binding, payload);
196
+ if (typeof match === "object") {
197
+ return null;
198
+ }
199
+ let items = await binding.ops.load(localId);
200
+ if (items.TAG !== "Ok") {
201
+ return null;
202
+ }
203
+ let item = items._0[0];
204
+ if (item === undefined) {
205
+ return null;
206
+ }
207
+ let obj = Object.assign({}, Stdlib_Option.getOr(Stdlib_JSON.Decode.object(item), {}));
208
+ obj["__typename"] = typeName;
209
+ obj["id"] = globalId;
210
+ return obj;
211
+ }
212
+
213
+ async function handler(payload, _context) {
214
+ let match = payload.kind;
215
+ let bindingKey;
216
+ switch (match) {
217
+ case "resolveMany" :
218
+ case "resolveOne" :
219
+ bindingKey = Stdlib_Option.getOr(payload.target, payload.readModelName);
220
+ break;
221
+ default:
222
+ bindingKey = payload.readModelName;
223
+ }
224
+ let match$1 = payload.kind;
225
+ if (match$1 === "node") {
226
+ return await handleNode(payload);
227
+ }
228
+ let binding = bindings[bindingKey];
229
+ if (binding !== undefined) {
230
+ return await dispatch(binding, payload);
231
+ } else {
232
+ return Stdlib_JsError.throwWithMessage("PgQueryResolver: no binding registered for read model " + bindingKey);
233
+ }
234
+ }
235
+
236
+ export {
237
+ log,
238
+ argObj,
239
+ argStr,
240
+ argStrs,
241
+ withId,
242
+ emptyConnection,
243
+ runInterceptor,
244
+ dispatch,
245
+ bindings,
246
+ register,
247
+ nodeTypeMap,
248
+ registerNodeType,
249
+ handleNode,
250
+ handler,
251
+ }
252
+ /* 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 */