@reventlessdev/reventless-aws 3.0.0-alpha.317 → 3.0.0-alpha.319
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 +23 -0
- package/package.json +10 -10
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res +12 -3
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res.mjs +16 -3
- package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res +106 -51
- package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res.mjs +52 -18
- package/src/adapter/QueryDb/QueryDbResolvers_Lambda.res +10 -2
- package/src/adapter/QueryDb/QueryDbResolvers_Lambda.res.mjs +3 -2
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +6 -3
- package/tests/AppSync_RetirementNarrowingTest.res +78 -0
- package/tests/AppSync_RetirementNarrowingTest.res.mjs +52 -0
- package/tests/PgQueryResolver_LambdaTest.res +166 -0
- package/tests/PgQueryResolver_LambdaTest.res.mjs +196 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,29 @@
|
|
|
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.319 (2026-08-22)
|
|
7
|
+
|
|
8
|
+
* feat(core)!: a slice that has given up says so ([f5559ab](https://github.com/ReventlessDev/reventless-core/commit/f5559abede3a9cf2c9a48aadc69a666c46cc75bf))
|
|
9
|
+
|
|
10
|
+
### BREAKING CHANGES
|
|
11
|
+
|
|
12
|
+
* `onExhausted` is required on the Translation and Automation
|
|
13
|
+
module types. ReScript has no optional module-type fields, so the alternative
|
|
14
|
+
was a PPX-injected default — and defaulting this one to silence is the bug.
|
|
15
|
+
A slice now states what abandonment means for its domain, `None` included.
|
|
16
|
+
GeocodeCustomerAddress answers with MarkAddressUnresolvable; the others say
|
|
17
|
+
nothing, on purpose.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# 3.0.0-alpha.318 (2026-08-21)
|
|
22
|
+
|
|
23
|
+
### Bug Fixes
|
|
24
|
+
|
|
25
|
+
* **core:** make @resolves/[@resolves](https://github.com/resolves)Many work end to end ([4c52957](https://github.com/ReventlessDev/reventless-core/commit/4c5295759dcb4b3eda4f0f4f2c1bc387fed88fcb))
|
|
26
|
+
* **deps:** update sury to 11.0.0-rc.2 to fix unreachable union constructors ([fa5744f](https://github.com/ReventlessDev/reventless-core/commit/fa5744fed8de975e2f14725c856c6e5ce7d04a74))
|
|
27
|
+
|
|
28
|
+
|
|
6
29
|
# 3.0.0-alpha.317 (2026-08-20)
|
|
7
30
|
|
|
8
31
|
### 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.319",
|
|
4
4
|
"description": "AWS adapters for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"dependencies": {
|
|
@@ -10,24 +10,24 @@
|
|
|
10
10
|
"@aws-sdk/client-location": "3.970.0",
|
|
11
11
|
"@aws-sdk/client-s3": "3.970.0",
|
|
12
12
|
"@aws-sdk/s3-request-presigner": "3.970.0",
|
|
13
|
-
"sury": "11.0.0-rc.
|
|
13
|
+
"sury": "11.0.0-rc.2",
|
|
14
14
|
"uuid": "^13.0.0",
|
|
15
15
|
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.12",
|
|
16
16
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
17
|
-
"@reventlessdev/rescript-node": "2.0.0-alpha.8",
|
|
18
17
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
19
|
-
"@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.
|
|
18
|
+
"@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.2",
|
|
19
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.8",
|
|
20
20
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
|
|
21
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.247",
|
|
21
22
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
22
|
-
"@reventlessdev/reventless-
|
|
23
|
-
"@reventlessdev/reventless-
|
|
24
|
-
"@reventlessdev/reventless-
|
|
25
|
-
"@reventlessdev/reventless-postgres": "3.0.0-alpha.
|
|
26
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.120"
|
|
23
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.150",
|
|
24
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.33",
|
|
25
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.122",
|
|
26
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.111"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"rescript": "12.3.0",
|
|
30
|
-
"sury-ppx": "11.0.0-rc.
|
|
30
|
+
"sury-ppx": "11.0.0-rc.2"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"rescript": "12.3.0"
|
|
@@ -443,10 +443,15 @@ let dispatch = async (
|
|
|
443
443
|
items->Array.filter(it => it->argStr(subField)->Option.getOr("") == subVal)
|
|
444
444
|
| _ => items
|
|
445
445
|
}
|
|
446
|
+
// The target's own rules, applied to the target's rows — `binding` here is
|
|
447
|
+
// the target's, so `ownerAllows` / `retiredAllows` ask its questions. A
|
|
448
|
+
// nested field takes no `includeRetired` argument, so a retired row never
|
|
449
|
+
// travels through one.
|
|
450
|
+
let allowed = filtered->Array.filter(item => ownerAllows(item) && retiredAllows(item))
|
|
446
451
|
if payload.multi->Option.getOr(false) {
|
|
447
|
-
JSON.Encode.array(
|
|
452
|
+
JSON.Encode.array(allowed)
|
|
448
453
|
} else {
|
|
449
|
-
switch
|
|
454
|
+
switch allowed->Array.get(0) {
|
|
450
455
|
| Some(item) => item
|
|
451
456
|
| None => JSON.Encode.null
|
|
452
457
|
}
|
|
@@ -459,7 +464,11 @@ let dispatch = async (
|
|
|
459
464
|
let target = payload.target->Option.getOr(rm)
|
|
460
465
|
let source = payload.source->Option.getOr(JSON.Encode.null)
|
|
461
466
|
let ids = source->argStrs(payload.sourceIdsField->Option.getOr(""))
|
|
462
|
-
JSON.Encode.array(
|
|
467
|
+
JSON.Encode.array(
|
|
468
|
+
(await binding.pushdowns.byIds(~readModelName=target, ids))->Array.filter(item =>
|
|
469
|
+
ownerAllows(item) && retiredAllows(item)
|
|
470
|
+
),
|
|
471
|
+
)
|
|
463
472
|
|
|
464
473
|
| other =>
|
|
465
474
|
// Fail loudly on unmapped kinds (feature-parity guard, per the B3.2 plan).
|
|
@@ -241,7 +241,13 @@ async function dispatch(binding, lookupBindingOpt, payload) {
|
|
|
241
241
|
let target = Stdlib_Option.getOr(payload.target, rm);
|
|
242
242
|
let source = Stdlib_Option.getOr(payload.source, null);
|
|
243
243
|
let ids$2 = argStrs(source, Stdlib_Option.getOr(payload.sourceIdsField, ""));
|
|
244
|
-
return await binding.pushdowns.byIds(target, ids$2)
|
|
244
|
+
return (await binding.pushdowns.byIds(target, ids$2)).filter(item => {
|
|
245
|
+
if (ownerAllows(item)) {
|
|
246
|
+
return retiredAllows(item);
|
|
247
|
+
} else {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
});
|
|
245
251
|
case "resolveOne" :
|
|
246
252
|
let target$1 = Stdlib_Option.getOr(payload.target, rm);
|
|
247
253
|
let source$1 = Stdlib_Option.getOr(payload.source, null);
|
|
@@ -265,10 +271,17 @@ async function dispatch(binding, lookupBindingOpt, payload) {
|
|
|
265
271
|
} else {
|
|
266
272
|
filtered = items$2;
|
|
267
273
|
}
|
|
274
|
+
let allowed = filtered.filter(item => {
|
|
275
|
+
if (ownerAllows(item)) {
|
|
276
|
+
return retiredAllows(item);
|
|
277
|
+
} else {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
});
|
|
268
281
|
if (Stdlib_Option.getOr(payload.multi, false)) {
|
|
269
|
-
return
|
|
282
|
+
return allowed;
|
|
270
283
|
}
|
|
271
|
-
let item =
|
|
284
|
+
let item = allowed[0];
|
|
272
285
|
if (item !== undefined) {
|
|
273
286
|
return item;
|
|
274
287
|
} else {
|
|
@@ -522,6 +522,32 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
522
522
|
None
|
|
523
523
|
}
|
|
524
524
|
|
|
525
|
+
// The GraphQL type the cross-table fields hang off. `name` is the QueryDb's
|
|
526
|
+
// own (capitalised spec) name; the SDL calls the type by its plugin-prefixed
|
|
527
|
+
// name, and a resolver naming the other one is refused at deploy with
|
|
528
|
+
// "No field named X found on type Y".
|
|
529
|
+
let parentTypeName = switch registryEntry {
|
|
530
|
+
| Some({returnTypeName}) => returnTypeName
|
|
531
|
+
| None => name
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// A cross-table field hands back rows from the TARGET's table, so the owner
|
|
535
|
+
// and retirement rules that apply are the target's. Read from the same
|
|
536
|
+
// registry this view reads its own from — populated for every queryable in
|
|
537
|
+
// the plugin before any resolver is built.
|
|
538
|
+
let targetScope = (targetName: string) => {
|
|
539
|
+
let schema = ReventlessCore.Plugin_Helpers.stateSchemaRegistry->Dict.get(targetName)
|
|
540
|
+
let retired =
|
|
541
|
+
schema
|
|
542
|
+
->Option.flatMap(Reventless.StateAnnotations.getSpec)
|
|
543
|
+
->Option.flatMap(spec => spec.retired)
|
|
544
|
+
(
|
|
545
|
+
schema->Option.flatMap(s => Reventless.Owner.fieldNames(s)->Array.get(0)),
|
|
546
|
+
retired->Option.map(r => r.field),
|
|
547
|
+
retired->Option.flatMap(r => r.values),
|
|
548
|
+
)
|
|
549
|
+
}
|
|
550
|
+
|
|
525
551
|
let idResolvers = idResolverConfigs->Array.map(config => {
|
|
526
552
|
let {
|
|
527
553
|
source: {idField: sourceIdField, subId: sourceSubId, resolvedField},
|
|
@@ -533,71 +559,100 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
533
559
|
| _ => ("", "")
|
|
534
560
|
}
|
|
535
561
|
let storageResource = storageResource(~pluginName=target.pluginName, ~tableName)
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
562
|
+
let (targetOwnerField, targetRetiredField, targetRetiredValues) = targetScope(tableName)
|
|
563
|
+
let (field, multi) = switch resolvedField {
|
|
564
|
+
| Single(field) => (field, false)
|
|
565
|
+
| Multi(field) => (field, true)
|
|
566
|
+
}
|
|
567
|
+
let response = Resolver.Functions.resolvedFieldResponse(
|
|
568
|
+
~multi,
|
|
569
|
+
~ownerField=targetOwnerField,
|
|
570
|
+
~elevatedGroups,
|
|
571
|
+
~retiredField=?targetRetiredField,
|
|
572
|
+
~retiredValues=?targetRetiredValues,
|
|
573
|
+
)
|
|
574
|
+
let dataSourceName =
|
|
575
|
+
DataSource.makeDynamoDBDataSourceWithTableName(
|
|
576
|
+
~name=name ++ (field->String.capitalize ++ "Resolver"),
|
|
550
577
|
~api,
|
|
551
|
-
~
|
|
552
|
-
~
|
|
553
|
-
~field=field->Pulumi.Input.make,
|
|
554
|
-
~code=switch (targetId, sourceSubId, target.subIdField) {
|
|
555
|
-
| (Id, Field(sourceSortField), Some(targetSortField)) =>
|
|
556
|
-
Resolver.Functions.resolveIdSort(~sourceIdField, ~sourceSortField, ~targetSortField)
|
|
557
|
-
| (Id, Argument(sourceSortArgument), Some(targetSortField)) =>
|
|
558
|
-
Resolver.Functions.resolveIdSortArgument(
|
|
559
|
-
~sourceIdField,
|
|
560
|
-
~sourceSortArgument,
|
|
561
|
-
~targetSortField,
|
|
562
|
-
)
|
|
563
|
-
| (Id, _, _) => Resolver.Functions.resolveId(~sourceIdField)
|
|
564
|
-
|
|
565
|
-
| (_, Field(sourceSortField), Some(targetSortField)) =>
|
|
566
|
-
Resolver.Functions.resolveIdByIndexSort(
|
|
567
|
-
~index,
|
|
568
|
-
~sourceIdField,
|
|
569
|
-
~targetIdField,
|
|
570
|
-
~sourceSortField,
|
|
571
|
-
~targetSortField,
|
|
572
|
-
)
|
|
573
|
-
| (_, Argument(sourceSortArgument), Some(targetSortField)) =>
|
|
574
|
-
Resolver.Functions.resolveIdByIndexSortArgument(
|
|
575
|
-
~index,
|
|
576
|
-
~sourceIdField,
|
|
577
|
-
~targetIdField,
|
|
578
|
-
~sourceSortArgument,
|
|
579
|
-
~targetSortField,
|
|
580
|
-
)
|
|
581
|
-
| _ => Resolver.Functions.resolveIdByIndex(~index, ~sourceIdField, ~targetIdField)
|
|
582
|
-
},
|
|
578
|
+
~tableName=storageResource.name,
|
|
579
|
+
~serviceRole=apiRole,
|
|
583
580
|
~opts,
|
|
584
|
-
)
|
|
585
|
-
|
|
581
|
+
).name->Pulumi.Output.asInput
|
|
582
|
+
|
|
583
|
+
Resolver.makeUnitJsResolver(
|
|
584
|
+
~name=name ++ field->String.capitalize,
|
|
585
|
+
~api,
|
|
586
|
+
~dataSourceName,
|
|
587
|
+
~type_=parentTypeName->Pulumi.Input.make,
|
|
588
|
+
~field=field->Pulumi.Input.make,
|
|
589
|
+
~code=switch (targetId, sourceSubId, target.subIdField) {
|
|
590
|
+
| (Id, Field(sourceSortField), Some(targetSortField)) =>
|
|
591
|
+
Resolver.Functions.resolveIdSort(
|
|
592
|
+
~sourceIdField,
|
|
593
|
+
~sourceSortField,
|
|
594
|
+
~targetSortField,
|
|
595
|
+
~response,
|
|
596
|
+
)
|
|
597
|
+
| (Id, Argument(sourceSortArgument), Some(targetSortField)) =>
|
|
598
|
+
Resolver.Functions.resolveIdSortArgument(
|
|
599
|
+
~sourceIdField,
|
|
600
|
+
~sourceSortArgument,
|
|
601
|
+
~targetSortField,
|
|
602
|
+
~response,
|
|
603
|
+
)
|
|
604
|
+
| (Id, _, _) => Resolver.Functions.resolveId(~sourceIdField, ~response)
|
|
605
|
+
|
|
606
|
+
| (_, Field(sourceSortField), Some(targetSortField)) =>
|
|
607
|
+
Resolver.Functions.resolveIdByIndexSort(
|
|
608
|
+
~index,
|
|
609
|
+
~sourceIdField,
|
|
610
|
+
~targetIdField,
|
|
611
|
+
~sourceSortField,
|
|
612
|
+
~targetSortField,
|
|
613
|
+
~response,
|
|
614
|
+
)
|
|
615
|
+
| (_, Argument(sourceSortArgument), Some(targetSortField)) =>
|
|
616
|
+
Resolver.Functions.resolveIdByIndexSortArgument(
|
|
617
|
+
~index,
|
|
618
|
+
~sourceIdField,
|
|
619
|
+
~targetIdField,
|
|
620
|
+
~sourceSortArgument,
|
|
621
|
+
~targetSortField,
|
|
622
|
+
~response,
|
|
623
|
+
)
|
|
624
|
+
| _ =>
|
|
625
|
+
Resolver.Functions.resolveIdByIndex(~index, ~sourceIdField, ~targetIdField, ~response)
|
|
626
|
+
},
|
|
627
|
+
~opts,
|
|
628
|
+
)
|
|
586
629
|
})
|
|
587
630
|
|
|
631
|
+
// `@resolvesMany` — BatchGetItem against the target's table. Runs on THIS
|
|
632
|
+
// view's data source rather than one of the target's: the tables map names
|
|
633
|
+
// the target explicitly, and every QueryDb in the plugin has already granted
|
|
634
|
+
// `dynamodb:*` on itself to the API role the data source assumes.
|
|
588
635
|
let idsResolvers = idsResolverConfigs->Array.map(config => {
|
|
589
636
|
let {source: {idsField, resolvedField}, target: {tableName} as target} = config
|
|
590
637
|
let storageResource = storageResource(~pluginName=target.pluginName, ~tableName)
|
|
638
|
+
let (targetOwnerField, targetRetiredField, targetRetiredValues) = targetScope(tableName)
|
|
591
639
|
|
|
592
640
|
Resolver.makeUnitJsResolver(
|
|
593
|
-
~name=name ++
|
|
641
|
+
~name=name ++ resolvedField->String.capitalize,
|
|
594
642
|
~api,
|
|
595
643
|
~dataSourceName,
|
|
596
|
-
~type_=
|
|
644
|
+
~type_=parentTypeName->Pulumi.Input.make,
|
|
597
645
|
~field=resolvedField->Pulumi.Input.make,
|
|
598
646
|
~code=generateCode(
|
|
599
647
|
~storageResource,
|
|
600
|
-
~template=Resolver.Functions.resolveIds(
|
|
648
|
+
~template=Resolver.Functions.resolveIds(
|
|
649
|
+
~idsField,
|
|
650
|
+
~sortField=target.subIdField,
|
|
651
|
+
~ownerField=?targetOwnerField,
|
|
652
|
+
~retiredField=?targetRetiredField,
|
|
653
|
+
~retiredValues=?targetRetiredValues,
|
|
654
|
+
~elevatedGroups,
|
|
655
|
+
),
|
|
601
656
|
),
|
|
602
657
|
~opts,
|
|
603
658
|
)
|
|
@@ -171,10 +171,22 @@ function make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolve
|
|
|
171
171
|
} else {
|
|
172
172
|
resolverRefs = undefined;
|
|
173
173
|
}
|
|
174
|
+
let parentTypeName = registryEntry !== undefined ? registryEntry.returnTypeName : name$1;
|
|
175
|
+
let targetScope = targetName => {
|
|
176
|
+
let schema = Plugin_Helpers$ReventlessCore.stateSchemaRegistry[targetName];
|
|
177
|
+
let retired = Stdlib_Option.flatMap(Stdlib_Option.flatMap(schema, StateAnnotations$Reventless.getSpec), spec => spec.retired);
|
|
178
|
+
return [
|
|
179
|
+
Stdlib_Option.flatMap(schema, s => Owner$Reventless.fieldNames(s)[0]),
|
|
180
|
+
Stdlib_Option.map(retired, r => r.field),
|
|
181
|
+
Stdlib_Option.flatMap(retired, r => r.values)
|
|
182
|
+
];
|
|
183
|
+
};
|
|
174
184
|
let idResolvers = idResolverConfigs.map(config => {
|
|
175
185
|
let target = config.target;
|
|
176
186
|
let targetId = target.idField;
|
|
187
|
+
let tableName = target.tableName;
|
|
177
188
|
let match = config.source;
|
|
189
|
+
let resolvedField = match.resolvedField;
|
|
178
190
|
let sourceSubId = match.subId;
|
|
179
191
|
let sourceIdField = match.idField;
|
|
180
192
|
let match$1;
|
|
@@ -197,40 +209,62 @@ function make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolve
|
|
|
197
209
|
}
|
|
198
210
|
let targetIdField = match$1[1];
|
|
199
211
|
let index$1 = match$1[0];
|
|
200
|
-
let storageResource$1 = storageResource(target.pluginName,
|
|
201
|
-
let
|
|
212
|
+
let storageResource$1 = storageResource(target.pluginName, tableName);
|
|
213
|
+
let match$2 = targetScope(tableName);
|
|
214
|
+
let match$3;
|
|
215
|
+
match$3 = resolvedField.TAG === "Single" ? [
|
|
216
|
+
resolvedField._0,
|
|
217
|
+
false
|
|
218
|
+
] : [
|
|
219
|
+
resolvedField._0,
|
|
220
|
+
true
|
|
221
|
+
];
|
|
222
|
+
let field = match$3[0];
|
|
223
|
+
let response = AppSync_Resolver_Functions$PulumiAws.resolvedFieldResponse(match$3[1], match$2[0], elevatedGroups, match$2[1], match$2[2]);
|
|
202
224
|
let dataSourceName = AppSync_DataSource$PulumiAws.makeDynamoDBDataSourceWithTableName(name$1 + (Stdlib_String.capitalize(field) + "Resolver"), api, storageResource$1.name, apiRole, opts).name;
|
|
203
|
-
let match$
|
|
225
|
+
let match$4 = target.subIdField;
|
|
204
226
|
let tmp;
|
|
205
227
|
let exit = 0;
|
|
228
|
+
let exit$1 = 0;
|
|
206
229
|
if (typeof targetId !== "object") {
|
|
207
|
-
tmp = typeof sourceSubId !== "object" ? AppSync_Resolver_Functions$PulumiAws.resolveId(sourceIdField) : (
|
|
230
|
+
tmp = typeof sourceSubId !== "object" ? AppSync_Resolver_Functions$PulumiAws.resolveId(sourceIdField, response) : (
|
|
208
231
|
sourceSubId.TAG === "Field" ? (
|
|
209
|
-
match$
|
|
232
|
+
match$4 !== undefined ? AppSync_Resolver_Functions$PulumiAws.resolveIdSort(sourceIdField, sourceSubId._0, match$4, response) : AppSync_Resolver_Functions$PulumiAws.resolveId(sourceIdField, response)
|
|
210
233
|
) : (
|
|
211
|
-
match$
|
|
234
|
+
match$4 !== undefined ? AppSync_Resolver_Functions$PulumiAws.resolveIdSortArgument(sourceIdField, sourceSubId._0, match$4, response) : AppSync_Resolver_Functions$PulumiAws.resolveId(sourceIdField, response)
|
|
212
235
|
)
|
|
213
236
|
);
|
|
214
237
|
} else {
|
|
215
|
-
exit =
|
|
238
|
+
exit$1 = 2;
|
|
239
|
+
}
|
|
240
|
+
if (exit$1 === 2) {
|
|
241
|
+
if (typeof sourceSubId !== "object") {
|
|
242
|
+
exit = 1;
|
|
243
|
+
} else if (sourceSubId.TAG === "Field") {
|
|
244
|
+
if (match$4 !== undefined) {
|
|
245
|
+
tmp = AppSync_Resolver_Functions$PulumiAws.resolveIdByIndexSort(index$1, sourceIdField, sourceSubId._0, targetIdField, match$4, response);
|
|
246
|
+
} else {
|
|
247
|
+
exit = 1;
|
|
248
|
+
}
|
|
249
|
+
} else if (match$4 !== undefined) {
|
|
250
|
+
tmp = AppSync_Resolver_Functions$PulumiAws.resolveIdByIndexSortArgument(index$1, sourceIdField, sourceSubId._0, targetIdField, match$4, response);
|
|
251
|
+
} else {
|
|
252
|
+
exit = 1;
|
|
253
|
+
}
|
|
216
254
|
}
|
|
217
255
|
if (exit === 1) {
|
|
218
|
-
tmp =
|
|
219
|
-
sourceSubId.TAG === "Field" ? (
|
|
220
|
-
match$2 !== undefined ? AppSync_Resolver_Functions$PulumiAws.resolveIdByIndexSort(index$1, sourceIdField, sourceSubId._0, targetIdField, match$2) : AppSync_Resolver_Functions$PulumiAws.resolveIdByIndex(index$1, sourceIdField, targetIdField)
|
|
221
|
-
) : (
|
|
222
|
-
match$2 !== undefined ? AppSync_Resolver_Functions$PulumiAws.resolveIdByIndexSortArgument(index$1, sourceIdField, sourceSubId._0, targetIdField, match$2) : AppSync_Resolver_Functions$PulumiAws.resolveIdByIndex(index$1, sourceIdField, targetIdField)
|
|
223
|
-
)
|
|
224
|
-
);
|
|
256
|
+
tmp = AppSync_Resolver_Functions$PulumiAws.resolveIdByIndex(index$1, sourceIdField, targetIdField, response);
|
|
225
257
|
}
|
|
226
|
-
return AppSync_Resolver_Retrying$ReventlessAws.makeUnitJsResolver(name$1 + Stdlib_String.capitalize(field), api, dataSourceName,
|
|
258
|
+
return AppSync_Resolver_Retrying$ReventlessAws.makeUnitJsResolver(name$1 + Stdlib_String.capitalize(field), api, dataSourceName, parentTypeName, field, tmp, opts);
|
|
227
259
|
});
|
|
228
260
|
let idsResolvers = idsResolverConfigs.map(config => {
|
|
229
261
|
let target = config.target;
|
|
262
|
+
let tableName = target.tableName;
|
|
230
263
|
let match = config.source;
|
|
231
|
-
let
|
|
232
|
-
let storageResource$1 = storageResource(target.pluginName,
|
|
233
|
-
|
|
264
|
+
let resolvedField = match.resolvedField;
|
|
265
|
+
let storageResource$1 = storageResource(target.pluginName, tableName);
|
|
266
|
+
let match$1 = targetScope(tableName);
|
|
267
|
+
return AppSync_Resolver_Retrying$ReventlessAws.makeUnitJsResolver(name$1 + Stdlib_String.capitalize(resolvedField), api, dataSourceName, parentTypeName, resolvedField, generateCode(storageResource$1, AppSync_Resolver_Functions$PulumiAws.resolveIds(match.idsField, target.subIdField, match$1[0], match$1[1], match$1[2], elevatedGroups)), opts);
|
|
234
268
|
});
|
|
235
269
|
let mainResolvers = resolverByIdMultiple !== undefined ? (
|
|
236
270
|
resolverByIds !== undefined ? [
|
|
@@ -231,6 +231,14 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
231
231
|
}
|
|
232
232
|
})
|
|
233
233
|
|
|
234
|
+
// The GraphQL type the cross-table fields hang off — the plugin-prefixed SDL
|
|
235
|
+
// name, not the QueryDb's own. A resolver naming the other one is refused at
|
|
236
|
+
// deploy with "No field named X found on type Y".
|
|
237
|
+
let parentTypeName = switch registryEntry {
|
|
238
|
+
| Some({returnTypeName}) => returnTypeName
|
|
239
|
+
| None => name
|
|
240
|
+
}
|
|
241
|
+
|
|
234
242
|
// @resolves — single cross-table field resolver on this (parent) type. The
|
|
235
243
|
// target's binding key is its capitalized spec name (mirrors how the binding
|
|
236
244
|
// registry is keyed); the parent object flows via ctx.source.
|
|
@@ -259,7 +267,7 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
259
267
|
~name=name ++ field->String.capitalize,
|
|
260
268
|
~api,
|
|
261
269
|
~dataSourceName,
|
|
262
|
-
~type_=
|
|
270
|
+
~type_=parentTypeName->Pulumi.Input.make,
|
|
263
271
|
~field=field->Pulumi.Input.make,
|
|
264
272
|
~code=invokeFieldTemplate(~sourceType=name, ~kind="resolveOne", ~target=targetKey, ~extra),
|
|
265
273
|
~opts,
|
|
@@ -275,7 +283,7 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
275
283
|
~name=name ++ resolvedField->String.capitalize,
|
|
276
284
|
~api,
|
|
277
285
|
~dataSourceName,
|
|
278
|
-
~type_=
|
|
286
|
+
~type_=parentTypeName->Pulumi.Input.make,
|
|
279
287
|
~field=resolvedField->Pulumi.Input.make,
|
|
280
288
|
~code=invokeFieldTemplate(~sourceType=name, ~kind="resolveMany", ~target=targetKey, ~extra),
|
|
281
289
|
~opts,
|
|
@@ -107,6 +107,7 @@ function make(name, api, param, dataSourceName, indexes, subIdField, idResolverC
|
|
|
107
107
|
return mkResolver(resolverName, field, "index", index, undefined, undefined);
|
|
108
108
|
}
|
|
109
109
|
});
|
|
110
|
+
let parentTypeName = registryEntry !== undefined ? registryEntry.returnTypeName : name$1;
|
|
110
111
|
let idResolvers = idResolverConfigs.map(config => {
|
|
111
112
|
let target = config.target;
|
|
112
113
|
let match = config.source;
|
|
@@ -139,14 +140,14 @@ function make(name, api, param, dataSourceName, indexes, subIdField, idResolverC
|
|
|
139
140
|
let extra = `\n sourceIdField: '` + match.idField + `',\n multi: ` + (
|
|
140
141
|
match$1[1] ? "true" : "false"
|
|
141
142
|
) + `,` + targetFrag + subIdFrag;
|
|
142
|
-
return AppSync_Resolver_Retrying$ReventlessAws.makeUnitJsResolver(name$1 + Stdlib_String.capitalize(field), api, dataSourceName,
|
|
143
|
+
return AppSync_Resolver_Retrying$ReventlessAws.makeUnitJsResolver(name$1 + Stdlib_String.capitalize(field), api, dataSourceName, parentTypeName, field, invokeFieldTemplate(name$1, "resolveOne", targetKey, extra), opts);
|
|
143
144
|
});
|
|
144
145
|
let idsResolvers = idsResolverConfigs.map(config => {
|
|
145
146
|
let match = config.source;
|
|
146
147
|
let resolvedField = match.resolvedField;
|
|
147
148
|
let targetKey = Stdlib_String.capitalize(config.target.tableName);
|
|
148
149
|
let extra = `\n sourceIdsField: '` + match.idsField + `',`;
|
|
149
|
-
return AppSync_Resolver_Retrying$ReventlessAws.makeUnitJsResolver(name$1 + Stdlib_String.capitalize(resolvedField), api, dataSourceName,
|
|
150
|
+
return AppSync_Resolver_Retrying$ReventlessAws.makeUnitJsResolver(name$1 + Stdlib_String.capitalize(resolvedField), api, dataSourceName, parentTypeName, resolvedField, invokeFieldTemplate(name$1, "resolveMany", targetKey, extra), opts);
|
|
150
151
|
});
|
|
151
152
|
return [
|
|
152
153
|
byId,
|
|
@@ -148,9 +148,12 @@ let makeSyncTodoItems = (
|
|
|
148
148
|
// by definition no fresher.
|
|
149
149
|
//
|
|
150
150
|
// Only `Pending` and `Failed` are read. `Completed` rows are the bulk of a
|
|
151
|
-
// mature table and are never actionable
|
|
152
|
-
//
|
|
153
|
-
//
|
|
151
|
+
// mature table and are never actionable, and neither are `Abandoned` ones — a
|
|
152
|
+
// row whose retry budget is spent is terminal, so it is left in the table rather
|
|
153
|
+
// than carried into memory. That the status says so is what lets this filter
|
|
154
|
+
// decide it: `maxRetries` is Spec-level and still not known here, and before the
|
|
155
|
+
// status existed an exhausted row had to be loaded and then filtered out by
|
|
156
|
+
// `phase2`.
|
|
154
157
|
let makeLoadTodoItems = (
|
|
155
158
|
~queryDbTableName: string,
|
|
156
159
|
~todoItems: dict<'row>,
|
|
@@ -308,3 +308,81 @@ describe("the unowned stub is callable where the door calls it", () => {
|
|
|
308
308
|
))->Expect.toEqual((true, true))
|
|
309
309
|
})
|
|
310
310
|
})
|
|
311
|
+
|
|
312
|
+
// ── The cross-table doors (`@resolves` / `@resolvesMany`) ────────────────────
|
|
313
|
+
//
|
|
314
|
+
// A field that follows a foreign key hands back the TARGET's rows, so the
|
|
315
|
+
// target's rules narrow them. The field takes no `includeRetired` argument, so
|
|
316
|
+
// `_wantsRetired` is never true through one — `{list}Refs` is the door that
|
|
317
|
+
// names a row the archive took.
|
|
318
|
+
|
|
319
|
+
describe("the cross-table batch door", () => {
|
|
320
|
+
let code = F.resolveIds(
|
|
321
|
+
~idsField="productIds",
|
|
322
|
+
~sortField=None,
|
|
323
|
+
~retiredField="shelfStatus",
|
|
324
|
+
~retiredValues=["Archived"],
|
|
325
|
+
~elevatedGroups=elevated,
|
|
326
|
+
)("ProductsTable")
|
|
327
|
+
|
|
328
|
+
// BatchGetItem answers `{data: {<table>: [...]}}`; returning `ctx.result`
|
|
329
|
+
// handed the whole envelope to a field typed as a list, which fails at
|
|
330
|
+
// execution for every caller that selects it.
|
|
331
|
+
testSync("returns the rows out of the batch envelope", () =>
|
|
332
|
+
expect(code->String.includes("ctx.result?.data?.['ProductsTable']"))->Expect.toBe(true)
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
testSync("drops retired rows from the returned array", () =>
|
|
336
|
+
expect(code->String.includes("item !== null && _live(item)"))->Expect.toBe(true)
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
// An empty id array used to fall through to a GetItem on the PARENT's own id
|
|
340
|
+
// against the target's table — a read that answers a shape the field cannot be.
|
|
341
|
+
testSync("short-circuits an empty id array instead of reading", () =>
|
|
342
|
+
expect((
|
|
343
|
+
code->String.includes("if (idList.length === 0) return runtime.earlyReturn([]);"),
|
|
344
|
+
code->String.includes("operation: 'GetItem'"),
|
|
345
|
+
))->toEqual((true, false))
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
testSync("is unchanged for a target that declares neither rule", () => {
|
|
349
|
+
let plain = F.resolveIds(~idsField="productIds", ~sortField=None)("ProductsTable")
|
|
350
|
+
expect((plain->String.includes("_owns"), plain->String.includes("_live")))->toEqual((
|
|
351
|
+
false,
|
|
352
|
+
false,
|
|
353
|
+
))
|
|
354
|
+
})
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
describe("the cross-table single door", () => {
|
|
358
|
+
let response = (~multi) =>
|
|
359
|
+
F.resolvedFieldResponse(
|
|
360
|
+
~multi,
|
|
361
|
+
~ownerField=Some("customerId"),
|
|
362
|
+
~elevatedGroups=elevated,
|
|
363
|
+
~retiredField="shelfStatus",
|
|
364
|
+
~retiredValues=["Archived"],
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
testSync("narrows the one row it hands back", () =>
|
|
368
|
+
expect(response(~multi=false)->String.includes("_owns(_row) && _live(_row) ? _row : null"))
|
|
369
|
+
->Expect.toBe(true)
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
testSync("narrows every row of the list form", () =>
|
|
373
|
+
expect(
|
|
374
|
+
response(~multi=true)->String.includes("filter(_row => _owns(_row) && _live(_row))"),
|
|
375
|
+
)->Expect.toBe(true)
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
// The list form used to answer with the first row alone, whatever the SDL said.
|
|
379
|
+
testSync("the list form returns the list", () =>
|
|
380
|
+
expect(response(~multi=true)->String.includes("ctx.result.items[0]"))->Expect.toBe(false)
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
testSync("a target with neither rule keeps the plain response", () => {
|
|
384
|
+
let plain = F.resolvedFieldResponse(~multi=false, ~ownerField=None, ~elevatedGroups=elevated)
|
|
385
|
+
expect((plain->String.includes("_owns"), plain->String.includes("ctx.result.items[0]")))
|
|
386
|
+
->toEqual((false, true))
|
|
387
|
+
})
|
|
388
|
+
})
|
|
@@ -206,6 +206,58 @@ globalThis.describe("the unowned stub is callable where the door calls it", () =
|
|
|
206
206
|
});
|
|
207
207
|
});
|
|
208
208
|
|
|
209
|
+
globalThis.describe("the cross-table batch door", () => {
|
|
210
|
+
let code = AppSync_Resolver_Functions$PulumiAws.resolveIds("productIds", undefined, undefined, "shelfStatus", ["Archived"], elevated)("ProductsTable");
|
|
211
|
+
globalThis.test("returns the rows out of the batch envelope", () => {
|
|
212
|
+
globalThis.expect(code.includes("ctx.result?.data?.['ProductsTable']")).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
globalThis.test("drops retired rows from the returned array", () => {
|
|
215
|
+
globalThis.expect(code.includes("item !== null && _live(item)")).toBe(true);
|
|
216
|
+
});
|
|
217
|
+
globalThis.test("short-circuits an empty id array instead of reading", () => {
|
|
218
|
+
globalThis.expect([
|
|
219
|
+
code.includes("if (idList.length === 0) return runtime.earlyReturn([]);"),
|
|
220
|
+
code.includes("operation: 'GetItem'")
|
|
221
|
+
]).toEqual([
|
|
222
|
+
true,
|
|
223
|
+
false
|
|
224
|
+
]);
|
|
225
|
+
});
|
|
226
|
+
globalThis.test("is unchanged for a target that declares neither rule", () => {
|
|
227
|
+
let plain = AppSync_Resolver_Functions$PulumiAws.resolveIds("productIds", undefined, undefined, undefined, undefined, undefined)("ProductsTable");
|
|
228
|
+
globalThis.expect([
|
|
229
|
+
plain.includes("_owns"),
|
|
230
|
+
plain.includes("_live")
|
|
231
|
+
]).toEqual([
|
|
232
|
+
false,
|
|
233
|
+
false
|
|
234
|
+
]);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
globalThis.describe("the cross-table single door", () => {
|
|
239
|
+
let response = multi => AppSync_Resolver_Functions$PulumiAws.resolvedFieldResponse(multi, "customerId", elevated, "shelfStatus", ["Archived"]);
|
|
240
|
+
globalThis.test("narrows the one row it hands back", () => {
|
|
241
|
+
globalThis.expect(response(false).includes("_owns(_row) && _live(_row) ? _row : null")).toBe(true);
|
|
242
|
+
});
|
|
243
|
+
globalThis.test("narrows every row of the list form", () => {
|
|
244
|
+
globalThis.expect(response(true).includes("filter(_row => _owns(_row) && _live(_row))")).toBe(true);
|
|
245
|
+
});
|
|
246
|
+
globalThis.test("the list form returns the list", () => {
|
|
247
|
+
globalThis.expect(response(true).includes("ctx.result.items[0]")).toBe(false);
|
|
248
|
+
});
|
|
249
|
+
globalThis.test("a target with neither rule keeps the plain response", () => {
|
|
250
|
+
let plain = AppSync_Resolver_Functions$PulumiAws.resolvedFieldResponse(false, undefined, elevated, undefined, undefined);
|
|
251
|
+
globalThis.expect([
|
|
252
|
+
plain.includes("_owns"),
|
|
253
|
+
plain.includes("ctx.result.items[0]")
|
|
254
|
+
]).toEqual([
|
|
255
|
+
false,
|
|
256
|
+
true
|
|
257
|
+
]);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
209
261
|
let F;
|
|
210
262
|
|
|
211
263
|
export {
|
|
@@ -612,3 +612,169 @@ describe("owner scoping", () => {
|
|
|
612
612
|
expect(r->ids)->toEqual(["p-1", "p-3"])
|
|
613
613
|
})
|
|
614
614
|
})
|
|
615
|
+
|
|
616
|
+
// ── nested union member types ───────────────────────────────────────────────
|
|
617
|
+
//
|
|
618
|
+
// The `__typename` a union member is resolved from is written into the stored
|
|
619
|
+
// row, above the DynamoDB/Postgres branch (`withUnionMemberTypes` wraps the
|
|
620
|
+
// result of `makeQueryDbOps`), and this resolver stamps only the *top level* of
|
|
621
|
+
// a `node` answer. So the Postgres door's whole claim is that it hands a nested
|
|
622
|
+
// union back exactly as stored — which no test made, and which no deploy can
|
|
623
|
+
// make either: nothing runs the Postgres backend, so this door cannot be called
|
|
624
|
+
// against a deployment the way the AppSync ones were.
|
|
625
|
+
//
|
|
626
|
+
// Losing the nested key is not visible as an error: the member resolves to null
|
|
627
|
+
// and the null takes its non-nullable parent, so rows leave the answer.
|
|
628
|
+
|
|
629
|
+
let geoRow = (id, status) => {
|
|
630
|
+
let geo = Dict.make()
|
|
631
|
+
geo->Dict.set("__typename", JSON.Encode.string("GeolocationLocated"))
|
|
632
|
+
geo->Dict.set("lat", JSON.Encode.float(48.2082))
|
|
633
|
+
geo->Dict.set("lng", JSON.Encode.float(16.3738))
|
|
634
|
+
let o = Dict.make()
|
|
635
|
+
o->Dict.set("id", JSON.Encode.string(id))
|
|
636
|
+
o->Dict.set("status", JSON.Encode.string(status))
|
|
637
|
+
o->Dict.set("name", JSON.Encode.string("Geo " ++ id))
|
|
638
|
+
o->Dict.set("owner", JSON.Encode.string("u-a"))
|
|
639
|
+
o->Dict.set("geolocation", JSON.Encode.object(geo))
|
|
640
|
+
JSON.Encode.object(o)
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
let geoStore = Dict.fromArray([("g-1", geoRow("g-1", "active")), ("g-2", geoRow("g-2", "active"))])
|
|
644
|
+
let geoItems = () => geoStore->Dict.valuesToArray
|
|
645
|
+
|
|
646
|
+
let geoConnection = (): JSON.t =>
|
|
647
|
+
JSON.Encode.object(
|
|
648
|
+
Dict.fromArray([
|
|
649
|
+
(
|
|
650
|
+
"edges",
|
|
651
|
+
JSON.Encode.array(
|
|
652
|
+
geoItems()->Array.map(item =>
|
|
653
|
+
JSON.Encode.object(
|
|
654
|
+
Dict.fromArray([("cursor", JSON.Encode.string("c")), ("node", item)]),
|
|
655
|
+
)
|
|
656
|
+
),
|
|
657
|
+
),
|
|
658
|
+
),
|
|
659
|
+
]),
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
let geoBinding = (): PgQueryResolver_Lambda.binding => {
|
|
663
|
+
ops: {
|
|
664
|
+
...ops,
|
|
665
|
+
load: async id => Ok(geoStore->Dict.get(id)->Option.mapOr([], i => [i])),
|
|
666
|
+
},
|
|
667
|
+
pushdowns: {
|
|
668
|
+
indexLookup: async (~readModelName as _, _field, _value) => geoItems(),
|
|
669
|
+
byIds: async (~readModelName as _, ids) => ids->Array.filterMap(id => geoStore->Dict.get(id)),
|
|
670
|
+
listPage: async (
|
|
671
|
+
~readModelName as _,
|
|
672
|
+
~argsDict as _,
|
|
673
|
+
~capability as _,
|
|
674
|
+
~labelField as _,
|
|
675
|
+
~ownerScope as _=?,
|
|
676
|
+
~retiredScope as _=?,
|
|
677
|
+
) => Some(geoConnection()),
|
|
678
|
+
itemsPage: async (
|
|
679
|
+
~readModelName as _,
|
|
680
|
+
~subIdField as _,
|
|
681
|
+
~id as _,
|
|
682
|
+
~argsDict as _,
|
|
683
|
+
~ownerScope as _=?,
|
|
684
|
+
~retiredScope as _=?,
|
|
685
|
+
) => geoConnection(),
|
|
686
|
+
scanAll: async (~readModelName as _) => geoItems(),
|
|
687
|
+
},
|
|
688
|
+
indexes: [
|
|
689
|
+
{index: "byStatus", type_: "S", idField: "status", projectionType: Reventless.ReadModel.ALL},
|
|
690
|
+
],
|
|
691
|
+
subIdField: Some("lineNo"),
|
|
692
|
+
capability,
|
|
693
|
+
labelField: "name",
|
|
694
|
+
includeIdParam: true,
|
|
695
|
+
authorization: Reventless.Authorization.AllowAnonymous,
|
|
696
|
+
ownerField: None,
|
|
697
|
+
retiredField: None,
|
|
698
|
+
retiredValues: None,
|
|
699
|
+
namedWhenRetired: false,
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// The member type of the first row a door answers with, whatever shape it took.
|
|
703
|
+
let memberOf = (r: JSON.t): option<string> => {
|
|
704
|
+
let fromRow = row =>
|
|
705
|
+
row->field("geolocation")->Option.flatMap(g => g->str("__typename"))
|
|
706
|
+
switch r->JSON.Decode.array {
|
|
707
|
+
| Some(rows) => rows->Array.get(0)->Option.flatMap(fromRow)
|
|
708
|
+
| None =>
|
|
709
|
+
switch r->field("edges")->Option.flatMap(JSON.Decode.array) {
|
|
710
|
+
| Some(edges) => edges->Array.get(0)->Option.flatMap(e => e->field("node"))->Option.flatMap(fromRow)
|
|
711
|
+
| None => fromRow(r)
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
describe("PgQueryResolver — a nested union survives every door", () => {
|
|
717
|
+
let doors = [
|
|
718
|
+
("getById", mkPayload(~kind="getById", ~args=objArgs([("id", JSON.Encode.string("g-1"))]), ())),
|
|
719
|
+
(
|
|
720
|
+
"byIds",
|
|
721
|
+
mkPayload(~kind="byIds", ~args=objArgs([("ids", JSON.Encode.array([JSON.Encode.string("g-1")]))]), ()),
|
|
722
|
+
),
|
|
723
|
+
(
|
|
724
|
+
"index",
|
|
725
|
+
mkPayload(~kind="index", ~index="byStatus", ~args=objArgs([("byStatus", JSON.Encode.string("active"))]), ()),
|
|
726
|
+
),
|
|
727
|
+
("list", mkPayload(~kind="list", ())),
|
|
728
|
+
("items", mkPayload(~kind="items", ~args=objArgs([("id", JSON.Encode.string("g-1"))]), ())),
|
|
729
|
+
// The cross-table doors answer with the *target's* rows, so a union on the
|
|
730
|
+
// target travels through a field on some other view's type.
|
|
731
|
+
(
|
|
732
|
+
"resolveOne",
|
|
733
|
+
{
|
|
734
|
+
readModelName: "Geos",
|
|
735
|
+
kind: "resolveOne",
|
|
736
|
+
target: "Geos",
|
|
737
|
+
source: objArgs([("ref", JSON.Encode.string("g-1"))]),
|
|
738
|
+
sourceIdField: "ref",
|
|
739
|
+
multi: false,
|
|
740
|
+
arguments: objArgs([]),
|
|
741
|
+
identity: Reventless.Identity.anonymous,
|
|
742
|
+
},
|
|
743
|
+
),
|
|
744
|
+
(
|
|
745
|
+
"resolveMany",
|
|
746
|
+
{
|
|
747
|
+
readModelName: "Geos",
|
|
748
|
+
kind: "resolveMany",
|
|
749
|
+
target: "Geos",
|
|
750
|
+
source: objArgs([("refs", JSON.Encode.array([JSON.Encode.string("g-1")]))]),
|
|
751
|
+
sourceIdsField: "refs",
|
|
752
|
+
arguments: objArgs([]),
|
|
753
|
+
identity: Reventless.Identity.anonymous,
|
|
754
|
+
},
|
|
755
|
+
),
|
|
756
|
+
]
|
|
757
|
+
|
|
758
|
+
doors->Array.forEach(((label, payload)) =>
|
|
759
|
+
testPromise(label ++ " hands the member type back as stored", async () => {
|
|
760
|
+
let r = await PgQueryResolver_Lambda.dispatch(~binding=geoBinding(), ~payload)
|
|
761
|
+
expect(memberOf(r))->toEqual(Some("GeolocationLocated"))
|
|
762
|
+
})
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
// `node` is the one door that writes a `__typename` of its own, at the top
|
|
766
|
+
// level. It must not reach down and overwrite the member's.
|
|
767
|
+
testPromise("node stamps the top level without disturbing the member", async () => {
|
|
768
|
+
PgQueryResolver_Lambda.register(~readModelName="Geos", geoBinding())
|
|
769
|
+
PgQueryResolver_Lambda.registerNodeType(~typeName="Geo", ~readModelName="Geos")
|
|
770
|
+
let payload: PgQueryResolver_Lambda.payload = {
|
|
771
|
+
readModelName: "",
|
|
772
|
+
kind: "node",
|
|
773
|
+
arguments: objArgs([("id", JSON.Encode.string(btoa("Geo:g-1")))]),
|
|
774
|
+
identity: Reventless.Identity.anonymous,
|
|
775
|
+
}
|
|
776
|
+
let r = await PgQueryResolver_Lambda.handler(payload, Obj.magic(Nullable.null))
|
|
777
|
+
expect(r->str("__typename"))->toEqual(Some("Geo"))
|
|
778
|
+
expect(memberOf(r))->toEqual(Some("GeolocationLocated"))
|
|
779
|
+
})
|
|
780
|
+
})
|
|
@@ -629,6 +629,196 @@ globalThis.describe("owner scoping", () => {
|
|
|
629
629
|
});
|
|
630
630
|
});
|
|
631
631
|
|
|
632
|
+
function geoRow(id, status) {
|
|
633
|
+
let geo = {};
|
|
634
|
+
geo["__typename"] = "GeolocationLocated";
|
|
635
|
+
geo["lat"] = 48.2082;
|
|
636
|
+
geo["lng"] = 16.3738;
|
|
637
|
+
let o = {};
|
|
638
|
+
o["id"] = id;
|
|
639
|
+
o["status"] = status;
|
|
640
|
+
o["name"] = "Geo " + id;
|
|
641
|
+
o["owner"] = "u-a";
|
|
642
|
+
o["geolocation"] = geo;
|
|
643
|
+
return o;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
let geoStore = Object.fromEntries([
|
|
647
|
+
[
|
|
648
|
+
"g-1",
|
|
649
|
+
geoRow("g-1", "active")
|
|
650
|
+
],
|
|
651
|
+
[
|
|
652
|
+
"g-2",
|
|
653
|
+
geoRow("g-2", "active")
|
|
654
|
+
]
|
|
655
|
+
]);
|
|
656
|
+
|
|
657
|
+
function geoItems() {
|
|
658
|
+
return Object.values(geoStore);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function geoConnection() {
|
|
662
|
+
return Object.fromEntries([[
|
|
663
|
+
"edges",
|
|
664
|
+
Object.values(geoStore).map(item => Object.fromEntries([
|
|
665
|
+
[
|
|
666
|
+
"cursor",
|
|
667
|
+
"c"
|
|
668
|
+
],
|
|
669
|
+
[
|
|
670
|
+
"node",
|
|
671
|
+
item
|
|
672
|
+
]
|
|
673
|
+
]))
|
|
674
|
+
]]);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function geoBinding() {
|
|
678
|
+
return {
|
|
679
|
+
ops: {
|
|
680
|
+
load: async id => ({
|
|
681
|
+
TAG: "Ok",
|
|
682
|
+
_0: Stdlib_Option.mapOr(geoStore[id], [], i => [i])
|
|
683
|
+
}),
|
|
684
|
+
loadStream: 0,
|
|
685
|
+
save: 0,
|
|
686
|
+
saveBatch: 0,
|
|
687
|
+
count: 0,
|
|
688
|
+
delete: 0,
|
|
689
|
+
deleteBatch: 0
|
|
690
|
+
},
|
|
691
|
+
pushdowns: {
|
|
692
|
+
indexLookup: async (param, _field, _value) => Object.values(geoStore),
|
|
693
|
+
byIds: async (param, ids) => Stdlib_Array.filterMap(ids, id => geoStore[id]),
|
|
694
|
+
listPage: async (param, param$1, param$2, param$3, param$4, param$5) => geoConnection(),
|
|
695
|
+
itemsPage: async (param, param$1, param$2, param$3, param$4, param$5) => geoConnection(),
|
|
696
|
+
scanAll: async param => Object.values(geoStore)
|
|
697
|
+
},
|
|
698
|
+
indexes: [{
|
|
699
|
+
index: "byStatus",
|
|
700
|
+
type_: "S",
|
|
701
|
+
idField: "status",
|
|
702
|
+
projectionType: "ALL"
|
|
703
|
+
}],
|
|
704
|
+
subIdField: "lineNo",
|
|
705
|
+
capability: capability,
|
|
706
|
+
labelField: "name",
|
|
707
|
+
includeIdParam: true,
|
|
708
|
+
authorization: "AllowAnonymous",
|
|
709
|
+
ownerField: undefined,
|
|
710
|
+
retiredField: undefined,
|
|
711
|
+
retiredValues: undefined,
|
|
712
|
+
namedWhenRetired: false
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function memberOf(r) {
|
|
717
|
+
let fromRow = row => Stdlib_Option.flatMap(field(row, "geolocation"), g => str(g, "__typename"));
|
|
718
|
+
let rows = Stdlib_JSON.Decode.array(r);
|
|
719
|
+
if (rows !== undefined) {
|
|
720
|
+
return Stdlib_Option.flatMap(rows[0], fromRow);
|
|
721
|
+
}
|
|
722
|
+
let edges = Stdlib_Option.flatMap(field(r, "edges"), Stdlib_JSON.Decode.array);
|
|
723
|
+
if (edges !== undefined) {
|
|
724
|
+
return Stdlib_Option.flatMap(Stdlib_Option.flatMap(edges[0], e => field(e, "node")), fromRow);
|
|
725
|
+
} else {
|
|
726
|
+
return fromRow(r);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
globalThis.describe("PgQueryResolver — a nested union survives every door", () => {
|
|
731
|
+
let doors = [
|
|
732
|
+
[
|
|
733
|
+
"getById",
|
|
734
|
+
mkPayload("getById", undefined, Object.fromEntries([[
|
|
735
|
+
"id",
|
|
736
|
+
"g-1"
|
|
737
|
+
]]), undefined, undefined)
|
|
738
|
+
],
|
|
739
|
+
[
|
|
740
|
+
"byIds",
|
|
741
|
+
mkPayload("byIds", undefined, Object.fromEntries([[
|
|
742
|
+
"ids",
|
|
743
|
+
["g-1"]
|
|
744
|
+
]]), undefined, undefined)
|
|
745
|
+
],
|
|
746
|
+
[
|
|
747
|
+
"index",
|
|
748
|
+
mkPayload("index", "byStatus", Object.fromEntries([[
|
|
749
|
+
"byStatus",
|
|
750
|
+
"active"
|
|
751
|
+
]]), undefined, undefined)
|
|
752
|
+
],
|
|
753
|
+
[
|
|
754
|
+
"list",
|
|
755
|
+
mkPayload("list", undefined, undefined, undefined, undefined)
|
|
756
|
+
],
|
|
757
|
+
[
|
|
758
|
+
"items",
|
|
759
|
+
mkPayload("items", undefined, Object.fromEntries([[
|
|
760
|
+
"id",
|
|
761
|
+
"g-1"
|
|
762
|
+
]]), undefined, undefined)
|
|
763
|
+
],
|
|
764
|
+
[
|
|
765
|
+
"resolveOne",
|
|
766
|
+
{
|
|
767
|
+
readModelName: "Geos",
|
|
768
|
+
kind: "resolveOne",
|
|
769
|
+
target: "Geos",
|
|
770
|
+
source: Object.fromEntries([[
|
|
771
|
+
"ref",
|
|
772
|
+
"g-1"
|
|
773
|
+
]]),
|
|
774
|
+
sourceIdField: "ref",
|
|
775
|
+
multi: false,
|
|
776
|
+
arguments: Object.fromEntries([]),
|
|
777
|
+
identity: Identity$Reventless.anonymous
|
|
778
|
+
}
|
|
779
|
+
],
|
|
780
|
+
[
|
|
781
|
+
"resolveMany",
|
|
782
|
+
{
|
|
783
|
+
readModelName: "Geos",
|
|
784
|
+
kind: "resolveMany",
|
|
785
|
+
target: "Geos",
|
|
786
|
+
source: Object.fromEntries([[
|
|
787
|
+
"refs",
|
|
788
|
+
["g-1"]
|
|
789
|
+
]]),
|
|
790
|
+
sourceIdsField: "refs",
|
|
791
|
+
arguments: Object.fromEntries([]),
|
|
792
|
+
identity: Identity$Reventless.anonymous
|
|
793
|
+
}
|
|
794
|
+
]
|
|
795
|
+
];
|
|
796
|
+
doors.forEach(param => {
|
|
797
|
+
let payload = param[1];
|
|
798
|
+
globalThis.test(param[0] + " hands the member type back as stored", async () => {
|
|
799
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(geoBinding(), undefined, payload);
|
|
800
|
+
globalThis.expect(memberOf(r)).toEqual("GeolocationLocated");
|
|
801
|
+
});
|
|
802
|
+
});
|
|
803
|
+
globalThis.test("node stamps the top level without disturbing the member", async () => {
|
|
804
|
+
PgQueryResolver_Lambda$ReventlessAws.register("Geos", geoBinding());
|
|
805
|
+
PgQueryResolver_Lambda$ReventlessAws.registerNodeType("Geo", "Geos");
|
|
806
|
+
let payload_arguments = Object.fromEntries([[
|
|
807
|
+
"id",
|
|
808
|
+
btoa("Geo:g-1")
|
|
809
|
+
]]);
|
|
810
|
+
let payload = {
|
|
811
|
+
readModelName: "",
|
|
812
|
+
kind: "node",
|
|
813
|
+
arguments: payload_arguments,
|
|
814
|
+
identity: Identity$Reventless.anonymous
|
|
815
|
+
};
|
|
816
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.handler(payload, null);
|
|
817
|
+
globalThis.expect(str(r, "__typename")).toEqual("Geo");
|
|
818
|
+
globalThis.expect(memberOf(r)).toEqual("GeolocationLocated");
|
|
819
|
+
});
|
|
820
|
+
});
|
|
821
|
+
|
|
632
822
|
export {
|
|
633
823
|
mk,
|
|
634
824
|
store,
|
|
@@ -647,5 +837,11 @@ export {
|
|
|
647
837
|
field,
|
|
648
838
|
str,
|
|
649
839
|
ids,
|
|
840
|
+
geoRow,
|
|
841
|
+
geoStore,
|
|
842
|
+
geoItems,
|
|
843
|
+
geoConnection,
|
|
844
|
+
geoBinding,
|
|
845
|
+
memberOf,
|
|
650
846
|
}
|
|
651
847
|
/* store Not a pure module */
|