From 053bdc1d4d988a9ec384d32a49300244e4954851 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Thu, 27 Jun 2024 15:13:33 +0200 Subject: [PATCH] new take on resolvers using fragments --- packages/relay | 2 +- .../__tests__/TestRelayUserResolver2.res | 17 +++++ .../__tests__/Test_relayResolvers-tests.js | 2 +- .../__tests__/Test_relayResolvers.res | 5 +- .../TestRelayResolversQuery_graphql.res | 67 +++++++++++------- .../TestRelayResolvers_user_graphql.res | 25 ++++++- .../TestRelayUserResolver2_graphql.res | 68 +++++++++++++++++++ .../User_relayResolvers_graphql.res | 7 +- .../rescript-relay-ppx/library/Fragment.ml | 36 ++++++++-- .../library/RescriptRelayPpxLibrary.ml | 1 + .../rescript-relay-ppx/library/Util.ml | 10 +++ packages/rescript-relay/src/RescriptRelay.res | 1 + .../rescript-relay/src/RescriptRelay.resi | 3 + .../src/RescriptRelay_Fragment.res | 10 +++ .../src/RescriptRelay_Fragment.resi | 6 ++ .../src/RescriptRelay_Internal.res | 8 +++ .../src/RescriptRelay_Internal.resi | 8 +++ 17 files changed, 240 insertions(+), 36 deletions(-) create mode 100644 packages/rescript-relay/__tests__/TestRelayUserResolver2.res create mode 100644 packages/rescript-relay/__tests__/__generated__/TestRelayUserResolver2_graphql.res diff --git a/packages/relay b/packages/relay index bf46d770..72f4958b 160000 --- a/packages/relay +++ b/packages/relay @@ -1 +1 @@ -Subproject commit bf46d77025c120e0405f1aff66a4791669029eda +Subproject commit 72f4958b940166a3c9ee9ffd21a0e1d232cb1797 diff --git a/packages/rescript-relay/__tests__/TestRelayUserResolver2.res b/packages/rescript-relay/__tests__/TestRelayUserResolver2.res new file mode 100644 index 00000000..39f4cf0b --- /dev/null +++ b/packages/rescript-relay/__tests__/TestRelayUserResolver2.res @@ -0,0 +1,17 @@ +module Fragment = %relay(` + fragment TestRelayUserResolver2 on User { + firstName + lastName + } +`) + +/** + * @RelayResolver User.fullName2(maxLength: Int!): String + * @rootFragment TestRelayUserResolver2 + * + * A users full name 2. + */ +let fullName2 = (user, args) => { + let user = Fragment.readResolverFragment(user) + `${user.firstName} ${user.lastName}`->Js.String2.slice(~from=0, ~to_=args.maxLength) +} diff --git a/packages/rescript-relay/__tests__/Test_relayResolvers-tests.js b/packages/rescript-relay/__tests__/Test_relayResolvers-tests.js index d3ec080a..75e7fc6e 100644 --- a/packages/rescript-relay/__tests__/Test_relayResolvers-tests.js +++ b/packages/rescript-relay/__tests__/Test_relayResolvers-tests.js @@ -20,6 +20,6 @@ describe("Relay Resolvers", () => { }); t.render(test_relayResolvers()); - await t.screen.findByText("First Last is online"); + await t.screen.findByText("First Last Fi is online"); }); }); diff --git a/packages/rescript-relay/__tests__/Test_relayResolvers.res b/packages/rescript-relay/__tests__/Test_relayResolvers.res index b341fd92..760abcfc 100644 --- a/packages/rescript-relay/__tests__/Test_relayResolvers.res +++ b/packages/rescript-relay/__tests__/Test_relayResolvers.res @@ -12,6 +12,7 @@ module Fragment = %relay(` fragment TestRelayResolvers_user on User { isOnline fullName + fullName2(maxLength: 2) } `) @@ -23,8 +24,8 @@ module Test = {
{switch data { - | {isOnline: Some(isOnline), fullName: Some(fullName)} => - React.string(`${fullName} is ${isOnline ? "online" : "offline"}`) + | {isOnline: Some(isOnline), fullName: Some(fullName), fullName2: Some(fullName2)} => + React.string(`${fullName} ${fullName2} is ${isOnline ? "online" : "offline"}`) | _ => React.string("-") }}
diff --git a/packages/rescript-relay/__tests__/__generated__/TestRelayResolversQuery_graphql.res b/packages/rescript-relay/__tests__/__generated__/TestRelayResolversQuery_graphql.res index 5ac7478f..eb4dedd1 100644 --- a/packages/rescript-relay/__tests__/__generated__/TestRelayResolversQuery_graphql.res +++ b/packages/rescript-relay/__tests__/__generated__/TestRelayResolversQuery_graphql.res @@ -81,7 +81,29 @@ type relayOperationNode type operationType = RescriptRelay.queryNode -let node: operationType = %raw(json` { +let node: operationType = %raw(json` (function(){ +var v0 = { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "firstName", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "lastName", + "storageKey": null + } + ], + "type": "User", + "abstractKey": null +}; +return { "fragment": { "argumentDefinitions": [], "kind": "Fragment", @@ -132,31 +154,25 @@ let node: operationType = %raw(json` { { "name": "fullName", "args": null, - "fragment": { - "kind": "InlineFragment", - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "firstName", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "lastName", - "storageKey": null - } - ], - "type": "User", - "abstractKey": null - }, + "fragment": (v0/*: any*/), "kind": "RelayResolver", "storageKey": null, "isOutputType": true }, + { + "name": "fullName2", + "args": [ + { + "kind": "Literal", + "name": "maxLength", + "value": 2 + } + ], + "fragment": (v0/*: any*/), + "kind": "RelayResolver", + "storageKey": "fullName2(maxLength:2)", + "isOutputType": true + }, { "alias": null, "args": null, @@ -170,14 +186,15 @@ let node: operationType = %raw(json` { ] }, "params": { - "cacheID": "f1931c3327fbb966e46860c167aab18d", + "cacheID": "fc699ed75840880919543f5be5cbe28e", "id": null, "metadata": {}, "name": "TestRelayResolversQuery", "operationKind": "query", - "text": "query TestRelayResolversQuery {\n loggedInUser {\n ...TestRelayResolvers_user\n id\n }\n}\n\nfragment TestRelayResolvers_user on User {\n isOnline\n ...TestRelayUserResolver\n}\n\nfragment TestRelayUserResolver on User {\n firstName\n lastName\n}\n" + "text": "query TestRelayResolversQuery {\n loggedInUser {\n ...TestRelayResolvers_user\n id\n }\n}\n\nfragment TestRelayResolvers_user on User {\n isOnline\n ...TestRelayUserResolver\n ...TestRelayUserResolver2\n}\n\nfragment TestRelayUserResolver on User {\n firstName\n lastName\n}\n\nfragment TestRelayUserResolver2 on User {\n firstName\n lastName\n}\n" } -} `) +}; +})() `) @live let load: ( ~environment: RescriptRelay.Environment.t, diff --git a/packages/rescript-relay/__tests__/__generated__/TestRelayResolvers_user_graphql.res b/packages/rescript-relay/__tests__/__generated__/TestRelayResolvers_user_graphql.res index 2765b07a..80f15e58 100644 --- a/packages/rescript-relay/__tests__/__generated__/TestRelayResolvers_user_graphql.res +++ b/packages/rescript-relay/__tests__/__generated__/TestRelayResolvers_user_graphql.res @@ -6,6 +6,7 @@ module Types = { type fragment = { fullName: option, + fullName2: option, isOnline: option, } } @@ -41,9 +42,10 @@ type relayOperationNode type operationType = RescriptRelay.fragmentNode -%%private(let makeNode = (resolverDataInjector, rescript_module_TestRelayUserResolver_fullName): operationType => { +%%private(let makeNode = (resolverDataInjector, rescript_module_TestRelayUserResolver_fullName, rescript_module_TestRelayUserResolver2_fullName2): operationType => { ignore(resolverDataInjector) ignore(rescript_module_TestRelayUserResolver_fullName) + ignore(rescript_module_TestRelayUserResolver2_fullName2) %raw(json`{ "argumentDefinitions": [], "kind": "Fragment", @@ -69,11 +71,30 @@ type operationType = RescriptRelay.fragmentNode "name": "fullName", "resolverModule": rescript_module_TestRelayUserResolver_fullName, "path": "fullName" + }, + { + "alias": null, + "args": [ + { + "kind": "Literal", + "name": "maxLength", + "value": 2 + } + ], + "fragment": { + "args": null, + "kind": "FragmentSpread", + "name": "TestRelayUserResolver2" + }, + "kind": "RelayResolver", + "name": "fullName2", + "resolverModule": rescript_module_TestRelayUserResolver2_fullName2, + "path": "fullName2" } ], "type": "User", "abstractKey": null }`) }) -let node: operationType = makeNode(RescriptRelay.resolverDataInjector, TestRelayUserResolver.fullName) +let node: operationType = makeNode(RescriptRelay.resolverDataInjector, TestRelayUserResolver.fullName, TestRelayUserResolver2.fullName2) diff --git a/packages/rescript-relay/__tests__/__generated__/TestRelayUserResolver2_graphql.res b/packages/rescript-relay/__tests__/__generated__/TestRelayUserResolver2_graphql.res new file mode 100644 index 00000000..14f438e8 --- /dev/null +++ b/packages/rescript-relay/__tests__/__generated__/TestRelayUserResolver2_graphql.res @@ -0,0 +1,68 @@ +/* @sourceLoc TestRelayUserResolver2.res */ +/* @generated */ +%%raw("/* @generated */") +module Types = { + @@warning("-30") + + type fragment = { + firstName: string, + lastName: string, + } +} + +module Internal = { + @live + type fragmentRaw + @live + let fragmentConverter: Js.Dict.t>> = %raw( + json`{}` + ) + @live + let fragmentConverterMap = () + @live + let convertFragment = v => v->RescriptRelay.convertObj( + fragmentConverter, + fragmentConverterMap, + Js.undefined + ) +} + +type t +type fragmentRef +external getFragmentRef: + RescriptRelay.fragmentRefs<[> | #TestRelayUserResolver2]> => fragmentRef = "%identity" + +module Utils = { + @@warning("-33") + open Types +} + +type relayOperationNode +type operationType = RescriptRelay.fragmentNode + + +let node: operationType = %raw(json` { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "TestRelayUserResolver2", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "firstName", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "lastName", + "storageKey": null + } + ], + "type": "User", + "abstractKey": null +} `) + diff --git a/packages/rescript-relay/__tests__/__generated__/User_relayResolvers_graphql.res b/packages/rescript-relay/__tests__/__generated__/User_relayResolvers_graphql.res index 062c28e9..c23bfe98 100644 --- a/packages/rescript-relay/__tests__/__generated__/User_relayResolvers_graphql.res +++ b/packages/rescript-relay/__tests__/__generated__/User_relayResolvers_graphql.res @@ -1,5 +1,10 @@ /* @generated */ @@warning("-30") -type fullNameResolver = () => RescriptRelay.any +type fullNameResolver = (RescriptRelay.resolverFragmentRefs<[#TestRelayUserResolver]>, ) => RescriptRelay.any + +type fullName2ResolverArgs = { + maxLength: int, +} +type fullName2Resolver = (RescriptRelay.resolverFragmentRefs<[#TestRelayUserResolver2]>, fullName2ResolverArgs) => string diff --git a/packages/rescript-relay/rescript-relay-ppx/library/Fragment.ml b/packages/rescript-relay/rescript-relay-ppx/library/Fragment.ml index 4d3c4edc..d3226c48 100644 --- a/packages/rescript-relay/rescript-relay-ppx/library/Fragment.ml +++ b/packages/rescript-relay/rescript-relay-ppx/library/Fragment.ml @@ -2,7 +2,7 @@ open Ppxlib open Util let make ~loc ~moduleName ~refetchableQueryName ~extractedConnectionInfo - ~hasInlineDirective = + ~hasInlineDirective ~isPlural = let typeFromGeneratedModule = makeTypeAccessor ~loc ~moduleName in let valFromGeneratedModule = makeExprAccessor ~loc ~moduleName in let moduleIdentFromGeneratedModule = makeModuleIdent ~loc ~moduleName in @@ -35,7 +35,8 @@ let make ~loc ~moduleName ~refetchableQueryName ~extractedConnectionInfo ~node:[%e valFromGeneratedModule ["node"]]]; [%stri let useOpt fRef : - [%t typeFromGeneratedModule ["Types"; "fragment"]] option = + [%t typeFromGeneratedModule ["Types"; "fragment"]] option + = RescriptRelay_Fragment.useFragmentOpt ~convertFragment ~fRef: (match fRef with @@ -45,6 +46,34 @@ let make ~loc ~moduleName ~refetchableQueryName ~extractedConnectionInfo |. [%e valFromGeneratedModule ["getFragmentRef"]]) | None -> None) ~node:[%e valFromGeneratedModule ["node"]]]; + (if isPlural then + [%stri + let readResolverFragment fRef : + [%t typeFromGeneratedModule ["Types"; "fragment"]] = + let fRef = + fRef + |. RescriptRelay_Internal + .internal_resolverFragmentRefsToFragmentRefsPlural + in + RescriptRelay_Fragment.read ~convertFragment + ~fRef: + (fRef + |. [%e valFromGeneratedModule ["getFragmentRef"]]) + ~node:[%e valFromGeneratedModule ["node"]]] + else + [%stri + let readResolverFragment fRef : + [%t typeFromGeneratedModule ["Types"; "fragment"]] = + let fRef = + fRef + |. RescriptRelay_Internal + .internal_resolverFragmentRefsToFragmentRefs + in + RescriptRelay_Fragment.read ~convertFragment + ~fRef: + (fRef + |. [%e valFromGeneratedModule ["getFragmentRef"]]) + ~node:[%e valFromGeneratedModule ["node"]]]); ] | true -> []); (match hasInlineDirective with @@ -73,8 +102,7 @@ let make ~loc ~moduleName ~refetchableQueryName ~extractedConnectionInfo let makeRefetchVariables = [%e valFromRefetchableModule ["Types"; "makeRefetchVariables"]] - [@@ocaml.doc "A helper to make refetch variables. "] - [@@live]]; + [@@ocaml.doc "A helper to make refetch variables. "] [@@live]]; [%stri let convertRefetchVariables : [%t diff --git a/packages/rescript-relay/rescript-relay-ppx/library/RescriptRelayPpxLibrary.ml b/packages/rescript-relay/rescript-relay-ppx/library/RescriptRelayPpxLibrary.ml index c1222117..ce5a7d77 100644 --- a/packages/rescript-relay/rescript-relay-ppx/library/RescriptRelayPpxLibrary.ml +++ b/packages/rescript-relay/rescript-relay-ppx/library/RescriptRelayPpxLibrary.ml @@ -26,6 +26,7 @@ let commonExtension = ~refetchableQueryName ~extractedConnectionInfo:(op |> extractFragmentConnectionInfo ~loc) ~hasInlineDirective:(op |> fragmentHasInlineDirective ~loc) + ~isPlural:(op |> fragmentIsPlural ~loc) ~loc | Operation {optype = Query} -> if Util.queryIsUpdatable op then diff --git a/packages/rescript-relay/rescript-relay-ppx/library/Util.ml b/packages/rescript-relay/rescript-relay-ppx/library/Util.ml index 5e169f19..2e643016 100755 --- a/packages/rescript-relay/rescript-relay-ppx/library/Util.ml +++ b/packages/rescript-relay/rescript-relay-ppx/library/Util.ml @@ -113,6 +113,16 @@ let fragmentHasInlineDirective ~loc op = |> List.exists (fun (directive : Graphql_parser.directive) -> directive.name = "inline") | _ -> false + +let fragmentIsPlural ~loc op = + match op with + | Graphql_parser.Fragment {name = _; directives} -> + directives + |> List.exists (fun (directive : Graphql_parser.directive) -> + directive.name = "relay" + && directive.arguments + |> List.exists (fun (n, v) -> n = "plural" && v = `Bool true)) + | _ -> false let getGraphQLModuleName opName = String.capitalize_ascii opName ^ "_graphql" let rec longidentFromStrings = function diff --git a/packages/rescript-relay/src/RescriptRelay.res b/packages/rescript-relay/src/RescriptRelay.res index af714d3d..98d076ef 100644 --- a/packages/rescript-relay/src/RescriptRelay.res +++ b/packages/rescript-relay/src/RescriptRelay.res @@ -9,6 +9,7 @@ type mutationNode<'node> type subscriptionNode<'node> type fragmentRefs<'fragments> +type resolverFragmentRefs<'fragments> type updatableFragmentRefs<'fragments> type dataId diff --git a/packages/rescript-relay/src/RescriptRelay.resi b/packages/rescript-relay/src/RescriptRelay.resi index f0ce7e94..ec44a54d 100644 --- a/packages/rescript-relay/src/RescriptRelay.resi +++ b/packages/rescript-relay/src/RescriptRelay.resi @@ -30,6 +30,9 @@ type subscriptionNode<'node> /**This type shows all of the fragments that has been spread on this particular object.*/ type fragmentRefs<'fragments> +/**This type shows the Relay resolver fragment that has been spread on this particular object.*/ +type resolverFragmentRefs<'fragments> + /**This type shows all of the updatable fragments that has been spread on this particular object.*/ type updatableFragmentRefs<'fragments> diff --git a/packages/rescript-relay/src/RescriptRelay_Fragment.res b/packages/rescript-relay/src/RescriptRelay_Fragment.res index 6476aa92..5e334ac6 100644 --- a/packages/rescript-relay/src/RescriptRelay_Fragment.res +++ b/packages/rescript-relay/src/RescriptRelay_Fragment.res @@ -52,6 +52,16 @@ let readInlineData = (~node, ~convertFragment: 'fragment => 'fragment, ~fRef) => (readInlineData_(node, fRef)->convertFragment) } +@module("relay-runtime/lib/store/ResolverFragments") +external read_: (fragmentNode<'node>, 'fragmentRef) => 'fragment = "readFragment" + +let read = (~node, ~convertFragment: 'fragment => 'fragment, ~fRef) => { + /** This lets you get the data for this fragment _outside \ + of React's render_. Useful for letting functions with \ + with fragments too, for things like logging etc.*/ + (read_(node, fRef)->convertFragment) +} + type refetchableFnOpts = { fetchPolicy?: fetchPolicy, onComplete?: Js.Nullable.t => unit, diff --git a/packages/rescript-relay/src/RescriptRelay_Fragment.resi b/packages/rescript-relay/src/RescriptRelay_Fragment.resi index 809ab9c3..7b6a3cb2 100644 --- a/packages/rescript-relay/src/RescriptRelay_Fragment.resi +++ b/packages/rescript-relay/src/RescriptRelay_Fragment.resi @@ -18,6 +18,12 @@ let readInlineData: ( ~fRef: 'b, ) => 'fragment +let read: ( + ~node: fragmentNode<'a>, + ~convertFragment: 'fragment => 'fragment, + ~fRef: 'b, +) => 'fragment + type paginationLoadMoreOptions = {onComplete?: Js.Nullable.t => unit} type paginationLoadMoreFn = (~count: int, ~onComplete: option => unit=?) => Disposable.t diff --git a/packages/rescript-relay/src/RescriptRelay_Internal.res b/packages/rescript-relay/src/RescriptRelay_Internal.res index d7e7e693..fffdbd92 100644 --- a/packages/rescript-relay/src/RescriptRelay_Internal.res +++ b/packages/rescript-relay/src/RescriptRelay_Internal.res @@ -47,3 +47,11 @@ let unwrapUnion: ('a, array) => 'a = %raw(` `) let wrapUnion = union => union + +external internal_resolverFragmentRefsToFragmentRefs: RescriptRelay.resolverFragmentRefs< + 'a, +> => RescriptRelay.fragmentRefs<'a> = "%identity" + +external internal_resolverFragmentRefsToFragmentRefsPlural: RescriptRelay.resolverFragmentRefs< + 'a, +> => array> = "%identity" diff --git a/packages/rescript-relay/src/RescriptRelay_Internal.resi b/packages/rescript-relay/src/RescriptRelay_Internal.resi index 964343e1..4496c2f5 100644 --- a/packages/rescript-relay/src/RescriptRelay_Internal.resi +++ b/packages/rescript-relay/src/RescriptRelay_Internal.resi @@ -8,3 +8,11 @@ let internal_nullableToOptionalExnHandler: option => 'a> => option< let unwrapUnion: ('a, array) => 'a let wrapUnion: 'a => 'a + +external internal_resolverFragmentRefsToFragmentRefs: RescriptRelay.resolverFragmentRefs< + 'a, +> => RescriptRelay.fragmentRefs<'a> = "%identity" + +external internal_resolverFragmentRefsToFragmentRefsPlural: RescriptRelay.resolverFragmentRefs< + 'a, +> => array> = "%identity"