@reventlessdev/reventless-aws 3.0.0-alpha.271 → 3.0.0-alpha.273

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 (38) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/package.json +9 -8
  3. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +20 -16
  4. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +7 -7
  5. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res +14 -5
  6. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res.mjs +10 -3
  7. package/src/adapter/Api/Platform_UIFragments_Lambda.res +15 -13
  8. package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +3 -2
  9. package/src/adapter/EventCollector/EventCollectorChannel_Helpers.res +8 -1
  10. package/src/adapter/EventCollector/EventCollectorChannel_Helpers.res.mjs +3 -2
  11. package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver.res +15 -13
  12. package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver.res.mjs +3 -2
  13. package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res +6 -12
  14. package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res.mjs +4 -8
  15. package/src/adapter/QueryDb/QueryInterceptor_Provisioning.res +155 -0
  16. package/src/adapter/QueryDb/QueryInterceptor_Provisioning.res.mjs +92 -0
  17. package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res +4 -9
  18. package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res.mjs +8 -2
  19. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +32 -62
  20. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +7 -20
  21. package/src/adapter/StateTopic/StateTopic_AppSync.res +15 -13
  22. package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +3 -2
  23. package/src/adapter/Upload/Upload_Claim_S3.res +15 -13
  24. package/src/adapter/Upload/Upload_Claim_S3.res.mjs +3 -2
  25. package/src/adapter/Upload/Upload_Presign_S3.res +15 -13
  26. package/src/adapter/Upload/Upload_Presign_S3.res.mjs +3 -2
  27. package/src/components/Api/AppSync_Adapter.res +13 -9
  28. package/src/components/Api/AppSync_Adapter.res.mjs +4 -3
  29. package/src/util/Util_LambdaLogging.res +69 -21
  30. package/src/util/Util_LambdaLogging.res.mjs +21 -8
  31. package/src/util/Util_LogGroup_Adopting.res +480 -0
  32. package/src/util/Util_LogGroup_Adopting.res.mjs +365 -0
  33. package/src/util/Util_LogGroup_Adopting_Js.mjs +20 -0
  34. package/src/util/Util_LogRetention.res +11 -12
  35. package/tests/Util_LambdaLoggingTest.res +36 -0
  36. package/tests/Util_LambdaLoggingTest.res.mjs +22 -0
  37. package/tests/Util_LogGroup_AdoptingTest.res +169 -0
  38. package/tests/Util_LogGroup_AdoptingTest.res.mjs +208 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,25 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 3.0.0-alpha.273 (2026-08-08)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **aws:** create a Lambda's log group before the function that writes to it ([8d1e459](https://github.com/ReventlessDev/reventless-core/commit/8d1e4591127489d2ae8e84e7ade3843a1a859eb6))
