@reventlessdev/reventless-aws 3.0.0-alpha.176 → 3.0.0-alpha.178

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 +23 -0
  2. package/docker-compose.postgres.yml +21 -0
  3. package/package.json +7 -6
  4. package/scripts/run-pg-integration-tests.sh +58 -0
  5. package/src/Platform.res +49 -0
  6. package/src/Platform.res.mjs +48 -2
  7. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +7 -1
  8. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +10 -1
  9. package/src/adapter/Api/Platform_UIFragments_Lambda.res +7 -1
  10. package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +10 -1
  11. package/src/adapter/Counter/CounterHandler_DynamoDbStream.res +6 -21
  12. package/src/adapter/Counter/CounterHandler_DynamoDbStream.res.mjs +2 -17
  13. package/src/adapter/DcbEventLog/DcbBackend.res +4 -0
  14. package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres.res +2 -2
  15. package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres.res.mjs +7 -1
  16. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +1 -11
  17. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +2 -24
  18. package/src/adapter/Postgres/PgConnection.res +70 -10
  19. package/src/adapter/Postgres/PgConnection.res.mjs +48 -3
  20. package/src/adapter/Postgres/PgMigration_Builder.res +127 -0
  21. package/src/adapter/Postgres/PgMigration_Builder.res.mjs +51 -0
  22. package/src/adapter/QueryDb/PgQueryResolver_Builder.res +279 -0
  23. package/src/adapter/QueryDb/PgQueryResolver_Builder.res.mjs +170 -0
  24. package/src/adapter/QueryDb/PgQueryResolver_Lambda.res +202 -11
  25. package/src/adapter/QueryDb/PgQueryResolver_Lambda.res.mjs +150 -9
  26. package/src/adapter/QueryDb/QueryDbBackend.res +8 -0
  27. package/src/adapter/QueryDb/QueryDbBackend.res.mjs +4 -1
  28. package/src/adapter/QueryDb/QueryDbResolvers.res +6 -5
  29. package/src/adapter/QueryDb/QueryDbResolvers.res.mjs +6 -3
  30. package/src/adapter/QueryDb/QueryDbResolvers_Lambda.res +288 -0
  31. package/src/adapter/QueryDb/QueryDbResolvers_Lambda.res.mjs +170 -0
  32. package/src/adapter/QueryDb/QueryDbStorage.res +6 -1
  33. package/src/adapter/QueryDb/QueryDbStorage.res.mjs +2 -1
  34. package/src/adapter/QueryDb/QueryDbStorage_Postgres.res +5 -1
  35. package/src/adapter/QueryDb/QueryDbStorage_Postgres.res.mjs +3 -3
  36. package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res +3 -14
  37. package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res.mjs +2 -25
  38. package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res +3 -14
  39. package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res.mjs +2 -25
  40. package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +7 -1
  41. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res +78 -15
  42. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res.mjs +43 -26
  43. package/src/adapter/Runtime/PgMigrationEntryPoint.mjs +40 -0
  44. package/src/adapter/Runtime/PgQueryResolverEntryPoint.mjs +88 -0
  45. package/src/adapter/Runtime/ReadModelEntryPoint.mjs +17 -5
  46. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +10 -0
  47. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +3 -0
  48. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +9 -14
  49. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +2 -25
  50. package/src/adapter/Runtime/StateTopicPublish.mjs +204 -0
  51. package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +17 -3
  52. package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res +86 -17
  53. package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res.mjs +50 -33
  54. package/src/adapter/StateTopic/StateTopic_AppSync.res +7 -1
  55. package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +10 -1
  56. package/src/plugin/cloner/ClonerRunner_Fargate.res +7 -1
  57. package/src/plugin/cloner/ClonerRunner_Fargate.res.mjs +10 -1
  58. package/src/util/Util_Bundle.res +71 -1
  59. package/src/util/Util_Bundle.res.mjs +49 -1
  60. package/tests/PgChangeFeedRelay_IntegrationTest.res +3 -3
  61. package/tests/PgConnectionJsonTest.res +84 -0
  62. package/tests/PgConnectionJsonTest.res.mjs +68 -0
  63. package/tests/PgMigrationEntryPoint_IntegrationTest.res +82 -0
  64. package/tests/PgMigrationEntryPoint_IntegrationTest.res.mjs +63 -0
  65. package/tests/PgPipeline_IntegrationTest.res +3 -1
  66. package/tests/PgQueryResolver_LambdaTest.res +163 -2
  67. package/tests/PgQueryResolver_LambdaTest.res.mjs +226 -14
  68. package/tests/StateTopicPublishTest.res +162 -0
  69. package/tests/StateTopicPublishTest.res.mjs +158 -0
  70. package/tests/Util_BundleEsmLoaderTest.res +57 -0
  71. package/tests/Util_BundleEsmLoaderTest.res.mjs +44 -0
@@ -32,6 +32,8 @@ function isPostgres() {
32
32
  return Stdlib_Option.isSome(selectionRef.contents);
33
33
  }
