@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.
Files changed (33) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +5 -5
  3. package/src/adapter/Geocoder/Geocoder_AwsLocation.res +60 -141
  4. package/src/adapter/Geocoder/Geocoder_AwsLocation.res.mjs +52 -118
  5. package/src/adapter/Geocoder/Geocoder_AwsLocation_Ops.res +137 -0
  6. package/src/adapter/Geocoder/Geocoder_AwsLocation_Ops.res.mjs +124 -0
  7. package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +51 -6
  8. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +60 -0
  9. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +29 -1
  10. package/src/adapter/Runtime/HeartbeatEntryPoint.res +44 -0
  11. package/src/adapter/Runtime/HeartbeatEntryPoint.res.mjs +47 -0
  12. package/src/adapter/Runtime/PluginExtensionPointEntryPoint.res +234 -0
  13. package/src/adapter/Runtime/PluginExtensionPointEntryPoint.res.mjs +226 -0
  14. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +49 -4
  15. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +22 -4
  16. package/src/adapter/Upload/Upload_Presign_S3.res +65 -164
  17. package/src/adapter/Upload/Upload_Presign_S3.res.mjs +56 -119
  18. package/src/adapter/Upload/Upload_Presign_S3_Ops.res +157 -0
  19. package/src/adapter/Upload/Upload_Presign_S3_Ops.res.mjs +120 -0
  20. package/src/components/InboundTranslationSlice_Builder.res +27 -1
  21. package/src/components/InboundTranslationSlice_Builder.res.mjs +15 -2
  22. package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res +1 -1
  23. package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res.mjs +1 -1
  24. package/src/plugin/runtime/PluginRuntime_Builder.res +59 -1
  25. package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +49 -2
  26. package/tests/DcbInboundTranslationRoutingTest.res +73 -0
  27. package/tests/DcbInboundTranslationRoutingTest.res.mjs +71 -0
  28. package/tests/EpInboundTestSlice.res +19 -0
  29. package/tests/EpInboundTestSlice.res.mjs +31 -0
  30. package/tests/EpInboundTestSliceTranslation.res +13 -0
  31. package/tests/EpInboundTestSliceTranslation.res.mjs +33 -0
  32. package/src/adapter/Runtime/HeartbeatEntryPoint.mjs +0 -38
  33. 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.all3((dcbTableName, queue.id, pgConnectionFragment))
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 CallbackFunction (handler closure serialized
4
- // by Pulumi), an IAM execution role scoped to `s3:PutObject` on the target
5
- // bucket, and a Function URL (no auth) so a browser can request a presigned PUT.
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
- // Runtime: `handlePresign` reads a JSON body `{fileName, contentType}`, derives
8
- // a target key `uploads/<identity?>/<uuid>/<fileName>`, presigns a PUT to the
9
- // bucket named in `UPLOAD_BUCKET` (expires in 300s), and returns
10
- // `{uploadUrl, storageRef}`. A Bearer token, when present, is decoded (no
11
- // signature verification here that is the auth layer's job) and its `sub`
12
- // namespaces the key; anonymous callers are still served for the demo.
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
- // Least-privilege: PutObject on any key under the target bucket.
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
- let environment: Lambda.CallbackFunction.Args.functionEnvironment = {
213
- // CallbackFunction under-types env values as `dict<string>`; Pulumi resolves
214
- // Output-valued variables at deploy time, so bridge the Input here.
215
- variables: Dict.fromArray([
216
- ("UPLOAD_BUCKET", bucketName),
217
- ("SERVED_PREFIX", Pulumi.Input.make(servedPrefix)),
218
- ])->Obj.magic,
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.CallbackFunction.make(
107
+ let lambda = Lambda.Function.make(
222
108
  ~name=serviceName,
223
- ~args=Lambda.CallbackFunction.Args.make(
224
- ~callback=handlePresign,
225
- ~role=lambdaRole,
226
- ~environment,
227
- ~timeout=30->Pulumi.Input.make,
228
- ~tags=AWS.Tags.make(
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