@reventlessdev/reventless-aws 3.0.0-alpha.282 → 3.0.0-alpha.284
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 +19 -0
- package/package.json +10 -10
- package/src/Platform.res +21 -12
- package/src/Platform.res.mjs +2 -2
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res +85 -31
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res.mjs +71 -36
- package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res +36 -1
- package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res.mjs +13 -2
- package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res +4 -0
- package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res.mjs +5 -3
- package/tests/PgQueryResolver_LambdaTest.res +239 -9
- package/tests/PgQueryResolver_LambdaTest.res.mjs +202 -40
- package/tests/QueryDbResolvers_AppSyncTest.res.mjs +3 -3
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.284 (2026-08-12)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **platform:** bake a curated component manifest as a static asset ([4f30265](https://github.com/ReventlessDev/reventless-core/commit/4f30265a51fc2c59e69afd8074cf1b2534c06378))
|
|
11
|
+
* **queries:** narrow owner-bearing reads to the caller ([ba9cc3d](https://github.com/ReventlessDev/reventless-core/commit/ba9cc3d58d7914a9e4827bda90a704a74b1b82dd))
|
|
12
|
+
* **spec:** let the environment name the groups exempt from owner scoping ([2de5c51](https://github.com/ReventlessDev/reventless-core/commit/2de5c5118347a46998f9ab603308fca09addf00b))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# 3.0.0-alpha.283 (2026-08-11)
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* **api:** one id contract across the read-side query doors ([c52fac2](https://github.com/ReventlessDev/reventless-core/commit/c52fac2bee87b3dd1fa33b4970a74f3cd3866280))
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
* **api:** infer a queryable's key field and publish its provenance ([c835a42](https://github.com/ReventlessDev/reventless-core/commit/c835a42a0da07cdc4a3f010212e1f340a4a0ca27))
|
|
23
|
+
|
|
24
|
+
|
|
6
25
|
# 3.0.0-alpha.282 (2026-08-10)
|
|
7
26
|
|
|
8
27
|
### Bug Fixes
|
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.284",
|
|
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.
|
|
15
|
+
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.7",
|
|
16
16
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
17
17
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
18
|
-
"@reventlessdev/rescript-
|
|
19
|
-
"@reventlessdev/rescript-
|
|
20
|
-
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
|
|
18
|
+
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.71",
|
|
19
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.4",
|
|
21
20
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
22
|
-
"@reventlessdev/
|
|
23
|
-
"@reventlessdev/reventless-
|
|
24
|
-
"@reventlessdev/reventless-
|
|
25
|
-
"@reventlessdev/reventless-
|
|
26
|
-
"@reventlessdev/reventless-
|
|
21
|
+
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
|
|
22
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.225",
|
|
23
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.135",
|
|
24
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.89",
|
|
25
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.109",
|
|
26
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.30"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"rescript": "12.3.0",
|
package/src/Platform.res
CHANGED
|
@@ -1124,18 +1124,6 @@ module MakeWithConfig = (
|
|
|
1124
1124
|
|
|
1125
1125
|
// In split mode, create a dedicated platform AppSync API and push the platform schema.
|
|
1126
1126
|
// In unified mode, makePlatform is a no-op (schema stitching handled by events).
|
|
1127
|
-
let makePlatform = (~version, ~plugins: array<module(PluginMaker)>) => {
|
|
1128
|
-
log.info(~comp="Platform", `v${version}`)
|
|
1129
|
-
let _ = plugins
|
|
1130
|
-
// Retired with the merged-API cutover: on AWS the platform deploys with
|
|
1131
|
-
// deployPlatform (staged) and each plugin with deployPlugin — makePlatform
|
|
1132
|
-
// predates the merge path and never gained merged wiring.
|
|
1133
|
-
failwith(
|
|
1134
|
-
"makePlatform is not supported on AWS — deploy the platform with deployPlatform and " ++
|
|
1135
|
-
"each plugin with deployPlugin (merged-API composition).",
|
|
1136
|
-
)
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
1127
|
// Optional host UI shell bundle: a static SPA (e.g. reventless-ui's host-shell)
|
|
1140
1128
|
// hosted on the platform's CloudFront-fronted S3 bucket. The platform writes a
|
|
1141
1129
|
// `config.json` next to `index.html` so the shell discovers `apiEndpoint` and
|
|
@@ -1154,6 +1142,10 @@ module MakeWithConfig = (
|
|
|
1154
1142
|
// written; the shell treats the resulting 404 as "no hints" and boots
|
|
1155
1143
|
// unchanged. Single-mode shape: one origin-relative file per deployment.
|
|
1156
1144
|
uiHintsFile?: string,
|
|
1145
|
+
// Optional baked component manifest. Declared here so a deployment can state
|
|
1146
|
+
// it once; the emission that writes the file is not wired on this platform
|
|
1147
|
+
// yet, and a declaration without it writes nothing.
|
|
1148
|
+
bakedManifest?: ReventlessInfra.Platform.bakedManifest,
|
|
1157
1149
|
// Optional geocoding place index, as returned by
|
|
1158
1150
|
// `Capability_Geocoding_AwsLocation.make`. When set, the deploy provisions the
|
|
1159
1151
|
// capability's client door — a Cognito-authenticated `Query.geocode` resolver
|
|
@@ -1198,6 +1190,23 @@ module MakeWithConfig = (
|
|
|
1198
1190
|
shellConfig?: dict<JSON.t>,
|
|
1199
1191
|
}
|
|
1200
1192
|
|
|
1193
|
+
let makePlatform = (
|
|
1194
|
+
~version,
|
|
1195
|
+
~plugins: array<module(PluginMaker)>,
|
|
1196
|
+
~hostUiBundle as _: option<hostUiBundleConfig>=?,
|
|
1197
|
+
) => {
|
|
1198
|
+
log.info(~comp="Platform", `v${version}`)
|
|
1199
|
+
let _ = plugins
|
|
1200
|
+
// Retired with the merged-API cutover: on AWS the platform deploys with
|
|
1201
|
+
// deployPlatform (staged) and each plugin with deployPlugin — makePlatform
|
|
1202
|
+
// predates the merge path and never gained merged wiring.
|
|
1203
|
+
failwith(
|
|
1204
|
+
"makePlatform is not supported on AWS — deploy the platform with deployPlatform and " ++
|
|
1205
|
+
"each plugin with deployPlugin (merged-API composition).",
|
|
1206
|
+
)
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
|
|
1201
1210
|
// Merged-mode outputs of deployPlatform — the merged API(s), plus the
|
|
1202
1211
|
// deploy-time merge gates (Outputs that resolve on MERGE_SUCCESS and fail
|
|
1203
1212
|
// the deploy on MERGE_FAILED; folded into the ARN exports so they are
|
package/src/Platform.res.mjs
CHANGED
|
@@ -753,7 +753,7 @@ function MakeWithConfig(Config) {
|
|
|
753
753
|
return;
|
|
754
754
|
}
|
|
755
755
|
};
|
|
756
|
-
let makePlatform = (version, plugins) => {
|
|
756
|
+
let makePlatform = (version, plugins, param) => {
|
|
757
757
|
log.info("Platform", undefined, `v` + version);
|
|
758
758
|
return Pervasives.failwith("makePlatform is not supported on AWS — deploy the platform with deployPlatform and each plugin with deployPlugin (merged-API composition).");
|
|
759
759
|
};
|
|
@@ -2009,7 +2009,7 @@ function Make($star) {
|
|
|
2009
2009
|
return;
|
|
2010
2010
|
}
|
|
2011
2011
|
};
|
|
2012
|
-
let makePlatform = (version, plugins) => {
|
|
2012
|
+
let makePlatform = (version, plugins, param) => {
|
|
2013
2013
|
log.info("Platform", undefined, `v` + version);
|
|
2014
2014
|
return Pervasives.failwith("makePlatform is not supported on AWS — deploy the platform with deployPlatform and each plugin with deployPlugin (merged-API composition).");
|
|
2015
2015
|
};
|
|
@@ -67,6 +67,7 @@ type pushdowns = {
|
|
|
67
67
|
~argsDict: dict<JSON.t>,
|
|
68
68
|
~capability: ReventlessCore.GraphQL_FragmentGenerator.serverCapability,
|
|
69
69
|
~labelField: string,
|
|
70
|
+
~ownerScope: (string, string)=?,
|
|
70
71
|
) => promise<option<JSON.t>>,
|
|
71
72
|
// Sub-id connection ({single}Items) — keyset over sub_key within a partition.
|
|
72
73
|
itemsPage: (
|
|
@@ -74,6 +75,7 @@ type pushdowns = {
|
|
|
74
75
|
~subIdField: string,
|
|
75
76
|
~id: string,
|
|
76
77
|
~argsDict: dict<JSON.t>,
|
|
78
|
+
~ownerScope: (string, string)=?,
|
|
77
79
|
) => promise<JSON.t>,
|
|
78
80
|
// Full materialisation for the list fallback (shapes listPage declines).
|
|
79
81
|
scanAll: (~readModelName: string) => promise<array<JSON.t>>,
|
|
@@ -89,6 +91,10 @@ type binding = {
|
|
|
89
91
|
labelField: string,
|
|
90
92
|
includeIdParam: bool,
|
|
91
93
|
authorization: Reventless.Authorization.permission,
|
|
94
|
+
/** The state's `@owner` field, when it declares one. Derived at Lambda init
|
|
95
|
+
from the same schema `capability` comes from, so the two cannot disagree
|
|
96
|
+
about which fields this read model has. */
|
|
97
|
+
ownerField: option<string>,
|
|
92
98
|
}
|
|
93
99
|
|
|
94
100
|
// -- arg helpers -------------------------------------------------------------
|
|
@@ -162,21 +168,55 @@ let dispatch = async (
|
|
|
162
168
|
| _ => JSON.Encode.array([])
|
|
163
169
|
}
|
|
164
170
|
| Allow =>
|
|
171
|
+
// Post-read narrowing for the doors that hand back rows rather than a page.
|
|
172
|
+
// Same decision as the list door takes; applied per row because there is no
|
|
173
|
+
// query to push it into.
|
|
174
|
+
let ownerAllows = (item: JSON.t) =>
|
|
175
|
+
switch payload.identity->Reventless.OwnerScope.decide(~ownerField=binding.ownerField) {
|
|
176
|
+
| Unscoped => true
|
|
177
|
+
| RefuseOwned => false
|
|
178
|
+
| ScopeTo(field, required) =>
|
|
179
|
+
item->argStr(field)->Option.mapOr(false, v => v == required)
|
|
180
|
+
}
|
|
165
181
|
switch payload.kind {
|
|
166
182
|
| "getById" =>
|
|
167
183
|
let id = payload.arguments->argStr("id")->Option.getOr("")
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
|
172
|
-
| None => JSON.Encode.null
|
|
184
|
+
let loadKey = async key =>
|
|
185
|
+
switch await binding.ops.load(key) {
|
|
186
|
+
| Ok(items) => items->Array.get(0)
|
|
187
|
+
| Error(_) => None
|
|
173
188
|
}
|
|
174
|
-
|
|
189
|
+
// A caller holding a row's Relay global id must reach the row through this
|
|
190
|
+
// door too. Raw key first — it is what this door has always taken, and a
|
|
191
|
+
// key that merely looks like base64 must keep resolving to its own row.
|
|
192
|
+
let (resolvedKey, found) = switch (
|
|
193
|
+
await loadKey(id),
|
|
194
|
+
ReventlessCore.Api_Ids.alternateKey(id),
|
|
195
|
+
) {
|
|
196
|
+
| (None, Some(localId)) => (localId, await loadKey(localId))
|
|
197
|
+
| (found, _) => (id, found)
|
|
198
|
+
}
|
|
199
|
+
switch found {
|
|
200
|
+
// A row the caller does not own answers as "not found". Saying "not yours"
|
|
201
|
+
// instead would make this door an oracle for which ids exist.
|
|
202
|
+
| Some(item) if !ownerAllows(item) => JSON.Encode.null
|
|
203
|
+
| Some(item) => binding.includeIdParam ? withId(item, resolvedKey) : item
|
|
204
|
+
| None => JSON.Encode.null
|
|
175
205
|
}
|
|
176
206
|
|
|
177
207
|
| "byIds" =>
|
|
178
208
|
let ids = payload.arguments->argStrs("ids")
|
|
179
|
-
|
|
209
|
+
let found = await binding.pushdowns.byIds(~readModelName=rm, ids)
|
|
210
|
+
// Only pay for the second lookup when the first came up short, and only for
|
|
211
|
+
// the ids that are global ones.
|
|
212
|
+
let missing =
|
|
213
|
+
found->Array.length < ids->Array.length
|
|
214
|
+
? ids->Array.filterMap(ReventlessCore.Api_Ids.alternateKey)
|
|
215
|
+
: []
|
|
216
|
+
let extra = missing->Array.length > 0
|
|
217
|
+
? await binding.pushdowns.byIds(~readModelName=rm, missing)
|
|
218
|
+
: []
|
|
219
|
+
JSON.Encode.array(Array.concat(found, extra)->Array.filter(ownerAllows))
|
|
180
220
|
|
|
181
221
|
| "items" =>
|
|
182
222
|
// Sub-id connection: {single}Items(id, filter, first/after/last/before).
|
|
@@ -184,12 +224,17 @@ let dispatch = async (
|
|
|
184
224
|
switch binding.subIdField {
|
|
185
225
|
| Some(subIdField) =>
|
|
186
226
|
let id = payload.arguments->argStr("id")->Option.getOr("")
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
227
|
+
switch payload.identity->Reventless.OwnerScope.decide(~ownerField=binding.ownerField) {
|
|
228
|
+
| RefuseOwned => emptyConnection()
|
|
229
|
+
| decision =>
|
|
230
|
+
await binding.pushdowns.itemsPage(
|
|
231
|
+
~readModelName=rm,
|
|
232
|
+
~subIdField,
|
|
233
|
+
~id,
|
|
234
|
+
~argsDict=payload.arguments->argObj,
|
|
235
|
+
~ownerScope=?Reventless.OwnerScope.scopeOf(decision),
|
|
236
|
+
)
|
|
237
|
+
}
|
|
193
238
|
| None => emptyConnection()
|
|
194
239
|
}
|
|
195
240
|
|
|
@@ -227,33 +272,42 @@ let dispatch = async (
|
|
|
227
272
|
| _ => true
|
|
228
273
|
}
|
|
229
274
|
if authorized {
|
|
230
|
-
JSON.Encode.array(
|
|
275
|
+
JSON.Encode.array(
|
|
276
|
+
(await binding.pushdowns.indexLookup(~readModelName=rm, field, value))->Array.filter(
|
|
277
|
+
ownerAllows,
|
|
278
|
+
),
|
|
279
|
+
)
|
|
231
280
|
} else {
|
|
232
281
|
JSON.Encode.array([])
|
|
233
282
|
}
|
|
234
283
|
|
|
235
284
|
| "list" =>
|
|
236
285
|
let argsDict = payload.arguments->argObj
|
|
237
|
-
switch
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
| Some(conn) => conn
|
|
244
|
-
| None =>
|
|
245
|
-
// Shapes listPage declines (search/searchPrefix/ids/backward) → run the
|
|
246
|
-
// shared spec over the materialised model. No Relay global-id decoding
|
|
247
|
-
// in the Lambda, so `decodeLocalId` is a no-op (the `ids` filter still
|
|
248
|
-
// matches raw item ids).
|
|
249
|
-
let items = await binding.pushdowns.scanAll(~readModelName=rm)
|
|
250
|
-
ReventlessCore.QueryDbListQuery.run(
|
|
251
|
-
~items,
|
|
286
|
+
switch payload.identity->Reventless.OwnerScope.decide(~ownerField=binding.ownerField) {
|
|
287
|
+
| RefuseOwned => emptyConnection()
|
|
288
|
+
| decision =>
|
|
289
|
+
let ownerScope = Reventless.OwnerScope.scopeOf(decision)
|
|
290
|
+
switch await binding.pushdowns.listPage(
|
|
291
|
+
~readModelName=rm,
|
|
252
292
|
~argsDict,
|
|
253
293
|
~capability=binding.capability,
|
|
254
294
|
~labelField=binding.labelField,
|
|
255
|
-
~
|
|
256
|
-
)
|
|
295
|
+
~ownerScope?,
|
|
296
|
+
) {
|
|
297
|
+
| Some(conn) => conn
|
|
298
|
+
| None =>
|
|
299
|
+
// Shapes listPage declines (search/searchPrefix/ids/backward) → run the
|
|
300
|
+
// shared spec over the materialised model, which decodes Relay global
|
|
301
|
+
// ids the same way every other door now does.
|
|
302
|
+
let items = await binding.pushdowns.scanAll(~readModelName=rm)
|
|
303
|
+
ReventlessCore.QueryDbListQuery.run(
|
|
304
|
+
~items,
|
|
305
|
+
~argsDict,
|
|
306
|
+
~capability=binding.capability,
|
|
307
|
+
~labelField=binding.labelField,
|
|
308
|
+
~ownerScope?,
|
|
309
|
+
)
|
|
310
|
+
}
|
|
257
311
|
}
|
|
258
312
|
|
|
259
313
|
// Cross-table single-ID field resolver (@resolves). `binding` is the TARGET
|
|
@@ -6,6 +6,8 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
6
6
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
7
7
|
import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
|
|
8
8
|
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
9
|
+
import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
|
|
10
|
+
import * as Api_Ids$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/Api_Ids.res.mjs";
|
|
9
11
|
import * as Authorization$Reventless from "@reventlessdev/reventless-spec/src/types/Authorization.res.mjs";
|
|
10
12
|
import * as QueryDbListQuery$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/QueryDbListQuery.res.mjs";
|
|
11
13
|
import * as QueryDb_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/QueryDb/QueryDb_Callback.res.mjs";
|
|
@@ -70,23 +72,45 @@ async function dispatch(binding, lookupBindingOpt, payload) {
|
|
|
70
72
|
let rm = payload.readModelName;
|
|
71
73
|
let match = await runInterceptor(binding, payload);
|
|
72
74
|
if (typeof match !== "object") {
|
|
75
|
+
let ownerAllows = item => {
|
|
76
|
+
let match = OwnerScope$Reventless.decide(payload.identity, binding.ownerField, undefined);
|
|
77
|
+
if (typeof match !== "object") {
|
|
78
|
+
return match === "Unscoped";
|
|
79
|
+
}
|
|
80
|
+
let required = match._1;
|
|
81
|
+
return Stdlib_Option.mapOr(argStr(item, match._0), false, v => v === required);
|
|
82
|
+
};
|
|
73
83
|
let other = payload.kind;
|
|
74
84
|
switch (other) {
|
|
75
85
|
case "byIds" :
|
|
76
86
|
let ids = argStrs(payload.arguments, "ids");
|
|
77
|
-
|
|
87
|
+
let found = await binding.pushdowns.byIds(rm, ids);
|
|
88
|
+
let missing = found.length < ids.length ? Stdlib_Array.filterMap(ids, Api_Ids$ReventlessCore.alternateKey) : [];
|
|
89
|
+
let extra = missing.length !== 0 ? await binding.pushdowns.byIds(rm, missing) : [];
|
|
90
|
+
return found.concat(extra).filter(ownerAllows);
|
|
78
91
|
case "getById" :
|
|
79
92
|
let id = Stdlib_Option.getOr(argStr(payload.arguments, "id"), "");
|
|
80
|
-
let
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
93
|
+
let loadKey = async key => {
|
|
94
|
+
let items = await binding.ops.load(key);
|
|
95
|
+
if (items.TAG === "Ok") {
|
|
96
|
+
return items._0[0];
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
let match$1 = await loadKey(id);
|
|
100
|
+
let match$2 = Api_Ids$ReventlessCore.alternateKey(id);
|
|
101
|
+
let match$3 = match$1 !== undefined || match$2 === undefined ? [
|
|
102
|
+
id,
|
|
103
|
+
match$1
|
|
104
|
+
] : [
|
|
105
|
+
match$2,
|
|
106
|
+
await loadKey(match$2)
|
|
107
|
+
];
|
|
108
|
+
let found$1 = match$3[1];
|
|
109
|
+
if (found$1 !== undefined && ownerAllows(found$1)) {
|
|
86
110
|
if (binding.includeIdParam) {
|
|
87
|
-
return withId(
|
|
111
|
+
return withId(found$1, match$3[0]);
|
|
88
112
|
} else {
|
|
89
|
-
return
|
|
113
|
+
return found$1;
|
|
90
114
|
}
|
|
91
115
|
} else {
|
|
92
116
|
return null;
|
|
@@ -95,14 +119,14 @@ async function dispatch(binding, lookupBindingOpt, payload) {
|
|
|
95
119
|
let indexName = Stdlib_Option.getOr(payload.index, "");
|
|
96
120
|
let field = Stdlib_Option.getOr(Stdlib_Option.flatMap(binding.indexes.find(ic => ic.index === indexName), ic => ic.idField), indexName);
|
|
97
121
|
let value = Stdlib_Option.getOr(argStr(payload.arguments, indexName), "");
|
|
98
|
-
let match$
|
|
99
|
-
let match$
|
|
122
|
+
let match$4 = payload.authGroup;
|
|
123
|
+
let match$5 = payload.authTable;
|
|
100
124
|
let authorized;
|
|
101
|
-
if (match$
|
|
102
|
-
let authBinding = lookupBinding(match$
|
|
125
|
+
if (match$4 !== undefined && match$5 !== undefined && Identity$Reventless.hasGroup(payload.identity, match$4)) {
|
|
126
|
+
let authBinding = lookupBinding(match$5);
|
|
103
127
|
if (authBinding !== undefined) {
|
|
104
|
-
let items
|
|
105
|
-
authorized = items
|
|
128
|
+
let items = await authBinding.ops.load(value);
|
|
129
|
+
authorized = items.TAG === "Ok" ? Stdlib_Option.mapOr(Stdlib_Option.flatMap(items._0[0], row => argStr(row, authIdField(match$4))), false, owner => owner === payload.identity.username) : false;
|
|
106
130
|
} else {
|
|
107
131
|
authorized = false;
|
|
108
132
|
}
|
|
@@ -110,7 +134,7 @@ async function dispatch(binding, lookupBindingOpt, payload) {
|
|
|
110
134
|
authorized = true;
|
|
111
135
|
}
|
|
112
136
|
if (authorized) {
|
|
113
|
-
return await binding.pushdowns.indexLookup(rm, field, value);
|
|
137
|
+
return (await binding.pushdowns.indexLookup(rm, field, value)).filter(ownerAllows);
|
|
114
138
|
} else {
|
|
115
139
|
return [];
|
|
116
140
|
}
|
|
@@ -120,15 +144,26 @@ async function dispatch(binding, lookupBindingOpt, payload) {
|
|
|
120
144
|
return emptyConnection();
|
|
121
145
|
}
|
|
122
146
|
let id$1 = Stdlib_Option.getOr(argStr(payload.arguments, "id"), "");
|
|
123
|
-
|
|
147
|
+
let decision = OwnerScope$Reventless.decide(payload.identity, binding.ownerField, undefined);
|
|
148
|
+
if (typeof decision !== "object" && decision !== "Unscoped") {
|
|
149
|
+
return emptyConnection();
|
|
150
|
+
}
|
|
151
|
+
return await binding.pushdowns.itemsPage(rm, subIdField, id$1, argObj(payload.arguments), OwnerScope$Reventless.scopeOf(decision));
|
|
152
|
+
break;
|
|
124
153
|
case "list" :
|
|
125
154
|
let argsDict = argObj(payload.arguments);
|
|
126
|
-
let
|
|
155
|
+
let decision$1 = OwnerScope$Reventless.decide(payload.identity, binding.ownerField, undefined);
|
|
156
|
+
if (typeof decision$1 !== "object" && decision$1 !== "Unscoped") {
|
|
157
|
+
return emptyConnection();
|
|
158
|
+
}
|
|
159
|
+
let ownerScope = OwnerScope$Reventless.scopeOf(decision$1);
|
|
160
|
+
let conn = await binding.pushdowns.listPage(rm, argsDict, binding.capability, binding.labelField, ownerScope);
|
|
127
161
|
if (conn !== undefined) {
|
|
128
162
|
return conn;
|
|
129
163
|
}
|
|
130
|
-
let items$
|
|
131
|
-
return QueryDbListQuery$ReventlessCore.run(items$
|
|
164
|
+
let items$1 = await binding.pushdowns.scanAll(rm);
|
|
165
|
+
return QueryDbListQuery$ReventlessCore.run(items$1, argsDict, binding.capability, binding.labelField, undefined, ownerScope);
|
|
166
|
+
break;
|
|
132
167
|
case "resolveMany" :
|
|
133
168
|
let target = Stdlib_Option.getOr(payload.target, rm);
|
|
134
169
|
let source = Stdlib_Option.getOr(payload.source, null);
|
|
@@ -139,30 +174,30 @@ async function dispatch(binding, lookupBindingOpt, payload) {
|
|
|
139
174
|
let source$1 = Stdlib_Option.getOr(payload.source, null);
|
|
140
175
|
let key = Stdlib_Option.getOr(argStr(source$1, Stdlib_Option.getOr(payload.sourceIdField, "")), "");
|
|
141
176
|
let ix = payload.targetIndex;
|
|
142
|
-
let items$
|
|
177
|
+
let items$2;
|
|
143
178
|
if (ix !== undefined) {
|
|
144
|
-
items$
|
|
179
|
+
items$2 = await binding.pushdowns.indexLookup(target$1, Stdlib_Option.getOr(payload.targetIndexIdField, ix), key);
|
|
145
180
|
} else {
|
|
146
|
-
let items$
|
|
147
|
-
items$
|
|
181
|
+
let items$3 = await binding.ops.load(key);
|
|
182
|
+
items$2 = items$3.TAG === "Ok" ? items$3._0 : [];
|
|
148
183
|
}
|
|
149
|
-
let match$
|
|
150
|
-
let match$
|
|
184
|
+
let match$6 = payload.sourceSubId;
|
|
185
|
+
let match$7 = binding.subIdField;
|
|
151
186
|
let filtered;
|
|
152
|
-
if (match$
|
|
153
|
-
let name = match$
|
|
154
|
-
let tmp = match$
|
|
187
|
+
if (match$6 !== undefined && match$7 !== undefined) {
|
|
188
|
+
let name = match$6.name;
|
|
189
|
+
let tmp = match$6.kind === "arg" ? argStr(payload.arguments, name) : argStr(source$1, name);
|
|
155
190
|
let subVal = Stdlib_Option.getOr(tmp, "");
|
|
156
|
-
filtered = items$
|
|
191
|
+
filtered = items$2.filter(it => Stdlib_Option.getOr(argStr(it, match$7), "") === subVal);
|
|
157
192
|
} else {
|
|
158
|
-
filtered = items$
|
|
193
|
+
filtered = items$2;
|
|
159
194
|
}
|
|
160
195
|
if (Stdlib_Option.getOr(payload.multi, false)) {
|
|
161
196
|
return filtered;
|
|
162
197
|
}
|
|
163
|
-
let item
|
|
164
|
-
if (item
|
|
165
|
-
return item
|
|
198
|
+
let item = filtered[0];
|
|
199
|
+
if (item !== undefined) {
|
|
200
|
+
return item;
|
|
166
201
|
} else {
|
|
167
202
|
return null;
|
|
168
203
|
}
|
|
@@ -172,8 +207,8 @@ async function dispatch(binding, lookupBindingOpt, payload) {
|
|
|
172
207
|
}
|
|
173
208
|
} else {
|
|
174
209
|
let isMulti = Stdlib_Option.getOr(payload.multi, false);
|
|
175
|
-
let match$
|
|
176
|
-
switch (match$
|
|
210
|
+
let match$8 = payload.kind;
|
|
211
|
+
switch (match$8) {
|
|
177
212
|
case "getById" :
|
|
178
213
|
return null;
|
|
179
214
|
case "items" :
|
|
@@ -205,7 +205,7 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
205
205
|
// with the SDL emitted by GraphQL_FragmentGenerator at runtime.
|
|
206
206
|
let stateSchemaOpt = ReventlessCore.Plugin_Helpers.stateSchemaRegistry->Dict.get(name)
|
|
207
207
|
let capability = switch stateSchemaOpt {
|
|
208
|
-
| Some(s) => ReventlessCore.GraphQL_FragmentGenerator.deriveServerCapability(s)
|
|
208
|
+
| Some(s) => ReventlessCore.GraphQL_FragmentGenerator.deriveServerCapability(~entityName=name, s)
|
|
209
209
|
| None => ReventlessCore.GraphQL_FragmentGenerator.emptyCapability
|
|
210
210
|
}
|
|
211
211
|
let filterFieldNames = capability.filterFields->Array.map(f => f.name)
|
|
@@ -238,6 +238,39 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
238
238
|
// (prefix-agnostic: real plugin rows always carry `name`, internal rows never do).
|
|
239
239
|
// See docs/plans/platform-plugins-admin-connection-null-rows.md.
|
|
240
240
|
let requireAttribute = internalRowRequiredAttr(name)
|
|
241
|
+
// Derived from the same schema `capability` came from, so the two cannot
|
|
242
|
+
// disagree about this read model's fields.
|
|
243
|
+
let ownerField = switch stateSchemaOpt {
|
|
244
|
+
| Some(s) => Reventless.Owner.fieldNames(s)->Array.get(0)
|
|
245
|
+
| None => None
|
|
246
|
+
}
|
|
247
|
+
// A DynamoDB FilterExpression is applied AFTER the page is read, so a scoped
|
|
248
|
+
// list over a table with no index on the owner field returns short pages —
|
|
249
|
+
// correct, but pathological once a caller owns a small fraction of the rows.
|
|
250
|
+
// Warned rather than refused: the resolver does serve the query, and a
|
|
251
|
+
// deployment may legitimately accept the cost on a small table. Mirrors the
|
|
252
|
+
// `@scanSort` alignment warning above, which exists for the same class of
|
|
253
|
+
// "works, but scans" mistake.
|
|
254
|
+
ReventlessCore.OwnerScopeDiagnostics.warnIfNoElevatedGroups(
|
|
255
|
+
~comp="QueryDbResolvers_AppSync",
|
|
256
|
+
~view=name,
|
|
257
|
+
~ownerField,
|
|
258
|
+
)
|
|
259
|
+
switch ownerField {
|
|
260
|
+
| Some(f) =>
|
|
261
|
+
let indexed =
|
|
262
|
+
indexes->Array.some(ic => ic.idField->Option.getOr(ic.index) == f) ||
|
|
263
|
+
subIdField->Option.getOr("") == f
|
|
264
|
+
if !indexed {
|
|
265
|
+
log.warn(
|
|
266
|
+
~comp="QueryDbResolvers_AppSync",
|
|
267
|
+
`${name}: @owner field "${f}" is not the key of any index on this table. ` ++
|
|
268
|
+
"Owner-scoped reads will Scan and filter, so pages shrink as the caller's " ++
|
|
269
|
+
"share of the rows falls. Add an @index on that field before this read model grows.",
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
| None => ()
|
|
273
|
+
}
|
|
241
274
|
let resolverAll = makeQueryResolver(
|
|
242
275
|
~resolverName=fieldNameForAll->String.capitalize,
|
|
243
276
|
~field=fieldNameForAll->Pulumi.Input.make,
|
|
@@ -248,6 +281,8 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
248
281
|
~rangeFields=rangeFieldNames,
|
|
249
282
|
~sortFields=sortFieldNames,
|
|
250
283
|
~requireAttribute?,
|
|
284
|
+
~ownerField?,
|
|
285
|
+
~elevatedGroups=Reventless.OwnerScope.elevatedGroups(),
|
|
251
286
|
)
|
|
252
287
|
} else {
|
|
253
288
|
Resolver.Functions.listAllItems
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
4
4
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
5
|
import * as Stdlib_String from "@rescript/runtime/lib/es6/Stdlib_String.js";
|
|
6
|
+
import * as Owner$Reventless from "@reventlessdev/reventless-spec/src/components/Owner.res.mjs";
|
|
6
7
|
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
8
|
+
import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
|
|
7
9
|
import * as Adapter$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Adapter.res.mjs";
|
|
8
10
|
import * as AppSync_Function$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/AppSync/AppSync_Function.res.mjs";
|
|
9
11
|
import * as Util_AppSync$ReventlessAws from "../../util/Util_AppSync.res.mjs";
|
|
@@ -12,6 +14,7 @@ import * as Util_QueryDb$ReventlessCore from "@reventlessdev/reventless-core/src
|
|
|
12
14
|
import * as AppSync_DataSource$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/AppSync/AppSync_DataSource.res.mjs";
|
|
13
15
|
import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_Helpers.res.mjs";
|
|
14
16
|
import * as AppSync_Resolver_Functions$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/AppSync/AppSync_Resolver_Functions.res.mjs";
|
|
17
|
+
import * as OwnerScopeDiagnostics$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/OwnerScopeDiagnostics.res.mjs";
|
|
15
18
|
import * as AppSync_Resolver_Retrying$ReventlessAws from "../Api/AppSync_Resolver_Retrying.res.mjs";
|
|
16
19
|
import * as GraphQL_FragmentGenerator$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_FragmentGenerator.res.mjs";
|
|
17
20
|
import * as QueryInterceptor_Provisioning$ReventlessAws from "./QueryInterceptor_Provisioning.res.mjs";
|
|
@@ -91,7 +94,7 @@ function make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolve
|
|
|
91
94
|
let resolverByIdMultiple = includeIdParam ? Stdlib_Option.map(subIdField, sortField => makeQueryResolver(Stdlib_String.capitalize(fieldNameForSingle) + "Items", fieldNameForSingle + "Items", AppSync_Resolver_Functions$PulumiAws.queryItemsWithSortConditions(sortField))) : undefined;
|
|
92
95
|
let labelField = registryEntry !== undefined ? Stdlib_Option.getOr(registryEntry.labelField, "id") : "id";
|
|
93
96
|
let stateSchemaOpt = Plugin_Helpers$ReventlessCore.stateSchemaRegistry[name$1];
|
|
94
|
-
let capability = stateSchemaOpt !== undefined ? GraphQL_FragmentGenerator$ReventlessCore.deriveServerCapability(stateSchemaOpt) : GraphQL_FragmentGenerator$ReventlessCore.emptyCapability;
|
|
97
|
+
let capability = stateSchemaOpt !== undefined ? GraphQL_FragmentGenerator$ReventlessCore.deriveServerCapability(name$1, stateSchemaOpt) : GraphQL_FragmentGenerator$ReventlessCore.emptyCapability;
|
|
95
98
|
let filterFieldNames = capability.filterFields.map(f => f.name);
|
|
96
99
|
let rangeFieldNames = Stdlib_Array.filterMap(capability.filterFields, f => {
|
|
97
100
|
if (f.range) {
|
|
@@ -106,7 +109,15 @@ function make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolve
|
|
|
106
109
|
GraphQL_FragmentGenerator$ReventlessCore.validateScanSortAlignment(stateSchemaOpt, name$1, knownSortFields).forEach(msg => log.warn("QueryDbResolvers_AppSync", undefined, msg));
|
|
107
110
|
}
|
|
108
111
|
let requireAttribute = internalRowRequiredAttr(name$1);
|
|
109
|
-
let
|
|
112
|
+
let ownerField = stateSchemaOpt !== undefined ? Owner$Reventless.fieldNames(stateSchemaOpt)[0] : undefined;
|
|
113
|
+
OwnerScopeDiagnostics$ReventlessCore.warnIfNoElevatedGroups("QueryDbResolvers_AppSync", name$1, ownerField);
|
|
114
|
+
if (ownerField !== undefined) {
|
|
115
|
+
let indexed = indexes.some(ic => Stdlib_Option.getOr(ic.idField, ic.index) === ownerField) || Stdlib_Option.getOr(subIdField, "") === ownerField;
|
|
116
|
+
if (!indexed) {
|
|
117
|
+
log.warn("QueryDbResolvers_AppSync", undefined, name$1 + `: @owner field "` + ownerField + `" is not the key of any index on this table. ` + "Owner-scoped reads will Scan and filter, so pages shrink as the caller's share of the rows falls. Add an @index on that field before this read model grows.");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
let resolverAll = makeQueryResolver(Stdlib_String.capitalize(fieldNameForAll), fieldNameForAll, connectionSpec ? AppSync_Resolver_Functions$PulumiAws.listAllItemsConnection(labelField, filterFieldNames, rangeFieldNames, sortFieldNames, requireAttribute, ownerField, OwnerScope$Reventless.elevatedGroups()) : AppSync_Resolver_Functions$PulumiAws.listAllItems);
|
|
110
121
|
let resolversByIndex = indexes.map(indexConfig => {
|
|
111
122
|
let index = indexConfig.index;
|
|
112
123
|
let stripLeadingBy = s => {
|
|
@@ -144,11 +144,15 @@ let registerBinding = (
|
|
|
144
144
|
indexes,
|
|
145
145
|
subIdField,
|
|
146
146
|
capability: ReventlessCore.GraphQL_FragmentGenerator.deriveServerCapability(
|
|
147
|
+
~entityName=entry.readModelName,
|
|
147
148
|
spec.stateSchema,
|
|
148
149
|
),
|
|
149
150
|
labelField: entry.labelField,
|
|
150
151
|
includeIdParam: entry.includeIdParam,
|
|
151
152
|
authorization: spec.authorization,
|
|
153
|
+
// From the same schema `capability` is derived from, one line above, so the
|
|
154
|
+
// two cannot end up disagreeing about this read model's fields.
|
|
155
|
+
ownerField: Reventless.Owner.fieldNames(spec.stateSchema)->Array.get(0),
|
|
152
156
|
},
|
|
153
157
|
)
|
|
154
158
|
logDebug("registered resolver binding for " ++ entry.readModelName, {comp: "PgQueryResolver"})
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
4
4
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
5
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as Owner$Reventless from "@reventlessdev/reventless-spec/src/components/Owner.res.mjs";
|
|
6
7
|
import * as PgRuntime$ReventlessAws from "../Postgres/PgRuntime.res.mjs";
|
|
7
8
|
import * as HandlerFactoryHelpersMjs from "./HandlerFactoryHelpers.mjs";
|
|
8
9
|
import * as PgQueryResolver_Lambda$ReventlessAws from "../QueryDb/PgQueryResolver_Lambda.res.mjs";
|
|
@@ -69,10 +70,11 @@ function registerBinding(pushdowns, pgConnection, entry, spec) {
|
|
|
69
70
|
pushdowns: pushdowns,
|
|
70
71
|
indexes: indexes,
|
|
71
72
|
subIdField: subIdField,
|
|
72
|
-
capability: GraphQL_FragmentGenerator$ReventlessCore.deriveServerCapability(spec.stateSchema),
|
|
73
|
+
capability: GraphQL_FragmentGenerator$ReventlessCore.deriveServerCapability(entry.readModelName, spec.stateSchema),
|
|
73
74
|
labelField: entry.labelField,
|
|
74
75
|
includeIdParam: entry.includeIdParam,
|
|
75
|
-
authorization: spec.authorization
|
|
76
|
+
authorization: spec.authorization,
|
|
77
|
+
ownerField: Owner$Reventless.fieldNames(spec.stateSchema)[0]
|
|
76
78
|
});
|
|
77
79
|
HandlerFactoryHelpersMjs.log.debug("registered resolver binding for " + entry.readModelName, {
|
|
78
80
|
comp: "PgQueryResolver"
|
|
@@ -91,4 +93,4 @@ export {
|
|
|
91
93
|
makePushdowns,
|
|
92
94
|
registerBinding,
|
|
93
95
|
}
|
|
94
|
-
/*
|
|
96
|
+
/* Owner-Reventless Not a pure module */
|