@reventlessdev/reventless-local 3.0.0-alpha.233 → 3.0.0-alpha.235
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/DomainGraphQL_Server.res +36 -0
- package/src/adapter/DomainGraphQL_Server.res.mjs +40 -1
- package/src/adapter/GraphQL_Server.res.mjs +6 -0
- package/src/adapter/PlatformGraphQL_Server.res +5 -0
- package/src/adapter/PlatformGraphQL_Server.res.mjs +4 -0
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +171 -2
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs +118 -1
- package/tests/adapter/QueryDbListResolverTest.res +150 -0
- package/tests/adapter/QueryDbListResolverTest.res.mjs +133 -0
- package/tests/adapter/QueryDbResolvedFieldsTest.res +253 -0
- package/tests/adapter/QueryDbResolvedFieldsTest.res.mjs +291 -0
- package/tests/components/automationslice/AutomationSliceCallbackTest.res +64 -0
- package/tests/components/automationslice/AutomationSliceCallbackTest.res.mjs +55 -0
- package/tests/components/automationslice/AutomationSliceFixtures.res +2 -0
- package/tests/components/automationslice/AutomationSliceFixtures.res.mjs +10 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +1 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +5 -0
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +1 -0
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res.mjs +5 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +135 -1
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res.mjs +202 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res +8 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res.mjs +15 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res +2 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res.mjs +5 -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.235 (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.234 (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.233 (2026-08-20)
|
|
7
30
|
|
|
8
31
|
### Features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-local",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.235",
|
|
4
4
|
"description": "Local platform for Reventless (in-memory or SQLite backend, for development and testing without AWS)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"bin": {
|
|
@@ -32,25 +32,25 @@
|
|
|
32
32
|
"graphql": "^16.13.2",
|
|
33
33
|
"graphql-ws": "^5.16.0",
|
|
34
34
|
"graphql-yoga": "^5.21.0",
|
|
35
|
-
"sury": "11.0.0-rc.
|
|
35
|
+
"sury": "11.0.0-rc.2",
|
|
36
36
|
"ws": "^8.18.0",
|
|
37
37
|
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.27",
|
|
38
38
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
39
39
|
"@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.21",
|
|
40
|
-
"@reventlessdev/rescript-node": "2.0.0-alpha.8",
|
|
41
40
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
41
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.8",
|
|
42
42
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
|
|
43
|
-
"@reventlessdev/reventless-core": "3.0.0-alpha.
|
|
44
|
-
"@reventlessdev/reventless-
|
|
43
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.247",
|
|
44
|
+
"@reventlessdev/reventless-graphql-server": "1.0.0-alpha.95",
|
|
45
|
+
"@reventlessdev/reventless-gwt": "1.0.0-alpha.194",
|
|
46
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.111",
|
|
47
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.150",
|
|
45
48
|
"@reventlessdev/reventless-seed": "1.0.0-alpha.17",
|
|
46
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.
|
|
47
|
-
"@reventlessdev/reventless-postgres": "3.0.0-alpha.109",
|
|
48
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.148",
|
|
49
|
-
"@reventlessdev/reventless-graphql-server": "1.0.0-alpha.93"
|
|
49
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.122"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"rescript": "12.3.0",
|
|
53
|
-
"sury-ppx": "11.0.0-rc.
|
|
53
|
+
"sury-ppx": "11.0.0-rc.2",
|
|
54
54
|
"@reventlessdev/reventless-ppx": "1.0.0-alpha.72"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
@@ -347,6 +347,10 @@ type bucket = {
|
|
|
347
347
|
mutationResolvers: dict<resolverFn>,
|
|
348
348
|
queryResolvers: dict<resolverFn>,
|
|
349
349
|
subscriptionResolvers: dict<resolverFn>,
|
|
350
|
+
// Object-type field resolvers, keyed type name → field name. Carries the
|
|
351
|
+
// cross-table fields `@resolves` / `@resolvesMany` add to a queryable; their
|
|
352
|
+
// SDL rides the plugin's schema fragment, so only the map lives here.
|
|
353
|
+
fieldResolvers: dict<dict<resolverFn>>,
|
|
350
354
|
}
|
|
351
355
|
|
|
352
356
|
let makeBucket = (~scope: string): bucket => {
|
|
@@ -363,6 +367,7 @@ let makeBucket = (~scope: string): bucket => {
|
|
|
363
367
|
mutationResolvers: Dict.make(),
|
|
364
368
|
queryResolvers: Dict.make(),
|
|
365
369
|
subscriptionResolvers: Dict.make(),
|
|
370
|
+
fieldResolvers: Dict.make(),
|
|
366
371
|
}
|
|
367
372
|
|
|
368
373
|
// Insertion order = composition order (platform bucket is forced first in
|
|
@@ -420,6 +425,15 @@ let relabelScope = (~from: string, ~to_: string) =>
|
|
|
420
425
|
b.subscriptionResolvers
|
|
421
426
|
->Dict.toArray
|
|
422
427
|
->Array.forEach(((k, v)) => existing.subscriptionResolvers->Dict.set(k, v))
|
|
428
|
+
b.fieldResolvers
|
|
429
|
+
->Dict.toArray
|
|
430
|
+
->Array.forEach(((typeName, byField)) =>
|
|
431
|
+
switch existing.fieldResolvers->Dict.get(typeName) {
|
|
432
|
+
| Some(target) =>
|
|
433
|
+
byField->Dict.toArray->Array.forEach(((k, v)) => target->Dict.set(k, v))
|
|
434
|
+
| None => existing.fieldResolvers->Dict.set(typeName, byField)
|
|
435
|
+
}
|
|
436
|
+
)
|
|
423
437
|
}
|
|
424
438
|
if currentScope.contents == from {
|
|
425
439
|
currentScope.contents = to_
|
|
@@ -453,6 +467,24 @@ let registerSubscriptions = (~sdlFields: array<string>, ~resolvers: dict<resolve
|
|
|
453
467
|
resolvers->Dict.toArray->Array.forEach(((k, v)) => b.subscriptionResolvers->Dict.set(k, v))
|
|
454
468
|
}
|
|
455
469
|
|
|
470
|
+
let registerFieldResolvers = (~typeName: string, ~resolvers: dict<resolverFn>) => {
|
|
471
|
+
let b = currentBucket()
|
|
472
|
+
let forType = switch b.fieldResolvers->Dict.get(typeName) {
|
|
473
|
+
| Some(d) => d
|
|
474
|
+
| None =>
|
|
475
|
+
let d = Dict.make()
|
|
476
|
+
b.fieldResolvers->Dict.set(typeName, d)
|
|
477
|
+
d
|
|
478
|
+
}
|
|
479
|
+
resolvers->Dict.toArray->Array.forEach(((k, v)) => forType->Dict.set(k, v))
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** Look up a registered object-type field resolver. Searches all scope buckets. */
|
|
483
|
+
let getFieldResolver = (~typeName: string, fieldName: string): option<resolverFn> =>
|
|
484
|
+
buckets.contents
|
|
485
|
+
->Dict.valuesToArray
|
|
486
|
+
->Array.findMap(b => b.fieldResolvers->Dict.get(typeName)->Option.flatMap(d => d->Dict.get(fieldName)))
|
|
487
|
+
|
|
456
488
|
/** Look up a registered mutation resolver by field name (used by MCP_Server).
|
|
457
489
|
Searches all scope buckets. */
|
|
458
490
|
let getMutationResolver = (fieldName: string): option<resolverFn> =>
|
|
@@ -620,6 +652,9 @@ let scopeResolverMap = (b: bucket, ~withRootDefaults: bool): dict<dict<resolverF
|
|
|
620
652
|
if b.subscriptionResolvers->Dict.keysToArray->Array.length > 0 {
|
|
621
653
|
resolvers->Dict.set("Subscription", b.subscriptionResolvers)
|
|
622
654
|
}
|
|
655
|
+
b.fieldResolvers
|
|
656
|
+
->Dict.toArray
|
|
657
|
+
->Array.forEach(((typeName, byField)) => resolvers->Dict.set(typeName, byField))
|
|
623
658
|
resolvers
|
|
624
659
|
}
|
|
625
660
|
|
|
@@ -913,6 +948,7 @@ let asInterface: ReventlessGraphqlServer.GraphQL_ServerInstance.t = {
|
|
|
913
948
|
registerQueries,
|
|
914
949
|
registerSubscriptions,
|
|
915
950
|
registerTypes,
|
|
951
|
+
registerFieldResolvers,
|
|
916
952
|
getMutationResolver,
|
|
917
953
|
getQueryResolver,
|
|
918
954
|
start,
|
|
@@ -252,7 +252,8 @@ function makeBucket(scope) {
|
|
|
252
252
|
typeDefinitions: scope === platformScope ? [] : GraphQL_Stitcher$ReventlessCore.relayBaseTypes.slice(),
|
|
253
253
|
mutationResolvers: {},
|
|
254
254
|
queryResolvers: {},
|
|
255
|
-
subscriptionResolvers: {}
|
|
255
|
+
subscriptionResolvers: {},
|
|
256
|
+
fieldResolvers: {}
|
|
256
257
|
};
|
|
257
258
|
}
|
|
258
259
|
|
|
@@ -316,6 +317,18 @@ function relabelScope(from, to_) {
|
|
|
316
317
|
Object.entries(b.subscriptionResolvers).forEach(param => {
|
|
317
318
|
existing.subscriptionResolvers[param[0]] = param[1];
|
|
318
319
|
});
|
|
320
|
+
Object.entries(b.fieldResolvers).forEach(param => {
|
|
321
|
+
let byField = param[1];
|
|
322
|
+
let typeName = param[0];
|
|
323
|
+
let target = existing.fieldResolvers[typeName];
|
|
324
|
+
if (target !== undefined) {
|
|
325
|
+
Object.entries(byField).forEach(param => {
|
|
326
|
+
target[param[0]] = param[1];
|
|
327
|
+
});
|
|
328
|
+
} else {
|
|
329
|
+
existing.fieldResolvers[typeName] = byField;
|
|
330
|
+
}
|
|
331
|
+
});
|
|
319
332
|
} else {
|
|
320
333
|
buckets.contents[to_] = b;
|
|
321
334
|
}
|
|
@@ -354,6 +367,26 @@ function registerSubscriptions(sdlFields, resolvers) {
|
|
|
354
367
|
});
|
|
355
368
|
}
|
|
356
369
|
|
|
370
|
+
function registerFieldResolvers(typeName, resolvers) {
|
|
371
|
+
let b = bucketFor(currentScope.contents);
|
|
372
|
+
let d = b.fieldResolvers[typeName];
|
|
373
|
+
let forType;
|
|
374
|
+
if (d !== undefined) {
|
|
375
|
+
forType = d;
|
|
376
|
+
} else {
|
|
377
|
+
let d$1 = {};
|
|
378
|
+
b.fieldResolvers[typeName] = d$1;
|
|
379
|
+
forType = d$1;
|
|
380
|
+
}
|
|
381
|
+
Object.entries(resolvers).forEach(param => {
|
|
382
|
+
forType[param[0]] = param[1];
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function getFieldResolver(typeName, fieldName) {
|
|
387
|
+
return Stdlib_Array.findMap(Object.values(buckets.contents), b => Stdlib_Option.flatMap(b.fieldResolvers[typeName], d => d[fieldName]));
|
|
388
|
+
}
|
|
389
|
+
|
|
357
390
|
function getMutationResolver(fieldName) {
|
|
358
391
|
return Stdlib_Array.findMap(Object.values(buckets.contents), b => b.mutationResolvers[fieldName]);
|
|
359
392
|
}
|
|
@@ -486,6 +519,9 @@ function scopeResolverMap(b, withRootDefaults) {
|
|
|
486
519
|
if (Object.keys(b.subscriptionResolvers).length !== 0) {
|
|
487
520
|
resolvers["Subscription"] = b.subscriptionResolvers;
|
|
488
521
|
}
|
|
522
|
+
Object.entries(b.fieldResolvers).forEach(param => {
|
|
523
|
+
resolvers[param[0]] = param[1];
|
|
524
|
+
});
|
|
489
525
|
return resolvers;
|
|
490
526
|
}
|
|
491
527
|
|
|
@@ -749,6 +785,7 @@ let asInterface = {
|
|
|
749
785
|
registerQueries: registerQueries,
|
|
750
786
|
registerSubscriptions: registerSubscriptions,
|
|
751
787
|
registerTypes: registerTypes,
|
|
788
|
+
registerFieldResolvers: registerFieldResolvers,
|
|
752
789
|
getMutationResolver: getMutationResolver,
|
|
753
790
|
getQueryResolver: getQueryResolver,
|
|
754
791
|
start: start,
|
|
@@ -798,6 +835,8 @@ export {
|
|
|
798
835
|
registerMutations,
|
|
799
836
|
registerQueries,
|
|
800
837
|
registerSubscriptions,
|
|
838
|
+
registerFieldResolvers,
|
|
839
|
+
getFieldResolver,
|
|
801
840
|
getMutationResolver,
|
|
802
841
|
getQueryResolver,
|
|
803
842
|
registerTypes,
|
|
@@ -62,6 +62,10 @@ let registerQueries = DomainGraphQL_Server$ReventlessLocal.registerQueries;
|
|
|
62
62
|
|
|
63
63
|
let registerSubscriptions = DomainGraphQL_Server$ReventlessLocal.registerSubscriptions;
|
|
64
64
|
|
|
65
|
+
let registerFieldResolvers = DomainGraphQL_Server$ReventlessLocal.registerFieldResolvers;
|
|
66
|
+
|
|
67
|
+
let getFieldResolver = DomainGraphQL_Server$ReventlessLocal.getFieldResolver;
|
|
68
|
+
|
|
65
69
|
let getMutationResolver = DomainGraphQL_Server$ReventlessLocal.getMutationResolver;
|
|
66
70
|
|
|
67
71
|
let getQueryResolver = DomainGraphQL_Server$ReventlessLocal.getQueryResolver;
|
|
@@ -157,6 +161,8 @@ export {
|
|
|
157
161
|
registerMutations,
|
|
158
162
|
registerQueries,
|
|
159
163
|
registerSubscriptions,
|
|
164
|
+
registerFieldResolvers,
|
|
165
|
+
getFieldResolver,
|
|
160
166
|
getMutationResolver,
|
|
161
167
|
getQueryResolver,
|
|
162
168
|
registerTypes,
|
|
@@ -48,6 +48,10 @@ let registerQueries = (~sdlFields, ~resolvers) =>
|
|
|
48
48
|
instance.registerQueries(~sdlFields, ~resolvers=wrapAdmin(resolvers))
|
|
49
49
|
let registerSubscriptions = instance.registerSubscriptions
|
|
50
50
|
let registerTypes = instance.registerTypes
|
|
51
|
+
// Not admin-wrapped: a field resolver is only reached through a parent field
|
|
52
|
+
// that already met the group check, so wrapping would re-refuse a caller the
|
|
53
|
+
// root already admitted.
|
|
54
|
+
let registerFieldResolvers = instance.registerFieldResolvers
|
|
51
55
|
let getMutationResolver = instance.getMutationResolver
|
|
52
56
|
let getQueryResolver = instance.getQueryResolver
|
|
53
57
|
let start = instance.start
|
|
@@ -67,6 +71,7 @@ let asInterface: ReventlessGraphqlServer.GraphQL_ServerInstance.t = {
|
|
|
67
71
|
registerQueries,
|
|
68
72
|
registerSubscriptions,
|
|
69
73
|
registerTypes,
|
|
74
|
+
registerFieldResolvers,
|
|
70
75
|
getMutationResolver,
|
|
71
76
|
getQueryResolver,
|
|
72
77
|
start,
|
|
@@ -25,6 +25,8 @@ let registerSubscriptions = instance.registerSubscriptions;
|
|
|
25
25
|
|
|
26
26
|
let registerTypes = instance.registerTypes;
|
|
27
27
|
|
|
28
|
+
let registerFieldResolvers = instance.registerFieldResolvers;
|
|
29
|
+
|
|
28
30
|
let getMutationResolver = instance.getMutationResolver;
|
|
29
31
|
|
|
30
32
|
let getQueryResolver = instance.getQueryResolver;
|
|
@@ -50,6 +52,7 @@ let asInterface = {
|
|
|
50
52
|
registerQueries: registerQueries,
|
|
51
53
|
registerSubscriptions: registerSubscriptions,
|
|
52
54
|
registerTypes: registerTypes,
|
|
55
|
+
registerFieldResolvers: registerFieldResolvers,
|
|
53
56
|
getMutationResolver: getMutationResolver,
|
|
54
57
|
getQueryResolver: getQueryResolver,
|
|
55
58
|
start: start,
|
|
@@ -69,6 +72,7 @@ export {
|
|
|
69
72
|
registerQueries,
|
|
70
73
|
registerSubscriptions,
|
|
71
74
|
registerTypes,
|
|
75
|
+
registerFieldResolvers,
|
|
72
76
|
getMutationResolver,
|
|
73
77
|
getQueryResolver,
|
|
74
78
|
start,
|
|
@@ -72,8 +72,8 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
72
72
|
~dataSourceName as _,
|
|
73
73
|
~indexes,
|
|
74
74
|
~subIdField,
|
|
75
|
-
~idResolverConfigs
|
|
76
|
-
~idsResolverConfigs
|
|
75
|
+
~idResolverConfigs,
|
|
76
|
+
~idsResolverConfigs,
|
|
77
77
|
~authorization,
|
|
78
78
|
~opts as _,
|
|
79
79
|
) => {
|
|
@@ -913,6 +913,175 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
913
913
|
},
|
|
914
914
|
)
|
|
915
915
|
|
|
916
|
+
// -- Cross-table fields: @resolves / @resolvesMany -------------------------
|
|
917
|
+
// Field resolvers on this view's object type, following a foreign key into
|
|
918
|
+
// another queryable's rows. Their SDL rides the plugin's schema fragment
|
|
919
|
+
// (`GraphQL_FragmentGenerator` emits it from the same config), so only the
|
|
920
|
+
// resolvers are registered here.
|
|
921
|
+
//
|
|
922
|
+
// The rows are the TARGET's, so the target's `@owner` / `@retired` rules
|
|
923
|
+
// narrow them — the same reading the AppSync resolver bakes into its
|
|
924
|
+
// response. A nested field takes no `includeRetired` argument, so a retired
|
|
925
|
+
// row never travels through one; `{list}Refs` is the door that names those.
|
|
926
|
+
let targetAllows = (~ctx, ~target: string, item: JSON.t): bool => {
|
|
927
|
+
let schema = Plugin_Helpers.stateSchemaRegistry->Dict.get(target)
|
|
928
|
+
let cell = field => item->JSON.Decode.object->Option.flatMap(d => d->Dict.get(field))
|
|
929
|
+
let identity = extractIdentity(ctx)
|
|
930
|
+
let ownerOk = switch identity->Reventless.OwnerScope.decide(
|
|
931
|
+
~ownerField=schema->Option.flatMap(s => Reventless.Owner.fieldNames(s)->Array.get(0)),
|
|
932
|
+
) {
|
|
933
|
+
| Unscoped => true
|
|
934
|
+
| RefuseOwned => false
|
|
935
|
+
| ScopeTo(field, required) =>
|
|
936
|
+
cell(field)->Option.flatMap(JSON.Decode.string)->Option.mapOr(false, v => v == required)
|
|
937
|
+
}
|
|
938
|
+
let retiredSpec =
|
|
939
|
+
schema
|
|
940
|
+
->Option.flatMap(Reventless.StateAnnotations.getSpec)
|
|
941
|
+
->Option.flatMap(spec => spec.retired)
|
|
942
|
+
let retiredOk = switch identity
|
|
943
|
+
->Reventless.OwnerScope.decideRetired(
|
|
944
|
+
~retiredField=retiredSpec->Option.map(r => r.field),
|
|
945
|
+
~retiredValues=?retiredSpec->Option.flatMap(r => r.values),
|
|
946
|
+
~asked=false,
|
|
947
|
+
)
|
|
948
|
+
->Reventless.OwnerScope.retiredScopeOf {
|
|
949
|
+
| None => true
|
|
950
|
+
| Some(scope) => !(scope->Reventless.OwnerScope.isRetiredValue(cell(scope.field)))
|
|
951
|
+
}
|
|
952
|
+
ownerOk && retiredOk
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// The storage key the row was loaded by, reported as `id` — the nested type
|
|
956
|
+
// is a `Node`, so the field is non-null, and `loadStream` hands back the
|
|
957
|
+
// stored value without one.
|
|
958
|
+
let withId = (~key: string, item: JSON.t): JSON.t => {
|
|
959
|
+
let obj = item->JSON.Decode.object->Option.mapOr(Dict.make(), Dict.copy)
|
|
960
|
+
obj->Dict.set("id", JSON.Encode.string(key))
|
|
961
|
+
JSON.Encode.object(obj)
|
|
962
|
+
}
|
|
963
|
+
let loadFrom = async (~target: string, key: string): array<JSON.t> =>
|
|
964
|
+
switch Bus.getQueryDb(target) {
|
|
965
|
+
| Some(ops) =>
|
|
966
|
+
await ops.loadStream(key)
|
|
967
|
+
->Stream.runCollect
|
|
968
|
+
->Effect.catchAll(_ => Effect.succeed([]))
|
|
969
|
+
->Effect.runPromise
|
|
970
|
+
| None => []
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
let lookupByIndexIn = (~target: string, ~field: string, value: string): array<JSON.t> =>
|
|
974
|
+
switch Bus.getQueryDbIndexLookup(target) {
|
|
975
|
+
| Some(lookup) => lookup(field, value)
|
|
976
|
+
| None =>
|
|
977
|
+
switch Bus.getQueryDbScan(target) {
|
|
978
|
+
| Some(scanAll) =>
|
|
979
|
+
scanAll()->Array.filter(item =>
|
|
980
|
+
item
|
|
981
|
+
->JSON.Decode.object
|
|
982
|
+
->Option.flatMap(d => d->Dict.get(field))
|
|
983
|
+
->Option.flatMap(JSON.Decode.string)
|
|
984
|
+
->Option.mapOr(false, v => v == value)
|
|
985
|
+
)
|
|
986
|
+
| None => []
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
let sourceField = (root: JSON.t, field: string): option<JSON.t> =>
|
|
991
|
+
root->JSON.Decode.object->Option.flatMap(d => d->Dict.get(field))
|
|
992
|
+
|
|
993
|
+
let fieldResolvers = Dict.make()
|
|
994
|
+
|
|
995
|
+
idResolverConfigs->Array.forEach((config: Reventless.ReadModel.idResolverConfig) => {
|
|
996
|
+
let {source: {idField, subId, resolvedField}, target} = config
|
|
997
|
+
let (fieldName, multi) = switch resolvedField {
|
|
998
|
+
| Single(f) => (f, false)
|
|
999
|
+
| Multi(f) => (f, true)
|
|
1000
|
+
}
|
|
1001
|
+
let targetName = target.tableName
|
|
1002
|
+
let resolver: ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn = async (
|
|
1003
|
+
root,
|
|
1004
|
+
args,
|
|
1005
|
+
ctx,
|
|
1006
|
+
) => {
|
|
1007
|
+
let key =
|
|
1008
|
+
root->sourceField(idField)->Option.flatMap(JSON.Decode.string)->Option.getOr("")
|
|
1009
|
+
let rows = if key == "" {
|
|
1010
|
+
[]
|
|
1011
|
+
} else {
|
|
1012
|
+
switch target.idField {
|
|
1013
|
+
| Id => (await loadFrom(~target=targetName, key))->Array.map(withId(~key, ...))
|
|
1014
|
+
| Index(index) => lookupByIndexIn(~target=targetName, ~field=index, key)
|
|
1015
|
+
| IndexWithId(_, targetIdField) =>
|
|
1016
|
+
lookupByIndexIn(~target=targetName, ~field=targetIdField, key)
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
// The target's sort key, taken from the parent row or from an argument on
|
|
1020
|
+
// the field — the same two sources the AppSync template reads.
|
|
1021
|
+
let narrowed = switch (subId, target.subIdField) {
|
|
1022
|
+
| (Field(sourceSortField), Some(targetSortField)) =>
|
|
1023
|
+
let want = root->sourceField(sourceSortField)->Option.flatMap(JSON.Decode.string)
|
|
1024
|
+
rows->Array.filter(item =>
|
|
1025
|
+
item
|
|
1026
|
+
->JSON.Decode.object
|
|
1027
|
+
->Option.flatMap(d => d->Dict.get(targetSortField))
|
|
1028
|
+
->Option.flatMap(JSON.Decode.string) == want
|
|
1029
|
+
)
|
|
1030
|
+
| (Argument(sortArgument), Some(targetSortField)) =>
|
|
1031
|
+
switch args->JSON.Decode.object->Option.flatMap(d => d->Dict.get(sortArgument)) {
|
|
1032
|
+
| None => rows
|
|
1033
|
+
| Some(want) =>
|
|
1034
|
+
rows->Array.filter(item =>
|
|
1035
|
+
item
|
|
1036
|
+
->JSON.Decode.object
|
|
1037
|
+
->Option.flatMap(d => d->Dict.get(targetSortField))
|
|
1038
|
+
->Option.flatMap(JSON.Decode.string) == want->JSON.Decode.string
|
|
1039
|
+
)
|
|
1040
|
+
}
|
|
1041
|
+
| _ => rows
|
|
1042
|
+
}
|
|
1043
|
+
let allowed = narrowed->Array.filter(item => targetAllows(~ctx, ~target=targetName, item))
|
|
1044
|
+
if multi {
|
|
1045
|
+
allowed->JSON.Encode.array
|
|
1046
|
+
} else {
|
|
1047
|
+
allowed->Array.get(0)->Option.getOr(JSON.Encode.null)
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
fieldResolvers->Dict.set(fieldName, resolver)
|
|
1051
|
+
})
|
|
1052
|
+
|
|
1053
|
+
idsResolverConfigs->Array.forEach((config: Reventless.ReadModel.idsResolverConfig) => {
|
|
1054
|
+
let {source: {idsField, resolvedField}, target} = config
|
|
1055
|
+
let targetName = target.tableName
|
|
1056
|
+
let resolver: ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn = async (
|
|
1057
|
+
root,
|
|
1058
|
+
_args,
|
|
1059
|
+
ctx,
|
|
1060
|
+
) => {
|
|
1061
|
+
let ids =
|
|
1062
|
+
root
|
|
1063
|
+
->sourceField(idsField)
|
|
1064
|
+
->Option.flatMap(JSON.Decode.array)
|
|
1065
|
+
->Option.getOr([])
|
|
1066
|
+
->Array.filterMap(JSON.Decode.string)
|
|
1067
|
+
// Missing ids drop out rather than becoming nulls, matching BatchGetItem
|
|
1068
|
+
// and the by-ids door built on it.
|
|
1069
|
+
let rows = await ids->Array.map(async key =>
|
|
1070
|
+
(await loadFrom(~target=targetName, key))->Array.get(0)->Option.map(withId(~key, ...))
|
|
1071
|
+
)->Promise.all
|
|
1072
|
+
rows
|
|
1073
|
+
->Array.filterMap(row =>
|
|
1074
|
+
row->Option.filter(item => targetAllows(~ctx, ~target=targetName, item))
|
|
1075
|
+
)
|
|
1076
|
+
->JSON.Encode.array
|
|
1077
|
+
}
|
|
1078
|
+
fieldResolvers->Dict.set(resolvedField, resolver)
|
|
1079
|
+
})
|
|
1080
|
+
|
|
1081
|
+
if fieldResolvers->Dict.keysToArray->Array.length > 0 {
|
|
1082
|
+
server.registerFieldResolvers(~typeName=returnTypeName, ~resolvers=fieldResolvers)
|
|
1083
|
+
}
|
|
1084
|
+
|
|
916
1085
|
// -- Register all fields --------------------------------------------------
|
|
917
1086
|
let allSdl =
|
|
918
1087
|
[byIdSdl]
|
|
@@ -8,6 +8,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
8
8
|
import * as Effect from "effect/Effect";
|
|
9
9
|
import * as Stdlib_Nullable from "@rescript/runtime/lib/es6/Stdlib_Nullable.js";
|
|
10
10
|
import * as Owner$Reventless from "@reventlessdev/reventless-spec/src/components/Owner.res.mjs";
|
|
11
|
+
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
11
12
|
import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
|
|
12
13
|
import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
|
|
13
14
|
import * as Api_Ids$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/Api_Ids.res.mjs";
|
|
@@ -54,7 +55,7 @@ function Make(Bus) {
|
|
|
54
55
|
nodeTypeRegistry: DomainGraphQL_Server$ReventlessLocal.nodeTypeRegistry
|
|
55
56
|
}
|
|
56
57
|
};
|
|
57
|
-
let make = (name, param, param$1, param$2, indexes, subIdField,
|
|
58
|
+
let make = (name, param, param$1, param$2, indexes, subIdField, idResolverConfigs, idsResolverConfigs, authorization, param$3) => {
|
|
58
59
|
let server = serverRef.contents;
|
|
59
60
|
let relay = relayRef.contents;
|
|
60
61
|
if (relay !== undefined) {
|
|
@@ -617,6 +618,122 @@ function Make(Bus) {
|
|
|
617
618
|
resolver
|
|
618
619
|
];
|
|
619
620
|
});
|
|
621
|
+
let targetAllows = (ctx, target, item) => {
|
|
622
|
+
let schema = Plugin_Helpers$ReventlessCore.stateSchemaRegistry[target];
|
|
623
|
+
let cell = field => Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[field]);
|
|
624
|
+
let identity = extractIdentity(ctx);
|
|
625
|
+
let match = OwnerScope$Reventless.decide(identity, Stdlib_Option.flatMap(schema, s => Owner$Reventless.fieldNames(s)[0]), undefined);
|
|
626
|
+
let ownerOk;
|
|
627
|
+
if (typeof match !== "object") {
|
|
628
|
+
ownerOk = match === "Unscoped";
|
|
629
|
+
} else {
|
|
630
|
+
let required = match._1;
|
|
631
|
+
ownerOk = Stdlib_Option.mapOr(Stdlib_Option.flatMap(cell(match._0), Stdlib_JSON.Decode.string), false, v => v === required);
|
|
632
|
+
}
|
|
633
|
+
let retiredSpec = Stdlib_Option.flatMap(Stdlib_Option.flatMap(schema, StateAnnotations$Reventless.getSpec), spec => spec.retired);
|
|
634
|
+
let scope = OwnerScope$Reventless.retiredScopeOf(OwnerScope$Reventless.decideRetired(identity, Stdlib_Option.map(retiredSpec, r => r.field), Stdlib_Option.flatMap(retiredSpec, r => r.values), false, undefined));
|
|
635
|
+
let retiredOk = scope !== undefined ? !OwnerScope$Reventless.isRetiredValue(scope, cell(scope.field)) : true;
|
|
636
|
+
if (ownerOk) {
|
|
637
|
+
return retiredOk;
|
|
638
|
+
} else {
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
let withId = (key, item) => {
|
|
643
|
+
let obj = Stdlib_Option.mapOr(Stdlib_JSON.Decode.object(item), {}, prim => Object.assign({}, prim));
|
|
644
|
+
obj["id"] = key;
|
|
645
|
+
return obj;
|
|
646
|
+
};
|
|
647
|
+
let loadFrom = async (target, key) => {
|
|
648
|
+
let ops = Bus.getQueryDb(target);
|
|
649
|
+
if (ops !== undefined) {
|
|
650
|
+
return await Effect.runPromise(Effect.catchAll(Stream.runCollect(ops.loadStream(key)), param => Effect.succeed([])));
|
|
651
|
+
} else {
|
|
652
|
+
return [];
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
let lookupByIndexIn = (target, field, value) => {
|
|
656
|
+
let lookup = Bus.getQueryDbIndexLookup(target);
|
|
657
|
+
if (lookup !== undefined) {
|
|
658
|
+
return lookup(field, value);
|
|
659
|
+
}
|
|
660
|
+
let scanAll = Bus.getQueryDbScan(target);
|
|
661
|
+
if (scanAll !== undefined) {
|
|
662
|
+
return scanAll().filter(item => Stdlib_Option.mapOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[field]), Stdlib_JSON.Decode.string), false, v => v === value));
|
|
663
|
+
} else {
|
|
664
|
+
return [];
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
let sourceField = (root, field) => Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(root), d => d[field]);
|
|
668
|
+
let fieldResolvers = {};
|
|
669
|
+
idResolverConfigs.forEach(config => {
|
|
670
|
+
let target = config.target;
|
|
671
|
+
let match = config.source;
|
|
672
|
+
let resolvedField = match.resolvedField;
|
|
673
|
+
let subId = match.subId;
|
|
674
|
+
let idField = match.idField;
|
|
675
|
+
let match$1;
|
|
676
|
+
match$1 = resolvedField.TAG === "Single" ? [
|
|
677
|
+
resolvedField._0,
|
|
678
|
+
false
|
|
679
|
+
] : [
|
|
680
|
+
resolvedField._0,
|
|
681
|
+
true
|
|
682
|
+
];
|
|
683
|
+
let multi = match$1[1];
|
|
684
|
+
let targetName = target.tableName;
|
|
685
|
+
let resolver = async (root, args, ctx) => {
|
|
686
|
+
let key = Stdlib_Option.getOr(Stdlib_Option.flatMap(sourceField(root, idField), Stdlib_JSON.Decode.string), "");
|
|
687
|
+
let rows;
|
|
688
|
+
if (key === "") {
|
|
689
|
+
rows = [];
|
|
690
|
+
} else {
|
|
691
|
+
let index = target.idField;
|
|
692
|
+
rows = typeof index !== "object" ? (await loadFrom(targetName, key)).map(extra => withId(key, extra)) : (
|
|
693
|
+
index.TAG === "Index" ? lookupByIndexIn(targetName, index._0, key) : lookupByIndexIn(targetName, index._1, key)
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
let match = target.subIdField;
|
|
697
|
+
let narrowed;
|
|
698
|
+
if (typeof subId !== "object") {
|
|
699
|
+
narrowed = rows;
|
|
700
|
+
} else if (subId.TAG === "Field") {
|
|
701
|
+
if (match !== undefined) {
|
|
702
|
+
let want = Stdlib_Option.flatMap(sourceField(root, subId._0), Stdlib_JSON.Decode.string);
|
|
703
|
+
narrowed = rows.filter(item => Primitive_object.equal(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[match]), Stdlib_JSON.Decode.string), want));
|
|
704
|
+
} else {
|
|
705
|
+
narrowed = rows;
|
|
706
|
+
}
|
|
707
|
+
} else if (match !== undefined) {
|
|
708
|
+
let sortArgument = subId._0;
|
|
709
|
+
let want$1 = Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(args), d => d[sortArgument]);
|
|
710
|
+
narrowed = want$1 !== undefined ? rows.filter(item => Primitive_object.equal(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[match]), Stdlib_JSON.Decode.string), Stdlib_JSON.Decode.string(want$1))) : rows;
|
|
711
|
+
} else {
|
|
712
|
+
narrowed = rows;
|
|
713
|
+
}
|
|
714
|
+
let allowed = narrowed.filter(item => targetAllows(ctx, targetName, item));
|
|
715
|
+
if (multi) {
|
|
716
|
+
return allowed;
|
|
717
|
+
} else {
|
|
718
|
+
return Stdlib_Option.getOr(allowed[0], null);
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
fieldResolvers[match$1[0]] = resolver;
|
|
722
|
+
});
|
|
723
|
+
idsResolverConfigs.forEach(config => {
|
|
724
|
+
let match = config.source;
|
|
725
|
+
let idsField = match.idsField;
|
|
726
|
+
let targetName = config.target.tableName;
|
|
727
|
+
let resolver = async (root, _args, ctx) => {
|
|
728
|
+
let ids = Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_Option.flatMap(sourceField(root, idsField), Stdlib_JSON.Decode.array), []), Stdlib_JSON.Decode.string);
|
|
729
|
+
let rows = await Promise.all(ids.map(async key => Stdlib_Option.map((await loadFrom(targetName, key))[0], extra => withId(key, extra))));
|
|
730
|
+
return Stdlib_Array.filterMap(rows, row => Stdlib_Option.filter(row, item => targetAllows(ctx, targetName, item)));
|
|
731
|
+
};
|
|
732
|
+
fieldResolvers[match.resolvedField] = resolver;
|
|
733
|
+
});
|
|
734
|
+
if (Object.keys(fieldResolvers).length !== 0) {
|
|
735
|
+
server.registerFieldResolvers(returnTypeName, fieldResolvers);
|
|
736
|
+
}
|
|
620
737
|
let allSdl = [byIdSdl].concat(byIdsSdl).concat(refsSdl).concat(match[0]).concat(itemsSdl).concat(indexSdlFields);
|
|
621
738
|
let resolvers = {};
|
|
622
739
|
resolvers[singleQueryName] = byIdResolver;
|