diff --git a/CHANGELOG.md b/CHANGELOG.md index a811e684..b0f1bb5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # master - Add support for the new Relay `@catch` directive. https://github.com/zth/rescript-relay/pull/549 +- Add support for `usePrefetchableForwardPagination`. https://github.com/zth/rescript-relay/pull/551 +- Remove `useBlockingPagination` since it's being removed from Relay. # 3.1.0 diff --git a/packages/rescript-relay/__tests__/Test_prefetchablePagination-tests.js b/packages/rescript-relay/__tests__/Test_prefetchablePagination-tests.js new file mode 100644 index 00000000..4e5a151d --- /dev/null +++ b/packages/rescript-relay/__tests__/Test_prefetchablePagination-tests.js @@ -0,0 +1,94 @@ +const t = require("@testing-library/react"); +const React = require("react"); +const queryMock = require("./queryMock"); +const ReactTestUtils = require("react-dom/test-utils"); + +const { + test_prefetchablePagination, +} = require("./Test_prefetchablePagination.bs"); + +describe("Prefetchable pagination", () => { + test("prefetching works", async () => { + queryMock.mockQuery({ + name: "TestPrefetchablePaginationQuery", + data: { + loggedInUser: { + __typename: "User", + id: "user-1", + friendsConnection: { + pageInfo: { + endCursor: "2", + hasNextPage: true, + startCursor: "", + hasPreviousPage: false, + }, + edges: [ + { + cursor: "1", + node: { + id: "user-2", + __typename: "User", + }, + }, + { + cursor: "2", + node: { + id: "user-3", + __typename: "User", + }, + }, + ], + }, + }, + }, + }); + + queryMock.mockQuery({ + name: "TestPrefetchablePaginationRefetchQuery", + variables: { + count: 2, + cursor: "2", + id: "user-1", + }, + data: { + node: { + __typename: "User", + id: "user-1", + friendsConnection: { + pageInfo: { + endCursor: "4", + hasNextPage: false, + startCursor: "", + hasPreviousPage: false, + }, + edges: [ + { + cursor: "3", + node: { + id: "user-4", + __typename: "User", + }, + }, + { + cursor: "4", + node: { + id: "user-5", + __typename: "User", + }, + }, + ], + }, + }, + }, + }); + + t.render(test_prefetchablePagination()); + await t.screen.findByText("user-2, user-3"); + + ReactTestUtils.act(() => { + t.fireEvent.click(t.screen.getByText("Load more")); + }); + + await t.screen.findByText("user-2, user-3, user-4, user-5"); + }); +}); diff --git a/packages/rescript-relay/__tests__/Test_prefetchablePagination.res b/packages/rescript-relay/__tests__/Test_prefetchablePagination.res new file mode 100644 index 00000000..ee29f5ae --- /dev/null +++ b/packages/rescript-relay/__tests__/Test_prefetchablePagination.res @@ -0,0 +1,67 @@ +module Query = %relay(` + query TestPrefetchablePaginationQuery { + loggedInUser { + ...TestPrefetchablePagination_user + } + } +`) + +module Fragment = %relay(` + fragment TestPrefetchablePagination_user on User + @refetchable(queryName: "TestPrefetchablePaginationRefetchQuery") + @argumentDefinitions( + count: { type: "Int", defaultValue: 2 } + cursor: { type: "String" } + ) { + friendsConnection( + first: $count + after: $cursor + ) @connection(key: "TestPrefetchablePagination_friendsConnection", prefetchable_pagination: true) { + edges { + node { + id + } + } + } + } +`) + +module Test = { + @react.component + let make = () => { + let query = Query.use(~variables=()) + let {edges, hasNext, isLoadingNext, loadNext} = Fragment.usePrefetchableForwardPagination( + query.loggedInUser.fragmentRefs, + ~bufferSize=2, + ) + +
+
+ {edges + ->Belt.Array.keepMap(({node}) => node) + ->Belt.Array.map(friend => friend.id) + ->Js.Array2.joinWith(", ") + ->React.string} +
+ {hasNext + ? + : React.null} +
+ } +} + +@live +let test_prefetchablePagination = () => { + let network = RescriptRelay.Network.makePromiseBased(~fetchFunction=RelayEnv.fetchQuery) + + let environment = RescriptRelay.Environment.make( + ~network, + ~store=RescriptRelay.Store.make(~source=RescriptRelay.RecordSource.make()), + ) + + + + +} diff --git a/packages/rescript-relay/__tests__/__generated__/TestPrefetchablePaginationQuery_graphql.res b/packages/rescript-relay/__tests__/__generated__/TestPrefetchablePaginationQuery_graphql.res new file mode 100644 index 00000000..556ca020 --- /dev/null +++ b/packages/rescript-relay/__tests__/__generated__/TestPrefetchablePaginationQuery_graphql.res @@ -0,0 +1,279 @@ +/* @sourceLoc Test_prefetchablePagination.res */ +/* @generated */ +%%raw("/* @generated */") +module Types = { + @@warning("-30") + + type rec response_loggedInUser = { + fragmentRefs: RescriptRelay.fragmentRefs<[ | #TestPrefetchablePagination_user]>, + } + type response = { + loggedInUser: response_loggedInUser, + } + @live + type rawResponse = response + @live + type variables = unit + @live + type refetchVariables = unit + @live let makeRefetchVariables = () => () +} + + +type queryRef + +module Internal = { + @live + let variablesConverter: Js.Dict.t>> = %raw( + json`{}` + ) + @live + let variablesConverterMap = () + @live + let convertVariables = v => v->RescriptRelay.convertObj( + variablesConverter, + variablesConverterMap, + Js.undefined + ) + @live + type wrapResponseRaw + @live + let wrapResponseConverter: Js.Dict.t>> = %raw( + json`{"__root":{"loggedInUser":{"f":""}}}` + ) + @live + let wrapResponseConverterMap = () + @live + let convertWrapResponse = v => v->RescriptRelay.convertObj( + wrapResponseConverter, + wrapResponseConverterMap, + Js.null + ) + @live + type responseRaw + @live + let responseConverter: Js.Dict.t>> = %raw( + json`{"__root":{"loggedInUser":{"f":""}}}` + ) + @live + let responseConverterMap = () + @live + let convertResponse = v => v->RescriptRelay.convertObj( + responseConverter, + responseConverterMap, + Js.undefined + ) + type wrapRawResponseRaw = wrapResponseRaw + @live + let convertWrapRawResponse = convertWrapResponse + type rawResponseRaw = responseRaw + @live + let convertRawResponse = convertResponse + type rawPreloadToken<'response> = {source: Js.Nullable.t>} + external tokenToRaw: queryRef => rawPreloadToken = "%identity" +} +module Utils = { + @@warning("-33") + open Types +} + +type relayOperationNode +type operationType = RescriptRelay.queryNode + + +let node: operationType = %raw(json` (function(){ +var v0 = [ + { + "kind": "Literal", + "name": "first", + "value": 2 + } +], +v1 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "TestPrefetchablePaginationQuery", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "User", + "kind": "LinkedField", + "name": "loggedInUser", + "plural": false, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "TestPrefetchablePagination_user" + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [], + "kind": "Operation", + "name": "TestPrefetchablePaginationQuery", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "User", + "kind": "LinkedField", + "name": "loggedInUser", + "plural": false, + "selections": [ + { + "alias": null, + "args": (v0/*: any*/), + "concreteType": "UserConnection", + "kind": "LinkedField", + "name": "friendsConnection", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "UserEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "User", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v1/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": "friendsConnection(first:2)" + }, + { + "alias": null, + "args": (v0/*: any*/), + "filters": null, + "handle": "connection", + "key": "TestPrefetchablePagination_friendsConnection", + "kind": "LinkedHandle", + "name": "friendsConnection" + }, + (v1/*: any*/) + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "51afb9846e52cc8eb683cb4382261e77", + "id": null, + "metadata": {}, + "name": "TestPrefetchablePaginationQuery", + "operationKind": "query", + "text": "query TestPrefetchablePaginationQuery {\n loggedInUser {\n ...TestPrefetchablePagination_user\n id\n }\n}\n\nfragment TestPrefetchablePagination_user on User {\n friendsConnection(first: 2) {\n edges {\n ...TestPrefetchablePagination_user__edges\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n id\n}\n\nfragment TestPrefetchablePagination_user__edges on UserEdge {\n node {\n id\n __typename\n }\n cursor\n}\n" + } +}; +})() `) + +@live let load: ( + ~environment: RescriptRelay.Environment.t, + ~variables: Types.variables, + ~fetchPolicy: RescriptRelay.fetchPolicy=?, + ~fetchKey: string=?, + ~networkCacheConfig: RescriptRelay.cacheConfig=?, +) => queryRef = ( + ~environment, + ~variables, + ~fetchPolicy=?, + ~fetchKey=?, + ~networkCacheConfig=?, +) => + RescriptRelay.loadQuery( + environment, + node, + variables->Internal.convertVariables, + { + fetchKey, + fetchPolicy, + networkCacheConfig, + }, + ) + +@live +let queryRefToObservable = token => { + let raw = token->Internal.tokenToRaw + raw.source->Js.Nullable.toOption +} + +@live +let queryRefToPromise = token => { + Js.Promise.make((~resolve, ~reject as _) => { + switch token->queryRefToObservable { + | None => resolve(Error()) + | Some(o) => + open RescriptRelay.Observable + let _: subscription = o->subscribe(makeObserver(~complete=() => resolve(Ok()))) + } + }) +} diff --git a/packages/rescript-relay/__tests__/__generated__/TestPrefetchablePaginationRefetchQuery_graphql.res b/packages/rescript-relay/__tests__/__generated__/TestPrefetchablePaginationRefetchQuery_graphql.res new file mode 100644 index 00000000..fc47adf7 --- /dev/null +++ b/packages/rescript-relay/__tests__/__generated__/TestPrefetchablePaginationRefetchQuery_graphql.res @@ -0,0 +1,349 @@ +/* @sourceLoc Test_prefetchablePagination.res */ +/* @generated */ +%%raw("/* @generated */") +module Types = { + @@warning("-30") + + @live + type rec response_node = { + @live __typename: string, + fragmentRefs: RescriptRelay.fragmentRefs<[ | #TestPrefetchablePagination_user]>, + } + @live + type response = { + node: option, + } + @live + type rawResponse = response + @live + type variables = { + count?: int, + cursor?: string, + @live id: string, + } + @live + type refetchVariables = { + count: option>, + cursor: option>, + @live id: option, + } + @live let makeRefetchVariables = ( + ~count=?, + ~cursor=?, + ~id=?, + ): refetchVariables => { + count: count, + cursor: cursor, + id: id + } + +} + + +type queryRef + +module Internal = { + @live + let variablesConverter: Js.Dict.t>> = %raw( + json`{}` + ) + @live + let variablesConverterMap = () + @live + let convertVariables = v => v->RescriptRelay.convertObj( + variablesConverter, + variablesConverterMap, + Js.undefined + ) + @live + type wrapResponseRaw + @live + let wrapResponseConverter: Js.Dict.t>> = %raw( + json`{"__root":{"node":{"f":""}}}` + ) + @live + let wrapResponseConverterMap = () + @live + let convertWrapResponse = v => v->RescriptRelay.convertObj( + wrapResponseConverter, + wrapResponseConverterMap, + Js.null + ) + @live + type responseRaw + @live + let responseConverter: Js.Dict.t>> = %raw( + json`{"__root":{"node":{"f":""}}}` + ) + @live + let responseConverterMap = () + @live + let convertResponse = v => v->RescriptRelay.convertObj( + responseConverter, + responseConverterMap, + Js.undefined + ) + type wrapRawResponseRaw = wrapResponseRaw + @live + let convertWrapRawResponse = convertWrapResponse + type rawResponseRaw = responseRaw + @live + let convertRawResponse = convertResponse + type rawPreloadToken<'response> = {source: Js.Nullable.t>} + external tokenToRaw: queryRef => rawPreloadToken = "%identity" +} +module Utils = { + @@warning("-33") + open Types +} + +type relayOperationNode +type operationType = RescriptRelay.queryNode + + +let node: operationType = %raw(json` (function(){ +var v0 = [ + { + "defaultValue": 2, + "kind": "LocalArgument", + "name": "count" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "cursor" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "id" + } +], +v1 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "id" + } +], +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v4 = [ + { + "kind": "Variable", + "name": "after", + "variableName": "cursor" + }, + { + "kind": "Variable", + "name": "first", + "variableName": "count" + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "TestPrefetchablePaginationRefetchQuery", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + { + "args": [ + { + "kind": "Variable", + "name": "count", + "variableName": "count" + }, + { + "kind": "Variable", + "name": "cursor", + "variableName": "cursor" + } + ], + "kind": "FragmentSpread", + "name": "TestPrefetchablePagination_user" + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "TestPrefetchablePaginationRefetchQuery", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + (v3/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": (v4/*: any*/), + "concreteType": "UserConnection", + "kind": "LinkedField", + "name": "friendsConnection", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "UserEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "User", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/), + (v2/*: any*/) + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": (v4/*: any*/), + "filters": null, + "handle": "connection", + "key": "TestPrefetchablePagination_friendsConnection", + "kind": "LinkedHandle", + "name": "friendsConnection" + } + ], + "type": "User", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "10c72bc33721e51cf118fdf91004b9de", + "id": null, + "metadata": {}, + "name": "TestPrefetchablePaginationRefetchQuery", + "operationKind": "query", + "text": "query TestPrefetchablePaginationRefetchQuery(\n $count: Int = 2\n $cursor: String\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...TestPrefetchablePagination_user_1G22uz\n id\n }\n}\n\nfragment TestPrefetchablePagination_user_1G22uz on User {\n friendsConnection(first: $count, after: $cursor) {\n edges {\n ...TestPrefetchablePagination_user__edges\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n id\n}\n\nfragment TestPrefetchablePagination_user__edges on UserEdge {\n node {\n id\n __typename\n }\n cursor\n}\n" + } +}; +})() `) + +@live let load: ( + ~environment: RescriptRelay.Environment.t, + ~variables: Types.variables, + ~fetchPolicy: RescriptRelay.fetchPolicy=?, + ~fetchKey: string=?, + ~networkCacheConfig: RescriptRelay.cacheConfig=?, +) => queryRef = ( + ~environment, + ~variables, + ~fetchPolicy=?, + ~fetchKey=?, + ~networkCacheConfig=?, +) => + RescriptRelay.loadQuery( + environment, + node, + variables->Internal.convertVariables, + { + fetchKey, + fetchPolicy, + networkCacheConfig, + }, + ) + +@live +let queryRefToObservable = token => { + let raw = token->Internal.tokenToRaw + raw.source->Js.Nullable.toOption +} + +@live +let queryRefToPromise = token => { + Js.Promise.make((~resolve, ~reject as _) => { + switch token->queryRefToObservable { + | None => resolve(Error()) + | Some(o) => + open RescriptRelay.Observable + let _: subscription = o->subscribe(makeObserver(~complete=() => resolve(Ok()))) + } + }) +} diff --git a/packages/rescript-relay/__tests__/__generated__/TestPrefetchablePagination_user__edges_graphql.res b/packages/rescript-relay/__tests__/__generated__/TestPrefetchablePagination_user__edges_graphql.res new file mode 100644 index 00000000..df23b9b5 --- /dev/null +++ b/packages/rescript-relay/__tests__/__generated__/TestPrefetchablePagination_user__edges_graphql.res @@ -0,0 +1,93 @@ +/* @sourceLoc Test_prefetchablePagination.res */ +/* @generated */ +%%raw("/* @generated */") +module Types = { + @@warning("-30") + + type rec fragment_node = { + @live __typename: [ | #User], + @live id: string, + } + type fragment_t = { + cursor: string, + node: option, + } + type fragment = array +} + +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: + array | #TestPrefetchablePagination_user__edges]>> => fragmentRef = "%identity" + +module Utils = { + @@warning("-33") + open Types +} + +type relayOperationNode +type operationType = RescriptRelay.fragmentNode + + +let node: operationType = %raw(json` { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": { + "plural": true + }, + "name": "TestPrefetchablePagination_user__edges", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "User", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + } + ], + "type": "UserEdge", + "abstractKey": null +} `) + diff --git a/packages/rescript-relay/__tests__/__generated__/TestPrefetchablePagination_user_graphql.res b/packages/rescript-relay/__tests__/__generated__/TestPrefetchablePagination_user_graphql.res new file mode 100644 index 00000000..510a6fd3 --- /dev/null +++ b/packages/rescript-relay/__tests__/__generated__/TestPrefetchablePagination_user_graphql.res @@ -0,0 +1,173 @@ +/* @sourceLoc Test_prefetchablePagination.res */ +/* @generated */ +%%raw("/* @generated */") +module Types = { + @@warning("-30") + + type rec fragment_friendsConnection_edges = { + fragmentRefs: RescriptRelay.fragmentRefs<[ | #TestPrefetchablePagination_user__edges]>, + } + and fragment_friendsConnection_pageInfo = { + endCursor: option, + hasNextPage: bool, + } + and fragment_friendsConnection = { + edges: option>>, + pageInfo: fragment_friendsConnection_pageInfo, + } + type fragment = { + friendsConnection: fragment_friendsConnection, + @live id: string, + } +} + +module Internal = { + @live + type fragmentRaw + @live + let fragmentConverter: Js.Dict.t>> = %raw( + json`{"__root":{"friendsConnection_edges":{"f":""}}}` + ) + @live + let fragmentConverterMap = () + @live + let convertFragment = v => v->RescriptRelay.convertObj( + fragmentConverter, + fragmentConverterMap, + Js.undefined + ) +} + +type t +type fragmentRef +external getFragmentRef: + RescriptRelay.fragmentRefs<[> | #TestPrefetchablePagination_user]> => fragmentRef = "%identity" + +module Utils = { + @@warning("-33") + open Types +} + +type relayOperationNode +type operationType = RescriptRelay.fragmentNode + + +%%private(let makeNode = (rescript_graphql_node_TestPrefetchablePaginationRefetchQuery, rescript_graphql_node_TestPrefetchablePagination_user__edges): operationType => { + ignore(rescript_graphql_node_TestPrefetchablePaginationRefetchQuery) + ignore(rescript_graphql_node_TestPrefetchablePagination_user__edges) + %raw(json`(function(){ +var v0 = [ + "friendsConnection" +]; +return { + "argumentDefinitions": [ + { + "defaultValue": 2, + "kind": "LocalArgument", + "name": "count" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "cursor" + } + ], + "kind": "Fragment", + "metadata": { + "connection": [ + { + "count": "count", + "cursor": "cursor", + "direction": "forward", + "path": (v0/*: any*/) + } + ], + "refetch": { + "connection": { + "forward": { + "count": "count", + "cursor": "cursor" + }, + "backward": null, + "path": (v0/*: any*/) + }, + "fragmentPathInResult": [ + "node" + ], + "operation": rescript_graphql_node_TestPrefetchablePaginationRefetchQuery, + "identifierInfo": { + "identifierField": "id", + "identifierQueryVariableName": "id" + }, + "edgesFragment": rescript_graphql_node_TestPrefetchablePagination_user__edges + } + }, + "name": "TestPrefetchablePagination_user", + "selections": [ + { + "alias": "friendsConnection", + "args": null, + "concreteType": "UserConnection", + "kind": "LinkedField", + "name": "__TestPrefetchablePagination_friendsConnection_connection", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "UserEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "TestPrefetchablePagination_user__edges" + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + } + ], + "type": "User", + "abstractKey": null +}; +})()`) +}) +let node: operationType = makeNode(TestPrefetchablePaginationRefetchQuery_graphql.node, TestPrefetchablePagination_user__edges_graphql.node) + diff --git a/packages/rescript-relay/rescript-relay-ppx/library/Fragment.ml b/packages/rescript-relay/rescript-relay-ppx/library/Fragment.ml index 13c12bad..1e9fc343 100644 --- a/packages/rescript-relay/rescript-relay-ppx/library/Fragment.ml +++ b/packages/rescript-relay/rescript-relay-ppx/library/Fragment.ml @@ -1,12 +1,12 @@ open Ppxlib open Util -let make ~loc ~moduleName ~refetchableQueryName ~extractedConnectionInfo - ~hasInlineDirective ~isPlural ~hasAutocodesplitDirective = +let make ~loc ~moduleName ~refetchableQueryName + ~(connectionInfo : connectionInfo option) ~hasInlineDirective ~isPlural + ~hasAutocodesplitDirective = let typeFromGeneratedModule = makeTypeAccessor ~loc ~moduleName in let valFromGeneratedModule = makeExprAccessor ~loc ~moduleName in let moduleIdentFromGeneratedModule = makeModuleIdent ~loc ~moduleName in - let hasConnection = extractedConnectionInfo in Ast_helper.Mod.mk (Pmod_structure (List.concat @@ -116,7 +116,7 @@ let make ~loc ~moduleName ~refetchableQueryName ~extractedConnectionInfo | false, false -> []); (match (!NonReactUtils.enabled, refetchableQueryName) with | true, _ | false, None -> [] - | false, Some refetchableQueryName -> + | false, Some refetchableQueryName -> ( let typeFromRefetchableModule = makeTypeAccessor ~loc ~moduleName:refetchableQueryName in @@ -147,7 +147,8 @@ let make ~loc ~moduleName ~refetchableQueryName ~extractedConnectionInfo ~node:[%e valFromGeneratedModule ["node"]]]; ] @ - if hasConnection then + match connectionInfo with + | Some {prefetchable_pagination = false} -> [ [%stri let usePagination fRef = @@ -157,15 +158,35 @@ let make ~loc ~moduleName ~refetchableQueryName ~extractedConnectionInfo (fRef |. [%e valFromGeneratedModule ["getFragmentRef"]]) ~node:[%e valFromGeneratedModule ["node"]]]; + ] + | Some {prefetchable_pagination = true} -> + let edgesModuleName = moduleName ^ "__edges" in + let typeFromEdgesModule = + makeTypeAccessor ~loc ~moduleName:edgesModuleName + in + let valFromEdgesModule = + makeExprAccessor ~loc ~moduleName:edgesModuleName + in + + [ [%stri - let useBlockingPagination fRef = - RescriptRelay_Fragment.useBlockingPaginationFragment - ~convertFragment ~convertRefetchVariables + let convertEdges : + [%t typeFromEdgesModule ["Types"; "fragment"]] -> + [%t typeFromEdgesModule ["Types"; "fragment"]] = + [%e valFromEdgesModule ["Internal"; "convertFragment"]]]; + [%stri + let usePrefetchableForwardPagination ~bufferSize + ?initialSize ?prefetchingLoadMoreOptions + ?minimumFetchSize fRef = + RescriptRelay_Fragment.usePrefetchableForwardPagination + ~convertFragment ~convertEdges ~convertRefetchVariables ~fRef: (fRef |. [%e valFromGeneratedModule ["getFragmentRef"]]) - ~node:[%e valFromGeneratedModule ["node"]]]; + ~node:[%e valFromGeneratedModule ["node"]] + ~bufferSize ?initialSize ?prefetchingLoadMoreOptions + ?minimumFetchSize]; ] - else []); + | None -> [])); ] |> List.map UncurriedUtils.mapStructureItem)) diff --git a/packages/rescript-relay/rescript-relay-ppx/library/RescriptRelayPpxLibrary.ml b/packages/rescript-relay/rescript-relay-ppx/library/RescriptRelayPpxLibrary.ml index 86b53e60..6766d33c 100644 --- a/packages/rescript-relay/rescript-relay-ppx/library/RescriptRelayPpxLibrary.ml +++ b/packages/rescript-relay/rescript-relay-ppx/library/RescriptRelayPpxLibrary.ml @@ -22,12 +22,12 @@ let commonExtension = let refetchableQueryName = op |> extractFragmentRefetchableQueryName ~loc in + let connectionInfo = op |> extractFragmentConnectionInfo ~loc in Fragment.make ~hasAutocodesplitDirective: (selection_set |> Util.hasAutocodesplitDirective) ~moduleName:(op |> extractTheFragmentName ~loc) - ~refetchableQueryName - ~extractedConnectionInfo:(op |> extractFragmentConnectionInfo ~loc) + ~refetchableQueryName ~connectionInfo ~hasInlineDirective:(op |> fragmentHasInlineDirective ~loc) ~isPlural:(op |> fragmentIsPlural ~loc) ~loc diff --git a/packages/rescript-relay/rescript-relay-ppx/library/Util.ml b/packages/rescript-relay/rescript-relay-ppx/library/Util.ml index 6e8d2276..1ee397b2 100755 --- a/packages/rescript-relay/rescript-relay-ppx/library/Util.ml +++ b/packages/rescript-relay/rescript-relay-ppx/library/Util.ml @@ -58,27 +58,30 @@ let extractFragmentRefetchableQueryName ~loc op = | _ -> ()); !refetchableQueryName | _ -> None -let rec selectionSetHasConnection selections = - match - selections - |> List.find_opt (fun sel -> - match sel with - | Graphql_parser.Field {directives; selection_set} -> ( - match - directives - |> List.find_opt (fun (dir : Graphql_parser.directive) -> - match dir with - | {name = "connection"} -> true - | _ -> false) - with - | Some _ -> true - | None -> selectionSetHasConnection selection_set) - | InlineFragment {selection_set} -> - selectionSetHasConnection selection_set - | _ -> false) - with - | Some _ -> true - | None -> false +type connectionInfo = {prefetchable_pagination: bool} +let rec findConnectionInfo selections = + selections + |> List.find_map (fun sel -> + match sel with + | Graphql_parser.Field {directives; selection_set} -> ( + match + directives + |> List.find_opt (fun (dir : Graphql_parser.directive) -> + match dir with + | {name = "connection"} -> true + | _ -> false) + with + | Some connectionDirective -> + Some + { + prefetchable_pagination = + connectionDirective.arguments + |> List.exists (fun (n, v) -> + n = "prefetchable_pagination" && v = `Bool true); + } + | None -> findConnectionInfo selection_set) + | InlineFragment {selection_set} -> findConnectionInfo selection_set + | _ -> None) let queryHasRawResponseTypeDirective ~loc op = match op with | Graphql_parser.Operation {optype = Query; name = Some _; directives} -> @@ -100,12 +103,11 @@ let queryIsUpdatable op = |> List.exists (fun (directive : Graphql_parser.directive) -> directive.name = "updatable") | _ -> false -type connectionConfig = {key: string} let extractFragmentConnectionInfo ~loc op = match op with | Graphql_parser.Fragment {name = _; selection_set} -> - selectionSetHasConnection selection_set - | _ -> false + findConnectionInfo selection_set + | _ -> None let fragmentHasInlineDirective ~loc op = match op with | Graphql_parser.Fragment {name = _; directives} -> diff --git a/packages/rescript-relay/src/RescriptRelay_Fragment.res b/packages/rescript-relay/src/RescriptRelay_Fragment.res index 601bd5a9..a6c0f7f3 100644 --- a/packages/rescript-relay/src/RescriptRelay_Fragment.res +++ b/packages/rescript-relay/src/RescriptRelay_Fragment.res @@ -84,18 +84,6 @@ type paginationFragmentReturnRaw<'fragment, 'refetchVariables> = { isLoadingPrevious: bool, refetch: ('refetchVariables, refetchableFnOpts) => Disposable.t, } -type paginationBlockingFragmentReturn<'fragment, 'refetchVariables> = { - data: 'fragment, - loadNext: paginationLoadMoreFn, - loadPrevious: paginationLoadMoreFn, - hasNext: bool, - hasPrevious: bool, - refetch: ( - ~variables: 'refetchVariables, - ~fetchPolicy: fetchPolicy=?, - ~onComplete: option => unit=?, - ) => Disposable.t, -} type paginationFragmentReturn<'fragment, 'refetchVariables> = { data: 'fragment, loadNext: paginationLoadMoreFn, @@ -117,10 +105,7 @@ external usePaginationFragment_: ( 'fragmentRef, ) => paginationFragmentReturnRaw<'fragment, 'refetchVariables> = "usePaginationFragment" -/** React hook for paginating a fragment. Paginating with \ - this hook will _not_ cause your component to suspend. \ - If you want pagination to trigger suspense, look into \ - using `Fragment.useBlockingPagination`.*/ +/** React hook for paginating a fragment. Paginating with this hook will _not_ cause your component to suspend. */ let usePaginationFragment = ( ~node, ~fRef, @@ -158,39 +143,71 @@ let usePaginationFragment = ( } } -@module("react-relay/lib/relay-hooks/legacy/useBlockingPaginationFragment") -external useBlockingPaginationFragment_: ( +type prefetchableForwardPaginationFragmentReturnRaw<'fragment, 'edges, 'refetchVariables> = { + data: 'fragment, + edges: 'edges, + loadNext: (int, paginationLoadMoreOptions) => Disposable.t, + hasNext: bool, + isLoadingNext: bool, + refetch: ('refetchVariables, refetchableFnOpts) => Disposable.t, +} +type prefetchableForwardPaginationFragmentReturn<'fragment, 'edges, 'refetchVariables> = { + data: 'fragment, + edges: 'edges, + loadNext: paginationLoadMoreFn, + hasNext: bool, + isLoadingNext: bool, + refetch: ( + ~variables: 'refetchVariables, + ~fetchPolicy: fetchPolicy=?, + ~onComplete: option => unit=?, + ) => Disposable.t, +} + +@module("react-relay") +external usePrefetchableForwardPaginationFragment_: ( fragmentNode<'node>, 'fragmentRef, -) => paginationFragmentReturnRaw<'fragment, 'refetchVariables> = "default" + ~bufferSize: int, + ~initialSize: int=?, + ~prefetchingLoadMoreOptions: paginationLoadMoreOptions=?, + ~minimumFetchSize: int=?, +) => prefetchableForwardPaginationFragmentReturnRaw<'fragment, 'edges, 'refetchVariables> = + "usePrefetchableForwardPaginationFragment_EXPERIMENTAL" -/** Like `Fragment.usePagination`, but calling the \ - pagination function will trigger suspense. Useful for \ - all-at-once pagination.*/ -let useBlockingPaginationFragment = ( +/** React hook for paginating a fragment. Paginating with this hook will _not_ cause your component to suspend. */ +let usePrefetchableForwardPagination = ( ~node, ~fRef, + ~convertEdges: 'edges => 'edges, ~convertFragment: 'fragment => 'fragment, ~convertRefetchVariables: 'refetchVariables => 'refetchVariables, + ~bufferSize: int, + ~initialSize: option=?, + ~prefetchingLoadMoreOptions: option=?, + ~minimumFetchSize: option=?, ) => { - let p = useBlockingPaginationFragment_(node, fRef) + let p = usePrefetchableForwardPaginationFragment_( + node, + fRef, + ~bufferSize, + ~initialSize?, + ~prefetchingLoadMoreOptions?, + ~minimumFetchSize?, + ) let data = RescriptRelay_Internal.internal_useConvertedValue(convertFragment, p.data) + let edges = RescriptRelay_Internal.internal_useConvertedValue(convertEdges, p.edges) { data, + edges, loadNext: React.useMemo1(() => (~count, ~onComplete=?) => { p.loadNext( count, {onComplete: ?onComplete->RescriptRelay_Internal.internal_nullableToOptionalExnHandler}, ) }, [p.loadNext]), - loadPrevious: React.useMemo1(() => (~count, ~onComplete=?) => { - p.loadPrevious( - count, - {onComplete: ?onComplete->RescriptRelay_Internal.internal_nullableToOptionalExnHandler}, - ) - }, [p.loadPrevious]), hasNext: p.hasNext, - hasPrevious: p.hasPrevious, + isLoadingNext: p.isLoadingNext, refetch: React.useMemo1(() => (~variables, ~fetchPolicy=?, ~onComplete=?) => { p.refetch( RescriptRelay_Internal.internal_cleanObjectFromUndefinedRaw( diff --git a/packages/rescript-relay/src/RescriptRelay_Fragment.resi b/packages/rescript-relay/src/RescriptRelay_Fragment.resi index 3cd6bf3d..b215b934 100644 --- a/packages/rescript-relay/src/RescriptRelay_Fragment.resi +++ b/packages/rescript-relay/src/RescriptRelay_Fragment.resi @@ -28,12 +28,14 @@ type paginationLoadMoreOptions = {onComplete?: Js.Nullable.t => unit} type paginationLoadMoreFn = (~count: int, ~onComplete: option => unit=?) => Disposable.t -type paginationBlockingFragmentReturn<'fragment, 'refetchVariables> = { +type paginationFragmentReturn<'fragment, 'refetchVariables> = { data: 'fragment, loadNext: paginationLoadMoreFn, loadPrevious: paginationLoadMoreFn, hasNext: bool, hasPrevious: bool, + isLoadingNext: bool, + isLoadingPrevious: bool, refetch: ( ~variables: 'refetchVariables, ~fetchPolicy: fetchPolicy=?, @@ -41,14 +43,19 @@ type paginationBlockingFragmentReturn<'fragment, 'refetchVariables> = { ) => Disposable.t, } -type paginationFragmentReturn<'fragment, 'refetchVariables> = { +let usePaginationFragment: ( + ~node: fragmentNode<'a>, + ~fRef: 'b, + ~convertFragment: 'fragment => 'fragment, + ~convertRefetchVariables: 'refetchVariables => 'refetchVariables, +) => paginationFragmentReturn<'fragment, 'refetchVariables> + +type prefetchableForwardPaginationFragmentReturn<'fragment, 'edges, 'refetchVariables> = { data: 'fragment, + edges: 'edges, loadNext: paginationLoadMoreFn, - loadPrevious: paginationLoadMoreFn, hasNext: bool, - hasPrevious: bool, isLoadingNext: bool, - isLoadingPrevious: bool, refetch: ( ~variables: 'refetchVariables, ~fetchPolicy: fetchPolicy=?, @@ -56,19 +63,17 @@ type paginationFragmentReturn<'fragment, 'refetchVariables> = { ) => Disposable.t, } -let usePaginationFragment: ( - ~node: fragmentNode<'a>, - ~fRef: 'b, - ~convertFragment: 'fragment => 'fragment, - ~convertRefetchVariables: 'refetchVariables => 'refetchVariables, -) => paginationFragmentReturn<'fragment, 'refetchVariables> - -let useBlockingPaginationFragment: ( +let usePrefetchableForwardPagination: ( ~node: fragmentNode<'a>, ~fRef: 'b, + ~convertEdges: 'edges => 'edges, ~convertFragment: 'fragment => 'fragment, ~convertRefetchVariables: 'refetchVariables => 'refetchVariables, -) => paginationBlockingFragmentReturn<'fragment, 'refetchVariables> + ~bufferSize: int, + ~initialSize: int=?, + ~prefetchingLoadMoreOptions: paginationLoadMoreOptions=?, + ~minimumFetchSize: int=?, +) => prefetchableForwardPaginationFragmentReturn<'fragment, 'edges, 'refetchVariables> let useRefetchableFragment: ( ~node: fragmentNode<'a>,