@reventlessdev/reventless-aws 3.0.0-alpha.306 → 3.0.0-alpha.308
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 +11 -11
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res +64 -0
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res.mjs +54 -2
- package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res +111 -33
- package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res.mjs +20 -17
- package/src/adapter/QueryDb/QueryDbResolvers_Lambda.res +19 -11
- package/src/adapter/QueryDb/QueryDbResolvers_Lambda.res.mjs +11 -10
- package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res +6 -0
- package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res.mjs +2 -1
- package/tests/AppSync_RetirementNarrowingTest.res +310 -0
- package/tests/AppSync_RetirementNarrowingTest.res.mjs +216 -0
- package/tests/PgQueryResolver_LambdaTest.res +2 -0
- package/tests/PgQueryResolver_LambdaTest.res.mjs +34 -30
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.308 (2026-08-18)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **aws:** make the unowned stub callable on the doors that call it ([931aa39](https://github.com/ReventlessDev/reventless-core/commit/931aa3968ae0e29befab7183125ed2453d4765cb))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.307 (2026-08-18)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **api:** make the by-index door answer, and let an elevated caller widen it ([0fe0c6f](https://github.com/ReventlessDev/reventless-core/commit/0fe0c6f8dec6228ecaba39577e28d780b4f79c83))
|
|
18
|
+
* **aws:** apply [@owner](https://github.com/owner) on the DynamoDB by-ids and by-index doors ([a6b5afc](https://github.com/ReventlessDev/reventless-core/commit/a6b5afc2923bfa4d3e0b2990367f67e8ffdd8877))
|
|
19
|
+
* **aws:** narrow retirement on every DynamoDB door, not only the list ([d6a799b](https://github.com/ReventlessDev/reventless-core/commit/d6a799b287b28e9c1f75e193adbf3f6328a6bf2d))
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
* **core:** let a reference name a retired row, and let an elevated caller open one ([9e2623a](https://github.com/ReventlessDev/reventless-core/commit/9e2623a4b22487561607fcc0ca19d51726069ee4))
|
|
23
|
+
|
|
24
|
+
|
|
6
25
|
# 3.0.0-alpha.306 (2026-08-16)
|
|
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.308",
|
|
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-
|
|
15
|
+
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.12",
|
|
16
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.8",
|
|
16
17
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
17
|
-
"@reventlessdev/rescript-aws
|
|
18
|
-
"@reventlessdev/rescript-
|
|
19
|
-
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.78",
|
|
20
|
-
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
18
|
+
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.80",
|
|
19
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
21
20
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
|
|
22
|
-
"@reventlessdev/
|
|
23
|
-
"@reventlessdev/reventless-
|
|
24
|
-
"@reventlessdev/reventless-
|
|
25
|
-
"@reventlessdev/reventless-
|
|
26
|
-
"@reventlessdev/reventless-
|
|
21
|
+
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
22
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.239",
|
|
23
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.145",
|
|
24
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.31",
|
|
25
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.117",
|
|
26
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.103"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"rescript": "12.3.0",
|
|
@@ -99,6 +99,10 @@ type binding = {
|
|
|
99
99
|
ownerField: option<string>,
|
|
100
100
|
retiredField: option<string>,
|
|
101
101
|
retiredValues: option<array<string>>,
|
|
102
|
+
/** Whether a reference to a retired row of this read model still resolves its
|
|
103
|
+
name — the `@namedWhenRetired` opt-in. Read only by the reference door;
|
|
104
|
+
every other door narrows as it always did. */
|
|
105
|
+
namedWhenRetired: bool,
|
|
102
106
|
}
|
|
103
107
|
|
|
104
108
|
// -- arg helpers -------------------------------------------------------------
|
|
@@ -253,6 +257,66 @@ let dispatch = async (
|
|
|
253
257
|
Array.concat(found, extra)->Array.filter(item => ownerAllows(item) && retiredAllows(item)),
|
|
254
258
|
)
|
|
255
259
|
|
|
260
|
+
| "refs" =>
|
|
261
|
+
// The reference door. The same by-ids read, projected on the way out to
|
|
262
|
+
// what naming a row needs, with the retirement lifted for a view that said
|
|
263
|
+
// its retired rows keep their names — and the owner rule left exactly where
|
|
264
|
+
// it was, so a row that is somebody's is still only theirs.
|
|
265
|
+
let ids = payload.arguments->argStrs("ids")
|
|
266
|
+
let found = await binding.pushdowns.byIds(~readModelName=rm, ids)
|
|
267
|
+
let missing =
|
|
268
|
+
found->Array.length < ids->Array.length
|
|
269
|
+
? ids->Array.filterMap(ReventlessCore.Api_Ids.alternateKey)
|
|
270
|
+
: []
|
|
271
|
+
let extra = missing->Array.length > 0
|
|
272
|
+
? await binding.pushdowns.byIds(~readModelName=rm, missing)
|
|
273
|
+
: []
|
|
274
|
+
let retirementOf = (item: JSON.t) =>
|
|
275
|
+
switch binding.retiredField {
|
|
276
|
+
| None => (false, None)
|
|
277
|
+
| Some(field) =>
|
|
278
|
+
let cell = item->JSON.Decode.object->Option.flatMap(d => d->Dict.get(field))
|
|
279
|
+
let scope: Reventless.OwnerScope.retiredScope = {
|
|
280
|
+
field,
|
|
281
|
+
values: binding.retiredValues,
|
|
282
|
+
}
|
|
283
|
+
let retired = scope->Reventless.OwnerScope.isRetiredValue(cell)
|
|
284
|
+
(
|
|
285
|
+
retired,
|
|
286
|
+
retired && binding.retiredValues->Option.isSome
|
|
287
|
+
? cell->Option.flatMap(JSON.Decode.string)
|
|
288
|
+
: None,
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
JSON.Encode.array(
|
|
292
|
+
Array.concat(found, extra)
|
|
293
|
+
->Array.filter(ownerAllows)
|
|
294
|
+
->Array.filterMap(item => {
|
|
295
|
+
let (retired, state) = retirementOf(item)
|
|
296
|
+
if retired && !binding.namedWhenRetired {
|
|
297
|
+
None
|
|
298
|
+
} else {
|
|
299
|
+
let get = key => item->JSON.Decode.object->Option.flatMap(d => d->Dict.get(key))
|
|
300
|
+
let id = get("id")->Option.flatMap(JSON.Decode.string)->Option.getOr("")
|
|
301
|
+
Some(
|
|
302
|
+
Dict.fromArray([
|
|
303
|
+
("id", JSON.Encode.string(id)),
|
|
304
|
+
(
|
|
305
|
+
"label",
|
|
306
|
+
JSON.Encode.string(
|
|
307
|
+
get(binding.labelField)
|
|
308
|
+
->Option.flatMap(JSON.Decode.string)
|
|
309
|
+
->Option.getOr(id),
|
|
310
|
+
),
|
|
311
|
+
),
|
|
312
|
+
("retired", JSON.Encode.bool(retired)),
|
|
313
|
+
("retiredState", state->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
|
|
314
|
+
])->JSON.Encode.object,
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
}),
|
|
318
|
+
)
|
|
319
|
+
|
|
256
320
|
| "items" =>
|
|
257
321
|
// Sub-id connection: {single}Items(id, filter, first/after/last/before).
|
|
258
322
|
// Requires a subIdField; without one it's an empty connection.
|
|
@@ -185,11 +185,63 @@ async function dispatch(binding, lookupBindingOpt, payload) {
|
|
|
185
185
|
let items$1 = await binding.pushdowns.scanAll(rm);
|
|
186
186
|
return QueryDbListQuery$ReventlessCore.run(items$1, argsDict, binding.capability, binding.labelField, undefined, ownerScope, retiredScope);
|
|
187
187
|
break;
|
|
188
|
+
case "refs" :
|
|
189
|
+
let ids$1 = argStrs(payload.arguments, "ids");
|
|
190
|
+
let found$2 = await binding.pushdowns.byIds(rm, ids$1);
|
|
191
|
+
let missing$1 = found$2.length < ids$1.length ? Stdlib_Array.filterMap(ids$1, Api_Ids$ReventlessCore.alternateKey) : [];
|
|
192
|
+
let extra$1 = missing$1.length !== 0 ? await binding.pushdowns.byIds(rm, missing$1) : [];
|
|
193
|
+
let retirementOf = item => {
|
|
194
|
+
let field = binding.retiredField;
|
|
195
|
+
if (field === undefined) {
|
|
196
|
+
return [
|
|
197
|
+
false,
|
|
198
|
+
undefined
|
|
199
|
+
];
|
|
200
|
+
}
|
|
201
|
+
let cell = Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[field]);
|
|
202
|
+
let scope_values = binding.retiredValues;
|
|
203
|
+
let scope = {
|
|
204
|
+
field: field,
|
|
205
|
+
values: scope_values
|
|
206
|
+
};
|
|
207
|
+
let retired = OwnerScope$Reventless.isRetiredValue(scope, cell);
|
|
208
|
+
return [
|
|
209
|
+
retired,
|
|
210
|
+
retired && Stdlib_Option.isSome(binding.retiredValues) ? Stdlib_Option.flatMap(cell, Stdlib_JSON.Decode.string) : undefined
|
|
211
|
+
];
|
|
212
|
+
};
|
|
213
|
+
return Stdlib_Array.filterMap(found$2.concat(extra$1).filter(ownerAllows), item => {
|
|
214
|
+
let match = retirementOf(item);
|
|
215
|
+
let retired = match[0];
|
|
216
|
+
if (retired && !binding.namedWhenRetired) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
let get = key => Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[key]);
|
|
220
|
+
let id = Stdlib_Option.getOr(Stdlib_Option.flatMap(get("id"), Stdlib_JSON.Decode.string), "");
|
|
221
|
+
return Object.fromEntries([
|
|
222
|
+
[
|
|
223
|
+
"id",
|
|
224
|
+
id
|
|
225
|
+
],
|
|
226
|
+
[
|
|
227
|
+
"label",
|
|
228
|
+
Stdlib_Option.getOr(Stdlib_Option.flatMap(get(binding.labelField), Stdlib_JSON.Decode.string), id)
|
|
229
|
+
],
|
|
230
|
+
[
|
|
231
|
+
"retired",
|
|
232
|
+
retired
|
|
233
|
+
],
|
|
234
|
+
[
|
|
235
|
+
"retiredState",
|
|
236
|
+
Stdlib_Option.mapOr(match[1], null, prim => prim)
|
|
237
|
+
]
|
|
238
|
+
]);
|
|
239
|
+
});
|
|
188
240
|
case "resolveMany" :
|
|
189
241
|
let target = Stdlib_Option.getOr(payload.target, rm);
|
|
190
242
|
let source = Stdlib_Option.getOr(payload.source, null);
|
|
191
|
-
let ids$
|
|
192
|
-
return await binding.pushdowns.byIds(target, ids$
|
|
243
|
+
let ids$2 = argStrs(source, Stdlib_Option.getOr(payload.sourceIdsField, ""));
|
|
244
|
+
return await binding.pushdowns.byIds(target, ids$2);
|
|
193
245
|
case "resolveOne" :
|
|
194
246
|
let target$1 = Stdlib_Option.getOr(payload.target, rm);
|
|
195
247
|
let source$1 = Stdlib_Option.getOr(payload.source, null);
|
|
@@ -184,13 +184,38 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
184
184
|
}
|
|
185
185
|
let elevatedGroups = Reventless.OwnerScope.elevatedGroups()
|
|
186
186
|
|
|
187
|
+
// Read here for exactly the reason `ownerField` above is, and after making
|
|
188
|
+
// the same mistake: the by-id, by-ids and by-index resolvers are built below
|
|
189
|
+
// and this lookup used to sit past all of them, so they were generated
|
|
190
|
+
// without a retirement predicate — a list that withheld archived rows beside
|
|
191
|
+
// by-key doors that handed any one of them over on request.
|
|
192
|
+
let retiredSpec =
|
|
193
|
+
stateSchemaOpt
|
|
194
|
+
->Option.flatMap(Reventless.StateAnnotations.getSpec)
|
|
195
|
+
->Option.flatMap(spec => spec.retired)
|
|
196
|
+
let retiredField = retiredSpec->Option.map(r => r.field)
|
|
197
|
+
let retiredValues = retiredSpec->Option.flatMap(r => r.values)
|
|
198
|
+
|
|
187
199
|
let resolverByIdSingle = if includeIdParam {
|
|
188
200
|
makeQueryResolver(
|
|
189
201
|
~resolverName=fieldNameForSingle->String.capitalize,
|
|
190
202
|
~field=fieldNameForSingle->Pulumi.Input.make,
|
|
191
203
|
~code=switch subIdField {
|
|
192
|
-
| Some(sortField) =>
|
|
193
|
-
|
|
204
|
+
| Some(sortField) =>
|
|
205
|
+
Resolver.Functions.queryByIdSort(
|
|
206
|
+
sortField,
|
|
207
|
+
~ownerField?,
|
|
208
|
+
~elevatedGroups,
|
|
209
|
+
~retiredField?,
|
|
210
|
+
~retiredValues?,
|
|
211
|
+
)
|
|
212
|
+
| None =>
|
|
213
|
+
Resolver.Functions.getItemById(
|
|
214
|
+
~ownerField?,
|
|
215
|
+
~elevatedGroups,
|
|
216
|
+
~retiredField?,
|
|
217
|
+
~retiredValues?,
|
|
218
|
+
)
|
|
194
219
|
},
|
|
195
220
|
)
|
|
196
221
|
} else {
|
|
@@ -285,12 +310,6 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
285
310
|
}
|
|
286
311
|
| None => ()
|
|
287
312
|
}
|
|
288
|
-
let retiredSpec =
|
|
289
|
-
stateSchemaOpt
|
|
290
|
-
->Option.flatMap(Reventless.StateAnnotations.getSpec)
|
|
291
|
-
->Option.flatMap(spec => spec.retired)
|
|
292
|
-
let retiredField = retiredSpec->Option.map(r => r.field)
|
|
293
|
-
let retiredValues = retiredSpec->Option.flatMap(r => r.values)
|
|
294
313
|
// The same class of "works, but scans" mistake as the owner warning above,
|
|
295
314
|
// and the retirement case degrades the same way: the FilterExpression is
|
|
296
315
|
// applied after the page is read, so pages shrink as the archive's share of
|
|
@@ -333,26 +352,16 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
333
352
|
},
|
|
334
353
|
)
|
|
335
354
|
let resolversByIndex = indexes->Array.map(({index} as indexConfig) => {
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
347
|
-
let resolverName =
|
|
348
|
-
fieldNameForSingle->String.capitalize ++
|
|
349
|
-
"By" ++
|
|
350
|
-
(index->stripLeadingBy->String.capitalize)
|
|
351
|
-
let fieldName =
|
|
352
|
-
fieldNameForSingle ++
|
|
353
|
-
"By" ++
|
|
354
|
-
(index->stripLeadingBy->String.capitalize)
|
|
355
|
-
let idField = indexConfig.idField->Option.getOr(index)
|
|
355
|
+
// Name and key field both come from `GraphQL_FragmentGenerator`, which is
|
|
356
|
+
// where the SDL field this attaches to is derived. Deriving them here as
|
|
357
|
+
// well is how the two came to disagree: the emitted field declared `id`
|
|
358
|
+
// while this resolver read the index key, so the door could not be called.
|
|
359
|
+
let fieldName = ReventlessCore.GraphQL_FragmentGenerator.indexQueryFieldName(
|
|
360
|
+
~singleFieldName=fieldNameForSingle,
|
|
361
|
+
~index,
|
|
362
|
+
)
|
|
363
|
+
let resolverName = fieldName->String.capitalize
|
|
364
|
+
let idField = ReventlessCore.GraphQL_FragmentGenerator.indexKeyField(indexConfig)
|
|
356
365
|
switch indexConfig.authorization {
|
|
357
366
|
| None =>
|
|
358
367
|
makeQueryResolver(
|
|
@@ -360,8 +369,24 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
360
369
|
~field=fieldName->Pulumi.Input.make,
|
|
361
370
|
~code=switch indexConfig.subIdField {
|
|
362
371
|
| Some(sortField) =>
|
|
363
|
-
Resolver.Functions.queryByIndexSortFiltered(
|
|
364
|
-
|
|
372
|
+
Resolver.Functions.queryByIndexSortFiltered(
|
|
373
|
+
~index,
|
|
374
|
+
~idField,
|
|
375
|
+
~sortField,
|
|
376
|
+
~ownerField?,
|
|
377
|
+
~retiredField?,
|
|
378
|
+
~retiredValues?,
|
|
379
|
+
~elevatedGroups,
|
|
380
|
+
)
|
|
381
|
+
| None =>
|
|
382
|
+
Resolver.Functions.queryByIndexFiltered(
|
|
383
|
+
~index,
|
|
384
|
+
~idField,
|
|
385
|
+
~ownerField?,
|
|
386
|
+
~retiredField?,
|
|
387
|
+
~retiredValues?,
|
|
388
|
+
~elevatedGroups,
|
|
389
|
+
)
|
|
365
390
|
},
|
|
366
391
|
)
|
|
367
392
|
| Some({tableName, group}) =>
|
|
@@ -387,7 +412,20 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
387
412
|
~name=resolverName,
|
|
388
413
|
~api,
|
|
389
414
|
~dataSource=dataSourceName,
|
|
390
|
-
|
|
415
|
+
// No `~ownerField`, deliberately: this index declares its own
|
|
416
|
+
// authorization, and the rows it grants a group member are by
|
|
417
|
+
// construction owned by somebody else — an order assigned to a
|
|
418
|
+
// fulfilment operator belongs to the customer who placed it. Adding the
|
|
419
|
+
// owner predicate here would return nothing and revoke the access the
|
|
420
|
+
// auth table exists to grant. Retirement still applies: an archived row
|
|
421
|
+
// is withdrawn from everyone who has not asked, whoever owns it.
|
|
422
|
+
~code=Resolver.Functions.queryByIndexFiltered(
|
|
423
|
+
~index,
|
|
424
|
+
~idField,
|
|
425
|
+
~retiredField?,
|
|
426
|
+
~retiredValues?,
|
|
427
|
+
~elevatedGroups,
|
|
428
|
+
),
|
|
391
429
|
~opts,
|
|
392
430
|
)
|
|
393
431
|
// The interceptor leads the chain, as it does in every other Query
|
|
@@ -440,7 +478,43 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
440
478
|
~field=byIdsField->Pulumi.Input.make,
|
|
441
479
|
~code=generateCode(
|
|
442
480
|
~storageResource=storage,
|
|
443
|
-
~template=Resolver.Functions.batchGetItemsByIds
|
|
481
|
+
~template=Resolver.Functions.batchGetItemsByIds(
|
|
482
|
+
~ownerField?,
|
|
483
|
+
~retiredField?,
|
|
484
|
+
~retiredValues?,
|
|
485
|
+
~elevatedGroups,
|
|
486
|
+
),
|
|
487
|
+
),
|
|
488
|
+
),
|
|
489
|
+
)
|
|
490
|
+
} else {
|
|
491
|
+
None
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// The reference door. Registered under the same conditions as by-ids because
|
|
495
|
+
// it is the same read; what differs is the projection on the way out, and
|
|
496
|
+
// that a view which named its retired rows lets them through it.
|
|
497
|
+
//
|
|
498
|
+
// Not conditional on the annotation: `GraphQL_FragmentGenerator` emits the
|
|
499
|
+
// field for every view — one SDL serves every backend — so a view without a
|
|
500
|
+
// resolver here would be a field that errors rather than one that is absent.
|
|
501
|
+
let resolverRefs = if includeIdParam && subIdField === None {
|
|
502
|
+
let storage = storageResource(~pluginName=None, ~tableName=name)
|
|
503
|
+
let refsField = fieldNameForAll ++ "Refs"
|
|
504
|
+
Some(
|
|
505
|
+
makeQueryResolver(
|
|
506
|
+
~resolverName=refsField->String.capitalize,
|
|
507
|
+
~field=refsField->Pulumi.Input.make,
|
|
508
|
+
~code=generateCode(
|
|
509
|
+
~storageResource=storage,
|
|
510
|
+
~template=Resolver.Functions.refsByIds(
|
|
511
|
+
~labelField,
|
|
512
|
+
~retiredField,
|
|
513
|
+
~retiredValues,
|
|
514
|
+
~namedWhenRetired=retiredSpec->Option.mapOr(false, r => r.namedWhenRetired),
|
|
515
|
+
~ownerField?,
|
|
516
|
+
~elevatedGroups,
|
|
517
|
+
),
|
|
444
518
|
),
|
|
445
519
|
),
|
|
446
520
|
)
|
|
@@ -535,7 +609,11 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
535
609
|
| (None, Some(byIds)) => [resolverByIdSingle, resolverAll, byIds]
|
|
536
610
|
| (None, None) => [resolverByIdSingle, resolverAll]
|
|
537
611
|
}
|
|
538
|
-
|
|
612
|
+
// Appended rather than folded into `mainResolvers`' four-way switch: the door
|
|
613
|
+
// is independent of whether this view has a sub-id door, and adding a third
|
|
614
|
+
// dimension to that match would spell eight cases to say one thing.
|
|
615
|
+
let refsResolvers = resolverRefs->Option.mapOr([], r => [r])
|
|
616
|
+
Array.flat([mainResolvers, refsResolvers, resolversByIndex, idResolvers, idsResolvers])
|
|
539
617
|
->Array.map(Util.AppSync.toResourceNative)
|
|
540
618
|
}
|
|
541
619
|
|
|
@@ -94,7 +94,10 @@ function make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolve
|
|
|
94
94
|
let stateSchemaOpt = Plugin_Helpers$ReventlessCore.stateSchemaRegistry[name$1];
|
|
95
95
|
let ownerField = stateSchemaOpt !== undefined ? Owner$Reventless.fieldNames(stateSchemaOpt)[0] : undefined;
|
|
96
96
|
let elevatedGroups = OwnerScope$Reventless.elevatedGroups();
|
|
97
|
-
let
|
|
97
|
+
let retiredSpec = Stdlib_Option.flatMap(Stdlib_Option.flatMap(stateSchemaOpt, StateAnnotations$Reventless.getSpec), spec => spec.retired);
|
|
98
|
+
let retiredField = Stdlib_Option.map(retiredSpec, r => r.field);
|
|
99
|
+
let retiredValues = Stdlib_Option.flatMap(retiredSpec, r => r.values);
|
|
100
|
+
let resolverByIdSingle = includeIdParam ? makeQueryResolver(Stdlib_String.capitalize(fieldNameForSingle), fieldNameForSingle, subIdField !== undefined ? AppSync_Resolver_Functions$PulumiAws.queryByIdSort(subIdField, ownerField, elevatedGroups, retiredField, retiredValues) : AppSync_Resolver_Functions$PulumiAws.getItemById(ownerField, elevatedGroups, retiredField, retiredValues)) : makeQueryResolver(Stdlib_String.capitalize(fieldNameForSingle), fieldNameForSingle, AppSync_Resolver_Functions$PulumiAws.listAllItems);
|
|
98
101
|
let resolverByIdMultiple = includeIdParam ? Stdlib_Option.map(subIdField, sortField => makeQueryResolver(Stdlib_String.capitalize(fieldNameForSingle) + "Items", fieldNameForSingle + "Items", AppSync_Resolver_Functions$PulumiAws.queryItemsWithSortConditions(sortField, ownerField, elevatedGroups))) : undefined;
|
|
99
102
|
let labelField = registryEntry !== undefined ? Stdlib_Option.getOr(registryEntry.labelField, "id") : "id";
|
|
100
103
|
let capability = stateSchemaOpt !== undefined ? GraphQL_FragmentGenerator$ReventlessCore.deriveServerCapability(name$1, stateSchemaOpt) : GraphQL_FragmentGenerator$ReventlessCore.emptyCapability;
|
|
@@ -123,30 +126,20 @@ function make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolve
|
|
|
123
126
|
if (ownerField !== undefined && !isIndexed(ownerField)) {
|
|
124
127
|
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.");
|
|
125
128
|
}
|
|
126
|
-
let retiredSpec = Stdlib_Option.flatMap(Stdlib_Option.flatMap(stateSchemaOpt, StateAnnotations$Reventless.getSpec), spec => spec.retired);
|
|
127
|
-
let retiredField = Stdlib_Option.map(retiredSpec, r => r.field);
|
|
128
|
-
let retiredValues = Stdlib_Option.flatMap(retiredSpec, r => r.values);
|
|
129
129
|
if (retiredField !== undefined && !isIndexed(retiredField)) {
|
|
130
130
|
log.warn("QueryDbResolvers_AppSync", undefined, name$1 + `: @retired field "` + retiredField + `" is not the key of any index on this table. ` + "Reads that exclude retired rows will Scan and filter, so pages shrink as the archive's share of the rows grows. Add an @index on that field before this read model grows.");
|
|
131
131
|
}
|
|
132
132
|
let resolverAll = makeQueryResolver(Stdlib_String.capitalize(fieldNameForAll), fieldNameForAll, connectionSpec ? AppSync_Resolver_Functions$PulumiAws.listAllItemsConnection(labelField, filterFieldNames, rangeFieldNames, sortFieldNames, requireAttribute, ownerField, elevatedGroups, retiredField, retiredValues) : AppSync_Resolver_Functions$PulumiAws.listAllItems);
|
|
133
133
|
let resolversByIndex = indexes.map(indexConfig => {
|
|
134
134
|
let index = indexConfig.index;
|
|
135
|
-
let
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
} else {
|
|
139
|
-
return s;
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
let resolverName = Stdlib_String.capitalize(fieldNameForSingle) + "By" + Stdlib_String.capitalize(stripLeadingBy(index));
|
|
143
|
-
let fieldName = fieldNameForSingle + "By" + Stdlib_String.capitalize(stripLeadingBy(index));
|
|
144
|
-
let idField = Stdlib_Option.getOr(indexConfig.idField, index);
|
|
135
|
+
let fieldName = GraphQL_FragmentGenerator$ReventlessCore.indexQueryFieldName(fieldNameForSingle, index);
|
|
136
|
+
let resolverName = Stdlib_String.capitalize(fieldName);
|
|
137
|
+
let idField = GraphQL_FragmentGenerator$ReventlessCore.indexKeyField(indexConfig);
|
|
145
138
|
let match = indexConfig.authorization;
|
|
146
139
|
if (match !== undefined) {
|
|
147
140
|
let authDataSource = AppSync_DataSource$PulumiAws.makeDynamoDBDataSourceWithTableName(resolverName + "Auth", api, Util_DynamoDb$ReventlessAws.findResource(Util_QueryDb$ReventlessCore.getLocalStorageResources(allQueryDbs, match.tableName)).name, apiRole, opts);
|
|
148
141
|
let authFunction = AppSync_Function$PulumiAws.makeJs(resolverName + "Auth", api, authDataSource.name, AppSync_Resolver_Functions$PulumiAws.authorizeIndexedAccess(index, match.group), opts);
|
|
149
|
-
let queryFunction = AppSync_Function$PulumiAws.makeJs(resolverName, api, dataSourceName, AppSync_Resolver_Functions$PulumiAws.queryByIndexFiltered(index, idField), opts);
|
|
142
|
+
let queryFunction = AppSync_Function$PulumiAws.makeJs(resolverName, api, dataSourceName, AppSync_Resolver_Functions$PulumiAws.queryByIndexFiltered(index, idField, undefined, retiredField, retiredValues, elevatedGroups), opts);
|
|
150
143
|
let interceptorFn = interceptorFunction(resolverName);
|
|
151
144
|
return AppSync_Resolver_Retrying$ReventlessAws.makePipelineJsResolver(resolverName, api, "Query", fieldName, AppSync_Resolver_Functions$PulumiAws.pipelinePassThrough, interceptorFn !== undefined ? [
|
|
152
145
|
interceptorFn,
|
|
@@ -158,7 +151,7 @@ function make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolve
|
|
|
158
151
|
], opts);
|
|
159
152
|
}
|
|
160
153
|
let sortField = indexConfig.subIdField;
|
|
161
|
-
return makeQueryResolver(resolverName, fieldName, sortField !== undefined ? AppSync_Resolver_Functions$PulumiAws.queryByIndexSortFiltered(index, idField, sortField) : AppSync_Resolver_Functions$PulumiAws.queryByIndexFiltered(index, idField));
|
|
154
|
+
return makeQueryResolver(resolverName, fieldName, sortField !== undefined ? AppSync_Resolver_Functions$PulumiAws.queryByIndexSortFiltered(index, idField, ownerField, sortField, retiredField, retiredValues, elevatedGroups) : AppSync_Resolver_Functions$PulumiAws.queryByIndexFiltered(index, idField, ownerField, retiredField, retiredValues, elevatedGroups));
|
|
162
155
|
});
|
|
163
156
|
let storageResource = (pluginName, tableName) => Adapter$ReventlessCore.outputToResource(Util_DynamoDb$ReventlessAws.findResourceInOutput(Plugin_Helpers$ReventlessCore.getStorageResources(allQueryDbs, pluginName, tableName)));
|
|
164
157
|
let generateCode = (storageResource, template) => storageResource.name.apply(realTableName => template(realTableName));
|
|
@@ -166,10 +159,18 @@ function make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolve
|
|
|
166
159
|
if (includeIdParam && subIdField === undefined) {
|
|
167
160
|
let storage = storageResource(undefined, name$1);
|
|
168
161
|
let byIdsField = fieldNameForAll + "ByIds";
|
|
169
|
-
resolverByIds = makeQueryResolver(Stdlib_String.capitalize(byIdsField), byIdsField, generateCode(storage, AppSync_Resolver_Functions$PulumiAws.batchGetItemsByIds));
|
|
162
|
+
resolverByIds = makeQueryResolver(Stdlib_String.capitalize(byIdsField), byIdsField, generateCode(storage, AppSync_Resolver_Functions$PulumiAws.batchGetItemsByIds(ownerField, retiredField, retiredValues, elevatedGroups)));
|
|
170
163
|
} else {
|
|
171
164
|
resolverByIds = undefined;
|
|
172
165
|
}
|
|
166
|
+
let resolverRefs;
|
|
167
|
+
if (includeIdParam && subIdField === undefined) {
|
|
168
|
+
let storage$1 = storageResource(undefined, name$1);
|
|
169
|
+
let refsField = fieldNameForAll + "Refs";
|
|
170
|
+
resolverRefs = makeQueryResolver(Stdlib_String.capitalize(refsField), refsField, generateCode(storage$1, AppSync_Resolver_Functions$PulumiAws.refsByIds(labelField, retiredField, retiredValues, Stdlib_Option.mapOr(retiredSpec, false, r => r.namedWhenRetired), ownerField, elevatedGroups)));
|
|
171
|
+
} else {
|
|
172
|
+
resolverRefs = undefined;
|
|
173
|
+
}
|
|
173
174
|
let idResolvers = idResolverConfigs.map(config => {
|
|
174
175
|
let target = config.target;
|
|
175
176
|
let targetId = target.idField;
|
|
@@ -252,8 +253,10 @@ function make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolve
|
|
|
252
253
|
resolverAll
|
|
253
254
|
]
|
|
254
255
|
);
|
|
256
|
+
let refsResolvers = Stdlib_Option.mapOr(resolverRefs, [], r => [r]);
|
|
255
257
|
return [
|
|
256
258
|
mainResolvers,
|
|
259
|
+
refsResolvers,
|
|
257
260
|
resolversByIndex,
|
|
258
261
|
idResolvers,
|
|
259
262
|
idsResolvers
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
//
|
|
10
10
|
// Field names / includeIdParam / connectionSpec come from the same registry the
|
|
11
11
|
// AppSync path reads, so the SDL emitted by GraphQL_FragmentGenerator stays in
|
|
12
|
-
// lockstep. Kinds covered: getById, list (connection), index, byIds. Deferred to
|
|
12
|
+
// lockstep. Kinds covered: getById, list (connection), index, byIds, refs. Deferred to
|
|
13
13
|
// B3.2c: items (sub-id connection), @resolves/@resolvesMany, node, auth-table.
|
|
14
14
|
|
|
15
15
|
module Resolver = AppSync_Resolver_Retrying
|
|
@@ -150,13 +150,6 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
150
150
|
~opts,
|
|
151
151
|
)
|
|
152
152
|
|
|
153
|
-
let stripLeadingBy = s =>
|
|
154
|
-
if s->String.startsWith("by") && s->String.length > 2 {
|
|
155
|
-
s->String.slice(~start=2, ~end=s->String.length)
|
|
156
|
-
} else {
|
|
157
|
-
s
|
|
158
|
-
}
|
|
159
|
-
|
|
160
153
|
// Resolvers are deferred into resourcesMaker (created inside the schema-pushed
|
|
161
154
|
// builderOutputs.apply, like the AppSync path) — so they only exist after the
|
|
162
155
|
// fields are ACTIVE and after PgQueryResolver_Builder.provision has resolved
|
|
@@ -202,14 +195,28 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
202
195
|
[]
|
|
203
196
|
}
|
|
204
197
|
|
|
198
|
+
// The reference door — {list}Refs(ids). Same shape as by-ids and registered
|
|
199
|
+
// under the same conditions, because it is the same read: a set of keys in,
|
|
200
|
+
// rows out. What differs is on the way out, where the row is projected to
|
|
201
|
+
// what a reference needs and a retirement is lifted for a view that named its
|
|
202
|
+
// rows.
|
|
203
|
+
let refs = if includeIdParam && subIdField === None {
|
|
204
|
+
let refsField = fieldNameForAll ++ "Refs"
|
|
205
|
+
[mkResolver(~resolverName=refsField->String.capitalize, ~field=refsField, ~kind="refs")]
|
|
206
|
+
} else {
|
|
207
|
+
[]
|
|
208
|
+
}
|
|
209
|
+
|
|
205
210
|
// Per-index equality queries: {single}By{Index}. A group-restricted index
|
|
206
211
|
// (indexConfig.authorization) bakes the auth-table pipeline params — the
|
|
207
212
|
// Lambda verifies group membership + ownership against the auth read model.
|
|
208
213
|
let byIndex = indexes->Array.map((ic: indexConfig) => {
|
|
209
214
|
let index = ic.index
|
|
210
|
-
let
|
|
211
|
-
fieldNameForSingle
|
|
212
|
-
|
|
215
|
+
let field = ReventlessCore.GraphQL_FragmentGenerator.indexQueryFieldName(
|
|
216
|
+
~singleFieldName=fieldNameForSingle,
|
|
217
|
+
~index,
|
|
218
|
+
)
|
|
219
|
+
let resolverName = field->String.capitalize
|
|
213
220
|
switch ic.authorization {
|
|
214
221
|
| Some({tableName, group}) =>
|
|
215
222
|
mkResolver(
|
|
@@ -278,6 +285,7 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
278
285
|
[byId, all]
|
|
279
286
|
->Array.concat(items)
|
|
280
287
|
->Array.concat(byIds)
|
|
288
|
+
->Array.concat(refs)
|
|
281
289
|
->Array.concat(byIndex)
|
|
282
290
|
->Array.concat(idResolvers)
|
|
283
291
|
->Array.concat(idsResolvers)
|
|
@@ -7,6 +7,7 @@ import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/s
|
|
|
7
7
|
import * as AppSync_Resolver_Functions$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/AppSync/AppSync_Resolver_Functions.res.mjs";
|
|
8
8
|
import * as PgQueryResolver_Builder$ReventlessAws from "./PgQueryResolver_Builder.res.mjs";
|
|
9
9
|
import * as AppSync_Resolver_Retrying$ReventlessAws from "../Api/AppSync_Resolver_Retrying.res.mjs";
|
|
10
|
+
import * as GraphQL_FragmentGenerator$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_FragmentGenerator.res.mjs";
|
|
10
11
|
|
|
11
12
|
let identityBlock = `id != null && id.sub != null
|
|
12
13
|
? { userId: id.sub, username: id.username, groups: id.claims?.['cognito:groups'] ?? [], claims: id.claims, provider: 'Cognito' }
|
|
@@ -77,13 +78,6 @@ function make(name, api, param, dataSourceName, indexes, subIdField, idResolverC
|
|
|
77
78
|
PgQueryResolver_Builder$ReventlessAws.registerNodeType(returnTypeName, name$1);
|
|
78
79
|
}
|
|
79
80
|
let mkResolver = (resolverName, field, kind, index, authTable, authGroup) => AppSync_Resolver_Retrying$ReventlessAws.makeUnitJsResolver(resolverName, api, dataSourceName, "Query", field, invokeTemplate(name$1, kind, index, authTable, authGroup), opts);
|
|
80
|
-
let stripLeadingBy = s => {
|
|
81
|
-
if (s.startsWith("by") && s.length > 2) {
|
|
82
|
-
return s.slice(2, s.length);
|
|
83
|
-
} else {
|
|
84
|
-
return s;
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
81
|
let resourcesMaker = _allQueryDbs => {
|
|
88
82
|
let byId = mkResolver(Stdlib_String.capitalize(fieldNameForSingle), fieldNameForSingle, includeIdParam ? "getById" : "list", undefined, undefined, undefined);
|
|
89
83
|
let all = mkResolver(Stdlib_String.capitalize(fieldNameForAll), fieldNameForAll, "list", undefined, undefined, undefined);
|
|
@@ -95,10 +89,17 @@ function make(name, api, param, dataSourceName, indexes, subIdField, idResolverC
|
|
|
95
89
|
} else {
|
|
96
90
|
byIds = [];
|
|
97
91
|
}
|
|
92
|
+
let refs;
|
|
93
|
+
if (includeIdParam && subIdField === undefined) {
|
|
94
|
+
let refsField = fieldNameForAll + "Refs";
|
|
95
|
+
refs = [mkResolver(Stdlib_String.capitalize(refsField), refsField, "refs", undefined, undefined, undefined)];
|
|
96
|
+
} else {
|
|
97
|
+
refs = [];
|
|
98
|
+
}
|
|
98
99
|
let byIndex = indexes.map(ic => {
|
|
99
100
|
let index = ic.index;
|
|
100
|
-
let
|
|
101
|
-
let
|
|
101
|
+
let field = GraphQL_FragmentGenerator$ReventlessCore.indexQueryFieldName(fieldNameForSingle, index);
|
|
102
|
+
let resolverName = Stdlib_String.capitalize(field);
|
|
102
103
|
let match = ic.authorization;
|
|
103
104
|
if (match !== undefined) {
|
|
104
105
|
return mkResolver(resolverName, field, "index", index, Stdlib_String.capitalize(match.tableName), match.group);
|
|
@@ -150,7 +151,7 @@ function make(name, api, param, dataSourceName, indexes, subIdField, idResolverC
|
|
|
150
151
|
return [
|
|
151
152
|
byId,
|
|
152
153
|
all
|
|
153
|
-
].concat(items).concat(byIds).concat(byIndex).concat(idResolvers).concat(idsResolvers).map(Util_AppSync$ReventlessAws.toResourceNative);
|
|
154
|
+
].concat(items).concat(byIds).concat(refs).concat(byIndex).concat(idResolvers).concat(idsResolvers).map(Util_AppSync$ReventlessAws.toResourceNative);
|
|
154
155
|
};
|
|
155
156
|
return {
|
|
156
157
|
resources: [],
|
|
@@ -163,6 +163,12 @@ let registerBinding = (
|
|
|
163
163
|
->Reventless.StateAnnotations.getSpec
|
|
164
164
|
->Option.flatMap(a => a.retired)
|
|
165
165
|
->Option.flatMap(r => r.values),
|
|
166
|
+
// Read off the retirement rather than from a second annotation: a record
|
|
167
|
+
// cannot declare the reach of a retirement it does not have.
|
|
168
|
+
namedWhenRetired: spec.stateSchema
|
|
169
|
+
->Reventless.StateAnnotations.getSpec
|
|
170
|
+
->Option.flatMap(a => a.retired)
|
|
171
|
+
->Option.mapOr(false, r => r.namedWhenRetired),
|
|
166
172
|
},
|
|
167
173
|
)
|
|
168
174
|
logDebug("registered resolver binding for " ++ entry.readModelName, {comp: "PgQueryResolver"})
|
|
@@ -77,7 +77,8 @@ function registerBinding(pushdowns, pgConnection, entry, spec) {
|
|
|
77
77
|
authorization: spec.authorization,
|
|
78
78
|
ownerField: Owner$Reventless.fieldNames(spec.stateSchema)[0],
|
|
79
79
|
retiredField: Stdlib_Option.map(Stdlib_Option.flatMap(StateAnnotations$Reventless.getSpec(spec.stateSchema), a => a.retired), r => r.field),
|
|
80
|
-
retiredValues: Stdlib_Option.flatMap(Stdlib_Option.flatMap(StateAnnotations$Reventless.getSpec(spec.stateSchema), a => a.retired), r => r.values)
|
|
80
|
+
retiredValues: Stdlib_Option.flatMap(Stdlib_Option.flatMap(StateAnnotations$Reventless.getSpec(spec.stateSchema), a => a.retired), r => r.values),
|
|
81
|
+
namedWhenRetired: Stdlib_Option.mapOr(Stdlib_Option.flatMap(StateAnnotations$Reventless.getSpec(spec.stateSchema), a => a.retired), false, r => r.namedWhenRetired)
|
|
81
82
|
});
|
|
82
83
|
HandlerFactoryHelpersMjs.log.debug("registered resolver binding for " + entry.readModelName, {
|
|
83
84
|
comp: "PgQueryResolver"
|