diff --git a/compiler/crates/relay-codegen/src/build_ast.rs b/compiler/crates/relay-codegen/src/build_ast.rs index 565f4809f35cd..fc15095b32fa4 100644 --- a/compiler/crates/relay-codegen/src/build_ast.rs +++ b/compiler/crates/relay-codegen/src/build_ast.rs @@ -7,6 +7,10 @@ use std::path::PathBuf; +use ::intern::intern; +use ::intern::string_key::Intern; +use ::intern::string_key::StringKey; +use ::intern::Lookup; use common::DirectiveName; use common::FeatureFlag; use common::NamedItem; @@ -32,9 +36,6 @@ use graphql_ir::Selection; use graphql_ir::Value; use graphql_ir::VariableDefinition; use graphql_syntax::OperationKind; -use intern::string_key::Intern; -use intern::string_key::StringKey; -use intern::Lookup; use lazy_static::lazy_static; use md5::Digest; use md5::Md5; @@ -595,6 +596,16 @@ impl<'schema, 'builder, 'config> CodegenBuilder<'schema, 'builder, 'config> { })), }); } + if refetch_metadata.is_prefetchable_pagination { + refetch_object.push(ObjectEntry { + key: intern!("edgesFragment"), + value: Primitive::GraphQLModuleDependency(GraphQLModuleDependency::Name( + ExecutableDefinitionName::FragmentDefinitionName(FragmentDefinitionName( + format!("{}__edges", fragment.name.item).intern(), + )), + )), + }); + } metadata.push(ObjectEntry { key: CODEGEN_CONSTANTS.refetch, diff --git a/compiler/crates/relay-codegen/tests/connections.rs b/compiler/crates/relay-codegen/tests/connections.rs index 9d7bdebf6185a..3c50397169b6d 100644 --- a/compiler/crates/relay-codegen/tests/connections.rs +++ b/compiler/crates/relay-codegen/tests/connections.rs @@ -47,8 +47,12 @@ pub async fn transform_fixture(fixture: &Fixture<'_>) -> Result validate_connections(&program, &connection_interface) .map_err(|diagnostics| diagnostics_to_sorted_string(fixture.content, &diagnostics))?; - let next_program = - transform_connections(&program, &connection_interface, &defer_stream_interface); + let next_program = transform_connections( + &program, + &connection_interface, + &defer_stream_interface, + false, + ); let mut printed = next_program .operations() diff --git a/compiler/crates/relay-compiler/src/build_project/artifact_generated_types.rs b/compiler/crates/relay-compiler/src/build_project/artifact_generated_types.rs index d53a727904e8d..ead7238e647e4 100644 --- a/compiler/crates/relay-compiler/src/build_project/artifact_generated_types.rs +++ b/compiler/crates/relay-compiler/src/build_project/artifact_generated_types.rs @@ -138,14 +138,26 @@ impl ArtifactGeneratedTypes { )), } } else if let Some(refetchable_metadata) = RefetchableMetadata::find(&fragment.directives) { - Self { - imported_types: "ReaderFragment, RefetchableFragment", - ast_type: "ReaderFragment", - exported_type: Some(format!( - "RefetchableFragment<\n {name}$fragmentType,\n {name}$data,\n {refetchable_name}$variables,\n>", - name = fragment.name.item, - refetchable_name = refetchable_metadata.operation_name - )), + if refetchable_metadata.is_prefetchable_pagination { + Self { + imported_types: "ReaderFragment, PrefetchableRefetchableFragment", + ast_type: "ReaderFragment", + exported_type: Some(format!( + "PrefetchableRefetchableFragment<\n {name}$fragmentType,\n {name}$data,\n {name}__edges$data,\n {refetchable_name}$variables,\n>", + name = fragment.name.item, + refetchable_name = refetchable_metadata.operation_name + )), + } + } else { + Self { + imported_types: "ReaderFragment, RefetchableFragment", + ast_type: "ReaderFragment", + exported_type: Some(format!( + "RefetchableFragment<\n {name}$fragmentType,\n {name}$data,\n {refetchable_name}$variables,\n>", + name = fragment.name.item, + refetchable_name = refetchable_metadata.operation_name + )), + } } } else if is_updatable_fragment { Self { diff --git a/compiler/crates/relay-compiler/tests/compile_relay_artifacts/fixtures/prefetchable-refetchable-fragment-with-connection.expected b/compiler/crates/relay-compiler/tests/compile_relay_artifacts/fixtures/prefetchable-refetchable-fragment-with-connection.expected new file mode 100644 index 0000000000000..eaa1126e760cb --- /dev/null +++ b/compiler/crates/relay-compiler/tests/compile_relay_artifacts/fixtures/prefetchable-refetchable-fragment-with-connection.expected @@ -0,0 +1,485 @@ +==================================== INPUT ==================================== +fragment prefetchableRefetchableFragmentWithConnection_PaginationFragment on Node + @refetchable(queryName: "RefetchableFragmentQuery") + @argumentDefinitions( + count: {type: "Int", defaultValue: 10} + cursor: {type: "ID"} + ) { + id + ... on User { + name + friends(after: $cursor, first: $count) + @connection(key: "PaginationFragment_friends", prefetchable_pagination: true) { + edges { + node { + id + } + } + } + } +} +==================================== OUTPUT =================================== +{ + "fragment": { + "argumentDefinitions": [ + { + "defaultValue": 10, + "kind": "LocalArgument", + "name": "count" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "cursor" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "id" + } + ], + "kind": "Fragment", + "metadata": null, + "name": "RefetchableFragmentQuery", + "selections": [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "id", + "variableName": "id" + } + ], + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "args": [ + { + "kind": "Variable", + "name": "count", + "variableName": "count" + }, + { + "kind": "Variable", + "name": "cursor", + "variableName": "cursor" + } + ], + "kind": "FragmentSpread", + "name": "prefetchableRefetchableFragmentWithConnection_PaginationFragment" + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + { + "defaultValue": 10, + "kind": "LocalArgument", + "name": "count" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "cursor" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "id" + } + ], + "kind": "Operation", + "name": "RefetchableFragmentQuery", + "selections": [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "id", + "variableName": "id" + } + ], + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + }, + { + "kind": "TypeDiscriminator", + "abstractKey": "__isNode" + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "after", + "variableName": "cursor" + }, + { + "kind": "Variable", + "name": "first", + "variableName": "count" + } + ], + "concreteType": "FriendsConnection", + "kind": "LinkedField", + "name": "friends", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "FriendsEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "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 + } + ], + "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": [ + { + "kind": "Variable", + "name": "after", + "variableName": "cursor" + }, + { + "kind": "Variable", + "name": "first", + "variableName": "count" + } + ], + "filters": null, + "handle": "connection", + "key": "PaginationFragment_friends", + "kind": "LinkedHandle", + "name": "friends" + } + ], + "type": "User", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "31dcd57f42128b8d975e6224202a17c9", + "id": null, + "metadata": {}, + "name": "RefetchableFragmentQuery", + "operationKind": "query", + "text": null + } +} + +QUERY: + +query RefetchableFragmentQuery( + $count: Int = 10 + $cursor: ID + $id: ID! +) { + node(id: $id) { + __typename + ...prefetchableRefetchableFragmentWithConnection_PaginationFragment_1G22uz + id + } +} + +fragment prefetchableRefetchableFragmentWithConnection_PaginationFragment_1G22uz on Node { + __isNode: __typename + id + ... on User { + name + friends(after: $cursor, first: $count) { + edges { + ...prefetchableRefetchableFragmentWithConnection_PaginationFragment__edges + } + pageInfo { + endCursor + hasNextPage + } + } + } +} + +fragment prefetchableRefetchableFragmentWithConnection_PaginationFragment__edges on FriendsEdge { + node { + id + __typename + } + cursor +} + + +{ + "argumentDefinitions": [ + { + "defaultValue": 10, + "kind": "LocalArgument", + "name": "count" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "cursor" + } + ], + "kind": "Fragment", + "metadata": { + "connection": [ + { + "count": "count", + "cursor": "cursor", + "direction": "forward", + "path": [ + "friends" + ] + } + ], + "refetch": { + "connection": { + "forward": { + "count": "count", + "cursor": "cursor" + }, + "backward": null, + "path": [ + "friends" + ] + }, + "fragmentPathInResult": [ + "node" + ], + "operation": require('RefetchableFragmentQuery.graphql'), + "identifierInfo": { + "identifierField": "id", + "identifierQueryVariableName": "id" + }, + "edgesFragment": require('prefetchableRefetchableFragmentWithConnection_PaginationFragment__edges.graphql') + } + }, + "name": "prefetchableRefetchableFragmentWithConnection_PaginationFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": "friends", + "args": null, + "concreteType": "FriendsConnection", + "kind": "LinkedField", + "name": "__PaginationFragment_friends_connection", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "FriendsEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "prefetchableRefetchableFragmentWithConnection_PaginationFragment__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 + } + ], + "type": "User", + "abstractKey": null + } + ], + "type": "Node", + "abstractKey": "__isNode" +} + +{ + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": { + "plural": true + }, + "name": "prefetchableRefetchableFragmentWithConnection_PaginationFragment__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": "FriendsEdge", + "abstractKey": null +} diff --git a/compiler/crates/relay-compiler/tests/compile_relay_artifacts/fixtures/prefetchable-refetchable-fragment-with-connection.graphql b/compiler/crates/relay-compiler/tests/compile_relay_artifacts/fixtures/prefetchable-refetchable-fragment-with-connection.graphql new file mode 100644 index 0000000000000..7e288ca8a90be --- /dev/null +++ b/compiler/crates/relay-compiler/tests/compile_relay_artifacts/fixtures/prefetchable-refetchable-fragment-with-connection.graphql @@ -0,0 +1,19 @@ +fragment prefetchableRefetchableFragmentWithConnection_PaginationFragment on Node + @refetchable(queryName: "RefetchableFragmentQuery") + @argumentDefinitions( + count: {type: "Int", defaultValue: 10} + cursor: {type: "ID"} + ) { + id + ... on User { + name + friends(after: $cursor, first: $count) + @connection(key: "PaginationFragment_friends", prefetchable_pagination: true) { + edges { + node { + id + } + } + } + } +} diff --git a/compiler/crates/relay-compiler/tests/compile_relay_artifacts_test.rs b/compiler/crates/relay-compiler/tests/compile_relay_artifacts_test.rs index 5f5e3c72b2d37..9d1dcfb0a178e 100644 --- a/compiler/crates/relay-compiler/tests/compile_relay_artifacts_test.rs +++ b/compiler/crates/relay-compiler/tests/compile_relay_artifacts_test.rs @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<85f6b556df80c4d819ff23344734487b>> */ mod compile_relay_artifacts; @@ -971,6 +971,13 @@ async fn plural_fragment() { test_fixture(transform_fixture, file!(), "plural-fragment.graphql", "compile_relay_artifacts/fixtures/plural-fragment.expected", input, expected).await; } +#[tokio::test] +async fn prefetchable_refetchable_fragment_with_connection() { + let input = include_str!("compile_relay_artifacts/fixtures/prefetchable-refetchable-fragment-with-connection.graphql"); + let expected = include_str!("compile_relay_artifacts/fixtures/prefetchable-refetchable-fragment-with-connection.expected"); + test_fixture(transform_fixture, file!(), "prefetchable-refetchable-fragment-with-connection.graphql", "compile_relay_artifacts/fixtures/prefetchable-refetchable-fragment-with-connection.expected", input, expected).await; +} + #[tokio::test] async fn prepend_node() { let input = include_str!("compile_relay_artifacts/fixtures/prepend-node.graphql"); diff --git a/compiler/crates/relay-compiler/tests/relay_compiler_integration/fixtures/prefetchable_refetchable_pagination.expected b/compiler/crates/relay-compiler/tests/relay_compiler_integration/fixtures/prefetchable_refetchable_pagination.expected new file mode 100644 index 0000000000000..08aa5a971b5a5 --- /dev/null +++ b/compiler/crates/relay-compiler/tests/relay_compiler_integration/fixtures/prefetchable_refetchable_pagination.expected @@ -0,0 +1,718 @@ +==================================== INPUT ==================================== +//- foo.js +graphql` + fragment foo on Viewer @refetchable(queryName: "RefetchableFragmentQuery") { + posts(first: $count, after: $cursor) @connection(key:"refetch_posts", prefetchable_pagination: true) { + edges{ + node { + title + } + } + } + }`; + +graphql` + query fooQuery($count: Int, $cursor: String) { + me { + ...foo + } + } +` + +//- relay.config.json +{ + "language": "flow", + "jsModuleFormat": "haste", + "schema": "./schema.graphql" +} + +//- schema.graphql +type Query { + me: Viewer + viewer: Viewer +} + +type Viewer { + posts(first: Int, after: String): PostsConnection +} + +type PostsConnection { + count: Int + edges: [PostEdge] + pageInfo: PageInfo +} + +type PostEdge { + cursor: String + node: Page +} + +type PageInfo { + hasPreviousPage: Boolean + hasNextPage: Boolean + endCursor: String + startCursor: String +} + +type Page { + title: String + id: ID +} +==================================== OUTPUT =================================== +//- __generated__/RefetchableFragmentQuery.graphql.js +/** + * SignedSource<<19c135fed3c2860b79d3e6208b163e1a>> + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { ConcreteRequest, Query } from 'relay-runtime'; +import type { FragmentType } from "relay-runtime"; +import type { foo$fragmentType } from "foo.graphql"; +export type RefetchableFragmentQuery$variables = {| + count?: ?number, + cursor?: ?string, +|}; +export type RefetchableFragmentQuery$data = {| + +viewer: ?{| + +$fragmentSpreads: foo$fragmentType, + |}, +|}; +export type RefetchableFragmentQuery = {| + response: RefetchableFragmentQuery$data, + variables: RefetchableFragmentQuery$variables, +|}; +*/ + +var node/*: ConcreteRequest*/ = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "count" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "cursor" + } +], +v1 = [ + { + "kind": "Variable", + "name": "after", + "variableName": "cursor" + }, + { + "kind": "Variable", + "name": "first", + "variableName": "count" + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "RefetchableFragmentQuery", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Viewer", + "kind": "LinkedField", + "name": "viewer", + "plural": false, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "foo" + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "RefetchableFragmentQuery", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Viewer", + "kind": "LinkedField", + "name": "viewer", + "plural": false, + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": "PostsConnection", + "kind": "LinkedField", + "name": "posts", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "PostEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Page", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "title", + "storageKey": null + }, + { + "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 + } + ], + "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": (v1/*: any*/), + "filters": null, + "handle": "connection", + "key": "refetch_posts", + "kind": "LinkedHandle", + "name": "posts" + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "7136bfbd09958dee2f5494d42f81da6a", + "id": null, + "metadata": {}, + "name": "RefetchableFragmentQuery", + "operationKind": "query", + "text": "query RefetchableFragmentQuery(\n $count: Int\n $cursor: String\n) {\n viewer {\n ...foo\n }\n}\n\nfragment foo on Viewer {\n posts(first: $count, after: $cursor) {\n edges {\n ...foo__edges\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment foo__edges on PostEdge {\n node {\n title\n id\n __typename\n }\n cursor\n}\n" + } +}; +})(); + +(node/*: any*/).hash = "dfac7950191066a5d1e0d99a67f503b7"; + +module.exports = ((node/*: any*/)/*: Query< + RefetchableFragmentQuery$variables, + RefetchableFragmentQuery$data, +>*/); + +//- __generated__/foo.graphql.js +/** + * SignedSource<<3963af2dff879eeff47ec478ec74e048>> + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { ReaderFragment, PrefetchableRefetchableFragment } from 'relay-runtime'; +import type { foo__edges$fragmentType } from "foo__edges.graphql"; +import type { FragmentType } from "relay-runtime"; +declare export opaque type foo$fragmentType: FragmentType; +import type { RefetchableFragmentQuery$variables } from "RefetchableFragmentQuery.graphql"; +import type { foo__edges$data } from "foo__edges.graphql"; +export type foo$data = {| + +posts: ?{| + +edges: ?$ReadOnlyArray, + +pageInfo: ?{| + +endCursor: ?string, + +hasNextPage: ?boolean, + |}, + |}, + +$fragmentType: foo$fragmentType, +|}; +export type foo$key = { + +$data?: foo$data, + +$fragmentSpreads: foo$fragmentType, + ... +}; +*/ + +var node/*: ReaderFragment*/ = (function(){ +var v0 = [ + "posts" +]; +return { + "argumentDefinitions": [ + { + "kind": "RootArgument", + "name": "count" + }, + { + "kind": "RootArgument", + "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": [ + "viewer" + ], + "operation": require('RefetchableFragmentQuery.graphql'), + "edgesFragment": require('foo__edges.graphql') + } + }, + "name": "foo", + "selections": [ + { + "alias": "posts", + "args": null, + "concreteType": "PostsConnection", + "kind": "LinkedField", + "name": "__refetch_posts_connection", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "PostEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "foo__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 + } + ], + "type": "Viewer", + "abstractKey": null +}; +})(); + +(node/*: any*/).hash = "dfac7950191066a5d1e0d99a67f503b7"; + +module.exports = ((node/*: any*/)/*: PrefetchableRefetchableFragment< + foo$fragmentType, + foo$data, + foo__edges$data, + RefetchableFragmentQuery$variables, +>*/); + +//- __generated__/fooQuery.graphql.js +/** + * SignedSource<<18e66ec469beae13e529e6c78e5c3e04>> + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { ConcreteRequest, Query } from 'relay-runtime'; +import type { foo$fragmentType } from "foo.graphql"; +export type fooQuery$variables = {| + count?: ?number, + cursor?: ?string, +|}; +export type fooQuery$data = {| + +me: ?{| + +$fragmentSpreads: foo$fragmentType, + |}, +|}; +export type fooQuery = {| + response: fooQuery$data, + variables: fooQuery$variables, +|}; +*/ + +var node/*: ConcreteRequest*/ = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "count" + }, + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "cursor" + } +], +v1 = [ + { + "kind": "Variable", + "name": "after", + "variableName": "cursor" + }, + { + "kind": "Variable", + "name": "first", + "variableName": "count" + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "fooQuery", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Viewer", + "kind": "LinkedField", + "name": "me", + "plural": false, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "foo" + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "fooQuery", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Viewer", + "kind": "LinkedField", + "name": "me", + "plural": false, + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": "PostsConnection", + "kind": "LinkedField", + "name": "posts", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "PostEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Page", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "title", + "storageKey": null + }, + { + "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 + } + ], + "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": (v1/*: any*/), + "filters": null, + "handle": "connection", + "key": "refetch_posts", + "kind": "LinkedHandle", + "name": "posts" + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "b9121ed3e6ec5e47846fd0f9a078457a", + "id": null, + "metadata": {}, + "name": "fooQuery", + "operationKind": "query", + "text": "query fooQuery(\n $count: Int\n $cursor: String\n) {\n me {\n ...foo\n }\n}\n\nfragment foo on Viewer {\n posts(first: $count, after: $cursor) {\n edges {\n ...foo__edges\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment foo__edges on PostEdge {\n node {\n title\n id\n __typename\n }\n cursor\n}\n" + } +}; +})(); + +(node/*: any*/).hash = "5b2116a3cb677e65d08c3df8b486f670"; + +module.exports = ((node/*: any*/)/*: Query< + fooQuery$variables, + fooQuery$data, +>*/); + +//- __generated__/foo__edges.graphql.js +/** + * SignedSource<> + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { Fragment, ReaderFragment } from 'relay-runtime'; +import type { FragmentType } from "relay-runtime"; +declare export opaque type foo__edges$fragmentType: FragmentType; +export type foo__edges$data = $ReadOnlyArray<{| + +cursor: ?string, + +node: ?{| + +__typename: "Page", + +title: ?string, + |}, + +$fragmentType: foo__edges$fragmentType, +|}>; +export type foo__edges$key = $ReadOnlyArray<{ + +$data?: foo__edges$data, + +$fragmentSpreads: foo__edges$fragmentType, + ... +}>; +*/ + +var node/*: ReaderFragment*/ = { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": { + "plural": true + }, + "name": "foo__edges", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Page", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "title", + "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": "PostEdge", + "abstractKey": null +}; + +module.exports = ((node/*: any*/)/*: Fragment< + foo__edges$fragmentType, + foo__edges$data, +>*/); diff --git a/compiler/crates/relay-compiler/tests/relay_compiler_integration/fixtures/prefetchable_refetchable_pagination.input b/compiler/crates/relay-compiler/tests/relay_compiler_integration/fixtures/prefetchable_refetchable_pagination.input new file mode 100644 index 0000000000000..62ff99acc0654 --- /dev/null +++ b/compiler/crates/relay-compiler/tests/relay_compiler_integration/fixtures/prefetchable_refetchable_pagination.input @@ -0,0 +1,59 @@ +//- foo.js +graphql` + fragment foo on Viewer @refetchable(queryName: "RefetchableFragmentQuery") { + posts(first: $count, after: $cursor) @connection(key:"refetch_posts", prefetchable_pagination: true) { + edges{ + node { + title + } + } + } + }`; + +graphql` + query fooQuery($count: Int, $cursor: String) { + me { + ...foo + } + } +` + +//- relay.config.json +{ + "language": "flow", + "jsModuleFormat": "haste", + "schema": "./schema.graphql" +} + +//- schema.graphql +type Query { + me: Viewer + viewer: Viewer +} + +type Viewer { + posts(first: Int, after: String): PostsConnection +} + +type PostsConnection { + count: Int + edges: [PostEdge] + pageInfo: PageInfo +} + +type PostEdge { + cursor: String + node: Page +} + +type PageInfo { + hasPreviousPage: Boolean + hasNextPage: Boolean + endCursor: String + startCursor: String +} + +type Page { + title: String + id: ID +} diff --git a/compiler/crates/relay-compiler/tests/relay_compiler_integration_test.rs b/compiler/crates/relay-compiler/tests/relay_compiler_integration_test.rs index ca5afbf743f9c..db820da036835 100644 --- a/compiler/crates/relay-compiler/tests/relay_compiler_integration_test.rs +++ b/compiler/crates/relay-compiler/tests/relay_compiler_integration_test.rs @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<00aa849a8a46f027d22d0712fd90b3eb>> + * @generated SignedSource<<44313b4ad0ca41fac58e6a820c78a157>> */ mod relay_compiler_integration; @@ -117,6 +117,13 @@ async fn multiple_resolvers_returns_interfaces_of_all_strong_model_type() { test_fixture(transform_fixture, file!(), "multiple_resolvers_returns_interfaces_of_all_strong_model_type.input", "relay_compiler_integration/fixtures/multiple_resolvers_returns_interfaces_of_all_strong_model_type.expected", input, expected).await; } +#[tokio::test] +async fn prefetchable_refetchable_pagination() { + let input = include_str!("relay_compiler_integration/fixtures/prefetchable_refetchable_pagination.input"); + let expected = include_str!("relay_compiler_integration/fixtures/prefetchable_refetchable_pagination.expected"); + test_fixture(transform_fixture, file!(), "prefetchable_refetchable_pagination.input", "relay_compiler_integration/fixtures/prefetchable_refetchable_pagination.expected", input, expected).await; +} + #[tokio::test] async fn preloadable_query_flow() { let input = include_str!("relay_compiler_integration/fixtures/preloadable_query_flow.input"); diff --git a/compiler/crates/relay-transforms/src/apply_transforms.rs b/compiler/crates/relay-transforms/src/apply_transforms.rs index 25ab8c6af5d8c..63764c125080d 100644 --- a/compiler/crates/relay-transforms/src/apply_transforms.rs +++ b/compiler/crates/relay-transforms/src/apply_transforms.rs @@ -174,6 +174,7 @@ fn apply_common_transforms( &program, &project_config.schema_config.connection_interface, &project_config.schema_config.defer_stream_interface, + false, ) }); program = log_event.time("mask", || mask(&program)); @@ -663,6 +664,16 @@ fn apply_typegen_transforms( ) })?; + // Split edge fragment for prefetchable pagination + program = log_event.time("transform_connections_typegen", || { + transform_connections( + &program, + &project_config.schema_config.connection_interface, + &project_config.schema_config.defer_stream_interface, + true, + ) + }); + program = log_event.time("mask", || mask(&program)); program = log_event.time("transform_match", || { transform_match( diff --git a/compiler/crates/relay-transforms/src/transform_connections.rs b/compiler/crates/relay-transforms/src/transform_connections.rs index 7879169b72df0..a6a8913785f7a 100644 --- a/compiler/crates/relay-transforms/src/transform_connections.rs +++ b/compiler/crates/relay-transforms/src/transform_connections.rs @@ -53,9 +53,16 @@ pub fn transform_connections( program: &Program, connection_interface: &ConnectionInterface, defer_stream_interface: &DeferStreamInterface, + // Does not do other validaiton and transforms only extract prefetchable pagination + // fragment. Needed for generating correct types in the typegen pipeline. + only_extract_prefetchable_pagination_fragment: bool, ) -> Program { - let mut transform = - ConnectionTransform::new(program, connection_interface, defer_stream_interface); + let mut transform = ConnectionTransform::new( + program, + connection_interface, + defer_stream_interface, + only_extract_prefetchable_pagination_fragment, + ); let mut program = transform .transform_program(program) .replace_or_else(|| program.clone()); @@ -74,6 +81,7 @@ struct ConnectionTransform<'s> { program: &'s Program, defer_stream_interface: &'s DeferStreamInterface, edge_fragments: Vec>, + only_extract_prefetchable_pagination_fragment: bool, } impl<'s> ConnectionTransform<'s> { @@ -81,6 +89,7 @@ impl<'s> ConnectionTransform<'s> { program: &'s Program, connection_interface: &'s ConnectionInterface, defer_stream_interface: &'s DeferStreamInterface, + only_extract_prefetchable_pagination_fragment: bool, ) -> Self { Self { connection_constants: ConnectionConstants::default(), @@ -91,6 +100,7 @@ impl<'s> ConnectionTransform<'s> { program, defer_stream_interface, edge_fragments: vec![], + only_extract_prefetchable_pagination_fragment, } } @@ -433,6 +443,11 @@ impl<'s> ConnectionTransform<'s> { false }, ); + if self.only_extract_prefetchable_pagination_fragment + && !connection_metadata.is_prefetchable_pagination + { + return Transformed::Keep; + } let next_connection_selections = self.transform_connection_selections( connection_field, &connection_metadata, diff --git a/compiler/crates/relay-transforms/tests/refetchable_fragment.rs b/compiler/crates/relay-transforms/tests/refetchable_fragment.rs index 0d3d7988f2f62..12974d09082b1 100644 --- a/compiler/crates/relay-transforms/tests/refetchable_fragment.rs +++ b/compiler/crates/relay-transforms/tests/refetchable_fragment.rs @@ -18,6 +18,7 @@ pub async fn transform_fixture(fixture: &Fixture<'_>) -> Result program, &ConnectionInterface::default(), &DeferStreamInterface::default(), + false, ); let base_fragments = Default::default(); transform_refetchable_fragment(&program, &Default::default(), &base_fragments, false) diff --git a/compiler/crates/relay-transforms/tests/transform_connections.rs b/compiler/crates/relay-transforms/tests/transform_connections.rs index 169eb95210664..71746a5bce666 100644 --- a/compiler/crates/relay-transforms/tests/transform_connections.rs +++ b/compiler/crates/relay-transforms/tests/transform_connections.rs @@ -39,8 +39,12 @@ pub async fn transform_fixture(fixture: &Fixture<'_>) -> Result validate_connections(&program, &connection_interface) .map_err(|diagnostics| diagnostics_to_sorted_string(fixture.content, &diagnostics))?; - let next_program = - transform_connections(&program, &connection_interface, &defer_stream_interface); + let next_program = transform_connections( + &program, + &connection_interface, + &defer_stream_interface, + false, + ); let printer_options = PrinterOptions { debug_directive_data: true, diff --git a/compiler/crates/relay-typegen/src/write.rs b/compiler/crates/relay-typegen/src/write.rs index eae0cbc7d9b4f..c8e3e9a2e4f3e 100644 --- a/compiler/crates/relay-typegen/src/write.rs +++ b/compiler/crates/relay-typegen/src/write.rs @@ -492,6 +492,27 @@ pub(crate) fn write_fragment_type_exports_section( )?; } } + let edges_name = format!("{}__edges$data", fragment_name); + if refetchable_metadata.is_prefetchable_pagination { + match typegen_context.project_config.js_module_format { + JsModuleFormat::CommonJS => { + if typegen_context.has_unified_output { + writer.write_import_fragment_type( + &[&edges_name], + &format!("./{}__edges.graphql", fragment_name), + )?; + } else { + writer.write_any_type_definition(&edges_name)?; + } + } + JsModuleFormat::Haste => { + writer.write_import_fragment_type( + &[&edges_name], + &format!("{}__edges.graphql", fragment_name), + )?; + } + } + } } if !is_assignable_fragment {