11
+ * **core:** attribute resources built after a builder's construct returns ([67d6d9c](https://github.com/ReventlessDev/reventless-core/commit/67d6d9c0ff3ff1d38cd9e8ad43a4b86121abf827))
12
+ ### Features
13
+
14
+ * **aws:** adopt an existing AppSync log group instead of failing on it ([ebed6c1](https://github.com/ReventlessDev/reventless-core/commit/ebed6c1ceb77b8deebff10a9c45715c107861f04))
15
+ * **core:** let an extension switch query interception on, and provision it ([4523b10](https://github.com/ReventlessDev/reventless-core/commit/4523b10597f1a028eedccc42391b250b25a6ddd3))
16
+
17
+
18
+ # 3.0.0-alpha.272 (2026-08-08)
19
+
20
+ ### Bug Fixes
21
+
22
+ * **aws:** ship the admin structure as a code asset, not an env var ([93bc8a4](https://github.com/ReventlessDev/reventless-core/commit/93bc8a4891b54efeb13b99845a96f990dea6d41c))
23
+
24
+
6
25
  # 3.0.0-alpha.271 (2026-08-08)
7
26
 
8
27
  **Note:** Version bump only for package @reventlessdev/reventless-aws
package/package.json CHANGED
@@ -1,27 +1,28 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.271",
3
+ "version": "3.0.0-alpha.273",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "dependencies": {
7
7
  "@aws-sdk/client-appsync": "3.970.0",
8
8
  "@aws-sdk/client-cloudfront": "3.970.0",
9
+ "@aws-sdk/client-cloudwatch-logs": "3.970.0",
9
10
  "@aws-sdk/client-location": "3.970.0",
10
11
  "@aws-sdk/client-s3": "3.970.0",
11
12
  "@aws-sdk/s3-request-presigner": "3.970.0",
12
13
  "sury": "11.0.0-alpha.4",
13
14
  "uuid": "^13.0.0",
14
15
  "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.4",
15
- "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
16
- "@reventlessdev/rescript-node": "2.0.0-alpha.2",
17
- "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.66",
18
16
  "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
19
- "@reventlessdev/reventless-core": "3.0.0-alpha.216",
17
+ "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.67",
18
+ "@reventlessdev/rescript-node": "2.0.0-alpha.2",
19
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
20
+ "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
21
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.128",
22
+ "@reventlessdev/reventless-core": "3.0.0-alpha.217",
20
23
  "@reventlessdev/reventless-interop": "3.0.0-alpha.30",
21
24
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
22
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.80",
23
- "@reventlessdev/reventless-infra": "3.0.0-alpha.128",
24
- "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
25
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.81",
25
26
  "@reventlessdev/reventless-spec": "3.0.0-alpha.102"
26
27
  },
27
28
  "devDependencies": {
@@ -115,14 +115,17 @@ let make = (
115
115
  // Bundle reventless-aws (the compiled `_Ops` handler lives inside it) and
116
116
  // re-export its `handler`; buildCodeArchive also ships the ESM resolve-hook so
117
117
  // the handler's bare @aws-sdk/* specifiers resolve from the managed runtime.
118
- // The admin entry is passed as an env var (ADMIN_ENTRY_JSON) rather than
119
- // interpolated into the source, so the archive hash is stable across deploys.
118
+ // The admin entry rides the archive as `adminEntry.json` rather than an env var:
119
+ // it alone encodes to ~3 KB, which together with the other variables exceeds the
120
+ // 4096-byte UpdateFunctionConfiguration limit. Its content participates in
121
+ // `sourceCodeHash`, so a change to the admin structure redeploys the code.
120
122
  let packageDirs = Dict.fromArray([
121
123
  ("@reventlessdev/reventless-aws", Util_Bundle.resolvePackageRoot("@reventlessdev/reventless-aws")),
122
124
  ])
123
125
  let {code, sourceCodeHash} = Util_Bundle.buildCodeArchive(
124
126
  ~entryPointModule="@reventlessdev/reventless-aws/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res.mjs",
125
127
  ~packageDirs,
128
+ ~extraStringAssets=Dict.fromArray([("adminEntry.json", adminEntryJson)]),
126
129
  ~bundleRuntimeExtensions=false,
127
130
  )
128
131
 
@@ -132,6 +135,20 @@ let make = (
132
135
  ->Option.getOr([])
133
136
  ->Pulumi.Input.make
134
137
 
138
+ // Created before the function that writes to it — see
139
+ // `Util_LambdaLogging.makeManagedLogGroup` for why the ordering matters.
140
+ let logGroup = Util_LambdaLogging.makeManagedLogGroup(
141
+ ~name=name ++ "Lambda",
142
+ ~tags=AWS.Tags.make(
143
+ ~name=name ++ "LambdaLogGroup",
144
+ ~kind=ReventlessCore.ComponentType.Platform,
145
+ ~role=Logs,
146
+ ~scope=Platform,
147
+ ),
148
+ ~opts,
149
+ (),
150
+ )
151
+
135
152
  let lambda = Lambda.Function.make(
136
153
  ~name=name ++ "Lambda",
137
154
  ~args={
@@ -150,30 +167,17 @@ let make = (
150
167
  ("Environment", Pulumi.Pulumi.getStackName()->Pulumi.Input.make),
151
168
  ("PLUGIN_RM_TABLE", pluginReadModelTableName->Pulumi.Output.asInput),
152
169
  ("OFFLOAD_BUCKET", offloadBucketName->Pulumi.Output.asInput),
153
- ("ADMIN_ENTRY_JSON", adminEntryJson->Pulumi.Input.make),
154
170
  ("NODE_OPTIONS", Util_Bundle.esmLoaderNodeOptions->Pulumi.Input.make),
155
171
  ("ESM_FALLBACK_DIRS", Util_Bundle.esmFallbackDirs->Pulumi.Input.make),
156
172
  Util_LambdaLogging.logLevelEntry(),
157
173
  ]),
158
174
  }: Lambda.Function.functionEnvironment
159
175
  )->Pulumi.Input.make,
176
+ loggingConfig: ?Util_LambdaLogging.loggingConfigFor(logGroup),
160
177
  },
161
178
  ~opts,
162
179
  )
163
180
 
164
- Util_LambdaLogging.makeManagedLogGroup(
165
- ~name=name ++ "Lambda",
166
- ~lambdaName=lambda.name,
167
- ~tags=AWS.Tags.make(
168
- ~name=name ++ "LambdaLogGroup",
169
- ~kind=ReventlessCore.ComponentType.Platform,
170
- ~role=Logs,
171
- ~scope=Platform,
172
- ),
173
- ~opts,
174
- (),
175
- )
176
-
177
181
  let dataSourceRole = IAM.Role.makeWithDefaultPolicy(
178
182
  ~name=name ++ "DataSource",
179
183
  ~servicePrincipal=AWS.AppSync.principal->Pulumi.Output.make,
@@ -75,8 +75,12 @@ function make(api, pluginReadModelTableName, offloadBucketName, schemaReady, opt
75
75
  "@reventlessdev/reventless-aws",
76
76
  Util_Bundle$ReventlessAws.resolvePackageRoot(undefined, "@reventlessdev/reventless-aws")
77
77
  ]]);
78
- let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res.mjs", packageDirs, undefined, false);
78
+ let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res.mjs", packageDirs, Object.fromEntries([[
79
+ "adminEntry.json",
80
+ adminEntryJson
81
+ ]]), false);
79
82
  let layers = Stdlib_Option.getOr(Stdlib_Option.map(Lambda$PulumiAws.reventlessLayerArn, arn => [arn]), []);
83
+ let logGroup = Util_LambdaLogging$ReventlessAws.makeManagedLogGroup(name + "Lambda", undefined, AWS_Tags$ReventlessAws.make(name + "LambdaLogGroup", "Platform", "Logs", "Platform", undefined, undefined, undefined, undefined), opts$1, undefined);
80
84
  let lambda = new (Aws.lambda.Function)(name + "Lambda", {
81
85
  handler: "index.handler",
82
86
  runtime: "nodejs22.x",
@@ -100,10 +104,6 @@ function make(api, pluginReadModelTableName, offloadBucketName, schemaReady, opt
100
104
  "OFFLOAD_BUCKET",
101
105
  offloadBucketName
102
106
  ],
103
- [
104
- "ADMIN_ENTRY_JSON",
105
- adminEntryJson
106
- ],
107
107
  [
108
108
  "NODE_OPTIONS",
109
109
  Util_Bundle$ReventlessAws.esmLoaderNodeOptions
@@ -115,9 +115,9 @@ function make(api, pluginReadModelTableName, offloadBucketName, schemaReady, opt
115
115
  Util_LambdaLogging$ReventlessAws.logLevelEntry()
116
116
  ])
117
117
  },
118
- sourceCodeHash: match.sourceCodeHash
118
+ sourceCodeHash: match.sourceCodeHash,
119
+ loggingConfig: Util_LambdaLogging$ReventlessAws.loggingConfigFor(logGroup)
119
120
  }, opts$1);
