@reventlessdev/reventless-aws 3.0.0-alpha.229 → 3.0.0-alpha.231
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 +15 -0
- package/package.json +5 -5
- package/src/adapter/Geocoder/Geocoder_AwsLocation.res +60 -141
- package/src/adapter/Geocoder/Geocoder_AwsLocation.res.mjs +52 -118
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Ops.res +137 -0
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Ops.res.mjs +124 -0
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +51 -6
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +60 -0
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +29 -1
- package/src/adapter/Runtime/HeartbeatEntryPoint.res +44 -0
- package/src/adapter/Runtime/HeartbeatEntryPoint.res.mjs +47 -0
- package/src/adapter/Runtime/PluginExtensionPointEntryPoint.res +234 -0
- package/src/adapter/Runtime/PluginExtensionPointEntryPoint.res.mjs +226 -0
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +49 -4
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +22 -4
- package/src/adapter/Upload/Upload_Presign_S3.res +65 -164
- package/src/adapter/Upload/Upload_Presign_S3.res.mjs +56 -119
- package/src/adapter/Upload/Upload_Presign_S3_Ops.res +157 -0
- package/src/adapter/Upload/Upload_Presign_S3_Ops.res.mjs +120 -0
- package/src/components/InboundTranslationSlice_Builder.res +27 -1
- package/src/components/InboundTranslationSlice_Builder.res.mjs +15 -2
- package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res +1 -1
- package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res.mjs +1 -1
- package/src/plugin/runtime/PluginRuntime_Builder.res +59 -1
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +49 -2
- package/tests/DcbInboundTranslationRoutingTest.res +73 -0
- package/tests/DcbInboundTranslationRoutingTest.res.mjs +71 -0
- package/tests/EpInboundTestSlice.res +19 -0
- package/tests/EpInboundTestSlice.res.mjs +31 -0
- package/tests/EpInboundTestSliceTranslation.res +13 -0
- package/tests/EpInboundTestSliceTranslation.res.mjs +33 -0
- package/src/adapter/Runtime/HeartbeatEntryPoint.mjs +0 -38
- package/src/adapter/Runtime/PluginExtensionPointEntryPoint.mjs +0 -127
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
6
|
+
import * as HandlerFactoryHelpersMjs from "./HandlerFactoryHelpers.mjs";
|
|
7
|
+
import * as CommandTopic_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic_Callback.res.mjs";
|
|
8
|
+
import * as ExtensionPoint_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/ExtensionPoint/ExtensionPoint_Callback.res.mjs";
|
|
9
|
+
import * as PluginExtensionPointSpec$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/PluginExtensionPointSpec.res.mjs";
|
|
10
|
+
import * as Util_PluginMessage_Runtime$ReventlessAws from "../../plugin/runtime/Util_PluginMessage_Runtime.res.mjs";
|
|
11
|
+
import * as PluginExtensionPoint_Plugin$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/connect/PluginExtensionPoint_Plugin.res.mjs";
|
|
12
|
+
import * as CommandTopicChannel_SQS_Runtime$ReventlessAws from "../CommandTopic/CommandTopicChannel_SQS_Runtime.res.mjs";
|
|
13
|
+
import * as ScheduledPublisher_CloudWatchEvents_Runtime$ReventlessAws from "../ScheduledPublisher/ScheduledPublisher_CloudWatchEvents_Runtime.res.mjs";
|
|
14
|
+
|
|
15
|
+
function setRequestId(prim) {
|
|
16
|
+
HandlerFactoryHelpersMjs.setRequestId(prim);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function runEffect(prim0, prim1) {
|
|
20
|
+
return HandlerFactoryHelpersMjs.runEffect(prim0, prim1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function extractMetaField(prim0, prim1) {
|
|
24
|
+
return HandlerFactoryHelpersMjs.extractMetaField(prim0, prim1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function extractSentTimestamp(prim) {
|
|
28
|
+
return HandlerFactoryHelpersMjs.extractSentTimestamp(prim);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function extractRetryCount(prim) {
|
|
32
|
+
return HandlerFactoryHelpersMjs.extractRetryCount(prim);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function logDebug(prim0, prim1) {
|
|
36
|
+
HandlerFactoryHelpersMjs.log.debug(prim0, prim1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function scanByTableName(prim0, prim1, prim2) {
|
|
40
|
+
return HandlerFactoryHelpersMjs.scanByTableName(prim0, prim1, prim2);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let config = JSON.parse(Stdlib_Option.getOr(process.env["HANDLER_CONFIG"], "{}"));
|
|
44
|
+
|
|
45
|
+
let runtimeOps = {
|
|
46
|
+
messagePublish: {
|
|
47
|
+
sendMessageToChannel: Util_PluginMessage_Runtime$ReventlessAws.sendMessage
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
let environment = Stdlib_Option.getOr(process.env["Environment"], "unknown");
|
|
52
|
+
|
|
53
|
+
let EpSpec = {
|
|
54
|
+
runtimeOps: runtimeOps,
|
|
55
|
+
environment: environment,
|
|
56
|
+
updateApiSchema: undefined,
|
|
57
|
+
manageSubscriptions: undefined
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
let PluginMappingInstance = PluginExtensionPoint_Plugin$ReventlessCore.Make(EpSpec);
|
|
61
|
+
|
|
62
|
+
let mappings = [PluginMappingInstance.Mapping];
|
|
63
|
+
|
|
64
|
+
let Mappings = {
|
|
65
|
+
mappings: mappings
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
let publishToAggregates = Object.fromEntries(Object.entries(Stdlib_Option.getOr(config.publishToAggregates, {})).map(param => {
|
|
69
|
+
let queueUrl = Stdlib_Option.getOr(process.env[param[1]], "");
|
|
70
|
+
let queue = {
|
|
71
|
+
id: queueUrl,
|
|
72
|
+
name: queueUrl,
|
|
73
|
+
arn: ""
|
|
74
|
+
};
|
|
75
|
+
return [
|
|
76
|
+
param[0],
|
|
77
|
+
CommandTopicChannel_SQS_Runtime$ReventlessAws.publishJsons(queue, "SQS_FIFO")
|
|
78
|
+
];
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
let pluginReadModelTableName = Stdlib_Option.getOr(config.pluginReadModelTableName, "");
|
|
82
|
+
|
|
83
|
+
function queryEngine_scan(param, filterConfigs, limit) {
|
|
84
|
+
return HandlerFactoryHelpersMjs.scanByTableName(pluginReadModelTableName, filterConfigs, limit);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function queryEngine_query(param, param$1, param$2, param$3, param$4, param$5, param$6) {
|
|
88
|
+
return Stdlib_JsError.throwWithMessage("QueryEngine.query not available in bundled Plugin EP handler");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let queryEngine = {
|
|
92
|
+
scan: queryEngine_scan,
|
|
93
|
+
query: queryEngine_query
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
let scheduler_createSchedule = ScheduledPublisher_CloudWatchEvents_Runtime$ReventlessAws.createSchedule(Stdlib_Option.getOr(config.schedulerRoleArn, ""));
|
|
97
|
+
|
|
98
|
+
let scheduler = {
|
|
99
|
+
createSchedule: scheduler_createSchedule,
|
|
100
|
+
deleteSchedule: ScheduledPublisher_CloudWatchEvents_Runtime$ReventlessAws.deleteSchedule
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
let schedulerQueueArn = Stdlib_Option.getOr(config.schedulerQueueArn, "");
|
|
104
|
+
|
|
105
|
+
let schedulerQueueName = Stdlib_Option.getOr(config.schedulerQueueName, "");
|
|
106
|
+
|
|
107
|
+
let commandTopicResources = schedulerQueueArn === "" ? [] : [{
|
|
108
|
+
name: schedulerQueueName,
|
|
109
|
+
id: schedulerQueueName,
|
|
110
|
+
urn: schedulerQueueArn,
|
|
111
|
+
resourceInfo: "NoInfo",
|
|
112
|
+
service: "unknown",
|
|
113
|
+
role: "",
|
|
114
|
+
region: "",
|
|
115
|
+
resourceType: "",
|
|
116
|
+
configuration: {},
|
|
117
|
+
tags: {}
|
|
118
|
+
}];
|
|
119
|
+
|
|
120
|
+
let invalidNameChars = /[^.\-_a-zA-Z0-9]/g;
|
|
121
|
+
|
|
122
|
+
function resourceNaming_validateName(n) {
|
|
123
|
+
return n.replace(invalidNameChars, "_");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function resourceNaming_urnName(arn) {
|
|
127
|
+
return Stdlib_Option.getOr(Stdlib_Option.filter(arn.split(":")[5], s => s !== ""), "unknown");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let resourceNaming = {
|
|
131
|
+
validateName: resourceNaming_validateName,
|
|
132
|
+
urnName: resourceNaming_urnName
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
let Callback = ExtensionPoint_Callback$ReventlessCore.Make({
|
|
136
|
+
publishToAggregates: publishToAggregates,
|
|
137
|
+
commandTopicResources: commandTopicResources,
|
|
138
|
+
scheduler: scheduler,
|
|
139
|
+
queryEngine: queryEngine,
|
|
140
|
+
resourceNaming: resourceNaming
|
|
141
|
+
})({
|
|
142
|
+
name: PluginExtensionPointSpec$ReventlessInfra.name,
|
|
143
|
+
moduleUrl: PluginExtensionPointSpec$ReventlessInfra.moduleUrl,
|
|
144
|
+
commandSchema: PluginExtensionPointSpec$ReventlessInfra.commandSchema,
|
|
145
|
+
eventSchema: PluginExtensionPointSpec$ReventlessInfra.eventSchema,
|
|
146
|
+
directiveSchema: PluginExtensionPointSpec$ReventlessInfra.directiveSchema
|
|
147
|
+
})(Mappings);
|
|
148
|
+
|
|
149
|
+
let SpecWithId = {
|
|
150
|
+
Id: undefined,
|
|
151
|
+
name: PluginExtensionPointSpec$ReventlessInfra.name,
|
|
152
|
+
commandSchema: PluginExtensionPointSpec$ReventlessInfra.commandSchema
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
let CommandTopicCallback = CommandTopic_Callback$ReventlessCore.Make({
|
|
156
|
+
Id: Id$Reventless.$$String,
|
|
157
|
+
name: PluginExtensionPointSpec$ReventlessInfra.name,
|
|
158
|
+
commandSchema: PluginExtensionPointSpec$ReventlessInfra.commandSchema
|
|
159
|
+
})({
|
|
160
|
+
Spec: {
|
|
161
|
+
Id: Id$Reventless.$$String,
|
|
162
|
+
name: PluginExtensionPointSpec$ReventlessInfra.name,
|
|
163
|
+
commandSchema: PluginExtensionPointSpec$ReventlessInfra.commandSchema
|
|
164
|
+
},
|
|
165
|
+
commandsHandler: Callback.handleIncomingCommands
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
let queue_id = Stdlib_Option.getOr(config.queueUrl, "");
|
|
169
|
+
|
|
170
|
+
let queue_name = Stdlib_Option.getOr(config.queueUrl, "");
|
|
171
|
+
|
|
172
|
+
let queue = {
|
|
173
|
+
id: queue_id,
|
|
174
|
+
name: queue_name,
|
|
175
|
+
arn: ""
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
let sqsHandler = CommandTopicChannel_SQS_Runtime$ReventlessAws.handleQueueEvent(queue, CommandTopicCallback.handleJsonCommands);
|
|
179
|
+
|
|
180
|
+
let comp = `ExtensionPoint(` + PluginExtensionPointSpec$ReventlessInfra.name + `)`;
|
|
181
|
+
|
|
182
|
+
async function handler(event, context) {
|
|
183
|
+
HandlerFactoryHelpersMjs.setRequestId(context.awsRequestId);
|
|
184
|
+
let records = event.Records;
|
|
185
|
+
let prim0 = `processing ` + records.length.toString() + ` record(s)`;
|
|
186
|
+
HandlerFactoryHelpersMjs.log.debug(prim0, {
|
|
187
|
+
comp: "PluginExtensionPointRuntime"
|
|
188
|
+
});
|
|
189
|
+
await HandlerFactoryHelpersMjs.runEffect(sqsHandler(event, context), {
|
|
190
|
+
correlationId: HandlerFactoryHelpersMjs.extractMetaField(records, "correlationId"),
|
|
191
|
+
causationId: HandlerFactoryHelpersMjs.extractMetaField(records, "causationId"),
|
|
192
|
+
comp: comp,
|
|
193
|
+
timestamp: HandlerFactoryHelpersMjs.extractSentTimestamp(records),
|
|
194
|
+
retryCount: HandlerFactoryHelpersMjs.extractRetryCount(records)
|
|
195
|
+
});
|
|
196
|
+
return "";
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export {
|
|
200
|
+
setRequestId,
|
|
201
|
+
runEffect,
|
|
202
|
+
extractMetaField,
|
|
203
|
+
extractSentTimestamp,
|
|
204
|
+
extractRetryCount,
|
|
205
|
+
logDebug,
|
|
206
|
+
scanByTableName,
|
|
207
|
+
config,
|
|
208
|
+
EpSpec,
|
|
209
|
+
PluginMappingInstance,
|
|
210
|
+
Mappings,
|
|
211
|
+
publishToAggregates,
|
|
212
|
+
pluginReadModelTableName,
|
|
213
|
+
queryEngine,
|
|
214
|
+
scheduler,
|
|
215
|
+
commandTopicResources,
|
|
216
|
+
invalidNameChars,
|
|
217
|
+
resourceNaming,
|
|
218
|
+
Callback,
|
|
219
|
+
SpecWithId,
|
|
220
|
+
CommandTopicCallback,
|
|
221
|
+
queue,
|
|
222
|
+
sqsHandler,
|
|
223
|
+
comp,
|
|
224
|
+
handler,
|
|
225
|
+
}
|
|
226
|
+
/* config Not a pure module */
|
|
@@ -13,10 +13,22 @@
|
|
|
13
13
|
|
|
14
14
|
let log = ReventlessCore.Logger.fromEnv()
|
|
15
15
|
|
|
16
|
+
// `(specPath, translationPath)` module specifiers per InboundTranslationSlice,
|
|
17
|
+
// plus the resolved audit QueryDb table-name Output (None on the Postgres path or
|
|
18
|
+
// if the component hasn't reported it). The entry point imports both modules,
|
|
19
|
+
// builds `InboundTranslationSlice_Callback.Make(Spec)(Translation).receive`, and
|
|
20
|
+
// — when `auditTableName` is present — persists the audit rows to that table.
|
|
21
|
+
type inboundSlicePaths = {
|
|
22
|
+
specPath: string,
|
|
23
|
+
translationPath: string,
|
|
24
|
+
auditTableName: option<Pulumi.Output.t<string>>,
|
|
25
|
+
}
|
|
26
|
+
|
|
16
27
|
// `slicePaths`: `(specPath, behaviorPath)` per StateChangeSlice (already merged
|
|
17
28
|
// from auto- and manually-registered sources by the caller).
|
|
18
29
|
let forDcbCommandTopic = (
|
|
19
30
|
~slicePaths: array<(string, string)>,
|
|
31
|
+
~inboundSlices: array<inboundSlicePaths>=[],
|
|
20
32
|
~dcbTableName: option<Pulumi.Output.t<string>>,
|
|
21
33
|
~pluginName: string,
|
|
22
34
|
~syncConfig: ReventlessCore.Runtime.commandHandlerConfig,
|
|
@@ -29,7 +41,7 @@ let forDcbCommandTopic = (
|
|
|
29
41
|
~connect,
|
|
30
42
|
dcbCommandTopic,
|
|
31
43
|
) =>
|
|
32
|
-
if slicePaths->Array.length == 0 {
|
|
44
|
+
if slicePaths->Array.length == 0 && inboundSlices->Array.length == 0 {
|
|
33
45
|
log.warn(
|
|
34
46
|
~comp="StateChangeSliceRuntime_Builder_Single",
|
|
35
47
|
"forDcbCommandTopic skipped (no slice specs)",
|
|
@@ -122,11 +134,37 @@ let forDcbCommandTopic = (
|
|
|
122
134
|
| None => Pulumi.Output.make("")
|
|
123
135
|
}
|
|
124
136
|
|
|
137
|
+
// InboundTranslationSlice modules — spec + translation paths and the resolved
|
|
138
|
+
// audit table name (Pulumi-generated, so it must be resolved here). Emitted as
|
|
139
|
+
// a `,"inboundTranslationSliceModules":[…]` fragment merged into HANDLER_CONFIG
|
|
140
|
+
// (absent → the entry point has no Route 0 registry, unchanged behaviour).
|
|
141
|
+
let inboundFragment = switch inboundSlices {
|
|
142
|
+
| [] => Pulumi.Output.make("")
|
|
143
|
+
| slices =>
|
|
144
|
+
slices
|
|
145
|
+
->Array.map(s => s.auditTableName->Option.getOr(Pulumi.Output.make("")))
|
|
146
|
+
->Pulumi.Output.all
|
|
147
|
+
->Pulumi.Output.apply(tableNames => {
|
|
148
|
+
let entries =
|
|
149
|
+
slices
|
|
150
|
+
->Array.mapWithIndex((s, i) => {
|
|
151
|
+
let spec = s.specPath->JSON.stringifyAny->Option.getOr(`""`)
|
|
152
|
+
let translation = s.translationPath->JSON.stringifyAny->Option.getOr(`""`)
|
|
153
|
+
let tableName = tableNames->Array.getUnsafe(i)
|
|
154
|
+
let auditJson =
|
|
155
|
+
tableName == "" ? "null" : tableName->JSON.stringifyAny->Option.getOr("null")
|
|
156
|
+
`{"spec":${spec},"translation":${translation},"auditTableName":${auditJson}}`
|
|
157
|
+
})
|
|
158
|
+
->Array.join(",")
|
|
159
|
+
`,"inboundTranslationSliceModules":[${entries}]`
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
|
|
125
163
|
let handlerConfigJson =
|
|
126
|
-
Pulumi.Output.
|
|
127
|
-
->Pulumi.Output.apply(((table, queueUrl, pgFragment)) => {
|
|
164
|
+
Pulumi.Output.all4((dcbTableName, queue.id, pgConnectionFragment, inboundFragment))
|
|
165
|
+
->Pulumi.Output.apply(((table, queueUrl, pgFragment, inbFragment)) => {
|
|
128
166
|
let pluginNameJson = pluginName->JSON.stringifyAny->Option.getOr(`""`)
|
|
129
|
-
`{"dcbEventLogTableName":"${table}","queueUrl":"${queueUrl}","pluginName":${pluginNameJson},"stateChangeSliceModules":[${sliceModulesJson}]${pgFragment}}`
|
|
167
|
+
`{"dcbEventLogTableName":"${table}","queueUrl":"${queueUrl}","pluginName":${pluginNameJson},"stateChangeSliceModules":[${sliceModulesJson}]${pgFragment}${inbFragment}}`
|
|
130
168
|
})
|
|
131
169
|
envVars->Dict.set("HANDLER_CONFIG", handlerConfigJson->Pulumi.Output.asInput)
|
|
132
170
|
|
|
@@ -138,6 +176,13 @@ let forDcbCommandTopic = (
|
|
|
138
176
|
let behaviorPkg = Util_Bundle.extractPackageName(behaviorPath)
|
|
139
177
|
packageDirs->Dict.set(behaviorPkg, Util_Bundle.resolvePackageRoot(behaviorPkg))
|
|
140
178
|
})
|
|
179
|
+
// Inbound slice spec + translation packages the entry point imports for Route 0.
|
|
180
|
+
inboundSlices->Array.forEach(s => {
|
|
181
|
+
let specPkg = Util_Bundle.extractPackageName(s.specPath)
|
|
182
|
+
packageDirs->Dict.set(specPkg, Util_Bundle.resolvePackageRoot(specPkg))
|
|
183
|
+
let translationPkg = Util_Bundle.extractPackageName(s.translationPath)
|
|
184
|
+
packageDirs->Dict.set(translationPkg, Util_Bundle.resolvePackageRoot(translationPkg))
|
|
185
|
+
})
|
|
141
186
|
// Include the framework packages alongside the entry point so the deployed
|
|
142
187
|
// Lambda picks up uncommitted local changes without waiting for the Lambda
|
|
143
188
|
// Layer rebuild (the layer fetches @reventlessdev/reventless-* from GitHub
|
|
@@ -16,10 +16,11 @@ import * as RuntimeEnvironment_Lambda$ReventlessAws from "./RuntimeEnvironment_L
|
|
|
16
16
|
|
|
17
17
|
let log = Logger$ReventlessCore.fromEnv();
|
|
18
18
|
|
|
19
|
-
function forDcbCommandTopic(slicePaths, dcbTableName, pluginName, syncConfig, asyncConfig, sliceMemoryFloorOpt, sliceTimeoutFloorOpt, connect, dcbCommandTopic) {
|
|
19
|
+
function forDcbCommandTopic(slicePaths, inboundSlicesOpt, dcbTableName, pluginName, syncConfig, asyncConfig, sliceMemoryFloorOpt, sliceTimeoutFloorOpt, connect, dcbCommandTopic) {
|
|
20
|
+
let inboundSlices = inboundSlicesOpt !== undefined ? inboundSlicesOpt : [];
|
|
20
21
|
let sliceMemoryFloor = sliceMemoryFloorOpt !== undefined ? sliceMemoryFloorOpt : 0;
|
|
21
22
|
let sliceTimeoutFloor = sliceTimeoutFloorOpt !== undefined ? sliceTimeoutFloorOpt : 0;
|
|
22
|
-
if (slicePaths.length === 0) {
|
|
23
|
+
if (slicePaths.length === 0 && inboundSlices.length === 0) {
|
|
23
24
|
return log.warn("StateChangeSliceRuntime_Builder_Single", undefined, "forDcbCommandTopic skipped (no slice specs)");
|
|
24
25
|
}
|
|
25
26
|
let commandTopicResource = Component$ReventlessCore.toPulumiResource(dcbCommandTopic);
|
|
@@ -58,13 +59,24 @@ function forDcbCommandTopic(slicePaths, dcbTableName, pluginName, syncConfig, as
|
|
|
58
59
|
return `{"spec":` + s + `,"behavior":` + b + `}`;
|
|
59
60
|
}).join(",");
|
|
60
61
|
let pgConnectionFragment = pgSelection !== undefined ? pgSelection.connectionConfig.apply(cc => `,"pgConnection":` + JSON.stringify(PgConnection$ReventlessAws.connectionConfigToJson(pgSelection.lockStrategy, cc))) : Pulumi.output("");
|
|
62
|
+
let inboundFragment = inboundSlices.length !== 0 ? Pulumi.all(inboundSlices.map(s => Stdlib_Option.getOr(s.auditTableName, Pulumi.output("")))).apply(tableNames => {
|
|
63
|
+
let entries = inboundSlices.map((s, i) => {
|
|
64
|
+
let spec = Stdlib_Option.getOr(JSON.stringify(s.specPath), `""`);
|
|
65
|
+
let translation = Stdlib_Option.getOr(JSON.stringify(s.translationPath), `""`);
|
|
66
|
+
let tableName = tableNames[i];
|
|
67
|
+
let auditJson = tableName === "" ? "null" : Stdlib_Option.getOr(JSON.stringify(tableName), "null");
|
|
68
|
+
return `{"spec":` + spec + `,"translation":` + translation + `,"auditTableName":` + auditJson + `}`;
|
|
69
|
+
}).join(",");
|
|
70
|
+
return `,"inboundTranslationSliceModules":[` + entries + `]`;
|
|
71
|
+
}) : Pulumi.output("");
|
|
61
72
|
let handlerConfigJson = Pulumi.all([
|
|
62
73
|
dcbTableName$1,
|
|
63
74
|
queue.id,
|
|
64
|
-
pgConnectionFragment
|
|
75
|
+
pgConnectionFragment,
|
|
76
|
+
inboundFragment
|
|
65
77
|
]).apply(param => {
|
|
66
78
|
let pluginNameJson = Stdlib_Option.getOr(JSON.stringify(pluginName), `""`);
|
|
67
|
-
return `{"dcbEventLogTableName":"` + param[0] + `","queueUrl":"` + param[1] + `","pluginName":` + pluginNameJson + `,"stateChangeSliceModules":[` + sliceModulesJson + `]` + param[2] + `}`;
|
|
79
|
+
return `{"dcbEventLogTableName":"` + param[0] + `","queueUrl":"` + param[1] + `","pluginName":` + pluginNameJson + `,"stateChangeSliceModules":[` + sliceModulesJson + `]` + param[2] + param[3] + `}`;
|
|
68
80
|
});
|
|
69
81
|
envVars["HANDLER_CONFIG"] = handlerConfigJson;
|
|
70
82
|
let packageDirs = {};
|
|
@@ -74,6 +86,12 @@ function forDcbCommandTopic(slicePaths, dcbTableName, pluginName, syncConfig, as
|
|
|
74
86
|
let behaviorPkg = Util_Bundle$ReventlessAws.extractPackageName(param[1]);
|
|
75
87
|
packageDirs[behaviorPkg] = Util_Bundle$ReventlessAws.resolvePackageRoot(behaviorPkg);
|
|
76
88
|
});
|
|
89
|
+
inboundSlices.forEach(s => {
|
|
90
|
+
let specPkg = Util_Bundle$ReventlessAws.extractPackageName(s.specPath);
|
|
91
|
+
packageDirs[specPkg] = Util_Bundle$ReventlessAws.resolvePackageRoot(specPkg);
|
|
92
|
+
let translationPkg = Util_Bundle$ReventlessAws.extractPackageName(s.translationPath);
|
|
93
|
+
packageDirs[translationPkg] = Util_Bundle$ReventlessAws.resolvePackageRoot(translationPkg);
|
|
94
|
+
});
|
|
77
95
|
packageDirs["@reventlessdev/reventless-aws"] = Util_Bundle$ReventlessAws.resolvePackageRoot("@reventlessdev/reventless-aws");
|
|
78
96
|
packageDirs["@reventlessdev/reventless-core"] = Util_Bundle$ReventlessAws.resolvePackageRoot("@reventlessdev/reventless-core");
|
|
79
97
|
let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs", packageDirs, undefined);
|
|
@@ -1,156 +1,25 @@
|
|
|
1
1
|
// Direct-to-S3 upload presign service behind a public Lambda Function URL.
|
|
2
2
|
//
|
|
3
|
-
// Deploy-time: `make` provisions a
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// Deploy-time only: `make` provisions a compiled-EntryPoint Lambda (a plain
|
|
4
|
+
// `Lambda.Function` whose code archive re-exports `handler` from the compiled,
|
|
5
|
+
// type-checked runtime module `Upload_Presign_S3_Ops` and ships the shared ESM
|
|
6
|
+
// resolve-hook loader), an IAM execution role scoped to CloudWatch Logs +
|
|
7
|
+
// `s3:PutObject` on the target bucket, and a Function URL (no auth) so a browser
|
|
8
|
+
// can request a presigned PUT.
|
|
6
9
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
10
|
+
// Why an EntryPoint and not a Pulumi `CallbackFunction`: the handler needs the
|
|
11
|
+
// AWS SDK v3 S3 presigner. A serialized closure bakes the deploy machine's
|
|
12
|
+
// version-specific SDK internals into the archive but then resolves `@smithy/*`
|
|
13
|
+
// and `@aws-sdk/*` transitives from independently-versioned layer/runtime sources
|
|
14
|
+
// that disagree at cold start (an inlined `client-s3` against a newer
|
|
15
|
+
// `@smithy/smithy-client` base whose constructor no longer sets `middlewareStack`
|
|
16
|
+
// → `new S3Client()` throws). Shipping the compiled `_Ops` module with bare
|
|
17
|
+
// `@aws-sdk/*` imports, resolved through the resolve-hook (`@aws-sdk/*` from the
|
|
18
|
+
// runtime, `@smithy/*`/`@reventlessdev/*` from the layer), loads one internally
|
|
19
|
+
// consistent SDK instead. The runtime logic lives in [Upload_Presign_S3_Ops.res].
|
|
13
20
|
|
|
14
21
|
open PulumiAws
|
|
15
22
|
|
|
16
|
-
// ── AWS SDK v3 bindings ─────────────────────────────────────────────────────
|
|
17
|
-
|
|
18
|
-
type s3Client
|
|
19
|
-
|
|
20
|
-
type putObjectInput = {
|
|
21
|
-
@as("Bucket") bucket: string,
|
|
22
|
-
@as("Key") key: string,
|
|
23
|
-
@as("ContentType") contentType?: string,
|
|
24
|
-
}
|
|
25
|
-
type putObjectCommand
|
|
26
|
-
|
|
27
|
-
@module("@aws-sdk/client-s3") @new external makeS3Client: unit => s3Client = "S3Client"
|
|
28
|
-
|
|
29
|
-
@module("@aws-sdk/client-s3") @new
|
|
30
|
-
external makePutObjectCommand: putObjectInput => putObjectCommand = "PutObjectCommand"
|
|
31
|
-
|
|
32
|
-
type presignOptions = {expiresIn: int}
|
|
33
|
-
|
|
34
|
-
@module("@aws-sdk/s3-request-presigner")
|
|
35
|
-
external getSignedUrl: (s3Client, putObjectCommand, presignOptions) => promise<string> =
|
|
36
|
-
"getSignedUrl"
|
|
37
|
-
|
|
38
|
-
// ── Function URL event / response shapes (payload format 2.0) ────────────────
|
|
39
|
-
|
|
40
|
-
type functionUrlEvent = {
|
|
41
|
-
body?: string,
|
|
42
|
-
headers?: dict<string>,
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
type response = {
|
|
46
|
-
statusCode: int,
|
|
47
|
-
headers?: dict<string>,
|
|
48
|
-
body: string,
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
let getEnv: string => option<string> = %raw(`
|
|
52
|
-
function(k) { var v = process.env[k]; return (v === undefined || v === null || v === "") ? undefined : v; }
|
|
53
|
-
`)
|
|
54
|
-
|
|
55
|
-
// Node 22 exposes globalThis.crypto.randomUUID; fall back to node:crypto.
|
|
56
|
-
let genUuid: unit => string = %raw(`
|
|
57
|
-
function() {
|
|
58
|
-
return (globalThis.crypto && globalThis.crypto.randomUUID)
|
|
59
|
-
? globalThis.crypto.randomUUID()
|
|
60
|
-
: require("crypto").randomUUID();
|
|
61
|
-
}
|
|
62
|
-
`)
|
|
63
|
-
|
|
64
|
-
// Decode a JWT payload's `sub` claim without verifying the signature (the auth
|
|
65
|
-
// layer verifies; here we only namespace the storage key).
|
|
66
|
-
let decodeJwtSub: string => option<string> = %raw(`
|
|
67
|
-
function(header) {
|
|
68
|
-
try {
|
|
69
|
-
var token = header.indexOf("Bearer ") === 0 ? header.slice(7) : header;
|
|
70
|
-
var parts = token.split(".");
|
|
71
|
-
if (parts.length < 2) return undefined;
|
|
72
|
-
var base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
73
|
-
var json = Buffer.from(base64, "base64").toString("utf8");
|
|
74
|
-
var claims = JSON.parse(json);
|
|
75
|
-
return (claims && claims.sub) ? String(claims.sub) : undefined;
|
|
76
|
-
} catch (_) { return undefined; }
|
|
77
|
-
}
|
|
78
|
-
`)
|
|
79
|
-
|
|
80
|
-
let corsHeaders = () =>
|
|
81
|
-
Dict.fromArray([
|
|
82
|
-
("content-type", "application/json"),
|
|
83
|
-
("access-control-allow-origin", "*"),
|
|
84
|
-
("access-control-allow-methods", "POST,OPTIONS"),
|
|
85
|
-
("access-control-allow-headers", "*"),
|
|
86
|
-
])
|
|
87
|
-
|
|
88
|
-
// Key prefix from the caller identity when a Bearer token is present.
|
|
89
|
-
let identityPrefix = (event: functionUrlEvent): string => {
|
|
90
|
-
let authHeader =
|
|
91
|
-
event.headers->Option.flatMap(h =>
|
|
92
|
-
h->Dict.get("authorization")->Option.orElse(h->Dict.get("Authorization"))
|
|
93
|
-
)
|
|
94
|
-
switch authHeader->Option.flatMap(decodeJwtSub) {
|
|
95
|
-
| Some(sub) => `${sub}/`
|
|
96
|
-
| None => ""
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// ── Runtime handler ─────────────────────────────────────────────────────────
|
|
101
|
-
|
|
102
|
-
let handlePresign = async (event: functionUrlEvent, _context: Lambda.context): response => {
|
|
103
|
-
try {
|
|
104
|
-
let bucket = getEnv("UPLOAD_BUCKET")->Option.getOr("")
|
|
105
|
-
let parsed =
|
|
106
|
-
event.body
|
|
107
|
-
->Option.getOr("{}")
|
|
108
|
-
->JSON.parseOrThrow
|
|
109
|
-
->JSON.Decode.object
|
|
110
|
-
->Option.getOr(Dict.make())
|
|
111
|
-
let fileName =
|
|
112
|
-
parsed->Dict.get("fileName")->Option.flatMap(JSON.Decode.string)->Option.getOr("upload")
|
|
113
|
-
let contentType = parsed->Dict.get("contentType")->Option.flatMap(JSON.Decode.string)
|
|
114
|
-
|
|
115
|
-
// The object key doubles as the served path segment: it is rooted at the
|
|
116
|
-
// served prefix (`SERVED_PREFIX`, e.g. `uploads`) so the CloudFront
|
|
117
|
-
// `{prefix}/*` behavior fronts it. Callers PUT to this exact key.
|
|
118
|
-
let servedPrefix = getEnv("SERVED_PREFIX")->Option.getOr("uploads")
|
|
119
|
-
let key = `${servedPrefix}/${identityPrefix(event)}${genUuid()}/${fileName}`
|
|
120
|
-
let client = makeS3Client()
|
|
121
|
-
let command = makePutObjectCommand({bucket, key, contentType: ?contentType})
|
|
122
|
-
let uploadUrl = await getSignedUrl(client, command, {expiresIn: 300})
|
|
123
|
-
|
|
124
|
-
// The stored ref is a same-origin relative URL `/{key}`: the served bucket is
|
|
125
|
-
// fronted read-only by the UI's own CloudFront distribution under
|
|
126
|
-
// `{prefix}/*`, so a command stores this directly-renderable value and an
|
|
127
|
-
// `image`-semantic field thumbnails it with no post-processing and no public
|
|
128
|
-
// bucket. See [docs/plans/done/ui-served-buckets.md].
|
|
129
|
-
let storageRef = `/${key}`
|
|
130
|
-
|
|
131
|
-
{
|
|
132
|
-
statusCode: 200,
|
|
133
|
-
headers: corsHeaders(),
|
|
134
|
-
body: Dict.fromArray([
|
|
135
|
-
("uploadUrl", JSON.Encode.string(uploadUrl)),
|
|
136
|
-
("storageRef", JSON.Encode.string(storageRef)),
|
|
137
|
-
])
|
|
138
|
-
->JSON.Encode.object
|
|
139
|
-
->JSON.stringify,
|
|
140
|
-
}
|
|
141
|
-
} catch {
|
|
142
|
-
| _ => {
|
|
143
|
-
statusCode: 400,
|
|
144
|
-
headers: corsHeaders(),
|
|
145
|
-
body: Dict.fromArray([("error", JSON.Encode.string("presign_failed"))])
|
|
146
|
-
->JSON.Encode.object
|
|
147
|
-
->JSON.stringify,
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// ── Deploy-time factory ─────────────────────────────────────────────────────
|
|
153
|
-
|
|
154
23
|
type serviceOutputs = {
|
|
155
24
|
url: Pulumi.Output.t<string>,
|
|
156
25
|
resources: array<Pulumi.Output.t<string>>,
|
|
@@ -181,7 +50,8 @@ let make = (
|
|
|
181
50
|
~opts?,
|
|
182
51
|
)
|
|
183
52
|
|
|
184
|
-
//
|
|
53
|
+
// CloudWatch Logs (so failures are observable) plus least-privilege
|
|
54
|
+
// `s3:PutObject` on any key under the target bucket.
|
|
185
55
|
let _policy =
|
|
186
56
|
bucketName
|
|
187
57
|
->Pulumi.Output.fromInput
|
|
@@ -193,6 +63,12 @@ let make = (
|
|
|
193
63
|
policy: PolicyDocument.make(
|
|
194
64
|
~id=`${serviceName}Policy`,
|
|
195
65
|
~statements=[
|
|
66
|
+
{
|
|
67
|
+
sid: "AllowLambdaLogging",
|
|
68
|
+
effect: Allow,
|
|
69
|
+
actions: Actions(["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"]),
|
|
70
|
+
resources: Resource("arn:aws:logs:*:*:*"),
|
|
71
|
+
},
|
|
196
72
|
{
|
|
197
73
|
sid: "AllowUploadPut",
|
|
198
74
|
effect: Allow,
|
|
@@ -209,29 +85,54 @@ let make = (
|
|
|
209
85
|
)
|
|
210
86
|
})
|
|
211
87
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
("
|
|
218
|
-
|
|
219
|
-
|
|
88
|
+
// Bundle reventless-aws (the compiled `_Ops` handler lives inside it) and
|
|
89
|
+
// re-export its `handler`; buildCodeArchive also ships the ESM resolve-hook.
|
|
90
|
+
let packageDirs = Dict.fromArray([
|
|
91
|
+
(
|
|
92
|
+
"@reventlessdev/reventless-aws",
|
|
93
|
+
Util_Bundle.resolvePackageRoot("@reventlessdev/reventless-aws"),
|
|
94
|
+
),
|
|
95
|
+
])
|
|
96
|
+
let {code, sourceCodeHash} = Util_Bundle.buildCodeArchive(
|
|
97
|
+
~entryPointModule="@reventlessdev/reventless-aws/src/adapter/Upload/Upload_Presign_S3_Ops.res.mjs",
|
|
98
|
+
~packageDirs,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
let layers =
|
|
102
|
+
Lambda.reventlessLayerArn
|
|
103
|
+
->Option.map(arn => [arn->Pulumi.Input.make])
|
|
104
|
+
->Option.getOr([])
|
|
105
|
+
->Pulumi.Input.make
|
|
220
106
|
|
|
221
|
-
let lambda = Lambda.
|
|
107
|
+
let lambda = Lambda.Function.make(
|
|
222
108
|
~name=serviceName,
|
|
223
|
-
~args=
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
109
|
+
~args={
|
|
110
|
+
handler: "index.handler"->Pulumi.Input.make,
|
|
111
|
+
runtime: "nodejs22.x"->Pulumi.Input.make,
|
|
112
|
+
code: code->Pulumi.Input.make,
|
|
113
|
+
sourceCodeHash: sourceCodeHash->Pulumi.Input.make,
|
|
114
|
+
role: lambdaRole.arn->Pulumi.Output.asInput,
|
|
115
|
+
memorySize: 256->Pulumi.Input.make,
|
|
116
|
+
timeout: 30->Pulumi.Input.make,
|
|
117
|
+
layers,
|
|
118
|
+
tags: AWS.Tags.make(
|
|
229
119
|
~name=serviceName,
|
|
230
120
|
~kind=ReventlessCore.ComponentType.Platform,
|
|
231
121
|
~role=Runtime,
|
|
232
122
|
~scope=Platform,
|
|
233
123
|
),
|
|
234
|
-
|
|
124
|
+
environment: (
|
|
125
|
+
{
|
|
126
|
+
Lambda.Function.variables: Dict.fromArray([
|
|
127
|
+
("Environment", Pulumi.Pulumi.getStackName()->Pulumi.Input.make),
|
|
128
|
+
("UPLOAD_BUCKET", bucketName),
|
|
129
|
+
("SERVED_PREFIX", Pulumi.Input.make(servedPrefix)),
|
|
130
|
+
("NODE_OPTIONS", Util_Bundle.esmLoaderNodeOptions->Pulumi.Input.make),
|
|
131
|
+
("ESM_FALLBACK_DIRS", Util_Bundle.esmFallbackDirs->Pulumi.Input.make),
|
|
132
|
+
]),
|
|
133
|
+
}: Lambda.Function.functionEnvironment
|
|
134
|
+
)->Pulumi.Input.make,
|
|
135
|
+
},
|
|
235
136
|
~opts?,
|
|
236
137
|
)
|
|
237
138
|
|