@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.
- package/CHANGELOG.md +23 -0
- package/docker-compose.postgres.yml +21 -0
- package/package.json +7 -6
- package/scripts/run-pg-integration-tests.sh +58 -0
- package/src/Platform.res +49 -0
- package/src/Platform.res.mjs +48 -2
- 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 +279 -0
- package/src/adapter/QueryDb/PgQueryResolver_Builder.res.mjs +170 -0
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res +202 -11
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res.mjs +150 -9
- package/src/adapter/QueryDb/QueryDbBackend.res +8 -0
- package/src/adapter/QueryDb/QueryDbBackend.res.mjs +4 -1
- package/src/adapter/QueryDb/QueryDbResolvers.res +6 -5
- package/src/adapter/QueryDb/QueryDbResolvers.res.mjs +6 -3
- package/src/adapter/QueryDb/QueryDbResolvers_Lambda.res +288 -0
- package/src/adapter/QueryDb/QueryDbResolvers_Lambda.res.mjs +170 -0
- package/src/adapter/QueryDb/QueryDbStorage.res +6 -1
- package/src/adapter/QueryDb/QueryDbStorage.res.mjs +2 -1
- package/src/adapter/QueryDb/QueryDbStorage_Postgres.res +5 -1
- package/src/adapter/QueryDb/QueryDbStorage_Postgres.res.mjs +3 -3
- 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/PgQueryResolverEntryPoint.mjs +88 -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 +163 -2
- package/tests/PgQueryResolver_LambdaTest.res.mjs +226 -14
- 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
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// StateTopicPublish.mjs
|
|
2
|
+
// B3.3: Postgres live-update publisher — the projection-Lambda-side counterpart
|
|
3
|
+
// of the DynamoDB StateTopic Lambda (StateTopic_AppSync.res).
|
|
4
|
+
//
|
|
5
|
+
// On the DynamoDB path a shared Lambda reads the table's stream and POSTs a
|
|
6
|
+
// change descriptor to the AppSync Events API. Postgres read models have no
|
|
7
|
+
// stream, so (design (a), mirroring the reventless-local Sqlite/InMemory
|
|
8
|
+
// backends) the projection Lambda publishes the SAME descriptor itself, right
|
|
9
|
+
// after it writes the `qdb_<name>` row. The wire contract — channel layout,
|
|
10
|
+
// descriptor shape, SigV4 signing — is byte-identical to StateTopic_AppSync so
|
|
11
|
+
// the browser/AutoLive side cannot tell the two backends apart.
|
|
12
|
+
//
|
|
13
|
+
// Channel: /default/{pathSegment(topicName)}/{pathSegment(entityKey)}
|
|
14
|
+
// Descriptor: { changeKind: "Updated" | "Removed", id: <entityKey>,
|
|
15
|
+
// sortKeyValue?: <updatedAt | createdAt if present> }
|
|
16
|
+
//
|
|
17
|
+
// changeKind is fixed per operation (save→"Updated", delete→"Removed"): a
|
|
18
|
+
// Postgres upsert doesn't distinguish insert vs update, and the Sqlite/InMemory
|
|
19
|
+
// backends already collapse save→"Updated" (LocalBus.res:213) — the UI treats a
|
|
20
|
+
// first-seen "Updated" row as an insert, so no Added detection is needed.
|
|
21
|
+
//
|
|
22
|
+
// Publish failures are LOGGED AND SWALLOWED: the projection row is already
|
|
23
|
+
// committed when this runs, and the projection Lambda is at-least-once — throwing
|
|
24
|
+
// would re-run the projection and duplicate work. A dropped live notification
|
|
25
|
+
// only means the UI refreshes the list on its next load, not data loss.
|
|
26
|
+
//
|
|
27
|
+
// Uses native Node.js crypto + fetch with SigV4; credentials come from the
|
|
28
|
+
// Lambda execution role via the process.env AWS_* variables.
|
|
29
|
+
|
|
30
|
+
import { createHmac, createHash } from "node:crypto";
|
|
31
|
+
|
|
32
|
+
function sha256hex(data) {
|
|
33
|
+
return createHash("sha256").update(typeof data === "string" ? data : JSON.stringify(data)).digest("hex");
|
|
34
|
+
}
|
|
35
|
+
function hmacBuf(key, data) {
|
|
36
|
+
return createHmac("sha256", key).update(data).digest();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function signedHeaders(host, path, body, region) {
|
|
40
|
+
const accessKeyId = process.env.AWS_ACCESS_KEY_ID;
|
|
41
|
+
const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
|
|
42
|
+
const sessionToken = process.env.AWS_SESSION_TOKEN;
|
|
43
|
+
const now = new Date();
|
|
44
|
+
const amzDate = now.toISOString().replace(/[:\-]|\..../g, "").slice(0, 15) + "Z";
|
|
45
|
+
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
46
|
+
const headers = { host, "x-amz-date": amzDate, ...(sessionToken ? { "x-amz-security-token": sessionToken } : {}) };
|
|
47
|
+
const canonH = Object.entries(headers).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}:${v}\n`).join("");
|
|
48
|
+
const signH = Object.keys(headers).sort().join(";");
|
|
49
|
+
const cr = ["POST", path, "", canonH, signH, sha256hex(body)].join("\n");
|
|
50
|
+
const scope = `${dateStamp}/${region}/appsync/aws4_request`;
|
|
51
|
+
const sts = ["AWS4-HMAC-SHA256", amzDate, scope, sha256hex(cr)].join("\n");
|
|
52
|
+
const kDate = hmacBuf("AWS4" + secretAccessKey, dateStamp);
|
|
53
|
+
const kSigning = hmacBuf(hmacBuf(hmacBuf(kDate, region), "appsync"), "aws4_request");
|
|
54
|
+
const sig = createHmac("sha256", kSigning).update(sts).digest("hex");
|
|
55
|
+
return { ...headers, Authorization: `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${scope}, SignedHeaders=${signH}, Signature=${sig}` };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// AppSync Events channel segments allow only [A-Za-z0-9-]; anything else
|
|
59
|
+
// collapses to `-`. The descriptor body keeps the ORIGINAL entityKey so the UI
|
|
60
|
+
// can match it against GraphQL row ids. Same rule as StateTopic_AppSync and the
|
|
61
|
+
// UI's AutoLive.normalizeSegment.
|
|
62
|
+
function pathSegment(value) {
|
|
63
|
+
return String(value).replace(/[^A-Za-z0-9-]/g, "-");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Pick the natural sort timestamp from the saved state (updatedAt preferred,
|
|
67
|
+
// createdAt fallback). Mirrors StateTopic_AppSync.pickSortKeyValue and
|
|
68
|
+
// LocalBus.pickSortKeyValue.
|
|
69
|
+
function pickSortKeyValue(state) {
|
|
70
|
+
if (state && typeof state === "object" && !Array.isArray(state)) {
|
|
71
|
+
if (typeof state.updatedAt === "string") return state.updatedAt;
|
|
72
|
+
if (typeof state.createdAt === "string") return state.createdAt;
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Build the descriptor + channel and POST to {endpoint}/event.
|
|
78
|
+
// - endpoint: base AppSync Events HTTP endpoint (no trailing /event).
|
|
79
|
+
// - region: AWS region for the SigV4 credential scope.
|
|
80
|
+
// - topicName: the plugin-prefixed query LIST field name (channel root).
|
|
81
|
+
// - entityKey: ORIGINAL entity key (id or `id-subKey`) — descriptor body + path.
|
|
82
|
+
// - changeKind: "Updated" (save) | "Removed" (delete).
|
|
83
|
+
// - sortKeyValue: optional; included when defined.
|
|
84
|
+
// - dedupeId: AppSync publish `id` (idempotency hint).
|
|
85
|
+
export async function publishStateChange({ endpoint, region, topicName, entityKey, changeKind, sortKeyValue, dedupeId }) {
|
|
86
|
+
if (!endpoint || !topicName) return; // not live-enabled — no-op
|
|
87
|
+
try {
|
|
88
|
+
const url = new URL(endpoint);
|
|
89
|
+
const channel = "/default/" + pathSegment(topicName) + "/" + pathSegment(entityKey);
|
|
90
|
+
const descriptor = { changeKind, id: entityKey };
|
|
91
|
+
if (sortKeyValue !== undefined) descriptor.sortKeyValue = sortKeyValue;
|
|
92
|
+
const body = JSON.stringify({
|
|
93
|
+
id: dedupeId || (topicName + ":" + entityKey + ":" + changeKind),
|
|
94
|
+
channel,
|
|
95
|
+
events: [JSON.stringify(descriptor)],
|
|
96
|
+
});
|
|
97
|
+
const auth = await signedHeaders(url.hostname, "/event", body, region || process.env.AWS_REGION || "eu-west-1");
|
|
98
|
+
const res = await fetch(endpoint + "/event", {
|
|
99
|
+
method: "POST",
|
|
100
|
+
headers: {
|
|
101
|
+
accept: "application/json, text/javascript",
|
|
102
|
+
"content-encoding": "amz-1.0",
|
|
103
|
+
"content-type": "application/json; charset=UTF-8",
|
|
104
|
+
...auth,
|
|
105
|
+
},
|
|
106
|
+
body,
|
|
107
|
+
});
|
|
108
|
+
if (!res.ok) {
|
|
109
|
+
const txt = await res.text();
|
|
110
|
+
// Swallow (see module docstring): the row is already committed. Log
|
|
111
|
+
// structurally so a CloudWatch metric filter on
|
|
112
|
+
// "PG_STATE_TOPIC_PUBLISH_FAILED" can surface persistent breakage.
|
|
113
|
+
console.error(
|
|
114
|
+
"PG_STATE_TOPIC_PUBLISH_FAILED status=" + res.status +
|
|
115
|
+
" channel=" + channel +
|
|
116
|
+
" body=" + txt.slice(0, 500)
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
} catch (e) {
|
|
120
|
+
console.error("PG_STATE_TOPIC_PUBLISH_FAILED status=network err=" + (e && e.message));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Compose the entity key from a save/delete's id + optional sub-key. Matches
|
|
125
|
+
// StateTopic_AppSync.entityKeyFromRecord and QueryDbStorage_Sqlite.entityKeyFor:
|
|
126
|
+
// single-key tables → id; composite tables → `id-subKeyValue`.
|
|
127
|
+
export function entityKeyFor(id, state, subIdField) {
|
|
128
|
+
if (subIdField === undefined || subIdField === null) return String(id);
|
|
129
|
+
const subValue =
|
|
130
|
+
state && typeof state === "object" && !Array.isArray(state) ? state[subIdField] : undefined;
|
|
131
|
+
return subValue === undefined || subValue === null
|
|
132
|
+
? String(id)
|
|
133
|
+
: String(id) + "-" + String(subValue);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Wrap a Postgres QueryDb operation set so every save/delete also publishes a
|
|
137
|
+
// live-update descriptor. `liveConfig` = { endpoint, region, topicName,
|
|
138
|
+
// subIdField }. When endpoint/topicName are absent the ops pass through
|
|
139
|
+
// unchanged (non-stream / non-live read models pay nothing).
|
|
140
|
+
//
|
|
141
|
+
// Note on batch ops: saveBatch/deleteBatch fan out one publish per item, after
|
|
142
|
+
// the batch write resolves — the row set is committed as a unit, and each row
|
|
143
|
+
// gets its own descriptor exactly as N single writes would.
|
|
144
|
+
export function withLiveUpdates(ops, liveConfig) {
|
|
145
|
+
const { endpoint, topicName, region, subIdField } = liveConfig || {};
|
|
146
|
+
if (!endpoint || !topicName) return ops;
|
|
147
|
+
// `publish` is injectable for headless testing (default = the real SigV4
|
|
148
|
+
// publisher); tests pass a capturing stub so the wrapping logic can be
|
|
149
|
+
// asserted without fetch/crypto.
|
|
150
|
+
const publish = (liveConfig && liveConfig.publish) || publishStateChange;
|
|
151
|
+
|
|
152
|
+
const publishSaved = async (id, state) =>
|
|
153
|
+
publish({
|
|
154
|
+
endpoint, region, topicName,
|
|
155
|
+
entityKey: entityKeyFor(id, state, subIdField),
|
|
156
|
+
changeKind: "Updated",
|
|
157
|
+
sortKeyValue: pickSortKeyValue(state),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const publishRemovedKey = async (entityKey) =>
|
|
161
|
+
publish({ endpoint, region, topicName, entityKey, changeKind: "Removed" });
|
|
162
|
+
|
|
163
|
+
// Resolve the entity keys a delete will remove — computed BEFORE the delete so
|
|
164
|
+
// a partition-wide delete on a composite table can still enumerate its rows
|
|
165
|
+
// (mirrors QueryDbStorage_Sqlite.delete's rowKeysForPartition). `subIdOpt` is
|
|
166
|
+
// the adapter's `option<(field, subValue)>`: undefined (partition-wide) or a
|
|
167
|
+
// `[field, subValue]` array (one specific row).
|
|
168
|
+
const removedKeysFor = async (id, subIdOpt) => {
|
|
169
|
+
if (subIdOpt) return [String(id) + "-" + String(subIdOpt[1])];
|
|
170
|
+
if (subIdField === undefined || subIdField === null) return [String(id)];
|
|
171
|
+
// Composite table, whole-partition delete (`Delete(id)` / `DeleteMany`):
|
|
172
|
+
// read the live rows so each removed sub-row gets its own descriptor,
|
|
173
|
+
// exactly as N DynamoDB stream REMOVE records would.
|
|
174
|
+
const res = await ops.load(id);
|
|
175
|
+
const rows = res && res.TAG === "Ok" ? res._0 : [];
|
|
176
|
+
return rows.map((row) => entityKeyFor(id, row, subIdField));
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
...ops,
|
|
181
|
+
save: async (id, state, saveMode, ttl) => {
|
|
182
|
+
const r = await ops.save(id, state, saveMode, ttl);
|
|
183
|
+
await publishSaved(id, state);
|
|
184
|
+
return r;
|
|
185
|
+
},
|
|
186
|
+
saveBatch: async (items) => {
|
|
187
|
+
const r = await ops.saveBatch(items);
|
|
188
|
+
await Promise.all(items.map(([id, state]) => publishSaved(id, state)));
|
|
189
|
+
return r;
|
|
190
|
+
},
|
|
191
|
+
delete: async (id, subIdOpt) => {
|
|
192
|
+
const keys = await removedKeysFor(id, subIdOpt);
|
|
193
|
+
const r = await ops.delete(id, subIdOpt);
|
|
194
|
+
await Promise.all(keys.map(publishRemovedKey));
|
|
195
|
+
return r;
|
|
196
|
+
},
|
|
197
|
+
deleteBatch: async (entries) => {
|
|
198
|
+
const keyGroups = await Promise.all(entries.map(([id, subIdOpt]) => removedKeysFor(id, subIdOpt)));
|
|
199
|
+
const r = await ops.deleteBatch(entries);
|
|
200
|
+
await Promise.all(keyGroups.flat().map(publishRemovedKey));
|
|
201
|
+
return r;
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
@@ -11,6 +11,7 @@ import { tag as requestContextTag } from "@reventlessdev/reventless-core/src/Req
|
|
|
11
11
|
import { handleAction } from "@reventlessdev/reventless-core/src/Projection.res.mjs";
|
|
12
12
|
import { load, loadStream, save, saveBatch, count, $$delete, deleteBatch } from "@reventlessdev/reventless-aws/src/adapter/QueryDb/QueryDbStorage_DynamoDb_Runtime.res.mjs";
|
|
13
13
|
import { opsFor as pgQdbOpsFor } from "@reventlessdev/reventless-aws/src/adapter/QueryDb/QueryDbStorage_Postgres_Runtime.res.mjs";
|
|
14
|
+
import { withLiveUpdates } from "./StateTopicPublish.mjs";
|
|
14
15
|
import { handleStreamEvent } from "@reventlessdev/reventless-aws/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream_Runtime.res.mjs";
|
|
15
16
|
|
|
16
17
|
const dynamicImport = (specifier) => import('/var/task/node_modules/' + specifier);
|
|
@@ -44,12 +45,22 @@ function groupBySource(records) {
|
|
|
44
45
|
// `pgConnection`, when present, selects the Postgres QueryDb runtime for this
|
|
45
46
|
// slice's view table (`queryDbTableName` is then the slice spec name, the shared
|
|
46
47
|
// `qdb_<name>` discriminator). Absent → the DynamoDB path is byte-identical.
|
|
47
|
-
function buildJsonEventsHandler(specModule, projectionModule, queryDbTableName, pgConnection) {
|
|
48
|
+
function buildJsonEventsHandler(specModule, projectionModule, queryDbTableName, pgConnection, stateTopicName) {
|
|
48
49
|
let queryDbOps;
|
|
49
50
|
if (pgConnection) {
|
|
50
51
|
const indexes = (specModule.config && specModule.config.indexes) || [];
|
|
51
52
|
const subIdField = specModule.subIdConfig ? specModule.subIdConfig.subIdField : undefined;
|
|
52
|
-
|
|
53
|
+
const pgOps = pgQdbOpsFor(pgConnection, queryDbTableName, indexes, subIdField);
|
|
54
|
+
// B3.3: subscription-enabled (Stream) Postgres view slice → publish a
|
|
55
|
+
// live-update descriptor after save/delete. `stateTopicName` + APPSYNC_ENDPOINT
|
|
56
|
+
// gate it; absent → withLiveUpdates passes pgOps through. The view slice's id
|
|
57
|
+
// is the save's first arg (no id injection), so no wrap-ordering concern.
|
|
58
|
+
queryDbOps = withLiveUpdates(pgOps, {
|
|
59
|
+
endpoint: process.env.APPSYNC_ENDPOINT,
|
|
60
|
+
region: process.env.AWS_REGION,
|
|
61
|
+
topicName: stateTopicName,
|
|
62
|
+
subIdField,
|
|
63
|
+
});
|
|
53
64
|
} else {
|
|
54
65
|
const table = makeTableRef(queryDbTableName);
|
|
55
66
|
queryDbOps = {
|
|
@@ -105,6 +116,9 @@ function expandHandlers(config) {
|
|
|
105
116
|
// Shared across a plugin's slices (all follow the platform backend toggle);
|
|
106
117
|
// hoisted to the top level of the compressed config like base/sourceUrn.
|
|
107
118
|
pgConnection: config.pgConnection,
|
|
119
|
+
// B3.3: per-slice AppSync Events channel root (list field name). Present
|
|
120
|
+
// only for subscription-enabled (Stream) Postgres view slices.
|
|
121
|
+
stateTopicName: h.t,
|
|
108
122
|
};
|
|
109
123
|
});
|
|
110
124
|
}
|
|
@@ -118,7 +132,7 @@ async function buildAllHandlers() {
|
|
|
118
132
|
dynamicImport(h.specModule),
|
|
119
133
|
dynamicImport(h.projectionModule),
|
|
120
134
|
]);
|
|
121
|
-
const jsonEventsHandler = buildJsonEventsHandler(specModule, projectionModule, h.queryDbTableName, h.pgConnection);
|
|
135
|
+
const jsonEventsHandler = buildJsonEventsHandler(specModule, projectionModule, h.queryDbTableName, h.pgConnection, h.stateTopicName);
|
|
122
136
|
const streamHandler = (event, context) => handleStreamEvent(jsonEventsHandler, event, context);
|
|
123
137
|
const existing = handlers[h.sourceUrn] || [];
|
|
124
138
|
existing.push(streamHandler);
|
|
@@ -13,6 +13,9 @@ type handlerEntry = {
|
|
|
13
13
|
projectionModule: string,
|
|
14
14
|
queryDbTableName: string,
|
|
15
15
|
sourceUrn: string,
|
|
16
|
+
// B3.3: AppSync Events channel root for a subscription-enabled Postgres view
|
|
17
|
+
// slice; "" when the slice isn't live-enabled (compact key `t`, omitted then).
|
|
18
|
+
stateTopicName: string,
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
// Longest common prefix of a set of strings. Used to factor the shared
|
|
@@ -40,6 +43,27 @@ type sliceInfo = {
|
|
|
40
43
|
|
|
41
44
|
let sliceInfos: dict<sliceInfo> = Dict.make()
|
|
42
45
|
|
|
46
|
+
// B3.3: the AppSync Events API a Postgres-backed stream view slice publishes live
|
|
47
|
+
// updates to. Set once by `Platform.makePlatform` before plugins build (mirrors
|
|
48
|
+
// EventCollectorRuntime_Builder_Single). None → no live updates.
|
|
49
|
+
type eventsApiConfig = {
|
|
50
|
+
endpoint: Pulumi.Output.t<string>,
|
|
51
|
+
apiArn: Pulumi.Output.t<string>,
|
|
52
|
+
}
|
|
53
|
+
let eventsApiConfig: ref<option<eventsApiConfig>> = ref(None)
|
|
54
|
+
let setEventsApiConfig = (cfg: eventsApiConfig) => eventsApiConfig := Some(cfg)
|
|
55
|
+
|
|
56
|
+
// Channel root for a subscription-enabled Postgres view slice (the plural LIST
|
|
57
|
+
// field name, same source as the DynamoDB subscriptionInfraHook); "" when the
|
|
58
|
+
// slice isn't stream-routed or there's no events API.
|
|
59
|
+
let stateTopicNameFor = (name: string): string =>
|
|
60
|
+
QueryDbBackend.postgresStreamRegistry->Set.has(name) && eventsApiConfig.contents->Option.isSome
|
|
61
|
+
? ReventlessCore.Plugin_Helpers.queryFieldNamesRegistry
|
|
62
|
+
->Dict.get(name)
|
|
63
|
+
->Option.map(qn => qn.listFieldName)
|
|
64
|
+
->Option.getOr(name)
|
|
65
|
+
: ""
|
|
66
|
+
|
|
43
67
|
let registerStateViewSlice = (
|
|
44
68
|
~name,
|
|
45
69
|
~specModulePath,
|
|
@@ -130,6 +154,9 @@ let buildLambda = (
|
|
|
130
154
|
~packageDirs,
|
|
131
155
|
~channelSpecs,
|
|
132
156
|
~feedQueue: option<PulumiAws.SQS.Queue.t>=None,
|
|
157
|
+
// B3.3: Some when any slice on this Lambda is a subscription-enabled Postgres
|
|
158
|
+
// view slice — arms the APPSYNC_ENDPOINT env + appsync:EventPublish IAM.
|
|
159
|
+
~pgStreamConfig: option<eventsApiConfig>=None,
|
|
133
160
|
~memorySize=1024,
|
|
134
161
|
~timeout=30,
|
|
135
162
|
) => {
|
|
@@ -141,20 +168,9 @@ let buildLambda = (
|
|
|
141
168
|
let qdbSelection = QueryDbBackend.get()
|
|
142
169
|
let pgConnectionFragment = switch qdbSelection {
|
|
143
170
|
| Some(sel) =>
|
|
144
|
-
sel.connectionConfig->Pulumi.Output.apply(cc =>
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
("host", cc.host->JSON.Encode.string),
|
|
148
|
-
("port", cc.port->Int.toFloat->JSON.Encode.float),
|
|
149
|
-
("database", cc.database->JSON.Encode.string),
|
|
150
|
-
("username", cc.username->JSON.Encode.string),
|
|
151
|
-
("secretArn", cc.secretArn->JSON.Encode.string),
|
|
152
|
-
]
|
|
153
|
-
->Dict.fromArray
|
|
154
|
-
->JSON.Encode.object
|
|
155
|
-
->JSON.stringify
|
|
156
|
-
`,"pgConnection":${pgConnectionJson}`
|
|
157
|
-
})
|
|
171
|
+
sel.connectionConfig->Pulumi.Output.apply(cc =>
|
|
172
|
+
`,"pgConnection":${cc->PgConnection.connectionConfigToJson->JSON.stringify}`
|
|
173
|
+
)
|
|
158
174
|
| None => Pulumi.Output.make("")
|
|
159
175
|
}
|
|
160
176
|
|
|
@@ -191,11 +207,16 @@ let buildLambda = (
|
|
|
191
207
|
->JSON.stringifyAny
|
|
192
208
|
->Option.getOr(`""`)
|
|
193
209
|
let q = h.queryDbTableName->JSON.stringifyAny->Option.getOr(`""`)
|
|
210
|
+
// B3.3: compact key `t` = live-update channel root; omit when empty.
|
|
211
|
+
let t =
|
|
212
|
+
h.stateTopicName == ""
|
|
213
|
+
? ""
|
|
214
|
+
: `,"t":${h.stateTopicName->JSON.Encode.string->JSON.stringify}`
|
|
194
215
|
if sharedUrn != "" {
|
|
195
|
-
`{"s":${s},"p":${p},"q":${q}}`
|
|
216
|
+
`{"s":${s},"p":${p},"q":${q}${t}}`
|
|
196
217
|
} else {
|
|
197
218
|
let u = h.sourceUrn->JSON.stringifyAny->Option.getOr(`""`)
|
|
198
|
-
`{"s":${s},"p":${p},"q":${q},"u":${u}}`
|
|
219
|
+
`{"s":${s},"p":${p},"q":${q},"u":${u}${t}}`
|
|
199
220
|
}
|
|
200
221
|
})
|
|
201
222
|
->Array.join(",")
|
|
@@ -207,6 +228,13 @@ let buildLambda = (
|
|
|
207
228
|
let envVars: dict<Pulumi.Input.t<string>> = Dict.make()
|
|
208
229
|
envVars->Dict.set("HANDLER_CONFIG", handlerConfigOutput->Pulumi.Output.asInput)
|
|
209
230
|
|
|
231
|
+
// B3.3: give the projection Lambda the AppSync Events endpoint when any of its
|
|
232
|
+
// slices publish live updates (IAM added below).
|
|
233
|
+
switch pgStreamConfig {
|
|
234
|
+
| Some(cfg) => envVars->Dict.set("APPSYNC_ENDPOINT", cfg.endpoint->Pulumi.Output.asInput)
|
|
235
|
+
| None => ()
|
|
236
|
+
}
|
|
237
|
+
|
|
210
238
|
// Bundle the framework packages alongside the entry point so the deployed
|
|
211
239
|
// Lambda picks up local edits without waiting for a Lambda Layer rebuild
|
|
212
240
|
// (the layer fetches @reventlessdev/reventless-* from GitHub Packages, not
|
|
@@ -286,6 +314,35 @@ let buildLambda = (
|
|
|
286
314
|
| None => ()
|
|
287
315
|
}
|
|
288
316
|
|
|
317
|
+
// B3.3: grant appsync:EventPublish so subscription-enabled Postgres view slices
|
|
318
|
+
// can push live-update descriptors from this Lambda.
|
|
319
|
+
switch pgStreamConfig {
|
|
320
|
+
| Some(cfg) =>
|
|
321
|
+
let _ = cfg.apiArn->Pulumi.Output.apply(apiArn => {
|
|
322
|
+
open PulumiAws.PolicyDocument
|
|
323
|
+
PulumiAws.IAM.RolePolicy.make(
|
|
324
|
+
~name="AllStateViewSlices-appsyncPublish",
|
|
325
|
+
~args={
|
|
326
|
+
policy: PulumiAws.PolicyDocument.make(
|
|
327
|
+
~id="AllStateViewSlices-appsyncPublishPolicy",
|
|
328
|
+
~statements=[
|
|
329
|
+
{
|
|
330
|
+
sid: "AllowPublishAppSyncEvents",
|
|
331
|
+
effect: Allow,
|
|
332
|
+
actions: Action("appsync:EventPublish"),
|
|
333
|
+
resources: Resource(apiArn ++ "/*"),
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
)
|
|
337
|
+
->PulumiAws.PolicyDocument.toJsonString
|
|
338
|
+
->Pulumi.Input.make,
|
|
339
|
+
role: runtime.parts.lambdaRole.id->Pulumi.Output.asInput,
|
|
340
|
+
},
|
|
341
|
+
)
|
|
342
|
+
})
|
|
343
|
+
| None => ()
|
|
344
|
+
}
|
|
345
|
+
|
|
289
346
|
let _connectResources = EventCollectorChannel.connect(
|
|
290
347
|
~name="AllStateViewSlices",
|
|
291
348
|
~channelSpecs,
|
|
@@ -347,7 +404,7 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
|
|
|
347
404
|
let packageDirs: dict<string> = Dict.make()
|
|
348
405
|
let allQueryDbResources: array<ReventlessInfra.Adapter.resource> = []
|
|
349
406
|
|
|
350
|
-
sliceInfos->Dict.forEachWithKey((info,
|
|
407
|
+
sliceInfos->Dict.forEachWithKey((info, name) => {
|
|
351
408
|
info.queryDbResources->Array.forEach(r => allQueryDbResources->Array.push(r)->ignore)
|
|
352
409
|
let specPkg = Util_Bundle.extractPackageName(info.specModulePath)
|
|
353
410
|
packageDirs->Dict.set(specPkg, Util_Bundle.resolvePackageRoot(specPkg))
|
|
@@ -369,18 +426,24 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
|
|
|
369
426
|
projectionModule: info.projectionModulePath,
|
|
370
427
|
queryDbTableName: tableName,
|
|
371
428
|
sourceUrn: urn,
|
|
429
|
+
stateTopicName: stateTopicNameFor(name),
|
|
372
430
|
}
|
|
373
431
|
entry
|
|
374
432
|
})
|
|
375
433
|
let _ = handlerOutputs->Array.push(handlerJson)
|
|
376
434
|
})
|
|
377
435
|
|
|
436
|
+
let anyPgStream =
|
|
437
|
+
sliceInfos
|
|
438
|
+
->Dict.keysToArray
|
|
439
|
+
->Array.some(n => QueryDbBackend.postgresStreamRegistry->Set.has(n))
|
|
378
440
|
buildLambda(
|
|
379
441
|
~parent,
|
|
380
442
|
~handlerOutputs,
|
|
381
443
|
~packageDirs,
|
|
382
444
|
~channelSpecs=[{channel: channel, eventTopics, resources: allQueryDbResources}],
|
|
383
445
|
~feedQueue,
|
|
446
|
+
~pgStreamConfig=anyPgStream ? eventsApiConfig.contents : None,
|
|
384
447
|
)
|
|
385
448
|
| None =>
|
|
386
449
|
log.warn(
|
|
@@ -445,6 +508,7 @@ let finish = () =>
|
|
|
445
508
|
queryDbTableName: tableName,
|
|
446
509
|
// No stream resource (Postgres) → dispatch on the feed queue ARN.
|
|
447
510
|
sourceUrn: urns->Array.get(0)->Option.getOr(feedArn),
|
|
511
|
+
stateTopicName: stateTopicNameFor(spec.componentName),
|
|
448
512
|
}
|
|
449
513
|
entry
|
|
450
514
|
})
|
|
@@ -457,12 +521,17 @@ let finish = () =>
|
|
|
457
521
|
}
|
|
458
522
|
})
|
|
459
523
|
|
|
524
|
+
let anyPgStream =
|
|
525
|
+
storedSpecs->Array.some(spec =>
|
|
526
|
+
QueryDbBackend.postgresStreamRegistry->Set.has(spec.componentName)
|
|
527
|
+
)
|
|
460
528
|
buildLambda(
|
|
461
529
|
~parent,
|
|
462
530
|
~handlerOutputs,
|
|
463
531
|
~packageDirs,
|
|
464
532
|
~channelSpecs=storedSpecs->Array.map(({channelSpec}) => channelSpec),
|
|
465
533
|
~feedQueue,
|
|
534
|
+
~pgStreamConfig=anyPgStream ? eventsApiConfig.contents : None,
|
|
466
535
|
~memorySize=maxMemorySize,
|
|
467
536
|
~timeout=maxTimeout,
|
|
468
537
|
)
|
|
@@ -13,8 +13,10 @@ import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/co
|
|
|
13
13
|
import * as DcbBackend$ReventlessAws from "../DcbEventLog/DcbBackend.res.mjs";
|
|
14
14
|
import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
|
|
15
15
|
import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
|
|
16
|
+
import * as PgConnection$ReventlessAws from "../Postgres/PgConnection.res.mjs";
|
|
16
17
|
import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
|
|
17
18
|
import * as QueryDbBackend$ReventlessAws from "../QueryDb/QueryDbBackend.res.mjs";
|
|
19
|
+
import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_Helpers.res.mjs";
|
|
18
20
|
import * as PgProjectionFeed$ReventlessAws from "../Postgres/PgProjectionFeed.res.mjs";
|
|
19
21
|
import * as RuntimeEnvironment_Lambda$ReventlessAws from "./RuntimeEnvironment_Lambda.res.mjs";
|
|
20
22
|
import * as EventCollectorChannel_DynamoDbStream$ReventlessAws from "../EventCollector/EventCollectorChannel_DynamoDbStream.res.mjs";
|
|
@@ -38,6 +40,22 @@ function commonPrefix(strings) {
|
|
|
38
40
|
|
|
39
41
|
let sliceInfos = {};
|
|
40
42
|
|
|
43
|
+
let eventsApiConfig = {
|
|
44
|
+
contents: undefined
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function setEventsApiConfig(cfg) {
|
|
48
|
+
eventsApiConfig.contents = cfg;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function stateTopicNameFor(name) {
|
|
52
|
+
if (QueryDbBackend$ReventlessAws.postgresStreamRegistry.has(name) && Stdlib_Option.isSome(eventsApiConfig.contents)) {
|
|
53
|
+
return Stdlib_Option.getOr(Stdlib_Option.map(Plugin_Helpers$ReventlessCore.queryFieldNamesRegistry[name], qn => qn.listFieldName), name);
|
|
54
|
+
} else {
|
|
55
|
+
return "";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
41
59
|
function registerStateViewSlice(name, specModulePath, projectionModulePath, queryDbTableName, queryDbResourcesOpt) {
|
|
42
60
|
let queryDbResources = queryDbResourcesOpt !== undefined ? queryDbResourcesOpt : [];
|
|
43
61
|
sliceInfos[name] = {
|
|
@@ -88,8 +106,9 @@ let finished = {
|
|
|
88
106
|
contents: false
|
|
89
107
|
};
|
|
90
108
|
|
|
91
|
-
function buildLambda(parent, handlerOutputs, packageDirs, channelSpecs, feedQueueOpt, memorySizeOpt, timeoutOpt) {
|
|
109
|
+
function buildLambda(parent, handlerOutputs, packageDirs, channelSpecs, feedQueueOpt, pgStreamConfigOpt, memorySizeOpt, timeoutOpt) {
|
|
92
110
|
let feedQueue = feedQueueOpt !== undefined ? Primitive_option.valFromOption(feedQueueOpt) : undefined;
|
|
111
|
+
let pgStreamConfig = pgStreamConfigOpt !== undefined ? Primitive_option.valFromOption(pgStreamConfigOpt) : undefined;
|
|
93
112
|
let memorySize = memorySizeOpt !== undefined ? memorySizeOpt : 1024;
|
|
94
113
|
let timeout = timeoutOpt !== undefined ? timeoutOpt : 30;
|
|
95
114
|
let opts_parent = parent;
|
|
@@ -97,31 +116,7 @@ function buildLambda(parent, handlerOutputs, packageDirs, channelSpecs, feedQueu
|
|
|
97
116
|
parent: opts_parent
|
|
98
117
|
};
|
|
99
118
|
let qdbSelection = QueryDbBackend$ReventlessAws.get();
|
|
100
|
-
let pgConnectionFragment = qdbSelection !== undefined ? qdbSelection.connectionConfig.apply(cc =>
|
|
101
|
-
let pgConnectionJson = JSON.stringify(Object.fromEntries([
|
|
102
|
-
[
|
|
103
|
-
"host",
|
|
104
|
-
cc.host
|
|
105
|
-
],
|
|
106
|
-
[
|
|
107
|
-
"port",
|
|
108
|
-
cc.port
|
|
109
|
-
],
|
|
110
|
-
[
|
|
111
|
-
"database",
|
|
112
|
-
cc.database
|
|
113
|
-
],
|
|
114
|
-
[
|
|
115
|
-
"username",
|
|
116
|
-
cc.username
|
|
117
|
-
],
|
|
118
|
-
[
|
|
119
|
-
"secretArn",
|
|
120
|
-
cc.secretArn
|
|
121
|
-
]
|
|
122
|
-
]));
|
|
123
|
-
return `,"pgConnection":` + pgConnectionJson;
|
|
124
|
-
}) : Pulumi.output("");
|
|
119
|
+
let pgConnectionFragment = qdbSelection !== undefined ? qdbSelection.connectionConfig.apply(cc => `,"pgConnection":` + JSON.stringify(PgConnection$ReventlessAws.connectionConfigToJson(undefined, cc))) : Pulumi.output("");
|
|
125
120
|
let handlerConfigOutput = Pulumi.all([
|
|
126
121
|
Pulumi.all(handlerOutputs),
|
|
127
122
|
pgConnectionFragment
|
|
@@ -140,11 +135,12 @@ function buildLambda(parent, handlerOutputs, packageDirs, channelSpecs, feedQueu
|
|
|
140
135
|
let s = Stdlib_Option.getOr(JSON.stringify(h.specModule.slice(baseLen, h.specModule.length)), `""`);
|
|
141
136
|
let p = Stdlib_Option.getOr(JSON.stringify(h.projectionModule.slice(baseLen, h.projectionModule.length)), `""`);
|
|
142
137
|
let q = Stdlib_Option.getOr(JSON.stringify(h.queryDbTableName), `""`);
|
|
138
|
+
let t = h.stateTopicName === "" ? "" : `,"t":` + JSON.stringify(h.stateTopicName);
|
|
143
139
|
if (sharedUrn !== "") {
|
|
144
|
-
return `{"s":` + s + `,"p":` + p + `,"q":` + q + `}`;
|
|
140
|
+
return `{"s":` + s + `,"p":` + p + `,"q":` + q + t + `}`;
|
|
145
141
|
}
|
|
146
142
|
let u = Stdlib_Option.getOr(JSON.stringify(h.sourceUrn), `""`);
|
|
147
|
-
return `{"s":` + s + `,"p":` + p + `,"q":` + q + `,"u":` + u + `}`;
|
|
143
|
+
return `{"s":` + s + `,"p":` + p + `,"q":` + q + `,"u":` + u + t + `}`;
|
|
148
144
|
}).join(",");
|
|
149
145
|
let baseJson = Stdlib_Option.getOr(JSON.stringify(base), `""`);
|
|
150
146
|
let urnJson = Stdlib_Option.getOr(JSON.stringify(sharedUrn), `""`);
|
|
@@ -152,6 +148,9 @@ function buildLambda(parent, handlerOutputs, packageDirs, channelSpecs, feedQueu
|
|
|
152
148
|
});
|
|
153
149
|
let envVars = {};
|
|
154
150
|
envVars["HANDLER_CONFIG"] = handlerConfigOutput;
|
|
151
|
+
if (pgStreamConfig !== undefined) {
|
|
152
|
+
envVars["APPSYNC_ENDPOINT"] = pgStreamConfig.endpoint;
|
|
153
|
+
}
|
|
155
154
|
packageDirs["@reventlessdev/reventless-aws"] = Util_Bundle$ReventlessAws.resolvePackageRoot("@reventlessdev/reventless-aws");
|
|
156
155
|
packageDirs["@reventlessdev/reventless-core"] = Util_Bundle$ReventlessAws.resolvePackageRoot("@reventlessdev/reventless-core");
|
|
157
156
|
let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/StateViewSliceEntryPoint.mjs", packageDirs, undefined);
|
|
@@ -171,6 +170,17 @@ function buildLambda(parent, handlerOutputs, packageDirs, channelSpecs, feedQueu
|
|
|
171
170
|
role: runtime.parts.lambdaRole.id
|
|
172
171
|
}));
|
|
173
172
|
}
|
|
173
|
+
if (pgStreamConfig !== undefined) {
|
|
174
|
+
pgStreamConfig.apiArn.apply(apiArn => new (Aws.iam.RolePolicy)("AllStateViewSlices-appsyncPublish", {
|
|
175
|
+
policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, "AllStateViewSlices-appsyncPublishPolicy", [{
|
|
176
|
+
Sid: "AllowPublishAppSyncEvents",
|
|
177
|
+
Effect: "Allow",
|
|
178
|
+
Action: "appsync:EventPublish",
|
|
179
|
+
Resource: apiArn + "/*"
|
|
180
|
+
}])),
|
|
181
|
+
role: runtime.parts.lambdaRole.id
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
174
184
|
EventCollectorChannel_DynamoDbStream$ReventlessAws.connect("AllStateViewSlices", channelSpecs, runtime, opts);
|
|
175
185
|
if (feedQueue !== undefined) {
|
|
176
186
|
return PgProjectionFeed$ReventlessAws.connect("AllStateViewSlicesFeed", feedQueue, runtime.parts.lambda, runtime.parts.lambdaRole, Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions(opts));
|
|
@@ -201,7 +211,7 @@ function finishWithDcbEventLog(dcbEventLog) {
|
|
|
201
211
|
let handlerOutputs = [];
|
|
202
212
|
let packageDirs = {};
|
|
203
213
|
let allQueryDbResources = [];
|
|
204
|
-
Stdlib_Dict.forEachWithKey(sliceInfos, (info,
|
|
214
|
+
Stdlib_Dict.forEachWithKey(sliceInfos, (info, name) => {
|
|
205
215
|
info.queryDbResources.forEach(r => {
|
|
206
216
|
allQueryDbResources.push(r);
|
|
207
217
|
});
|
|
@@ -219,15 +229,17 @@ function finishWithDcbEventLog(dcbEventLog) {
|
|
|
219
229
|
specModule: info.specModulePath,
|
|
220
230
|
projectionModule: info.projectionModulePath,
|
|
221
231
|
queryDbTableName: param[0],
|
|
222
|
-
sourceUrn: param[1]
|
|
232
|
+
sourceUrn: param[1],
|
|
233
|
+
stateTopicName: stateTopicNameFor(name)
|
|
223
234
|
}));
|
|
224
235
|
handlerOutputs.push(handlerJson);
|
|
225
236
|
});
|
|
237
|
+
let anyPgStream = Object.keys(sliceInfos).some(n => QueryDbBackend$ReventlessAws.postgresStreamRegistry.has(n));
|
|
226
238
|
buildLambda(parent, handlerOutputs, packageDirs, [{
|
|
227
239
|
channel: channel,
|
|
228
240
|
eventTopics: eventTopics,
|
|
229
241
|
resources: allQueryDbResources
|
|
230
|
-
}], Primitive_option.some(feedQueue), undefined, undefined);
|
|
242
|
+
}], Primitive_option.some(feedQueue), Primitive_option.some(anyPgStream ? eventsApiConfig.contents : undefined), undefined, undefined);
|
|
231
243
|
} else {
|
|
232
244
|
log.warn("StateViewSliceRuntime_Builder_Single", undefined, "finishWithDcbEventLog: DCB EventLog has no parent");
|
|
233
245
|
}
|
|
@@ -273,11 +285,13 @@ function finish() {
|
|
|
273
285
|
specModule: info.specModulePath,
|
|
274
286
|
projectionModule: info.projectionModulePath,
|
|
275
287
|
queryDbTableName: param[0],
|
|
276
|
-
sourceUrn: Stdlib_Option.getOr(param[1][0], param[2])
|
|
288
|
+
sourceUrn: Stdlib_Option.getOr(param[1][0], param[2]),
|
|
289
|
+
stateTopicName: stateTopicNameFor(spec.componentName)
|
|
277
290
|
}));
|
|
278
291
|
handlerOutputs.push(handlerJson);
|
|
279
292
|
});
|
|
280
|
-
|
|
293
|
+
let anyPgStream = storedSpecs.some(spec => QueryDbBackend$ReventlessAws.postgresStreamRegistry.has(spec.componentName));
|
|
294
|
+
buildLambda(parent, handlerOutputs, packageDirs, storedSpecs.map(param => param.channelSpec), Primitive_option.some(feedQueue), Primitive_option.some(anyPgStream ? eventsApiConfig.contents : undefined), match[0], match[1]);
|
|
281
295
|
} else {
|
|
282
296
|
log.warn("StateViewSliceRuntime_Builder_Single", undefined, `finish: grandParent not set`);
|
|
283
297
|
}
|
|
@@ -295,6 +309,9 @@ export {
|
|
|
295
309
|
log,
|
|
296
310
|
commonPrefix,
|
|
297
311
|
sliceInfos,
|
|
312
|
+
eventsApiConfig,
|
|
313
|
+
setEventsApiConfig,
|
|
314
|
+
stateTopicNameFor,
|
|
298
315
|
registerStateViewSlice,
|
|
299
316
|
storedSpecs,
|
|
300
317
|
grandParent,
|
|
@@ -354,8 +354,12 @@ let finish = (
|
|
|
354
354
|
"index.mjs",
|
|
355
355
|
Pulumi.Asset.stringAsset(handlerCode)->Pulumi.Archive.assetToAssetOrArchive,
|
|
356
356
|
)
|
|
357
|
+
// ESM self-containment: the handler imports @aws-sdk/util-dynamodb, provided
|
|
358
|
+
// by the nodejs22.x runtime only under /var/runtime — unreachable from
|
|
359
|
+
// /var/task ESM without the resolver hook. Ship the loader + set its env vars.
|
|
360
|
+
let loaderHash = Util_Bundle.addEsmLoaderAssets(archiveContents)
|
|
357
361
|
let code = Pulumi.Archive.assetArchive(archiveContents)
|
|
358
|
-
let sourceCodeHash = Util_Bundle.hashString(handlerCode)
|
|
362
|
+
let sourceCodeHash = Util_Bundle.hashString(handlerCode ++ "\n---\n" ++ loaderHash)
|
|
359
363
|
|
|
360
364
|
let layers =
|
|
361
365
|
Lambda.reventlessLayerArn
|
|
@@ -383,6 +387,8 @@ let finish = (
|
|
|
383
387
|
("Environment", Pulumi.Pulumi.getStackName()->Pulumi.Input.make),
|
|
384
388
|
("APPSYNC_ENDPOINT", appsyncEndpoint->Pulumi.Output.asInput),
|
|
385
389
|
("STATE_TOPIC_MAP", topicMapJson->Pulumi.Output.asInput),
|
|
390
|
+
("NODE_OPTIONS", Util_Bundle.esmLoaderNodeOptions->Pulumi.Input.make),
|
|
391
|
+
("ESM_FALLBACK_DIRS", Util_Bundle.esmFallbackDirs->Pulumi.Input.make),
|
|
386
392
|
]),
|
|
387
393
|
}: Lambda.Function.functionEnvironment
|
|
388
394
|
)->Pulumi.Input.make,
|