34
34
 
35
+ let postgresStreamRegistry = new Set();
36
+
35
37
  export {
36
38
  selectionRef,
37
39
  set,
@@ -40,5 +42,6 @@ export {
40
42
  exempt,
41
43
  isPostgresFor,
42
44
  isPostgres,
45
+ postgresStreamRegistry,
43
46
  }
44
- /* No side effect */
47
+ /* postgresStreamRegistry Not a pure module */
@@ -1,10 +1,11 @@
1
1
  module AppSync = QueryDbResolvers_AppSync
2
2
  module NoOp = QueryDbResolvers_NoOp
3
+ module Lambda = QueryDbResolvers_Lambda
3
4
 
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.
5
+ // B3.2b: Postgres-backed read models have no DynamoDB data source — their
6
+ // GraphQL Query fields are served by the shared PgQueryResolver Lambda data
7
+ // source (`QueryDbResolvers_Lambda`, Invoke templates PgQueryResolver_Lambda).
8
+ // DynamoDB-backed (incl. admin-exempt) read models keep the direct resolver set.
8
9
  module Selectable = {
9
10
  type api = QueryDbResolvers_AppSync_NoOp.api
10
11
  type role = QueryDbResolvers_AppSync_NoOp.role
@@ -21,7 +22,7 @@ module Selectable = {
21
22
  ~opts,
22
23
  ) =>
23
24
  if QueryDbBackend.isPostgresFor(name) {
24
- QueryDbResolvers_AppSync_NoOp.make(
25
+ QueryDbResolvers_Lambda.make(
25
26
  ~name,
26
27
  ~api,
27
28
  ~apiRole,
@@ -1,12 +1,12 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as QueryDbBackend$ReventlessAws from "./QueryDbBackend.res.mjs";
4
+ import * as QueryDbResolvers_Lambda$ReventlessAws from "./QueryDbResolvers_Lambda.res.mjs";
4
5
  import * as QueryDbResolvers_AppSync$ReventlessAws from "./QueryDbResolvers_AppSync.res.mjs";
5
- import * as QueryDbResolvers_AppSync_NoOp$ReventlessAws from "./QueryDbResolvers_AppSync_NoOp.res.mjs";
6
6
 
7
7
  function make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolverConfigs, idsResolverConfigs, authorization, opts) {
8
8
  if (QueryDbBackend$ReventlessAws.isPostgresFor(name)) {
9
- return QueryDbResolvers_AppSync_NoOp$ReventlessAws.make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolverConfigs, idsResolverConfigs, authorization, opts);
9
+ return QueryDbResolvers_Lambda$ReventlessAws.make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolverConfigs, idsResolverConfigs, authorization, opts);
10
10
  } else {
11
11
  return QueryDbResolvers_AppSync$ReventlessAws.make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolverConfigs, idsResolverConfigs, authorization, opts);
12
12
  }
@@ -20,9 +20,12 @@ let AppSync;
20
20
 
21
21
  let NoOp;
22
22
 
23
+ let Lambda;
24
+
23
25
  export {
24
26
  AppSync,
25
27
  NoOp,
28
+ Lambda,
26
29
  Selectable,
27
30
  }
28
- /* QueryDbResolvers_AppSync-ReventlessAws Not a pure module */
31
+ /* QueryDbBackend-ReventlessAws Not a pure module */
@@ -0,0 +1,288 @@
1
+ // AppSync resolvers for Postgres-backed read models (B3.2b) — the Lambda-data-
2
+ // source parallel to QueryDbResolvers_AppSync's direct DynamoDB resolvers.
3
+ //
4
+ // Every Query field becomes a thin APPSYNC_JS unit resolver whose `Invoke`
5
+ // template carries { readModelName, kind, index?, arguments, identity } to the
6
+ // ONE shared PgQueryResolver Lambda (PgQueryResolver_Builder), which dispatches
7
+ // via PgQueryResolver_Lambda.dispatch. `dataSourceName` is the shared data
8
+ // source's (deferred) name, the same for every Postgres read model.
9
+ //
10
+ // Field names / includeIdParam / connectionSpec come from the same registry the
11
+ // AppSync path reads, so the SDL emitted by GraphQL_FragmentGenerator stays in
12
+ // lockstep. Kinds covered: getById, list (connection), index, byIds. Deferred to
13
+ // B3.2c: items (sub-id connection), @resolves/@resolvesMany, node, auth-table.
14
+
15
+ module Resolver = AppSync_Resolver_Retrying
16
+ open Reventless.ReadModel
17
+
18
+ type api = Types.AppSync.api
19
+ type role = Types.AppSync.role
20
+
21
+ // Identity block shared by every Invoke template (Cognito vs IAM), matching the
22
+ // shape PgQueryResolver_Lambda decodes (Reventless.Identity.t).
23
+ let identityBlock = `id != null && id.sub != null
24
+ ? { userId: id.sub, username: id.username, groups: id.claims?.['cognito:groups'] ?? [], claims: id.claims, provider: 'Cognito' }
25
+ : id != null
26
+ ? { userArn: id.userArn ?? null, accountId: id.accountId ?? null, username: id.username ?? null, provider: 'IAM' }
27
+ : null`
28
+
29
+ let invokeTemplate = (
30
+ ~readModelName: string,
31
+ ~kind: string,
32
+ ~index: option<string>=?,
33
+ ~authTable: option<string>=?,
34
+ ~authGroup: option<string>=?,
35
+ ) => {
36
+ let indexFrag = switch index {
37
+ | Some(ix) => `\n index: '${ix}',`
38
+ | None => ""
39
+ }
40
+ let authFrag = switch (authTable, authGroup) {
41
+ | (Some(t), Some(g)) => `\n authTable: '${t}',\n authGroup: '${g}',`
42
+ | _ => ""
43
+ }
44
+ `import { util } from '@aws-appsync/utils';
45
+ export function request(ctx) {
46
+ const id = ctx.identity;
47
+ return {
48
+ operation: 'Invoke',
49
+ payload: {
50
+ readModelName: '${readModelName}',
51
+ kind: '${kind}',${indexFrag}${authFrag}
52
+ arguments: ctx.args,
53
+ identity: ${identityBlock}
54
+ }
55
+ };
56
+ }
57
+ export function response(ctx) {
58
+ if (ctx.error) util.error(ctx.error.message, ctx.error.type);
59
+ return ctx.result;
60
+ }
61
+ `->Pulumi.Input.make
62
+ }
63
+
64
+ // Cross-table field resolver (@resolves/@resolvesMany, B3.2c): on a parent
65
+ // entity type's field, carrying the parent object (ctx.source) + baked target
66
+ // metadata (`extra`). `sourceType` is the parent read model (used only for auth
67
+ // scoping in dispatch); `target` is the target read model's binding key.
68
+ let invokeFieldTemplate = (~sourceType: string, ~kind: string, ~target: string, ~extra: string) =>
69
+ `import { util } from '@aws-appsync/utils';
70
+ export function request(ctx) {
71
+ const id = ctx.identity;
72
+ return {
73
+ operation: 'Invoke',
74
+ payload: {
75
+ readModelName: '${sourceType}',
76
+ kind: '${kind}',
77
+ target: '${target}',
78
+ source: ctx.source,
79
+ arguments: ctx.args,${extra}
80
+ identity: ${identityBlock}
81
+ }
82
+ };
83
+ }
84
+ export function response(ctx) {
85
+ if (ctx.error) util.error(ctx.error.message, ctx.error.type);
86
+ return ctx.result;
87
+ }
88
+ `->Pulumi.Input.make
89
+
90
+ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
91
+ ~name: string,
92
+ ~api: api,
93
+ ~apiRole as _: role,
94
+ ~dataSourceName,
95
+ ~indexes: array<indexConfig>,
96
+ ~subIdField,
97
+ ~idResolverConfigs: array<idResolverConfig>,
98
+ ~idsResolverConfigs: array<idsResolverConfig>,
99
+ ~authorization as _: Reventless.Authorization.permission,
100
+ ~opts,
101
+ ) => {
102
+ let dataSourceName = dataSourceName->Pulumi.Output.asInput
103
+ let name = name->String.capitalize
104
+ let registryEntry = ReventlessCore.Plugin_Helpers.queryFieldNamesRegistry->Dict.get(name)
105
+
106
+ let fieldNameForSingle = switch registryEntry {
107
+ | Some({singleFieldName}) => singleFieldName
108
+ | None => name->Resolver.Functions.uncapitalize
109
+ }
110
+ let includeIdParam = switch registryEntry {
111
+ | Some({includeIdParam}) => includeIdParam
112
+ | None => true
113
+ }
114
+ let connectionSpec = switch registryEntry {
115
+ | Some({connectionSpec}) => connectionSpec
116
+ | None => true
117
+ }
118
+ let fieldNameForAll = switch registryEntry {
119
+ | Some({listFieldName}) => listFieldName
120
+ | None => name ++ "s"
121
+ }
122
+ let labelField = switch registryEntry {
123
+ | Some({labelField: ?lf}) => lf->Option.getOr("id")
124
+ | None => "id"
125
+ }
126
+ let returnTypeName = switch registryEntry {
127
+ | Some({returnTypeName: rt}) => rt
128
+ | None => name
129
+ }
130
+
131
+ // Register the binding info the shared Lambda's env config needs (the entry
132
+ // point gets indexes/subIdField/schema from the spec module; labelField and
133
+ // includeIdParam come from this deploy-time registry). Keyed by spec name.
134
+ PgQueryResolver_Builder.register({readModelName: name, labelField, includeIdParam})
135
+
136
+ // Relay node type → this read model (for the shared node(id) resolver, B3.2c).
137
+ // Only entities addressable by id participate in node resolution.
138
+ if includeIdParam {
139
+ PgQueryResolver_Builder.registerNodeType(~typeName=returnTypeName, ~readModelName=name)
140
+ }
141
+
142
+ let mkResolver = (~resolverName, ~field, ~kind, ~index=?, ~authTable=?, ~authGroup=?) =>
143
+ Resolver.makeUnitJsResolver(
144
+ ~name=resolverName,
145
+ ~api,
146
+ ~dataSourceName,
147
+ ~type_="Query"->Pulumi.Input.make,
148
+ ~field=field->Pulumi.Input.make,
149
+ ~code=invokeTemplate(~readModelName=name, ~kind, ~index?, ~authTable?, ~authGroup?),
150
+ ~opts,
151
+ )
152
+
153
+ let stripLeadingBy = s =>
154
+ if s->String.startsWith("by") && s->String.length > 2 {
155
+ s->String.slice(~start=2, ~end=s->String.length)
156
+ } else {
157
+ s
158
+ }
159
+
160
+ // Resolvers are deferred into resourcesMaker (created inside the schema-pushed
161
+ // builderOutputs.apply, like the AppSync path) — so they only exist after the
162
+ // fields are ACTIVE and after PgQueryResolver_Builder.provision has resolved
163
+ // the shared data source name.
164
+ let resourcesMaker: ReventlessCore.QueryDb.resolversResourcesMaker = _allQueryDbs => {
165
+ // getById (or listAll when the read model has no id param, e.g. singletons).
166
+ let byId = mkResolver(
167
+ ~resolverName=fieldNameForSingle->String.capitalize,
168
+ ~field=fieldNameForSingle,
169
+ ~kind=includeIdParam ? "getById" : "list",
170
+ )
171
+
172
+ // Main list — Relay connection (or legacy list when connectionSpec=false).
173
+ let all = mkResolver(
174
+ ~resolverName=fieldNameForAll->String.capitalize,
175
+ ~field=fieldNameForAll,
176
+ ~kind=connectionSpec ? "list" : "list",
177
+ )
178
+
179
+ // Sub-id connection — {single}Items(id, filter, …); only when subId configured.
180
+ let items = switch subIdField {
181
+ | Some(_) => [
182
+ mkResolver(
183
+ ~resolverName=fieldNameForSingle->String.capitalize ++ "Items",
184
+ ~field=fieldNameForSingle ++ "Items",
185
+ ~kind="items",
186
+ ),
187
+ ]
188
+ | None => []
189
+ }
190
+
191
+ // Batched-by-ids — single-key projections only (mirrors the SDL).
192
+ let byIds = if includeIdParam && subIdField === None {
193
+ let byIdsField = fieldNameForAll ++ "ByIds"
194
+ [
195
+ mkResolver(
196
+ ~resolverName=byIdsField->String.capitalize,
197
+ ~field=byIdsField,
198
+ ~kind="byIds",
199
+ ),
200
+ ]
201
+ } else {
202
+ []
203
+ }
204
+
205
+ // Per-index equality queries: {single}By{Index}. A group-restricted index
206
+ // (indexConfig.authorization) bakes the auth-table pipeline params — the
207
+ // Lambda verifies group membership + ownership against the auth read model.
208
+ let byIndex = indexes->Array.map((ic: indexConfig) => {
209
+ let index = ic.index
210
+ let resolverName =
211
+ fieldNameForSingle->String.capitalize ++ "By" ++ index->stripLeadingBy->String.capitalize
212
+ let field = fieldNameForSingle ++ "By" ++ index->stripLeadingBy->String.capitalize
213
+ switch ic.authorization {
214
+ | Some({tableName, group}) =>
215
+ mkResolver(
216
+ ~resolverName,
217
+ ~field,
218
+ ~kind="index",
219
+ ~index,
220
+ ~authTable=tableName->String.capitalize,
221
+ ~authGroup=group,
222
+ )
223
+ | None => mkResolver(~resolverName, ~field, ~kind="index", ~index)
224
+ }
225
+ })
226
+
227
+ // @resolves — single cross-table field resolver on this (parent) type. The
228
+ // target's binding key is its capitalized spec name (mirrors how the binding
229
+ // registry is keyed); the parent object flows via ctx.source.
230
+ let idResolvers = idResolverConfigs->Array.map(config => {
231
+ let {source: {idField, subId, resolvedField}, target} = config
232
+ let targetKey = target.tableName->String.capitalize
233
+ let (field, multi) = switch resolvedField {
234
+ | Single(f) => (f, false)
235
+ | Multi(f) => (f, true)
236
+ }
237
+ let targetFrag = switch target.idField {
238
+ | Index(ix) => `\n targetIndex: '${ix}',\n targetIndexIdField: '${ix}',`
239
+ | IndexWithId(ix, idf) => `\n targetIndex: '${ix}',\n targetIndexIdField: '${idf}',`
240
+ | Id => ""
241
+ }
242
+ let subIdFrag = switch subId {
243
+ | Field(f) => `\n sourceSubId: { kind: 'field', name: '${f}' },`
244
+ | Argument(a) => `\n sourceSubId: { kind: 'arg', name: '${a}' },`
245
+ | NoSubId => ""
246
+ }
247
+ let extra =
248
+ `\n sourceIdField: '${idField}',\n multi: ${multi ? "true" : "false"},` ++
249
+ targetFrag ++
250
+ subIdFrag
251
+ Resolver.makeUnitJsResolver(
252
+ ~name=name ++ field->String.capitalize,
253
+ ~api,
254
+ ~dataSourceName,
255
+ ~type_=name->Pulumi.Input.make,
256
+ ~field=field->Pulumi.Input.make,
257
+ ~code=invokeFieldTemplate(~sourceType=name, ~kind="resolveOne", ~target=targetKey, ~extra),
258
+ ~opts,
259
+ )
260
+ })
261
+
262
+ // @resolvesMany — batch cross-table field resolver (BatchGet by ids).
263
+ let idsResolvers = idsResolverConfigs->Array.map(config => {
264
+ let {source: {idsField, resolvedField}, target} = config
265
+ let targetKey = target.tableName->String.capitalize
266
+ let extra = `\n sourceIdsField: '${idsField}',`
267
+ Resolver.makeUnitJsResolver(
268
+ ~name=name ++ resolvedField->String.capitalize,
269
+ ~api,
270
+ ~dataSourceName,
271
+ ~type_=name->Pulumi.Input.make,
272
+ ~field=resolvedField->Pulumi.Input.make,
273
+ ~code=invokeFieldTemplate(~sourceType=name, ~kind="resolveMany", ~target=targetKey, ~extra),
274
+ ~opts,
275
+ )
276
+ })
277
+
278
+ [byId, all]
279
+ ->Array.concat(items)
280
+ ->Array.concat(byIds)
281
+ ->Array.concat(byIndex)
282
+ ->Array.concat(idResolvers)
283
+ ->Array.concat(idsResolvers)
284
+ ->Array.map(Util.AppSync.toResourceNative)
285
+ }
286
+
287
+ {ReventlessCore.QueryDb_Adapter.resources: [], resourcesMaker}
288
+ }
@@ -0,0 +1,170 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
+ import * as Stdlib_String from "@rescript/runtime/lib/es6/Stdlib_String.js";
5
+ import * as Util_AppSync$ReventlessAws from "../../util/Util_AppSync.res.mjs";
6
+ import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_Helpers.res.mjs";
7
+ import * as AppSync_Resolver_Functions$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/AppSync/AppSync_Resolver_Functions.res.mjs";
8
+ import * as PgQueryResolver_Builder$ReventlessAws from "./PgQueryResolver_Builder.res.mjs";
9
+ import * as AppSync_Resolver_Retrying$ReventlessAws from "../Api/AppSync_Resolver_Retrying.res.mjs";
10
+
11
+ let identityBlock = `id != null && id.sub != null
12
+ ? { userId: id.sub, username: id.username, groups: id.claims?.['cognito:groups'] ?? [], claims: id.claims, provider: 'Cognito' }
13
+ : id != null
14
+ ? { userArn: id.userArn ?? null, accountId: id.accountId ?? null, username: id.username ?? null, provider: 'IAM' }
15
+ : null`;
16
+
17
+ function invokeTemplate(readModelName, kind, index, authTable, authGroup) {
18
+ let indexFrag = index !== undefined ? `\n index: '` + index + `',` : "";
19
+ let authFrag = authTable !== undefined && authGroup !== undefined ? `\n authTable: '` + authTable + `',\n authGroup: '` + authGroup + `',` : "";
20
+ return `import { util } from '@aws-appsync/utils';
21
+ export function request(ctx) {
22
+ const id = ctx.identity;
23
+ return {
24
+ operation: 'Invoke',
25
+ payload: {
26
+ readModelName: '` + readModelName + `',
27
+ kind: '` + kind + `',` + indexFrag + authFrag + `
28
+ arguments: ctx.args,
29
+ identity: ` + identityBlock + `
30
+ }
31
+ };
32
+ }
33
+ export function response(ctx) {
34
+ if (ctx.error) util.error(ctx.error.message, ctx.error.type);
35
+ return ctx.result;
36
+ }
37
+ `;
38
+ }
39
+
40
+ function invokeFieldTemplate(sourceType, kind, target, extra) {
41
+ return `import { util } from '@aws-appsync/utils';
42
+ export function request(ctx) {
43
+ const id = ctx.identity;
44
+ return {
45
+ operation: 'Invoke',
46
+ payload: {
47
+ readModelName: '` + sourceType + `',
48
+ kind: '` + kind + `',
49
+ target: '` + target + `',
50
+ source: ctx.source,
51
+ arguments: ctx.args,` + extra + `
52
+ identity: ` + identityBlock + `
53
+ }
54
+ };
55
+ }
56
+ export function response(ctx) {
57
+ if (ctx.error) util.error(ctx.error.message, ctx.error.type);
58
+ return ctx.result;
59
+ }
60
+ `;
61
+ }
62
+
63
+ function make(name, api, param, dataSourceName, indexes, subIdField, idResolverConfigs, idsResolverConfigs, param$1, opts) {
64
+ let name$1 = Stdlib_String.capitalize(name);
65
+ let registryEntry = Plugin_Helpers$ReventlessCore.queryFieldNamesRegistry[name$1];
66
+ let fieldNameForSingle = registryEntry !== undefined ? registryEntry.singleFieldName : AppSync_Resolver_Functions$PulumiAws.uncapitalize(name$1);
67
+ let includeIdParam = registryEntry !== undefined ? registryEntry.includeIdParam : true;
68
+ let fieldNameForAll = registryEntry !== undefined ? registryEntry.listFieldName : name$1 + "s";
69
+ let labelField = registryEntry !== undefined ? Stdlib_Option.getOr(registryEntry.labelField, "id") : "id";
70
+ let returnTypeName = registryEntry !== undefined ? registryEntry.returnTypeName : name$1;
71
+ PgQueryResolver_Builder$ReventlessAws.register({
72
+ readModelName: name$1,
73
+ labelField: labelField,
74
+ includeIdParam: includeIdParam
75
+ });
76
+ if (includeIdParam) {
77
+ PgQueryResolver_Builder$ReventlessAws.registerNodeType(returnTypeName, name$1);
78
+ }
79
+ let mkResolver = (resolverName, field, kind, index, authTable, authGroup) => AppSync_Resolver_Retrying$ReventlessAws.makeUnitJsResolver(resolverName, api, dataSourceName, "Query", field, invokeTemplate(name$1, kind, index, authTable, authGroup), opts);
80
+ let stripLeadingBy = s => {
81
+ if (s.startsWith("by") && s.length > 2) {
82
+ return s.slice(2, s.length);
83
+ } else {
84
+ return s;
85
+ }
86
+ };
87
+ let resourcesMaker = _allQueryDbs => {
88
+ let byId = mkResolver(Stdlib_String.capitalize(fieldNameForSingle), fieldNameForSingle, includeIdParam ? "getById" : "list", undefined, undefined, undefined);
89
+ let all = mkResolver(Stdlib_String.capitalize(fieldNameForAll), fieldNameForAll, "list", undefined, undefined, undefined);
90
+ let items = subIdField !== undefined ? [mkResolver(Stdlib_String.capitalize(fieldNameForSingle) + "Items", fieldNameForSingle + "Items", "items", undefined, undefined, undefined)] : [];
91
+ let byIds;
92
+ if (includeIdParam && subIdField === undefined) {
93
+ let byIdsField = fieldNameForAll + "ByIds";
94
+ byIds = [mkResolver(Stdlib_String.capitalize(byIdsField), byIdsField, "byIds", undefined, undefined, undefined)];
95
+ } else {
96
+ byIds = [];
97
+ }
98
+ let byIndex = indexes.map(ic => {
99
+ let index = ic.index;
100
+ let resolverName = Stdlib_String.capitalize(fieldNameForSingle) + "By" + Stdlib_String.capitalize(stripLeadingBy(index));
101
+ let field = fieldNameForSingle + "By" + Stdlib_String.capitalize(stripLeadingBy(index));
102
+ let match = ic.authorization;
103
+ if (match !== undefined) {
104
+ return mkResolver(resolverName, field, "index", index, Stdlib_String.capitalize(match.tableName), match.group);
105
+ } else {
106
+ return mkResolver(resolverName, field, "index", index, undefined, undefined);
107
+ }
108
+ });
109
+ let idResolvers = idResolverConfigs.map(config => {
110
+ let target = config.target;
111
+ let match = config.source;
112
+ let resolvedField = match.resolvedField;
113
+ let subId = match.subId;
114
+ let targetKey = Stdlib_String.capitalize(target.tableName);
115
+ let match$1;
116
+ match$1 = resolvedField.TAG === "Single" ? [
117
+ resolvedField._0,
118
+ false
119
+ ] : [
120
+ resolvedField._0,
121
+ true
122
+ ];
123
+ let field = match$1[0];
124
+ let ix = target.idField;
125
+ let targetFrag;
126
+ if (typeof ix !== "object") {
127
+ targetFrag = "";
128
+ } else if (ix.TAG === "Index") {
129
+ let ix$1 = ix._0;
130
+ targetFrag = `\n targetIndex: '` + ix$1 + `',\n targetIndexIdField: '` + ix$1 + `',`;
131
+ } else {
132
+ targetFrag = `\n targetIndex: '` + ix._0 + `',\n targetIndexIdField: '` + ix._1 + `',`;
133
+ }
134
+ let subIdFrag;
135
+ subIdFrag = typeof subId !== "object" ? "" : (
136
+ subId.TAG === "Field" ? `\n sourceSubId: { kind: 'field', name: '` + subId._0 + `' },` : `\n sourceSubId: { kind: 'arg', name: '` + subId._0 + `' },`
137
+ );
138
+ let extra = `\n sourceIdField: '` + match.idField + `',\n multi: ` + (
139
+ match$1[1] ? "true" : "false"
140
+ ) + `,` + targetFrag + subIdFrag;
141
+ return AppSync_Resolver_Retrying$ReventlessAws.makeUnitJsResolver(name$1 + Stdlib_String.capitalize(field), api, dataSourceName, name$1, field, invokeFieldTemplate(name$1, "resolveOne", targetKey, extra), opts);
142
+ });
143
+ let idsResolvers = idsResolverConfigs.map(config => {
144
+ let match = config.source;
145
+ let resolvedField = match.resolvedField;
146
+ let targetKey = Stdlib_String.capitalize(config.target.tableName);
147
+ let extra = `\n sourceIdsField: '` + match.idsField + `',`;
148
+ return AppSync_Resolver_Retrying$ReventlessAws.makeUnitJsResolver(name$1 + Stdlib_String.capitalize(resolvedField), api, dataSourceName, name$1, resolvedField, invokeFieldTemplate(name$1, "resolveMany", targetKey, extra), opts);
149
+ });
150
+ return [
151
+ byId,
152
+ all
153
+ ].concat(items).concat(byIds).concat(byIndex).concat(idResolvers).concat(idsResolvers).map(Util_AppSync$ReventlessAws.toResourceNative);
154
+ };
155
+ return {
156
+ resources: [],
157
+ resourcesMaker: resourcesMaker
158
+ };
159
+ }
160
+
161
+ let Resolver;
162
+
163
+ export {
164
+ Resolver,
165
+ identityBlock,
166
+ invokeTemplate,
167
+ invokeFieldTemplate,
168
+ make,
169
+ }
170
+ /* Util_AppSync-ReventlessAws Not a pure module */
@@ -9,7 +9,11 @@ module Postgres = QueryDbStorage_Postgres
9
9
  // platform when a `PgConnection` is supplied — and dispatch per read model:
10
10
  // Postgres (no table, no data source) unless the name is admin-exempt. The Stream
11
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).
12
+ // stream, so live updates (B3.3) are published from the projection Lambda after
13
+ // each save/delete instead. SelectableStream records subscription-enabled
14
+ // Postgres read models in `QueryDbBackend.postgresStreamRegistry` (a leaf module,
15
+ // to avoid a build cycle) so the projection-Lambda runtime builder knows which
16
+ // handlers need the AppSync-Events publish wiring.
13
17
  module Selectable = {
14
18
  type api = QueryDbStorage_Postgres.api
15
19
  type role = QueryDbStorage_Postgres.role
@@ -42,6 +46,7 @@ module SelectableStream = {
42
46
  ~opts,
43
47
  ) =>
44
48
  if QueryDbBackend.isPostgresFor(name) {
49
+ QueryDbBackend.postgresStreamRegistry->Set.add(name)
45
50
  Postgres.make(~name, ~indexes, ~subIdField?, ~ttl?, ~api, ~apiRole, ~opts)
46
51
  } else {
47
52
  DynamoDbStream.make(~name, ~indexes, ~subIdField?, ~ttl?, ~api, ~apiRole, ~opts)
@@ -19,6 +19,7 @@ let Selectable = {
19
19
 
20
20
  function make$1(name, indexes, subIdField, ttl, api, apiRole, opts) {
21
21
  if (QueryDbBackend$ReventlessAws.isPostgresFor(name)) {
22
+ QueryDbBackend$ReventlessAws.postgresStreamRegistry.add(name);
22
23
  return QueryDbStorage_Postgres$ReventlessAws.make(name, indexes, subIdField, ttl, api, apiRole, opts);
23
24
  } else {
24
25
  return QueryDbStorage_DynamoDbStream$ReventlessAws.make(name, indexes, subIdField, ttl, api, apiRole, opts);
@@ -48,4 +49,4 @@ export {
48
49
  Selectable,
49
50
  SelectableStream,
50
51
  }
51
- /* QueryDbStorage_DynamoDb-ReventlessAws Not a pure module */
52
+ /* QueryDbBackend-ReventlessAws Not a pure module */
@@ -37,7 +37,11 @@ let make: ReventlessCore.QueryDb_Adapter.storageMaker<api, role> = (
37
37
  }
38
38
  {
39
39
  ReventlessCore.QueryDb_Adapter.resources: [],
40
- dataSourceName: ""->Pulumi.Output.make,
40
+ // B3.2b: the shared PgQueryResolver Lambda data source. Deferred — resolved
41
+ // by PgQueryResolver_Builder.provision after every plugin is built (the
42
+ // resolvers that read it are created even later, inside the schema-pushed
43
+ // resourcesMaker). Same value for every Postgres read model.
44
+ dataSourceName: PgQueryResolver_Builder.dataSourceName,
41
45
  operations,
42
46
  }
43
47
  }
@@ -1,8 +1,8 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
- import * as Pulumi from "@pulumi/pulumi";
4
3
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
5
4
  import * as QueryDbBackend$ReventlessAws from "./QueryDbBackend.res.mjs";
5
+ import * as PgQueryResolver_Builder$ReventlessAws from "./PgQueryResolver_Builder.res.mjs";
6
6
  import * as QueryDbStorage_Postgres_Runtime$ReventlessAws from "./QueryDbStorage_Postgres_Runtime.res.mjs";
7
7
 
8
8
  function make(name, indexes, subIdField, param, param$1, param$2, param$3) {
@@ -10,7 +10,7 @@ function make(name, indexes, subIdField, param, param$1, param$2, param$3) {
10
10
  let operations = match !== undefined ? match.connectionConfig.apply(config => QueryDbStorage_Postgres_Runtime$ReventlessAws.opsFor(config, name, indexes, subIdField)) : Stdlib_JsError.throwWithMessage("QueryDbStorage_Postgres.make called without a QueryDbBackend selection");
11
11
  return {
12
12
  resources: [],
13
- dataSourceName: Pulumi.output(""),
13
+ dataSourceName: PgQueryResolver_Builder$ReventlessAws.dataSourceName,
14
14
  operations: operations
15
15
  };
16
16
  }
@@ -18,4 +18,4 @@ function make(name, indexes, subIdField, param, param$1, param$2, param$3) {
18
18
  export {
19
19
  make,
20
20
  }
21
- /* @pulumi/pulumi Not a pure module */
21
+ /* QueryDbBackend-ReventlessAws Not a pure module */
@@ -214,20 +214,9 @@ let finish = () =>
214
214
  let pgSelection = EventLogBackend.get()
215
215
  let pgConnectionFragment = switch pgSelection {
216
216
  | Some(sel) =>
217
- sel.connectionConfig->Pulumi.Output.apply(cc => {
218
- let pgConnectionJson =
219
- [
220
- ("host", cc.host->JSON.Encode.string),
221
- ("port", cc.port->Int.toFloat->JSON.Encode.float),
222
- ("database", cc.database->JSON.Encode.string),
223
- ("username", cc.username->JSON.Encode.string),
224
- ("secretArn", cc.secretArn->JSON.Encode.string),
225
- ]
226
- ->Dict.fromArray
227
- ->JSON.Encode.object
228
- ->JSON.stringify
229
- `,"pgConnection":${pgConnectionJson}`
230
- })
217
+ sel.connectionConfig->Pulumi.Output.apply(cc =>
218
+ `,"pgConnection":${cc->PgConnection.connectionConfigToJson->JSON.stringify}`
219
+ )
231
220
  | None => Pulumi.Output.make("")
232
221
  }
233
222
 
@@ -10,6 +10,7 @@ import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/
10
10
  import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
11
11
  import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
12
12
  import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
13
+ import * as PgConnection$ReventlessAws from "../Postgres/PgConnection.res.mjs";
13
14
  import * as EventLogBackend$ReventlessAws from "../EventLog/EventLogBackend.res.mjs";
14
15
  import * as RuntimeEnvironment_Lambda$ReventlessAws from "./RuntimeEnvironment_Lambda.res.mjs";
15
16
  import * as CommandTopicChannel_SQS_Sync$ReventlessAws from "../CommandTopic/CommandTopicChannel_SQS_Sync.res.mjs";
@@ -156,31 +157,7 @@ function finish() {
156
157
  let handlerOutputs = [];
157
158
  let packageDirs = {};
158
159
  let pgSelection = EventLogBackend$ReventlessAws.get();
159
- let pgConnectionFragment = pgSelection !== undefined ? pgSelection.connectionConfig.apply(cc => {
160
- let pgConnectionJson = JSON.stringify(Object.fromEntries([
161
- [
162
- "host",
163
- cc.host
164
- ],
165
- [
166
- "port",
167
- cc.port
168
- ],
169
- [
170
- "database",
171
- cc.database
172
- ],
173
- [
174
- "username",
175
- cc.username
176
- ],
177
- [
178
- "secretArn",
179
- cc.secretArn
180
- ]
181
- ]));
182
- return `,"pgConnection":` + pgConnectionJson;
183
- }) : Pulumi.output("");
160
+ let pgConnectionFragment = pgSelection !== undefined ? pgSelection.connectionConfig.apply(cc => `,"pgConnection":` + JSON.stringify(PgConnection$ReventlessAws.connectionConfigToJson(undefined, cc))) : Pulumi.output("");
184
161
  specs.forEach(spec => {
185
162
  let info = aggregateInfos[spec.aggregateName];
186
163
  if (info === undefined) {