@reventlessdev/reventless-aws 3.0.0-alpha.278 → 3.0.0-alpha.279
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 +12 -0
- package/package.json +6 -6
- package/src/adapter/EventCollector/EventCollectorChannel_Helpers.res +58 -35
- package/src/adapter/EventCollector/EventCollectorChannel_Helpers.res.mjs +21 -16
- package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res +42 -20
- package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res.mjs +13 -8
- package/src/adapter/QueryDb/QueryInterceptor_Lambda.res +21 -0
- package/src/adapter/QueryDb/QueryInterceptor_Lambda.res.mjs +6 -1
- package/src/adapter/Runtime/HandlerFactoryHelpers.mjs +5 -47
- package/src/adapter/Runtime/RuntimeExtensionsReady.mjs +59 -0
- package/src/adapter/StateTopic/StateTopic_AppSync.res +82 -7
- package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +17 -5
- package/src/adapter/StateTopic/StateTopic_AppSync_Helpers.res +36 -0
- package/src/adapter/StateTopic/StateTopic_AppSync_Helpers.res.mjs +17 -0
- package/tests/EventCollectorConnectLambdaPreviewTest.res +143 -0
- package/tests/EventCollectorConnectLambdaPreviewTest.res.mjs +101 -0
- package/tests/QueryInterceptor_LambdaTest.res +87 -0
- package/tests/QueryInterceptor_LambdaTest.res.mjs +103 -0
- package/tests/StateTopicKeySchemaTest.res +49 -0
- package/tests/StateTopicKeySchemaTest.res.mjs +40 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
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.279 (2026-08-10)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **aws:** load runtime extensions in the query interceptor, not just in entry shells ([d686407](https://github.com/ReventlessDev/reventless-core/commit/d686407239136c7370f5200c45b462109f94fdbe))
|
|
11
|
+
* **aws:** stop an unknown view table from deleting the collector's ESMs ([569e0b9](https://github.com/ReventlessDev/reventless-core/commit/569e0b9c86adca4286d82286ca58e096e62ffee5))
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
* **aws:** admit non-read-model tables to the state-topic relay ([9f7084f](https://github.com/ReventlessDev/reventless-core/commit/9f7084f4eb4040cafaead1c27044ae99e703dd97))
|
|
15
|
+
* **aws:** route every top-level Query field through the interceptor, not only the generated ones ([db709a9](https://github.com/ReventlessDev/reventless-core/commit/db709a9bf2f665642b5d48ef4a83e18978e9d9d7))
|
|
16
|
+
|
|
17
|
+
|
|
6
18
|
# 3.0.0-alpha.278 (2026-08-09)
|
|
7
19
|
|
|
8
20
|
**Note:** Version bump only for package @reventlessdev/reventless-aws
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-aws",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.279",
|
|
4
4
|
"description": "AWS adapters for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"dependencies": {
|
|
@@ -12,18 +12,18 @@
|
|
|
12
12
|
"@aws-sdk/s3-request-presigner": "3.970.0",
|
|
13
13
|
"sury": "11.0.0-alpha.4",
|
|
14
14
|
"uuid": "^13.0.0",
|
|
15
|
+
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.6",
|
|
15
16
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
16
17
|
"@reventlessdev/rescript-node": "2.0.0-alpha.3",
|
|
17
18
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
18
|
-
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.6",
|
|
19
19
|
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.69",
|
|
20
20
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
|
|
21
21
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
22
|
-
"@reventlessdev/reventless-core": "3.0.0-alpha.
|
|
23
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.132",
|
|
22
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.222",
|
|
24
23
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.30",
|
|
25
|
-
"@reventlessdev/reventless-postgres": "3.0.0-alpha.
|
|
26
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.106"
|
|
24
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.86",
|
|
25
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.106",
|
|
26
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.132"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"rescript": "12.3.0",
|
|
@@ -118,21 +118,29 @@ let connectLambda = (
|
|
|
118
118
|
resources: array<ReventlessInfra.Adapter.resource>,
|
|
119
119
|
opts: Pulumi.CustomResourceOptions.t,
|
|
120
120
|
) => {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
121
|
+
// The three input sets are resolved SEPARATELY, and each resource is gated on
|
|
122
|
+
// the narrowest one it actually needs.
|
|
123
|
+
//
|
|
124
|
+
// They used to share one `Pulumi.Output.all3` apply that created the role policy
|
|
125
|
+
// and every EventSourceMapping. An apply whose inputs are unknown does not run
|
|
126
|
+
// during preview, and a resource the program never registers reads as a DELETE —
|
|
127
|
+
// so an unknown in ANY of the three deleted ALL of them, including mappings that
|
|
128
|
+
// do not depend on it. `targetResources` (the collector's own view tables) is the
|
|
129
|
+
// one that goes unknown in practice: switching a plugin's slices to the stream
|
|
130
|
+
// builder gives each view table a computed `streamArn`, which Pulumi cannot know
|
|
131
|
+
// in the preview that enables it, so the plugin's event-log mapping and role
|
|
132
|
+
// policy — neither of which reads a view table — vanished from that preview.
|
|
133
|
+
let eventTopicResourcesOutput = eventTopics->toResources
|
|
134
|
+
let queueArnsOutput = queues->Array.map(queue => queue.arn)->Pulumi.Output.all
|
|
135
|
+
let targetResourcesOutput = resources->ReventlessCore.Adapter.resourcesToResolvedOutput
|
|
136
|
+
|
|
137
|
+
// Only the policy DOCUMENT needs all three. The RolePolicy resource itself is
|
|
138
|
+
// registered at top level with the document as an Output input, so an unknown
|
|
139
|
+
// input previews as "update, value unknown" instead of removing the policy.
|
|
140
|
+
let lambdaPolicyJson =
|
|
141
|
+
(eventTopicResourcesOutput, queueArnsOutput, targetResourcesOutput)
|
|
127
142
|
->Pulumi.Output.all3
|
|
128
|
-
|
|
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
|
-
)) => {
|
|
143
|
+
->Pulumi.Output.flatMap(((eventTopicResources, queueArns, resources)) => {
|
|
136
144
|
log.debug(
|
|
137
145
|
~comp="EventCollector",
|
|
138
146
|
`connectLambda ${name}: ${eventTopicResources->Array.length->Int.toString} topic resource(s), ${resources->Array.length->Int.toString} resource(s)`,
|
|
@@ -246,27 +254,42 @@ let connectLambda = (
|
|
|
246
254
|
)
|
|
247
255
|
: None
|
|
248
256
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
allowLambdaPublishSNS,
|
|
260
|
-
allowLambdaSendSQS,
|
|
261
|
-
]->Array.keepSome,
|
|
262
|
-
)->Pulumi.Output.asInput,
|
|
263
|
-
role: lambdaRole.id->Pulumi.Output.asInput,
|
|
264
|
-
},
|
|
265
|
-
~opts,
|
|
257
|
+
PulumiAws.PolicyDocument.mergePolicyDocuments(
|
|
258
|
+
name ++ "LambdaPolicy",
|
|
259
|
+
[
|
|
260
|
+
Some(PulumiAws.Lambda.defaultLoggingPolicyDocument),
|
|
261
|
+
allowLambdaReceiveSQS,
|
|
262
|
+
allowLambdaReadDynamoDbStream,
|
|
263
|
+
allowLambdaReadWriteDynamoDb,
|
|
264
|
+
allowLambdaPublishSNS,
|
|
265
|
+
allowLambdaSendSQS,
|
|
266
|
+
]->Array.keepSome,
|
|
266
267
|
)
|
|
268
|
+
})
|
|
267
269
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
+
let _lambdaPolicy = PulumiAws.IAM.RolePolicy.make(
|
|
271
|
+
~name,
|
|
272
|
+
~args={
|
|
273
|
+
policy: lambdaPolicyJson->Pulumi.Output.asInput,
|
|
274
|
+
role: lambdaRole.id->Pulumi.Output.asInput,
|
|
275
|
+
},
|
|
276
|
+
~opts,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
// The mappings read nothing but the event topics, so that is all they wait for.
|
|
280
|
+
// They cannot be hoisted out of an apply the way the policy can: an ESM's Pulumi
|
|
281
|
+
// name is derived from the resolved source name, so a brand-new event log still
|
|
282
|
+
// previews without them — inherent, and no longer reachable from a view table.
|
|
283
|
+
//
|
|
284
|
+
// Attribution is captured at THIS call, not inside the callback: by the time an
|
|
285
|
+
// apply runs, the builder's construct has returned and the ambient context is
|
|
286
|
+
// empty. (The top-level policy above is created while it is still ambient.)
|
|
287
|
+
let _ =
|
|
288
|
+
eventTopicResourcesOutput->ReventlessCore.ResourceAttribution_Deploytime.applyAttributed(
|
|
289
|
+
eventTopicResources =>
|
|
290
|
+
eventTopicResources
|
|
291
|
+
->dynamoDbStreamResources
|
|
292
|
+
->Array.map(dynamoDbStreamResource =>
|
|
270
293
|
Util_EventSourceMapping.subscribe(
|
|
271
294
|
~lambda,
|
|
272
295
|
~targetName=name,
|
|
@@ -278,8 +301,8 @@ let connectLambda = (
|
|
|
278
301
|
),
|
|
279
302
|
~opts,
|
|
280
303
|
)
|
|
281
|
-
)
|
|
282
|
-
|
|
304
|
+
),
|
|
305
|
+
)
|
|
283
306
|
|
|
284
307
|
let subscriptionResources = queues->Array.mapWithIndex((queue, idx) => {
|
|
285
308
|
let esmName = queues->Array.length > 1 ? `${name}Sqs${Int.toString(idx)}` : name
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as Aws from "@pulumi/aws";
|
|
4
4
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
5
|
+
import * as Output$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Output.res.mjs";
|
|
5
6
|
import * as Pulumi from "@pulumi/pulumi";
|
|
6
7
|
import * as Lambda$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/Lambda/Lambda.res.mjs";
|
|
7
8
|
import * as AWS$ReventlessAws from "../AWS.res.mjs";
|
|
@@ -77,10 +78,13 @@ function esmTags(name, esmName) {
|
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
function connectLambda(lambda, name, lambdaRole, queues, eventTopics, resources, opts) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
let eventTopicResourcesOutput = toResources(eventTopics);
|
|
82
|
+
let queueArnsOutput = Pulumi.all(queues.map(queue => queue.arn));
|
|
83
|
+
let targetResourcesOutput = Adapter$ReventlessCore.resourcesToResolvedOutput(resources);
|
|
84
|
+
let lambdaPolicyJson = Output$Pulumi.flatMap(Pulumi.all([
|
|
85
|
+
eventTopicResourcesOutput,
|
|
86
|
+
queueArnsOutput,
|
|
87
|
+
targetResourcesOutput
|
|
84
88
|
]), param => {
|
|
85
89
|
let resources = param[2];
|
|
86
90
|
let queueArns = param[1];
|
|
@@ -138,19 +142,20 @@ function connectLambda(lambda, name, lambdaRole, queues, eventTopics, resources,
|
|
|
138
142
|
],
|
|
139
143
|
Resource: queueArns
|
|
140
144
|
}]) : undefined;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
])),
|
|
150
|
-
role: lambdaRole.id
|
|
151
|
-
}, opts);
|
|
152
|
-
dynamoDbStreamResources.map(dynamoDbStreamResource => Util_EventSourceMapping$ReventlessAws.subscribe(undefined, lambda, name, dynamoDbStreamResource.name, AdapterDeploytime$ReventlessCore.resolvedToResource(dynamoDbStreamResource), esmTags(name, Util$ReventlessCore.baseName(dynamoDbStreamResource.name) + ("2" + name)), opts));
|
|
145
|
+
return PolicyDocument$PulumiAws.mergePolicyDocuments(name + "LambdaPolicy", Stdlib_Array.keepSome([
|
|
146
|
+
Lambda$PulumiAws.defaultLoggingPolicyDocument,
|
|
147
|
+
allowLambdaReceiveSQS,
|
|
148
|
+
allowLambdaReadDynamoDbStream,
|
|
149
|
+
allowLambdaReadWriteDynamoDb,
|
|
150
|
+
allowLambdaPublishSNS,
|
|
151
|
+
allowLambdaSendSQS
|
|
152
|
+
]));
|
|
153
153
|
});
|
|
154
|
+
new (Aws.iam.RolePolicy)(name, {
|
|
155
|
+
policy: lambdaPolicyJson,
|
|
156
|
+
role: lambdaRole.id
|
|
157
|
+
}, opts);
|
|
158
|
+
ResourceAttribution_Deploytime$ReventlessCore.applyAttributed(eventTopicResourcesOutput, eventTopicResources => Adapter_Helpers$ReventlessAws.dynamoDbStreamResources(eventTopicResources).map(dynamoDbStreamResource => Util_EventSourceMapping$ReventlessAws.subscribe(undefined, lambda, name, dynamoDbStreamResource.name, AdapterDeploytime$ReventlessCore.resolvedToResource(dynamoDbStreamResource), esmTags(name, Util$ReventlessCore.baseName(dynamoDbStreamResource.name) + ("2" + name)), opts)));
|
|
154
159
|
return queues.map((queue, idx) => {
|
|
155
160
|
let esmName = queues.length > 1 ? name + `Sqs` + idx.toString() : name;
|
|
156
161
|
return Util_EventSourceMapping$ReventlessAws.subscribeSqs(lambda, esmName, queue, undefined, esmTags(name, esmName), opts);
|
|
@@ -96,13 +96,34 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
96
96
|
| None => true
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
// The interceptor step for one Query resolver, or `None` when this deployment
|
|
100
|
+
// did not switch interception on. Provisioning it is the framework's job — see
|
|
101
|
+
// `QueryInterceptor_Provisioning`; the first Query resolver under a plugin
|
|
102
|
+
// creates the data source and the rest reuse it.
|
|
103
|
+
//
|
|
104
|
+
// **Every top-level Query field on this read model gets one when interception
|
|
105
|
+
// is on, whatever its access path.** The hook reports `readModelName`, so all
|
|
106
|
+
// of them resolve the SAME read model and a consumer counting resolutions must
|
|
107
|
+
// see all of them or its number is a function of which access paths a model
|
|
108
|
+
// happens to expose — two models with identical traffic would report different
|
|
109
|
+
// totals because one is queried through an index. A partial pipeline here would
|
|
110
|
+
// make that skew invisible rather than absent.
|
|
111
|
+
let interceptorFunction = (~resolverName) =>
|
|
112
|
+
QueryInterceptor_Provisioning.dataSourceName(~api, ~opts)->Option.map(interceptorDsName =>
|
|
113
|
+
Function.makeJs(
|
|
114
|
+
~name=resolverName ++ "Interceptor",
|
|
115
|
+
~api,
|
|
116
|
+
~dataSource=interceptorDsName,
|
|
117
|
+
~code=interceptorCode(name),
|
|
118
|
+
~opts,
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
|
|
99
122
|
// Creates either a unit resolver (no interceptor) or a pipeline resolver
|
|
100
|
-
// (interceptor Lambda → DynamoDB query),
|
|
101
|
-
//
|
|
102
|
-
// is the framework's job — see `QueryInterceptor_Provisioning`; the first
|
|
103
|
-
// Query resolver under a plugin creates it and the rest reuse it.
|
|
123
|
+
// (interceptor Lambda → DynamoDB query), for the Query fields that have no
|
|
124
|
+
// second step of their own.
|
|
104
125
|
let makeQueryResolver = (~resolverName, ~field, ~code) =>
|
|
105
|
-
switch
|
|
126
|
+
switch interceptorFunction(~resolverName) {
|
|
106
127
|
| None =>
|
|
107
128
|
Resolver.makeUnitJsResolver(
|
|
108
129
|
~name=resolverName,
|
|
@@ -113,14 +134,7 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
113
134
|
~code,
|
|
114
135
|
~opts,
|
|
115
136
|
)
|
|
116
|
-
| Some(
|
|
117
|
-
let interceptorFn = Function.makeJs(
|
|
118
|
-
~name=resolverName ++ "Interceptor",
|
|
119
|
-
~api,
|
|
120
|
-
~dataSource=interceptorDsName,
|
|
121
|
-
~code=interceptorCode(name),
|
|
122
|
-
~opts,
|
|
123
|
-
)
|
|
137
|
+
| Some(interceptorFn) =>
|
|
124
138
|
let queryFn = Function.makeJs(
|
|
125
139
|
~name=resolverName ++ "Query",
|
|
126
140
|
~api,
|
|
@@ -258,18 +272,14 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
258
272
|
let idField = indexConfig.idField->Option.getOr(index)
|
|
259
273
|
switch indexConfig.authorization {
|
|
260
274
|
| None =>
|
|
261
|
-
|
|
262
|
-
~
|
|
263
|
-
~api,
|
|
264
|
-
~dataSourceName,
|
|
265
|
-
~type_="Query"->Pulumi.Input.make,
|
|
275
|
+
makeQueryResolver(
|
|
276
|
+
~resolverName,
|
|
266
277
|
~field=fieldName->Pulumi.Input.make,
|
|
267
278
|
~code=switch indexConfig.subIdField {
|
|
268
279
|
| Some(sortField) =>
|
|
269
280
|
Resolver.Functions.queryByIndexSortFiltered(~index, ~idField, ~sortField)
|
|
270
281
|
| None => Resolver.Functions.queryByIndexFiltered(~index, ~idField)
|
|
271
282
|
},
|
|
272
|
-
~opts,
|
|
273
283
|
)
|
|
274
284
|
| Some({tableName, group}) =>
|
|
275
285
|
let authDataSource = DataSource.makeDynamoDBDataSourceWithTableName(
|
|
@@ -297,13 +307,25 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
297
307
|
~code=Resolver.Functions.queryByIndexFiltered(~index, ~idField),
|
|
298
308
|
~opts,
|
|
299
309
|
)
|
|
310
|
+
// The interceptor leads the chain, as it does in every other Query
|
|
311
|
+
// pipeline. That position means an attempt refused by the row-level index
|
|
312
|
+
// authorization below has still been counted — the same "attempts, not
|
|
313
|
+
// outcomes" bound the hook has everywhere, since it fires before the read
|
|
314
|
+
// and never learns how it ended. Putting it after the auth step would buy
|
|
315
|
+
// outcome-accurate counts and cost the property that matters more: the
|
|
316
|
+
// interceptor is the one place a read can be REFUSED, so it has to be
|
|
317
|
+
// reachable before the pipeline spends a second table read deciding
|
|
318
|
+
// whether this caller may use the index.
|
|
300
319
|
Resolver.makePipelineJsResolver(
|
|
301
320
|
~name=resolverName,
|
|
302
321
|
~api,
|
|
303
322
|
~type_="Query"->Pulumi.Input.make,
|
|
304
323
|
~field=fieldName->Pulumi.Input.make,
|
|
305
324
|
~code=Resolver.Functions.pipelinePassThrough,
|
|
306
|
-
~functions=
|
|
325
|
+
~functions=switch interceptorFunction(~resolverName) {
|
|
326
|
+
| None => [authFunction, queryFunction]
|
|
327
|
+
| Some(interceptorFn) => [interceptorFn, authFunction, queryFunction]
|
|
328
|
+
},
|
|
307
329
|
~opts,
|
|
308
330
|
)
|
|
309
331
|
}
|
|
@@ -66,12 +66,12 @@ function make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolve
|
|
|
66
66
|
let fieldNameForSingle = registryEntry !== undefined ? registryEntry.singleFieldName : AppSync_Resolver_Functions$PulumiAws.uncapitalize(name$1);
|
|
67
67
|
let includeIdParam = registryEntry !== undefined ? registryEntry.includeIdParam : true;
|
|
68
68
|
let connectionSpec = registryEntry !== undefined ? registryEntry.connectionSpec : true;
|
|
69
|
+
let interceptorFunction = resolverName => Stdlib_Option.map(QueryInterceptor_Provisioning$ReventlessAws.dataSourceName(api, opts), interceptorDsName => AppSync_Function$PulumiAws.makeJs(resolverName + "Interceptor", api, interceptorDsName, interceptorCode(name$1), opts));
|
|
69
70
|
let makeQueryResolver = (resolverName, field, code) => {
|
|
70
|
-
let
|
|
71
|
-
if (
|
|
71
|
+
let interceptorFn = interceptorFunction(resolverName);
|
|
72
|
+
if (interceptorFn === undefined) {
|
|
72
73
|
return AppSync_Resolver_Retrying$ReventlessAws.makeUnitJsResolver(resolverName, api, dataSourceName, "Query", field, code, opts);
|
|
73
74
|
}
|
|
74
|
-
let interceptorFn = AppSync_Function$PulumiAws.makeJs(resolverName + "Interceptor", api, interceptorDsName, interceptorCode(name$1), opts);
|
|
75
75
|
let queryFn = AppSync_Function$PulumiAws.makeJs(resolverName + "Query", api, dataSourceName, code, opts);
|
|
76
76
|
return AppSync_Resolver_Retrying$ReventlessAws.makePipelineJsResolver(resolverName, api, "Query", field, AppSync_Resolver_Functions$PulumiAws.pipelinePassThrough, [
|
|
77
77
|
interceptorFn,
|
|
@@ -117,13 +117,18 @@ function make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolve
|
|
|
117
117
|
let authDataSource = AppSync_DataSource$PulumiAws.makeDynamoDBDataSourceWithTableName(resolverName + "Auth", api, Util_DynamoDb$ReventlessAws.findResource(Util_QueryDb$ReventlessCore.getLocalStorageResources(allQueryDbs, match.tableName)).name, apiRole, opts);
|
|
118
118
|
let authFunction = AppSync_Function$PulumiAws.makeJs(resolverName + "Auth", api, authDataSource.name, AppSync_Resolver_Functions$PulumiAws.authorizeIndexedAccess(index, match.group), opts);
|
|
119
119
|
let queryFunction = AppSync_Function$PulumiAws.makeJs(resolverName, api, dataSourceName, AppSync_Resolver_Functions$PulumiAws.queryByIndexFiltered(index, idField), opts);
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
let interceptorFn = interceptorFunction(resolverName);
|
|
121
|
+
return AppSync_Resolver_Retrying$ReventlessAws.makePipelineJsResolver(resolverName, api, "Query", fieldName, AppSync_Resolver_Functions$PulumiAws.pipelinePassThrough, interceptorFn !== undefined ? [
|
|
122
|
+
interceptorFn,
|
|
123
|
+
authFunction,
|
|
124
|
+
queryFunction
|
|
125
|
+
] : [
|
|
126
|
+
authFunction,
|
|
127
|
+
queryFunction
|
|
128
|
+
], opts);
|
|
124
129
|
}
|
|
125
130
|
let sortField = indexConfig.subIdField;
|
|
126
|
-
return
|
|
131
|
+
return makeQueryResolver(resolverName, fieldName, sortField !== undefined ? AppSync_Resolver_Functions$PulumiAws.queryByIndexSortFiltered(index, idField, sortField) : AppSync_Resolver_Functions$PulumiAws.queryByIndexFiltered(index, idField));
|
|
127
132
|
});
|
|
128
133
|
let storageResource = (pluginName, tableName) => Adapter$ReventlessCore.outputToResource(Util_DynamoDb$ReventlessAws.findResourceInOutput(Plugin_Helpers$ReventlessCore.getStorageResources(allQueryDbs, pluginName, tableName)));
|
|
129
134
|
let generateCode = (storageResource, template) => storageResource.name.apply(realTableName => template(realTableName));
|
|
@@ -1,10 +1,31 @@
|
|
|
1
|
+
// The runtime that `QueryInterception.use` puts in front of every DynamoDB-backed
|
|
2
|
+
// Query resolver — one pipeline step whose only job is to consult the hook.
|
|
3
|
+
//
|
|
4
|
+
// The hook is a module-level `ref`, and in a deployed runtime the only thing that
|
|
5
|
+
// ever fills it is a `RuntimeExtension`'s `onColdStart`. Every other runtime
|
|
6
|
+
// reaches those through a compiled entry shell that awaits `runtimeExtensionsReady`
|
|
7
|
+
// before it serves anything; this handler is not built by a shell, so it awaits
|
|
8
|
+
// the same promise itself. Without it the extensions the archive already carries
|
|
9
|
+
// are never imported, the hook stays `None`, and interception degrades to a
|
|
10
|
+
// passthrough that costs an invocation on every read and observes nothing — the
|
|
11
|
+
// one failure mode this path cannot afford, because it is silent and its whole
|
|
12
|
+
// price has already been paid.
|
|
13
|
+
//
|
|
14
|
+
// Awaited from the small module rather than from `HandlerFactoryHelpers`, which
|
|
15
|
+
// re-exports the same binding behind the Effect runtime and the DynamoDB clients:
|
|
16
|
+
// this runtime is sized for a decision, not for work.
|
|
17
|
+
|
|
1
18
|
type payload = {
|
|
2
19
|
readModelName: string,
|
|
3
20
|
arguments: JSON.t,
|
|
4
21
|
identity: Reventless.Identity.t,
|
|
5
22
|
}
|
|
6
23
|
|
|
24
|
+
@module("../Runtime/RuntimeExtensionsReady.mjs")
|
|
25
|
+
external runtimeExtensionsReady: promise<unit> = "runtimeExtensionsReady"
|
|
26
|
+
|
|
7
27
|
let handler = async (event: payload, _context) => {
|
|
28
|
+
await runtimeExtensionsReady
|
|
8
29
|
switch ReventlessCore.QueryDb_Callback.queryInterceptorHook.contents {
|
|
9
30
|
| None => true
|
|
10
31
|
| Some(interceptor) =>
|
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
4
4
|
import * as QueryDb_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/QueryDb/QueryDb_Callback.res.mjs";
|
|
5
|
+
import * as RuntimeExtensionsReadyMjs from "../Runtime/RuntimeExtensionsReady.mjs";
|
|
6
|
+
|
|
7
|
+
let runtimeExtensionsReady = RuntimeExtensionsReadyMjs.runtimeExtensionsReady;
|
|
5
8
|
|
|
6
9
|
async function handler(event, _context) {
|
|
10
|
+
await runtimeExtensionsReady;
|
|
7
11
|
let interceptor = QueryDb_Callback$ReventlessCore.queryInterceptorHook.contents;
|
|
8
12
|
if (interceptor === undefined) {
|
|
9
13
|
return true;
|
|
@@ -17,6 +21,7 @@ async function handler(event, _context) {
|
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
export {
|
|
24
|
+
runtimeExtensionsReady,
|
|
20
25
|
handler,
|
|
21
26
|
}
|
|
22
|
-
/*
|
|
27
|
+
/* runtimeExtensionsReady Not a pure module */
|
|
@@ -8,11 +8,11 @@ import {
|
|
|
8
8
|
} from "@reventlessdev/reventless-core/src/RequestContext.res.mjs";
|
|
9
9
|
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
10
10
|
import { DynamoDBDocumentClient, ScanCommand } from "@aws-sdk/lib-dynamodb";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
} from "./
|
|
11
|
+
|
|
12
|
+
// Re-exported, not defined here: the seam must fire once per process, and a
|
|
13
|
+
// runtime too small to want this module's other imports awaits the same binding
|
|
14
|
+
// straight from its own module. See RuntimeExtensionsReady.mjs.
|
|
15
|
+
export { runtimeExtensionsReady } from "./RuntimeExtensionsReady.mjs";
|
|
16
16
|
|
|
17
17
|
// ─── Structured logging from the .mjs entry-point shims ─────────────────────
|
|
18
18
|
// Mirrors ReScript's Logger.t API and Logger.emit's JSON sink shape (time,
|
|
@@ -197,48 +197,6 @@ export function extractRetryCount(records) {
|
|
|
197
197
|
return Number.isFinite(count) && count > 0 ? count : 1;
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
// ─── Runtime extension cold start ───────────────────────────────────────────
|
|
201
|
-
// Extension packages are bundled into the code archive next to the spec and
|
|
202
|
-
// behavior packages, so they resolve from /var/task/node_modules by the same
|
|
203
|
-
// absolute path every shell uses for user modules.
|
|
204
|
-
const dynamicImport = (specifier) => import("/var/task/node_modules/" + specifier);
|
|
205
|
-
|
|
206
|
-
// The framework ships four runtime callback hooks (command interception, query
|
|
207
|
-
// interception, before/after publish) whose registrars are module-level refs.
|
|
208
|
-
// They are only reachable if something calls them in this process before the
|
|
209
|
-
// first request, and the entry shells import framework and domain modules only —
|
|
210
|
-
// so an out-of-tree extension had no way in. RUNTIME_EXTENSIONS closes that: it
|
|
211
|
-
// names the extension modules the archive carries (see
|
|
212
|
-
// ReventlessCore.RuntimeExtension and Util_Bundle.addRuntimeExtensionPackages),
|
|
213
|
-
// and each one's `onColdStart` runs here, once, before any handler is built.
|
|
214
|
-
//
|
|
215
|
-
// This is the seam's one untyped step: the modules are named at deploy time and
|
|
216
|
-
// their types are unknowable here. Parsing and invocation live in the typed
|
|
217
|
-
// RuntimeExtensionEntryPoint_Ops.res, so the call's arity is compiler-checked.
|
|
218
|
-
//
|
|
219
|
-
// Started at module load and exported as a promise every entry shell awaits in
|
|
220
|
-
// its own cold-start init — that is what makes "before the first request" a fact
|
|
221
|
-
// rather than a hope, and it holds for the shells that build handlers lazily.
|
|
222
|
-
// Absent env var ⇒ resolves immediately, having done nothing.
|
|
223
|
-
export const runtimeExtensionsReady = (async () => {
|
|
224
|
-
const config = parseRuntimeExtensionConfig(process.env["RUNTIME_EXTENSIONS"]);
|
|
225
|
-
if (config === undefined) return;
|
|
226
|
-
const hooks = [];
|
|
227
|
-
for (const specifier of config.modules) {
|
|
228
|
-
try {
|
|
229
|
-
const mod = await dynamicImport(specifier);
|
|
230
|
-
if (typeof mod.onColdStart === "function") {
|
|
231
|
-
hooks.push(mod.onColdStart);
|
|
232
|
-
} else {
|
|
233
|
-
reportRuntimeExtensionLoadFailure(specifier, "module exports no onColdStart function");
|
|
234
|
-
}
|
|
235
|
-
} catch (err) {
|
|
236
|
-
reportRuntimeExtensionLoadFailure(specifier, err?.message ?? String(err));
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
fireRuntimeExtensions(config, hooks);
|
|
240
|
-
})();
|
|
241
|
-
|
|
242
200
|
// Patch a spec module's Id field to use IdString if undefined.
|
|
243
201
|
// Workaround for `module Id = Id.String` not producing an ESM export.
|
|
244
202
|
export const patchSpecId = (specModule) => ({ ...specModule, Id: specModule.Id || IdString });
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// The runtime-extension cold start, on its own so a runtime can await it without
|
|
2
|
+
// pulling the rest of the entry-point toolkit in behind it.
|
|
3
|
+
//
|
|
4
|
+
// It lived in HandlerFactoryHelpers because every compiled entry shell wants it
|
|
5
|
+
// alongside the Effect runtime, the DynamoDB clients and the logging shims it
|
|
6
|
+
// already imports from there. The query interceptor is the first runtime that
|
|
7
|
+
// wants ONLY this: it sits in front of every read and is sized for a decision
|
|
8
|
+
// rather than for work, so paying that whole import graph on each cold start to
|
|
9
|
+
// reach one promise is the wrong trade.
|
|
10
|
+
//
|
|
11
|
+
// A module-level singleton, which is why HandlerFactoryHelpers re-exports this
|
|
12
|
+
// binding rather than keeping a copy: two definitions would fire the seam twice
|
|
13
|
+
// in any runtime that reached both.
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
parseConfig as parseRuntimeExtensionConfig,
|
|
17
|
+
fire as fireRuntimeExtensions,
|
|
18
|
+
reportLoadFailure as reportRuntimeExtensionLoadFailure,
|
|
19
|
+
} from "./RuntimeExtensionEntryPoint_Ops.res.mjs";
|
|
20
|
+
|
|
21
|
+
// Extension packages ride in the code archive rather than beside this module, so
|
|
22
|
+
// they resolve under /var/task and not relative to here.
|
|
23
|
+
const dynamicImport = (specifier) => import("/var/task/node_modules/" + specifier);
|
|
24
|
+
|
|
25
|
+
// The framework ships four runtime callback hooks (command interception, query
|
|
26
|
+
// interception, before/after publish) whose registrars are module-level refs.
|
|
27
|
+
// They are only reachable if something calls them in this process before the
|
|
28
|
+
// first request, and the entry shells import framework and domain modules only —
|
|
29
|
+
// so an out-of-tree extension had no way in. RUNTIME_EXTENSIONS closes that: it
|
|
30
|
+
// names the extension modules the archive carries (see
|
|
31
|
+
// ReventlessCore.RuntimeExtension and Util_Bundle.addRuntimeExtensionPackages),
|
|
32
|
+
// and each one's `onColdStart` runs here, once, before any handler is built.
|
|
33
|
+
//
|
|
34
|
+
// This is the seam's one untyped step: the modules are named at deploy time and
|
|
35
|
+
// their types are unknowable here. Parsing and invocation live in the typed
|
|
36
|
+
// RuntimeExtensionEntryPoint_Ops.res, so the call's arity is compiler-checked.
|
|
37
|
+
//
|
|
38
|
+
// Started at module load and exported as a promise every entry shell awaits in
|
|
39
|
+
// its own cold-start init — that is what makes "before the first request" a fact
|
|
40
|
+
// rather than a hope, and it holds for the shells that build handlers lazily.
|
|
41
|
+
// Absent env var ⇒ resolves immediately, having done nothing.
|
|
42
|
+
export const runtimeExtensionsReady = (async () => {
|
|
43
|
+
const config = parseRuntimeExtensionConfig(process.env["RUNTIME_EXTENSIONS"]);
|
|
44
|
+
if (config === undefined) return;
|
|
45
|
+
const hooks = [];
|
|
46
|
+
for (const specifier of config.modules) {
|
|
47
|
+
try {
|
|
48
|
+
const mod = await dynamicImport(specifier);
|
|
49
|
+
if (typeof mod.onColdStart === "function") {
|
|
50
|
+
hooks.push(mod.onColdStart);
|
|
51
|
+
} else {
|
|
52
|
+
reportRuntimeExtensionLoadFailure(specifier, "module exports no onColdStart function");
|
|
53
|
+
}
|
|
54
|
+
} catch (err) {
|
|
55
|
+
reportRuntimeExtensionLoadFailure(specifier, err?.message ?? String(err));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
fireRuntimeExtensions(config, hooks);
|
|
59
|
+
})();
|