120
- Util_LambdaLogging$ReventlessAws.makeManagedLogGroup(name + "Lambda", lambda.name, AWS_Tags$ReventlessAws.make(name + "LambdaLogGroup", "Platform", "Logs", "Platform", undefined, undefined, undefined, undefined), opts$1, undefined);
121
121
  let dataSourceRole = IAM$PulumiAws.Role.makeWithDefaultPolicy(name + "DataSource", Pulumi.output(AWS$ReventlessAws.AppSync.principal), AWS_Tags$ReventlessAws.make(name + "DataSource", "Platform", "Identity", "Platform", undefined, undefined, undefined, undefined), opts$1);
122
122
  Pulumi.all([
123
123
  lambda.arn,
@@ -68,17 +68,26 @@ let toEntryWith = (~filter: bool, item: dict<JSON.t>, ~name: string): option<JSO
68
68
  }
69
69
 
70
70
 
71
- // The built-in Platform_Admin entry, injected at deploy time as the
72
- // ADMIN_ENTRY_JSON env var (the admin never Connects to itself, so its structure
73
- // never enters the Plugin read model).
71
+ // The built-in Platform_Admin entry, written at deploy time as an asset file in
72
+ // the code archive (the admin never Connects to itself, so its structure never
73
+ // enters the Plugin read model). It rides the archive rather than an env var
74
+ // because it alone is ~3 KB and the function's variables must fit AWS's 4096-byte
75
+ // UpdateFunctionConfiguration limit. process.cwd() is /var/task in Lambda, where
76
+ // the archive is extracted.
74
77
  //
75
78
  // Shared by both fields. The admin structure has no Internal components and no
76
79
  // extension points, so its filtered and complete encodings carry the same
77
80
  // components; the structure-level fields the complete entry adds are absent from
78
81
  // this JSON and resolve to null, which is what `extensionPoints: None` means.
79
82
  let adminEntry: option<JSON.t> =
80
- switch NodeProcess.env->Dict.get("ADMIN_ENTRY_JSON") {
81
- | Some(s) if s != "" => Some(JSON.parseOrThrow(s))
83
+ try {
84
+ switch NodePath.join([NodeProcess.cwd(), "adminEntry.json"])
85
+ ->NodeFs.readFileSync
86
+ ->JSON.parseOrThrow {
87
+ | JSON.Null => None
88
+ | json => Some(json)
89
+ }
90
+ } catch {
82
91
  | _ => None
83
92
  }
84
93
 
@@ -1,6 +1,8 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Nodefs from "node:fs";
3
4
  import * as S3$AwsSdk from "@reventlessdev/rescript-aws-sdk/src/S3.res.mjs";
5
+ import * as Nodepath from "node:path";
4
6
  import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
5
7
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
8
  import * as Offload$Reventless from "@reventlessdev/reventless-spec/src/semantic/Offload.res.mjs";
@@ -50,9 +52,14 @@ function toEntryWith(filter, item, name) {
50
52
  return entry;
51
53
  }
52
54
 
53
- let s = process.env["ADMIN_ENTRY_JSON"];
55
+ let adminEntry;
54
56
 
55
- let adminEntry = s !== undefined && s !== "" ? JSON.parse(s) : undefined;
57
+ try {
58
+ let json = JSON.parse(Nodefs.readFileSync(Nodepath.join(process.cwd(), "adminEntry.json"), "utf8"));
59
+ adminEntry = json === null ? undefined : json;
60
+ } catch (exn) {
61
+ adminEntry = undefined;
62
+ }
56
63
 
57
64
  function resolveStructure(fetch, item) {
58
65
  let refJson = Stdlib_Option.flatMap(Stdlib_Option.flatMap(item["structure"], Stdlib_JSON.Decode.object), o => o[Offload$Reventless.sentinelKey]);
@@ -112,4 +119,4 @@ export {
112
119
  isComplete,
113
120
  handler,
114
121
  }
115
- /* s Not a pure module */
122
+ /* adminEntry Not a pure module */
@@ -93,6 +93,20 @@ let make = (
93
93
  ->Option.getOr([])
94
94
  ->Pulumi.Input.make
95
95
 
96
+ // Created before the function that writes to it — see
97
+ // `Util_LambdaLogging.makeManagedLogGroup` for why the ordering matters.
98
+ let logGroup = Util_LambdaLogging.makeManagedLogGroup(
99
+ ~name=name ++ "Lambda",
100
+ ~tags=AWS.Tags.make(
101
+ ~name=name ++ "LambdaLogGroup",
102
+ ~kind=ReventlessCore.ComponentType.Platform,
103
+ ~role=Logs,
104
+ ~scope=Platform,
105
+ ),
106
+ ~opts,
107
+ (),
108
+ )
109
+
96
110
  let lambda = Lambda.Function.make(
97
111
  ~name=name ++ "Lambda",
98
112
  ~args={
@@ -116,23 +130,11 @@ let make = (
116
130
  ]),
117
131
  }: Lambda.Function.functionEnvironment
118
132
  )->Pulumi.Input.make,
133
+ loggingConfig: ?Util_LambdaLogging.loggingConfigFor(logGroup),
119
134
  },
120
135
  ~opts,
121
136
  )
122
137
 
123
- Util_LambdaLogging.makeManagedLogGroup(
124
- ~name=name ++ "Lambda",
125
- ~lambdaName=lambda.name,
126
- ~tags=AWS.Tags.make(
127
- ~name=name ++ "LambdaLogGroup",
128
- ~kind=ReventlessCore.ComponentType.Platform,
129
- ~role=Logs,
130
- ~scope=Platform,
131
- ),
132
- ~opts,
133
- (),
134
- )
135
-
136
138
  let dataSourceRole = IAM.Role.makeWithDefaultPolicy(
137
139
  ~name=name ++ "DataSource",
138
140
  ~servicePrincipal=AWS.AppSync.principal->Pulumi.Output.make,
@@ -54,6 +54,7 @@ function make(api, uiFragmentRegistryTableName, schemaReady, opts) {
54
54
  ]]);
