@reventlessdev/reventless-aws 3.0.0-alpha.177 → 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.
- package/CHANGELOG.md +14 -0
- package/docker-compose.postgres.yml +21 -0
- package/package.json +6 -5
- package/scripts/run-pg-integration-tests.sh +58 -0
- package/src/Platform.res +37 -0
- package/src/Platform.res.mjs +41 -4
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +7 -1
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +10 -1
- package/src/adapter/Api/Platform_UIFragments_Lambda.res +7 -1
- package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +10 -1
- package/src/adapter/Counter/CounterHandler_DynamoDbStream.res +6 -21
- package/src/adapter/Counter/CounterHandler_DynamoDbStream.res.mjs +2 -17
- package/src/adapter/DcbEventLog/DcbBackend.res +4 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres.res +2 -2
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres.res.mjs +7 -1
- package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +1 -11
- package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +2 -24
- package/src/adapter/Postgres/PgConnection.res +70 -10
- package/src/adapter/Postgres/PgConnection.res.mjs +48 -3
- package/src/adapter/Postgres/PgMigration_Builder.res +127 -0
- package/src/adapter/Postgres/PgMigration_Builder.res.mjs +51 -0
- package/src/adapter/QueryDb/PgQueryResolver_Builder.res +11 -13
- package/src/adapter/QueryDb/PgQueryResolver_Builder.res.mjs +5 -24
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res +51 -3
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res.mjs +48 -18
- package/src/adapter/QueryDb/QueryDbBackend.res +8 -0
- package/src/adapter/QueryDb/QueryDbBackend.res.mjs +4 -1
- package/src/adapter/QueryDb/QueryDbResolvers.res.mjs +1 -1
- package/src/adapter/QueryDb/QueryDbResolvers_Lambda.res +31 -7
- package/src/adapter/QueryDb/QueryDbResolvers_Lambda.res.mjs +16 -10
- package/src/adapter/QueryDb/QueryDbStorage.res +6 -1
- package/src/adapter/QueryDb/QueryDbStorage.res.mjs +2 -1
- package/src/adapter/QueryDb/QueryDbStorage_Postgres.res.mjs +1 -1
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res +3 -14
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res.mjs +2 -25
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res +3 -14
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res.mjs +2 -25
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +7 -1
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res +78 -15
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res.mjs +43 -26
- package/src/adapter/Runtime/PgMigrationEntryPoint.mjs +40 -0
- package/src/adapter/Runtime/ReadModelEntryPoint.mjs +17 -5
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +10 -0
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +3 -0
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +9 -14
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +2 -25
- package/src/adapter/Runtime/StateTopicPublish.mjs +204 -0
- package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +17 -3
- package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res +86 -17
- package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res.mjs +50 -33
- package/src/adapter/StateTopic/StateTopic_AppSync.res +7 -1
- package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +10 -1
- package/src/plugin/cloner/ClonerRunner_Fargate.res +7 -1
- package/src/plugin/cloner/ClonerRunner_Fargate.res.mjs +10 -1
- package/src/util/Util_Bundle.res +71 -1
- package/src/util/Util_Bundle.res.mjs +49 -1
- package/tests/PgChangeFeedRelay_IntegrationTest.res +3 -3
- package/tests/PgConnectionJsonTest.res +84 -0
- package/tests/PgConnectionJsonTest.res.mjs +68 -0
- package/tests/PgMigrationEntryPoint_IntegrationTest.res +82 -0
- package/tests/PgMigrationEntryPoint_IntegrationTest.res.mjs +63 -0
- package/tests/PgPipeline_IntegrationTest.res +3 -1
- package/tests/PgQueryResolver_LambdaTest.res +59 -0
- package/tests/PgQueryResolver_LambdaTest.res.mjs +95 -16
- package/tests/StateTopicPublishTest.res +162 -0
- package/tests/StateTopicPublishTest.res.mjs +158 -0
- package/tests/Util_BundleEsmLoaderTest.res +57 -0
- package/tests/Util_BundleEsmLoaderTest.res.mjs +44 -0
|
@@ -4,6 +4,7 @@ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
|
4
4
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
5
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
6
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
7
|
+
import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
|
|
7
8
|
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
8
9
|
import * as Authorization$Reventless from "@reventlessdev/reventless-spec/src/types/Authorization.res.mjs";
|
|
9
10
|
import * as QueryDbListQuery$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/QueryDbListQuery.res.mjs";
|
|
@@ -11,6 +12,15 @@ import * as QueryDb_Callback$ReventlessCore from "@reventlessdev/reventless-core
|
|
|
11
12
|
|
|
12
13
|
let log = Logger$ReventlessCore.fromEnv();
|
|
13
14
|
|
|
15
|
+
function authIdField(group) {
|
|
16
|
+
let c = group[0];
|
|
17
|
+
if (c !== undefined) {
|
|
18
|
+
return c.toLowerCase() + group.slice(1, group.length) + "Id";
|
|
19
|
+
} else {
|
|
20
|
+
return "Id";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
14
24
|
function argObj(args) {
|
|
15
25
|
return Stdlib_Option.getOr(Stdlib_JSON.Decode.object(args), {});
|
|
16
26
|
}
|
|
@@ -55,7 +65,8 @@ async function runInterceptor(binding, payload) {
|
|
|
55
65
|
}
|
|
56
66
|
}
|
|
57
67
|
|
|
58
|
-
async function dispatch(binding, payload) {
|
|
68
|
+
async function dispatch(binding, lookupBindingOpt, payload) {
|
|
69
|
+
let lookupBinding = lookupBindingOpt !== undefined ? lookupBindingOpt : param => {};
|
|
59
70
|
let rm = payload.readModelName;
|
|
60
71
|
let match = await runInterceptor(binding, payload);
|
|
61
72
|
if (typeof match !== "object") {
|
|
@@ -84,7 +95,25 @@ async function dispatch(binding, payload) {
|
|
|
84
95
|
let indexName = Stdlib_Option.getOr(payload.index, "");
|
|
85
96
|
let field = Stdlib_Option.getOr(Stdlib_Option.flatMap(binding.indexes.find(ic => ic.index === indexName), ic => ic.idField), indexName);
|
|
86
97
|
let value = Stdlib_Option.getOr(argStr(payload.arguments, indexName), "");
|
|
87
|
-
|
|
98
|
+
let match$1 = payload.authGroup;
|
|
99
|
+
let match$2 = payload.authTable;
|
|
100
|
+
let authorized;
|
|
101
|
+
if (match$1 !== undefined && match$2 !== undefined && Identity$Reventless.hasGroup(payload.identity, match$1)) {
|
|
102
|
+
let authBinding = lookupBinding(match$2);
|
|
103
|
+
if (authBinding !== undefined) {
|
|
104
|
+
let items$1 = await authBinding.ops.load(value);
|
|
105
|
+
authorized = items$1.TAG === "Ok" ? Stdlib_Option.mapOr(Stdlib_Option.flatMap(items$1._0[0], row => argStr(row, authIdField(match$1))), false, owner => owner === payload.identity.username) : false;
|
|
106
|
+
} else {
|
|
107
|
+
authorized = false;
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
authorized = true;
|
|
111
|
+
}
|
|
112
|
+
if (authorized) {
|
|
113
|
+
return await binding.pushdowns.indexLookup(rm, field, value);
|
|
114
|
+
} else {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
88
117
|
case "items" :
|
|
89
118
|
let subIdField = binding.subIdField;
|
|
90
119
|
if (subIdField === undefined) {
|
|
@@ -98,8 +127,8 @@ async function dispatch(binding, payload) {
|
|
|
98
127
|
if (conn !== undefined) {
|
|
99
128
|
return conn;
|
|
100
129
|
}
|
|
101
|
-
let items$
|
|
102
|
-
return QueryDbListQuery$ReventlessCore.run(items$
|
|
130
|
+
let items$2 = await binding.pushdowns.scanAll(rm);
|
|
131
|
+
return QueryDbListQuery$ReventlessCore.run(items$2, argsDict, binding.capability, binding.labelField, param => {});
|
|
103
132
|
case "resolveMany" :
|
|
104
133
|
let target = Stdlib_Option.getOr(payload.target, rm);
|
|
105
134
|
let source = Stdlib_Option.getOr(payload.source, null);
|
|
@@ -110,23 +139,23 @@ async function dispatch(binding, payload) {
|
|
|
110
139
|
let source$1 = Stdlib_Option.getOr(payload.source, null);
|
|
111
140
|
let key = Stdlib_Option.getOr(argStr(source$1, Stdlib_Option.getOr(payload.sourceIdField, "")), "");
|
|
112
141
|
let ix = payload.targetIndex;
|
|
113
|
-
let items$
|
|
142
|
+
let items$3;
|
|
114
143
|
if (ix !== undefined) {
|
|
115
|
-
items$
|
|
144
|
+
items$3 = await binding.pushdowns.indexLookup(target$1, Stdlib_Option.getOr(payload.targetIndexIdField, ix), key);
|
|
116
145
|
} else {
|
|
117
|
-
let items$
|
|
118
|
-
items$
|
|
146
|
+
let items$4 = await binding.ops.load(key);
|
|
147
|
+
items$3 = items$4.TAG === "Ok" ? items$4._0 : [];
|
|
119
148
|
}
|
|
120
|
-
let match$
|
|
121
|
-
let match$
|
|
149
|
+
let match$3 = payload.sourceSubId;
|
|
150
|
+
let match$4 = binding.subIdField;
|
|
122
151
|
let filtered;
|
|
123
|
-
if (match$
|
|
124
|
-
let name = match$
|
|
125
|
-
let tmp = match$
|
|
152
|
+
if (match$3 !== undefined && match$4 !== undefined) {
|
|
153
|
+
let name = match$3.name;
|
|
154
|
+
let tmp = match$3.kind === "arg" ? argStr(payload.arguments, name) : argStr(source$1, name);
|
|
126
155
|
let subVal = Stdlib_Option.getOr(tmp, "");
|
|
127
|
-
filtered = items$
|
|
156
|
+
filtered = items$3.filter(it => Stdlib_Option.getOr(argStr(it, match$4), "") === subVal);
|
|
128
157
|
} else {
|
|
129
|
-
filtered = items$
|
|
158
|
+
filtered = items$3;
|
|
130
159
|
}
|
|
131
160
|
if (Stdlib_Option.getOr(payload.multi, false)) {
|
|
132
161
|
return filtered;
|
|
@@ -143,8 +172,8 @@ async function dispatch(binding, payload) {
|
|
|
143
172
|
}
|
|
144
173
|
} else {
|
|
145
174
|
let isMulti = Stdlib_Option.getOr(payload.multi, false);
|
|
146
|
-
let match$
|
|
147
|
-
switch (match$
|
|
175
|
+
let match$5 = payload.kind;
|
|
176
|
+
switch (match$5) {
|
|
148
177
|
case "getById" :
|
|
149
178
|
return null;
|
|
150
179
|
case "items" :
|
|
@@ -227,7 +256,7 @@ async function handler(payload, _context) {
|
|
|
227
256
|
}
|
|
228
257
|
let binding = bindings[bindingKey];
|
|
229
258
|
if (binding !== undefined) {
|
|
230
|
-
return await dispatch(binding, payload);
|
|
259
|
+
return await dispatch(binding, name => bindings[name], payload);
|
|
231
260
|
} else {
|
|
232
261
|
return Stdlib_JsError.throwWithMessage("PgQueryResolver: no binding registered for read model " + bindingKey);
|
|
233
262
|
}
|
|
@@ -235,6 +264,7 @@ async function handler(payload, _context) {
|
|
|
235
264
|
|
|
236
265
|
export {
|
|
237
266
|
log,
|
|
267
|
+
authIdField,
|
|
238
268
|
argObj,
|
|
239
269
|
argStr,
|
|
240
270
|
argStrs,
|
|
@@ -50,3 +50,11 @@ let isPostgresFor = (name: string): bool =>
|
|
|
50
50
|
/** Whether ANY read model on this platform is Postgres-backed (used to decide
|
|
51
51
|
whether the projection Lambdas need VPC/secret access). */
|
|
52
52
|
let isPostgres = (): bool => selectionRef.contents->Option.isSome
|
|
53
|
+
|
|
54
|
+
// B3.3: Postgres read models routed through `QueryDbStorage.SelectableStream`
|
|
55
|
+
// (i.e. subscription-enabled). Lives here — a leaf both the storage selector and
|
|
56
|
+
// the projection-Lambda runtime builders already import — so the runtime builder
|
|
57
|
+
// can consult it without pulling in QueryDbStorage (which would close a build
|
|
58
|
+
// cycle via QueryDbStorage_Postgres → PgQueryResolver_Builder). Membership means
|
|
59
|
+
// "publish live updates from the projection Lambda after save/delete".
|
|
60
|
+
let postgresStreamRegistry: Set.t<string> = Set.make()
|
|
@@ -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
|
-
/*
|
|
47
|
+
/* postgresStreamRegistry Not a pure module */
|
|
@@ -26,11 +26,21 @@ let identityBlock = `id != null && id.sub != null
|
|
|
26
26
|
? { userArn: id.userArn ?? null, accountId: id.accountId ?? null, username: id.username ?? null, provider: 'IAM' }
|
|
27
27
|
: null`
|
|
28
28
|
|
|
29
|
-
let invokeTemplate = (
|
|
29
|
+
let invokeTemplate = (
|
|
30
|
+
~readModelName: string,
|
|
31
|
+
~kind: string,
|
|
32
|
+
~index: option<string>=?,
|
|
33
|
+
~authTable: option<string>=?,
|
|
34
|
+
~authGroup: option<string>=?,
|
|
35
|
+
) => {
|
|
30
36
|
let indexFrag = switch index {
|
|
31
37
|
| Some(ix) => `\n index: '${ix}',`
|
|
32
38
|
| None => ""
|
|
33
39
|
}
|
|
40
|
+
let authFrag = switch (authTable, authGroup) {
|
|
41
|
+
| (Some(t), Some(g)) => `\n authTable: '${t}',\n authGroup: '${g}',`
|
|
42
|
+
| _ => ""
|
|
43
|
+
}
|
|
34
44
|
`import { util } from '@aws-appsync/utils';
|
|
35
45
|
export function request(ctx) {
|
|
36
46
|
const id = ctx.identity;
|
|
@@ -38,7 +48,7 @@ export function request(ctx) {
|
|
|
38
48
|
operation: 'Invoke',
|
|
39
49
|
payload: {
|
|
40
50
|
readModelName: '${readModelName}',
|
|
41
|
-
kind: '${kind}',${indexFrag}
|
|
51
|
+
kind: '${kind}',${indexFrag}${authFrag}
|
|
42
52
|
arguments: ctx.args,
|
|
43
53
|
identity: ${identityBlock}
|
|
44
54
|
}
|
|
@@ -129,14 +139,14 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
129
139
|
PgQueryResolver_Builder.registerNodeType(~typeName=returnTypeName, ~readModelName=name)
|
|
130
140
|
}
|
|
131
141
|
|
|
132
|
-
let mkResolver = (~resolverName, ~field, ~kind, ~index=?) =>
|
|
142
|
+
let mkResolver = (~resolverName, ~field, ~kind, ~index=?, ~authTable=?, ~authGroup=?) =>
|
|
133
143
|
Resolver.makeUnitJsResolver(
|
|
134
144
|
~name=resolverName,
|
|
135
145
|
~api,
|
|
136
146
|
~dataSourceName,
|
|
137
147
|
~type_="Query"->Pulumi.Input.make,
|
|
138
148
|
~field=field->Pulumi.Input.make,
|
|
139
|
-
~code=invokeTemplate(~readModelName=name, ~kind, ~index?),
|
|
149
|
+
~code=invokeTemplate(~readModelName=name, ~kind, ~index?, ~authTable?, ~authGroup?),
|
|
140
150
|
~opts,
|
|
141
151
|
)
|
|
142
152
|
|
|
@@ -192,12 +202,26 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
192
202
|
[]
|
|
193
203
|
}
|
|
194
204
|
|
|
195
|
-
// Per-index equality queries: {single}By{Index}.
|
|
196
|
-
|
|
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
|
|
197
210
|
let resolverName =
|
|
198
211
|
fieldNameForSingle->String.capitalize ++ "By" ++ index->stripLeadingBy->String.capitalize
|
|
199
212
|
let field = fieldNameForSingle ++ "By" ++ index->stripLeadingBy->String.capitalize
|
|
200
|
-
|
|
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
|
+
}
|
|
201
225
|
})
|
|
202
226
|
|
|
203
227
|
// @resolves — single cross-table field resolver on this (parent) type. The
|
|
@@ -14,8 +14,9 @@ let identityBlock = `id != null && id.sub != null
|
|
|
14
14
|
? { userArn: id.userArn ?? null, accountId: id.accountId ?? null, username: id.username ?? null, provider: 'IAM' }
|
|
15
15
|
: null`;
|
|
16
16
|
|
|
17
|
-
function invokeTemplate(readModelName, kind, index) {
|
|
17
|
+
function invokeTemplate(readModelName, kind, index, authTable, authGroup) {
|
|
18
18
|
let indexFrag = index !== undefined ? `\n index: '` + index + `',` : "";
|
|
19
|
+
let authFrag = authTable !== undefined && authGroup !== undefined ? `\n authTable: '` + authTable + `',\n authGroup: '` + authGroup + `',` : "";
|
|
19
20
|
return `import { util } from '@aws-appsync/utils';
|
|
20
21
|
export function request(ctx) {
|
|
21
22
|
const id = ctx.identity;
|
|
@@ -23,7 +24,7 @@ export function request(ctx) {
|
|
|
23
24
|
operation: 'Invoke',
|
|
24
25
|
payload: {
|
|
25
26
|
readModelName: '` + readModelName + `',
|
|
26
|
-
kind: '` + kind + `',` + indexFrag + `
|
|
27
|
+
kind: '` + kind + `',` + indexFrag + authFrag + `
|
|
27
28
|
arguments: ctx.args,
|
|
28
29
|
identity: ` + identityBlock + `
|
|
29
30
|
}
|
|
@@ -75,7 +76,7 @@ function make(name, api, param, dataSourceName, indexes, subIdField, idResolverC
|
|
|
75
76
|
if (includeIdParam) {
|
|
76
77
|
PgQueryResolver_Builder$ReventlessAws.registerNodeType(returnTypeName, name$1);
|
|
77
78
|
}
|
|
78
|
-
let mkResolver = (resolverName, field, kind, index) => AppSync_Resolver_Retrying$ReventlessAws.makeUnitJsResolver(resolverName, api, dataSourceName, "Query", field, invokeTemplate(name$1, kind, index), opts);
|
|
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);
|
|
79
80
|
let stripLeadingBy = s => {
|
|
80
81
|
if (s.startsWith("by") && s.length > 2) {
|
|
81
82
|
return s.slice(2, s.length);
|
|
@@ -84,21 +85,26 @@ function make(name, api, param, dataSourceName, indexes, subIdField, idResolverC
|
|
|
84
85
|
}
|
|
85
86
|
};
|
|
86
87
|
let resourcesMaker = _allQueryDbs => {
|
|
87
|
-
let byId = mkResolver(Stdlib_String.capitalize(fieldNameForSingle), fieldNameForSingle, includeIdParam ? "getById" : "list", undefined);
|
|
88
|
-
let all = mkResolver(Stdlib_String.capitalize(fieldNameForAll), fieldNameForAll, "list", undefined);
|
|
89
|
-
let items = subIdField !== undefined ? [mkResolver(Stdlib_String.capitalize(fieldNameForSingle) + "Items", fieldNameForSingle + "Items", "items", undefined)] : [];
|
|
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)] : [];
|
|
90
91
|
let byIds;
|
|
91
92
|
if (includeIdParam && subIdField === undefined) {
|
|
92
93
|
let byIdsField = fieldNameForAll + "ByIds";
|
|
93
|
-
byIds = [mkResolver(Stdlib_String.capitalize(byIdsField), byIdsField, "byIds", undefined)];
|
|
94
|
+
byIds = [mkResolver(Stdlib_String.capitalize(byIdsField), byIdsField, "byIds", undefined, undefined, undefined)];
|
|
94
95
|
} else {
|
|
95
96
|
byIds = [];
|
|
96
97
|
}
|
|
97
|
-
let byIndex = indexes.map(
|
|
98
|
-
let index =
|
|
98
|
+
let byIndex = indexes.map(ic => {
|
|
99
|
+
let index = ic.index;
|
|
99
100
|
let resolverName = Stdlib_String.capitalize(fieldNameForSingle) + "By" + Stdlib_String.capitalize(stripLeadingBy(index));
|
|
100
101
|
let field = fieldNameForSingle + "By" + Stdlib_String.capitalize(stripLeadingBy(index));
|
|
101
|
-
|
|
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
|
+
}
|
|
102
108
|
});
|
|
103
109
|
let idResolvers = idResolverConfigs.map(config => {
|
|
104
110
|
let target = config.target;
|
|
@@ -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
|
|
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
|
-
/*
|
|
52
|
+
/* 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
|
-
|
|
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) {
|
|
@@ -201,20 +201,9 @@ let finish = () =>
|
|
|
201
201
|
let pgSelection = EventLogBackend.get()
|
|
202
202
|
let pgConnectionFragment = switch pgSelection {
|
|
203
203
|
| Some(sel) =>
|
|
204
|
-
sel.connectionConfig->Pulumi.Output.apply(cc =>
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
("host", cc.host->JSON.Encode.string),
|
|
208
|
-
("port", cc.port->Int.toFloat->JSON.Encode.float),
|
|
209
|
-
("database", cc.database->JSON.Encode.string),
|
|
210
|
-
("username", cc.username->JSON.Encode.string),
|
|
211
|
-
("secretArn", cc.secretArn->JSON.Encode.string),
|
|
212
|
-
]
|
|
213
|
-
->Dict.fromArray
|
|
214
|
-
->JSON.Encode.object
|
|
215
|
-
->JSON.stringify
|
|
216
|
-
`,"pgConnection":${pgConnectionJson}`
|
|
217
|
-
})
|
|
204
|
+
sel.connectionConfig->Pulumi.Output.apply(cc =>
|
|
205
|
+
`,"pgConnection":${cc->PgConnection.connectionConfigToJson->JSON.stringify}`
|
|
206
|
+
)
|
|
218
207
|
| None => Pulumi.Output.make("")
|
|
219
208
|
}
|
|
220
209
|
|
|
@@ -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_Async$ReventlessAws from "../CommandTopic/CommandTopicChannel_SQS_Async.res.mjs";
|
|
@@ -148,31 +149,7 @@ function finish() {
|
|
|
148
149
|
let handlerOutputs = [];
|
|
149
150
|
let packageDirs = {};
|
|
150
151
|
let pgSelection = EventLogBackend$ReventlessAws.get();
|
|
151
|
-
let pgConnectionFragment = pgSelection !== undefined ? pgSelection.connectionConfig.apply(cc =>
|
|
152
|
-
let pgConnectionJson = JSON.stringify(Object.fromEntries([
|
|
153
|
-
[
|
|
154
|
-
"host",
|
|
155
|
-
cc.host
|
|
156
|
-
],
|
|
157
|
-
[
|
|
158
|
-
"port",
|
|
159
|
-
cc.port
|
|
160
|
-
],
|
|
161
|
-
[
|
|
162
|
-
"database",
|
|
163
|
-
cc.database
|
|
164
|
-
],
|
|
165
|
-
[
|
|
166
|
-
"username",
|
|
167
|
-
cc.username
|
|
168
|
-
],
|
|
169
|
-
[
|
|
170
|
-
"secretArn",
|
|
171
|
-
cc.secretArn
|
|
172
|
-
]
|
|
173
|
-
]));
|
|
174
|
-
return `,"pgConnection":` + pgConnectionJson;
|
|
175
|
-
}) : Pulumi.output("");
|
|
152
|
+
let pgConnectionFragment = pgSelection !== undefined ? pgSelection.connectionConfig.apply(cc => `,"pgConnection":` + JSON.stringify(PgConnection$ReventlessAws.connectionConfigToJson(undefined, cc))) : Pulumi.output("");
|
|
176
153
|
specs.forEach(spec => {
|
|
177
154
|
let info = aggregateInfos[spec.aggregateName];
|
|
178
155
|
if (info === undefined) {
|
|
@@ -99,7 +99,13 @@ export async function buildHandlersForConfig(config, opts = {}) {
|
|
|
99
99
|
// (see docs/plans/aws-postgres-change-feed-bridge.md), not by this Lambda.
|
|
100
100
|
const rawStorageOps = config.pgConnection
|
|
101
101
|
? (() => {
|
|
102
|
-
|
|
102
|
+
// C2: `lockStrategy` ("AdvisoryLocks" | "RowLocks") is the polyvariant's
|
|
103
|
+
// string name; `opsFor` defaults to "AdvisoryLocks" when it's absent.
|
|
104
|
+
const pgOps = pgDcbEventLogOpsFor(
|
|
105
|
+
config.pgConnection,
|
|
106
|
+
config.dcbEventLogTableName,
|
|
107
|
+
config.pgConnection.lockStrategy,
|
|
108
|
+
);
|
|
103
109
|
return { read: pgOps.read, append: pgOps.append, readStream: pgOps.readStream };
|
|
104
110
|
})()
|
|
105
111
|
: (() => {
|
|
@@ -18,6 +18,18 @@ type readModelInfo = {
|
|
|
18
18
|
|
|
19
19
|
let readModelInfos: dict<readModelInfo> = Dict.make()
|
|
20
20
|
|
|
21
|
+
// B3.3: the AppSync Events API a Postgres-backed stream read model publishes live
|
|
22
|
+
// updates to. Set once by `Platform.makePlatform` (monolithic mode) before the
|
|
23
|
+
// plugins build, so `finish` can add the APPSYNC_ENDPOINT env + appsync:EventPublish
|
|
24
|
+
// IAM to the projection Lambda. None → no live updates (no events API, or the
|
|
25
|
+
// platform runs without one).
|
|
26
|
+
type eventsApiConfig = {
|
|
27
|
+
endpoint: Pulumi.Output.t<string>,
|
|
28
|
+
apiArn: Pulumi.Output.t<string>,
|
|
29
|
+
}
|
|
30
|
+
let eventsApiConfig: ref<option<eventsApiConfig>> = ref(None)
|
|
31
|
+
let setEventsApiConfig = (cfg: eventsApiConfig) => eventsApiConfig := Some(cfg)
|
|
32
|
+
|
|
21
33
|
let registerReadModel = (
|
|
22
34
|
~readModelName,
|
|
23
35
|
~specModulePath,
|
|
@@ -147,20 +159,9 @@ let finish = () =>
|
|
|
147
159
|
readModelInfos->Dict.valuesToArray->Array.some(info => info.pgBacked)
|
|
148
160
|
let pgConnectionFragment = switch (qdbSelection, anyPgBacked) {
|
|
149
161
|
| (Some(sel), true) =>
|
|
150
|
-
sel.connectionConfig->Pulumi.Output.apply(cc =>
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
("host", cc.host->JSON.Encode.string),
|
|
154
|
-
("port", cc.port->Int.toFloat->JSON.Encode.float),
|
|
155
|
-
("database", cc.database->JSON.Encode.string),
|
|
156
|
-
("username", cc.username->JSON.Encode.string),
|
|
157
|
-
("secretArn", cc.secretArn->JSON.Encode.string),
|
|
158
|
-
]
|
|
159
|
-
->Dict.fromArray
|
|
160
|
-
->JSON.Encode.object
|
|
161
|
-
->JSON.stringify
|
|
162
|
-
`,"pgConnection":${pgConnectionJson}`
|
|
163
|
-
})
|
|
162
|
+
sel.connectionConfig->Pulumi.Output.apply(cc =>
|
|
163
|
+
`,"pgConnection":${cc->PgConnection.connectionConfigToJson->JSON.stringify}`
|
|
164
|
+
)
|
|
164
165
|
| _ => Pulumi.Output.make("")
|
|
165
166
|
}
|
|
166
167
|
|
|
@@ -192,6 +193,23 @@ let finish = () =>
|
|
|
192
193
|
let handlerPgFragment = info.pgBacked
|
|
193
194
|
? pgConnectionFragment
|
|
194
195
|
: Pulumi.Output.make("")
|
|
196
|
+
// B3.3: a subscription-enabled Postgres read model publishes live
|
|
197
|
+
// updates from the projection Lambda — bake its AppSync Events channel
|
|
198
|
+
// root (the plural LIST field name, same source as the DynamoDB
|
|
199
|
+
// subscriptionInfraHook). Deploy-time resolved, so a plain string.
|
|
200
|
+
let stateTopicFragment =
|
|
201
|
+
info.pgBacked &&
|
|
202
|
+
QueryDbBackend.postgresStreamRegistry->Set.has(spec.componentName) &&
|
|
203
|
+
eventsApiConfig.contents->Option.isSome
|
|
204
|
+
? {
|
|
205
|
+
let topic =
|
|
206
|
+
ReventlessCore.Plugin_Helpers.queryFieldNamesRegistry
|
|
207
|
+
->Dict.get(spec.componentName)
|
|
208
|
+
->Option.map(qn => qn.listFieldName)
|
|
209
|
+
->Option.getOr(spec.componentName)
|
|
210
|
+
`,"stateTopicName":${topic->JSON.Encode.string->JSON.stringify}`
|
|
211
|
+
}
|
|
212
|
+
: ""
|
|
195
213
|
let handlerJson =
|
|
196
214
|
Pulumi.Output.all4((
|
|
197
215
|
info.queryDbTableName,
|
|
@@ -203,7 +221,7 @@ let finish = () =>
|
|
|
203
221
|
// No stream resource (Postgres-backed source) → dispatch on the
|
|
204
222
|
// feed queue's ARN instead of a stream URN.
|
|
205
223
|
let sourceUrn = urns->Array.get(0)->Option.getOr(feedArn)
|
|
206
|
-
`{"specModule":${specModule},"mappingsModule":${mappingsModule},"queryDbTableName":"${tableName}","sourceUrn":"${sourceUrn}"${pgFragment}}`
|
|
224
|
+
`{"specModule":${specModule},"mappingsModule":${mappingsModule},"queryDbTableName":"${tableName}","sourceUrn":"${sourceUrn}"${pgFragment}${stateTopicFragment}}`
|
|
207
225
|
})
|
|
208
226
|
let _ = handlerOutputs->Array.push(handlerJson)
|
|
209
227
|
| None =>
|
|
@@ -223,6 +241,22 @@ let finish = () =>
|
|
|
223
241
|
let envVars: dict<Pulumi.Input.t<string>> = Dict.make()
|
|
224
242
|
envVars->Dict.set("HANDLER_CONFIG", handlerConfigOutput->Pulumi.Output.asInput)
|
|
225
243
|
|
|
244
|
+
// B3.3: when any Postgres-backed read model is subscription-enabled, the
|
|
245
|
+
// projection Lambda publishes live updates to the AppSync Events API —
|
|
246
|
+
// give it the endpoint (env) and appsync:EventPublish (IAM, below).
|
|
247
|
+
let anyPgStream =
|
|
248
|
+
readModelInfos
|
|
249
|
+
->Dict.keysToArray
|
|
250
|
+
->Array.some(rmName =>
|
|
251
|
+
(readModelInfos->Dict.get(rmName)->Option.mapOr(false, i => i.pgBacked)) &&
|
|
252
|
+
QueryDbBackend.postgresStreamRegistry->Set.has(rmName)
|
|
253
|
+
)
|
|
254
|
+
let pgStreamConfig = anyPgStream ? eventsApiConfig.contents : None
|
|
255
|
+
switch pgStreamConfig {
|
|
256
|
+
| Some(cfg) => envVars->Dict.set("APPSYNC_ENDPOINT", cfg.endpoint->Pulumi.Output.asInput)
|
|
257
|
+
| None => ()
|
|
258
|
+
}
|
|
259
|
+
|
|
226
260
|
let {code, sourceCodeHash} = Util_Bundle.buildCodeArchive(
|
|
227
261
|
~entryPointModule="@reventlessdev/reventless-aws/src/adapter/Runtime/ReadModelEntryPoint.mjs",
|
|
228
262
|
~packageDirs,
|
|
@@ -287,6 +321,35 @@ let finish = () =>
|
|
|
287
321
|
| _ => ()
|
|
288
322
|
}
|
|
289
323
|
|
|
324
|
+
// B3.3: grant the projection Lambda appsync:EventPublish on the events API
|
|
325
|
+
// so a Postgres-backed stream read model can push live-update descriptors.
|
|
326
|
+
switch pgStreamConfig {
|
|
327
|
+
| Some(cfg) =>
|
|
328
|
+
let _ = cfg.apiArn->Pulumi.Output.apply(apiArn => {
|
|
329
|
+
open PulumiAws.PolicyDocument
|
|
330
|
+
PulumiAws.IAM.RolePolicy.make(
|
|
331
|
+
~name="AllReadModels-appsyncPublish",
|
|
332
|
+
~args={
|
|
333
|
+
policy: PulumiAws.PolicyDocument.make(
|
|
334
|
+
~id="AllReadModels-appsyncPublishPolicy",
|
|
335
|
+
~statements=[
|
|
336
|
+
{
|
|
337
|
+
sid: "AllowPublishAppSyncEvents",
|
|
338
|
+
effect: Allow,
|
|
339
|
+
actions: Action("appsync:EventPublish"),
|
|
340
|
+
resources: Resource(apiArn ++ "/*"),
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
)
|
|
344
|
+
->PulumiAws.PolicyDocument.toJsonString
|
|
345
|
+
->Pulumi.Input.make,
|
|
346
|
+
role: runtime.parts.lambdaRole.id->Pulumi.Output.asInput,
|
|
347
|
+
},
|
|
348
|
+
)
|
|
349
|
+
})
|
|
350
|
+
| None => ()
|
|
351
|
+
}
|
|
352
|
+
|
|
290
353
|
let channelSpecs = storedSpecs->Array.map(({channelSpec}) => channelSpec)
|
|
291
354
|
let _connectResources = EventCollectorChannel.connect(
|
|
292
355
|
~name="AllReadModels",
|