55
55
  let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Api/Platform_UIFragments_Lambda_Ops.res.mjs", packageDirs, undefined, false);
56
56
  let layers = Stdlib_Option.getOr(Stdlib_Option.map(Lambda$PulumiAws.reventlessLayerArn, arn => [arn]), []);
57
+ let logGroup = Util_LambdaLogging$ReventlessAws.makeManagedLogGroup(name + "Lambda", undefined, AWS_Tags$ReventlessAws.make(name + "LambdaLogGroup", "Platform", "Logs", "Platform", undefined, undefined, undefined, undefined), opts$1, undefined);
57
58
  let lambda = new (Aws.lambda.Function)(name + "Lambda", {
58
59
  handler: "index.handler",
59
60
  runtime: "nodejs22.x",
@@ -84,9 +85,9 @@ function make(api, uiFragmentRegistryTableName, schemaReady, opts) {
84
85
  Util_LambdaLogging$ReventlessAws.logLevelEntry()
85
86
  ])
86
87
  },
87
- sourceCodeHash: match.sourceCodeHash
88
+ sourceCodeHash: match.sourceCodeHash,
89
+ loggingConfig: Util_LambdaLogging$ReventlessAws.loggingConfigFor(logGroup)
88
90
  }, opts$1);
89
- Util_LambdaLogging$ReventlessAws.makeManagedLogGroup(name + "Lambda", lambda.name, AWS_Tags$ReventlessAws.make(name + "LambdaLogGroup", "Platform", "Logs", "Platform", undefined, undefined, undefined, undefined), opts$1, undefined);
90
91
  let dataSourceRole = IAM$PulumiAws.Role.makeWithDefaultPolicy(name + "DataSource", Pulumi.output(AWS$ReventlessAws.AppSync.principal), AWS_Tags$ReventlessAws.make(name + "DataSource", "Platform", "Identity", "Platform", undefined, undefined, undefined, undefined), opts$1);
91
92
  Pulumi.all([
92
93
  lambda.arn,
@@ -125,7 +125,14 @@ let connectLambda = (
125
125
  resources->ReventlessCore.Adapter.resourcesToResolvedOutput,
126
126
  )
127
127
  ->Pulumi.Output.all3
128
- ->Pulumi.Output.apply(((eventTopicResources, queueArns, resources)) => {
128
+ // Provisions the collector's role, policies and event-source mappings, so it
129
+ // carries the plugin's attribution across the apply — by the time this runs
130
+ // the builder's construct has returned and the ambient context is empty.
131
+ ->ReventlessCore.ResourceAttribution_Deploytime.applyAttributed(((
132
+ eventTopicResources,
133
+ queueArns,
134
+ resources,
135
+ )) => {
129
136
  log.debug(
130
137
  ~comp="EventCollector",
131
138
  `connectLambda ${name}: ${eventTopicResources->Array.length->Int.toString} topic resource(s), ${resources->Array.length->Int.toString} resource(s)`,
@@ -15,6 +15,7 @@ import * as Adapter_Helpers$ReventlessAws from "../Adapter_Helpers.res.mjs";
15
15
  import * as EventCollector$ReventlessCore from "@reventlessdev/reventless-core/src/components/EventCollector/EventCollector.res.mjs";
16
16
  import * as AdapterDeploytime$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/AdapterDeploytime.res.mjs";
17
17
  import * as Util_EventSourceMapping$ReventlessAws from "../../util/Util_EventSourceMapping.res.mjs";
18
+ import * as ResourceAttribution_Deploytime$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/ResourceAttribution_Deploytime.res.mjs";
18
19
 
19
20
  let log = Logger$ReventlessCore.fromEnv();
20
21
 
@@ -76,11 +77,11 @@ function esmTags(name, esmName) {
76
77
  }
77
78
 
78
79
  function connectLambda(lambda, name, lambdaRole, queues, eventTopics, resources, opts) {
79
- Pulumi.all([
80
+ ResourceAttribution_Deploytime$ReventlessCore.applyAttributed(Pulumi.all([
80
81
  toResources(eventTopics),
81
82
  Pulumi.all(queues.map(queue => queue.arn)),
82
83
  Adapter$ReventlessCore.resourcesToResolvedOutput(resources)
83
- ]).apply(param => {
84
+ ]), param => {
84
85
  let resources = param[2];
85
86
  let queueArns = param[1];
86
87
  let eventTopicResources = param[0];
@@ -118,6 +118,20 @@ let make = (
118
118
  ->Option.getOr([])
119
119
  ->Pulumi.Input.make
120
120
 
121
+ // Created before the function that writes to it — see
122
+ // `Util_LambdaLogging.makeManagedLogGroup` for why the ordering matters.
123
+ let logGroup = Util_LambdaLogging.makeManagedLogGroup(
124
+ ~name=name ++ "Lambda",
125
+ ~tags=AWS.Tags.make(
126
+ ~name=name ++ "LambdaLogGroup",
127
+ ~kind=ReventlessCore.ComponentType.Platform,
128
+ ~role=Logs,
129
+ ~scope=Platform,
130
+ ),
131
+ ~opts,
132
+ (),
133
+ )
134
+
121
135
  let lambda = Lambda.Function.make(
122
136
  ~name=name ++ "Lambda",
123
137
  ~args={
@@ -146,23 +160,11 @@ let make = (
146
160
  ]),
147
161
  }: Lambda.Function.functionEnvironment
148
162
  )->Pulumi.Input.make,
163
+ loggingConfig: ?Util_LambdaLogging.loggingConfigFor(logGroup),
149
164
  },
150
165
  ~opts,
151
166
  )
152
167
 
153
- Util_LambdaLogging.makeManagedLogGroup(
154
- ~name=name ++ "Lambda",
155
- ~lambdaName=lambda.name,
156
- ~tags=AWS.Tags.make(
157
- ~name=name ++ "LambdaLogGroup",
158
- ~kind=ReventlessCore.ComponentType.Platform,
159
- ~role=Logs,
160
- ~scope=Platform,
161
- ),
162
- ~opts,
163
- (),
164
- )
165
-
166
168
  let dataSourceRole = IAM.Role.makeWithDefaultPolicy(
167
169
  ~name=name ++ "DataSource",
168
170
  ~servicePrincipal=AWS.AppSync.principal->Pulumi.Output.make,
@@ -58,6 +58,7 @@ function make(api, placeIndexName, nameOpt, opts) {
58
58
  ]]);
59
59
  let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver_Ops.res.mjs", packageDirs, undefined, false);
60
60
  let layers = Stdlib_Option.getOr(Stdlib_Option.map(Lambda$PulumiAws.reventlessLayerArn, arn => [arn]), []);
61
+ let logGroup = Util_LambdaLogging$ReventlessAws.makeManagedLogGroup(name + "Lambda", undefined, AWS_Tags$ReventlessAws.make(name + "LambdaLogGroup", "Platform", "Logs", "Platform", undefined, undefined, undefined, undefined), opts$1, undefined);
61
62
  let lambda = new (Aws.lambda.Function)(name + "Lambda", {
62
63
  handler: "index.handler",
63
64
  runtime: "nodejs22.x",
@@ -88,9 +89,9 @@ function make(api, placeIndexName, nameOpt, opts) {
88
89
  Util_LambdaLogging$ReventlessAws.logLevelEntry()
89
90
  ])
90
91
  },
91
- sourceCodeHash: match.sourceCodeHash
92
+ sourceCodeHash: match.sourceCodeHash,
93
+ loggingConfig: Util_LambdaLogging$ReventlessAws.loggingConfigFor(logGroup)
92
94
  }, opts$1);
93
- Util_LambdaLogging$ReventlessAws.makeManagedLogGroup(name + "Lambda", lambda.name, AWS_Tags$ReventlessAws.make(name + "LambdaLogGroup", "Platform", "Logs", "Platform", undefined, undefined, undefined, undefined), opts$1, undefined);
94
95
  let dataSourceRole = IAM$PulumiAws.Role.makeWithDefaultPolicy(name + "DataSource", Pulumi.output(AWS$ReventlessAws.AppSync.principal), AWS_Tags$ReventlessAws.make(name + "DataSource", "Platform", "Identity", "Platform", undefined, undefined, undefined, undefined), opts$1);
95
96
  Pulumi.all([
96
97
  lambda.arn,
@@ -13,15 +13,6 @@ let log = ReventlessCore.Logger.fromEnv()
13
13
  type api = Types.AppSync.api
14
14
  type role = Types.AppSync.role
15
15
 
16
- type interceptorConfig = {
17
- dataSourceName: Pulumi.Input.t<string>,
18
- }
19
-
20
- /** Deploy-time config for the query interceptor. When set, all top-level Query
21
- resolvers become pipeline resolvers with an interceptor Lambda function
22
- preceding the DynamoDB query. Set this before calling `make`. */
23
- let queryInterceptorConfig: ref<option<interceptorConfig>> = ref(None)
24
-
25
16
  let interceptorCode = readModelName =>
26
17
  `import { util } from '@aws-appsync/utils';
27
18
  export function request(ctx) {
@@ -106,9 +97,12 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
106
97
  }
107
98
 
108
99
  // Creates either a unit resolver (no interceptor) or a pipeline resolver
109
- // (interceptor Lambda → DynamoDB query) depending on queryInterceptorConfig.
100
+ // (interceptor Lambda → DynamoDB query), depending on whether an extension
101
+ // switched interception on for this deployment. Provisioning the interceptor
102
+ // is the framework's job — see `QueryInterceptor_Provisioning`; the first
103
+ // Query resolver under a plugin creates it and the rest reuse it.
110
104
  let makeQueryResolver = (~resolverName, ~field, ~code) =>
111
- switch queryInterceptorConfig.contents {
105
+ switch QueryInterceptor_Provisioning.dataSourceName(~api, ~opts) {
112
106
  | None =>
113
107
  Resolver.makeUnitJsResolver(
114
108
  ~name=resolverName,
@@ -119,7 +113,7 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
119
113
  ~code,
120
114
  ~opts,
121
115
  )
122
- | Some({dataSourceName: interceptorDsName}) =>
116
+ | Some(interceptorDsName) =>
123
117
  let interceptorFn = Function.makeJs(
124
118
  ~name=resolverName ++ "Interceptor",
125
119
  ~api,
@@ -14,13 +14,10 @@ import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/s
14
14
  import * as AppSync_Resolver_Functions$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/AppSync/AppSync_Resolver_Functions.res.mjs";
15
15
  import * as AppSync_Resolver_Retrying$ReventlessAws from "../Api/AppSync_Resolver_Retrying.res.mjs";
16
16
  import * as GraphQL_FragmentGenerator$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_FragmentGenerator.res.mjs";
17
+ import * as QueryInterceptor_Provisioning$ReventlessAws from "./QueryInterceptor_Provisioning.res.mjs";
17
18
 
18
19
  let log = Logger$ReventlessCore.fromEnv();
19
20
 
20
- let queryInterceptorConfig = {
21
- contents: undefined
22
- };
23
-
24
21
  function interceptorCode(readModelName) {
25
22
  return `import { util } from '@aws-appsync/utils';
26
23
  export function request(ctx) {
@@ -70,11 +67,11 @@ function make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolve
70
67
  let includeIdParam = registryEntry !== undefined ? registryEntry.includeIdParam : true;
71
68
  let connectionSpec = registryEntry !== undefined ? registryEntry.connectionSpec : true;
72
69
  let makeQueryResolver = (resolverName, field, code) => {
73
- let match = queryInterceptorConfig.contents;
74
- if (match === undefined) {
70
+ let interceptorDsName = QueryInterceptor_Provisioning$ReventlessAws.dataSourceName(api, opts);
71
+ if (interceptorDsName === undefined) {
75
72
  return AppSync_Resolver_Retrying$ReventlessAws.makeUnitJsResolver(resolverName, api, dataSourceName, "Query", field, code, opts);
76
73
  }
77
- let interceptorFn = AppSync_Function$PulumiAws.makeJs(resolverName + "Interceptor", api, match.dataSourceName, interceptorCode(name$1), opts);
74
+ let interceptorFn = AppSync_Function$PulumiAws.makeJs(resolverName + "Interceptor", api, interceptorDsName, interceptorCode(name$1), opts);
78
75
  let queryFn = AppSync_Function$PulumiAws.makeJs(resolverName + "Query", api, dataSourceName, code, opts);
79
76
  return AppSync_Resolver_Retrying$ReventlessAws.makePipelineJsResolver(resolverName, api, "Query", field, AppSync_Resolver_Functions$PulumiAws.pipelinePassThrough, [
80
77
  interceptorFn,
@@ -238,7 +235,6 @@ let Resolver;
238
235
  export {
239
236
  Resolver,
240
237
  log,
241
- queryInterceptorConfig,
242
238
  interceptorCode,
243
239
  internalRowRequiredAttr,
244
240
  make,
@@ -0,0 +1,155 @@
1
+ /** Provisions the query-interceptor path on AppSync, when an extension has asked
2
+ for it via `ReventlessCore.QueryInterception.use`.
3
+
4
+ The framework owns everything provider-shaped here — the runtime, its
5
+ execution role, the AppSync Lambda data source and the role that lets AppSync
6
+ invoke it — because all of it needs the api handle and a service role, which
7
+ exist only inside the plugin build. An extension supplies none of it; see the
8
+ seam's own docstring for why that split is the point.
9
+
10
+ **One interceptor per api, not per read model.** A data source belongs to one
11
+ api, so it is memoised on the api handle *by identity* — the builders thread
12
+ a single `api` value through every read model that resolves against it, and
13
+ identity is what guarantees the data source ends up attached to the very api
14
+ the resolver names. Keying on anything looser (the owning plugin, say) would
15
+ be right until one key spanned two apis, and the failure then is a resolver
16
+ pointing at a data source that does not exist on its api — rejected at deploy
17
+ time, with nothing in the message about why.
18
+
19
+ The api's *contents* are an `Output` and are unknown at
20
+ resolver-construction time, which is why the ambient `ResourceAttribution`
21
+ plugin supplies the resource NAMES instead. That is presentation, not
22
+ identity: a second api under one plugin gets a suffixed name rather than a
23
+ shared data source. Substrate built outside any plugin construct (the admin
24
+ read models on the platform api) is named `Platform`.
25
+
26
+ The runtime is built through `RuntimeEnvironment_Lambda.makeFromCodeAsset`
27
+ rather than a bespoke `Lambda.Function.make`, which is what makes the seams
28
+ compose: that path bundles registered runtime extensions into the archive and
29
+ writes `RUNTIME_EXTENSIONS`, so an extension registering its interceptor in
30
+ `onColdStart` reaches this runtime without a second registration. */
31
+
32
+ open PulumiAws
33
+
34
+ // Memoised on the api handle itself — see the module docstring for why identity
35
+ // rather than a name. The value is the data-source name the resolver pipeline
36
+ // references.
37
+ let byApi: Map.t<Types.AppSync.api, Pulumi.Input.t<string>> = Map.make()
38
+
39
+ // How many interceptors a given owner already named, so a second api under one
40
+ // owner gets a distinct resource name instead of colliding with the first.
41
+ let namesUsed: dict<int> = Dict.make()
42
+
43
+ /** A stable, readable resource-name stem for the interceptor: the owning plugin,
44
+ or `Platform` for substrate provisioned outside any plugin construct. */
45
+ let nameFor = () => {
46
+ let owner = ReventlessCore.ResourceAttribution.current.contents.plugin->Option.getOr("Platform")
47
+ let used = namesUsed->Dict.get(owner)->Option.getOr(0)
48
+ namesUsed->Dict.set(owner, used + 1)
49
+ used == 0
50
+ ? owner ++ "QueryInterceptor"
51
+ : owner ++ "QueryInterceptor" ++ (used + 1)->Int.toString
52
+ }
53
+
54
+ let buildInterceptor = (~api: Types.AppSync.api, ~opts) => {
55
+ let name = nameFor()
56
+ let componentOpts: Pulumi.ComponentResource.options = {
57
+ parent: ?(opts: Pulumi.CustomResourceOptions.t).parent,
58
+ }
59
+
60
+ let packageDirs = Dict.fromArray([
61
+ (
62
+ "@reventlessdev/reventless-aws",
63
+ Util_Bundle.resolvePackageRoot("@reventlessdev/reventless-aws"),
64
+ ),
65
+ ])
66
+ let {code, sourceCodeHash} = Util_Bundle.buildCodeArchive(
67
+ ~entryPointModule="@reventlessdev/reventless-aws/src/adapter/QueryDb/QueryInterceptor_Lambda.res.mjs",
68
+ ~packageDirs,
69
+ )
70
+
71
+ // Sits in front of every read, so it is sized for a decision rather than for
72
+ // work: small memory, short timeout. A hook that needs longer than this is
73
+ // doing something a read path cannot afford anyway.
74
+ let runtime = RuntimeEnvironment_Lambda.makeFromCodeAsset(
75
+ ~name,
76
+ ~unitKind=ReventlessCore.Monitoring.Other("QueryInterceptor"),
77
+ ~componentKind=ReventlessCore.ComponentType.Plugin,
78
+ ~code,
79
+ ~sourceCodeHash,
80
+ ~memorySize=256,
81
+ ~timeout=10,
82
+ ~opts=componentOpts,
83
+ )
84
+ let lambdaArn = runtime.parts.lambda->Pulumi.Output.flatMap(lambda => lambda.arn)
85
+
86
+ let dataSourceRole = IAM.Role.makeWithDefaultPolicy(
87
+ ~name=name ++ "DataSource",
88
+ ~servicePrincipal=AWS.AppSync.principal->Pulumi.Output.make,
89
+ ~tags=AWS.Tags.make(
90
+ ~name=name ++ "DataSource",
91
+ ~kind=ReventlessCore.ComponentType.Plugin,
92
+ ~role=Identity,
93
+ ~scope=Plugin,
94
+ ),
95
+ ~opts,
96
+ )
97
+
98
+ let _ =
99
+ (lambdaArn, dataSourceRole.id)
100
+ ->Pulumi.Output.all2
101
+ ->Pulumi.Output.apply(((lambdaArn, dataSourceRoleId)) => {
102
+ open PolicyDocument
103
+ let _attach = IAM.RolePolicy.make(
104
+ ~name=name ++ "DataSource",
105
+ ~args={
106
+ IAM.RolePolicy.policy: PolicyDocument.make(
107
+ ~id=name ++ "DataSourcePolicy",
108
+ ~statements=[
109
+ {
110
+ sid: "AllowDataSourceInvokeInterceptor",
111
+ effect: Allow,
112
+ actions: Action("lambda:InvokeFunction"),
113
+ resources: Resource(lambdaArn),
114
+ },
115
+ ],
116
+ )
117
+ ->PolicyDocument.toJsonString
118
+ ->Pulumi.Input.make,
119
+ role: dataSourceRoleId->Pulumi.Input.make,
120
+ },
121
+ ~opts,
122
+ )
123
+ })
124
+
125
+ let dataSource = AppSync.DataSource.make(
126
+ ~name=name ++ "DataSource",
127
+ ~args={
128
+ type_: AWS_LAMBDA,
129
+ apiId: api->Pulumi.Output.flatMap(api => api.id)->Pulumi.Output.asInput,
130
+ lambdaConfig: {
131
+ AppSync.DataSource.functionArn: lambdaArn->Pulumi.Output.asInput,
132
+ }->Pulumi.Input.make,
133
+ serviceRoleArn: dataSourceRole.arn->Pulumi.Output.asInput,
134
+ },
135
+ ~opts=Some(opts),
136
+ )
137
+
138
+ dataSource.name->Pulumi.Output.asInput
139
+ }
140
+
141
+ /** The data source a Query resolver should put in front of its DynamoDB read, or
142
+ `None` when interception is off — in which case the caller builds the unit
143
+ resolver it always did and nothing here is provisioned. */
144
+ let dataSourceName = (~api: Types.AppSync.api, ~opts): option<Pulumi.Input.t<string>> =>
145
+ if !ReventlessCore.QueryInterception.isEnabled() {
146
+ None
147
+ } else {
148
+ switch byApi->Map.get(api) {
149
+ | Some(existing) => Some(existing)
150
+ | None =>
151
+ let created = buildInterceptor(~api, ~opts)
152
+ byApi->Map.set(api, created)
153
+ Some(created)
154
+ }
155
+ }