diff --git a/packages/react-relay/__tests__/ClientEdges-test.js b/packages/react-relay/__tests__/ClientEdges-test.js index 2517dda8324eb..7b32de74a9ec7 100644 --- a/packages/react-relay/__tests__/ClientEdges-test.js +++ b/packages/react-relay/__tests__/ClientEdges-test.js @@ -11,6 +11,8 @@ 'use strict'; +import type {ClientEdgesTestUpperName$key} from './__generated__/ClientEdgesTestUpperName.graphql'; + const React = require('react'); const { RelayEnvironmentProvider, @@ -24,6 +26,7 @@ const { RecordSource, RelayFeatureFlags, graphql, + readFragment, } = require('relay-runtime'); const RelayObservable = require('relay-runtime/network/RelayObservable'); const RelayModernStore = require('relay-runtime/store/RelayModernStore'); @@ -44,6 +47,23 @@ export function same_user_client_edge(): {id: string} { return {id: '1'}; } +/** + * @RelayResolver User.upper_name: String + * @rootFragment ClientEdgesTestUpperName + */ + +export function upper_name(key: ClientEdgesTestUpperName$key): ?string { + const user = readFragment( + graphql` + fragment ClientEdgesTestUpperName on User { + name + } + `, + key, + ); + return user?.name?.toUpperCase(); +} + describe.each([[true], [false]])( 'RelayFeatureFlags.ENABLE_ACTIVITY_COMPATIBILITY = %s', (activityEnabled: boolean) => { @@ -402,5 +422,75 @@ describe.each([[true], [false]])( // }); // expect(renderer?.toJSON()).toBe('Alice'); }); + + it('should fetch data missing client edge to server data in resolver @rootFragment', () => { + function TestComponent() { + return ( + + + + + + ); + } + + const variables = {}; + function InnerComponent() { + const data = useLazyLoadQuery( + graphql` + query ClientEdgesTest6Query { + me { + same_user_client_edge @waterfall { + # No fields here means that we render without detecting any + # missing data here and don't attempt to fetch the @waterfall + # query. + # + # The same bug can be triggered by adding a field that is already + # in the store for an unrelated reason. + upper_name + # Adding "name" here will cause the query to be fetched. + } + } + } + `, + variables, + ); + + return data.me?.same_user_client_edge?.upper_name; + } + + // This will be updated when we add the new assertions as part of a fix for + // this bug. + let renderer; + TestRenderer.act(() => { + renderer = TestRenderer.create(); + }); + + expect(fetchFn.mock.calls.length).toEqual(1); + // We should send the client-edge query + // $FlowFixMe[invalid-tuple-index] Error found while enabling LTI on this file + expect(fetchFn.mock.calls[0][0].name).toBe( + 'ClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge', + ); + // Check variables + // $FlowFixMe[invalid-tuple-index] Error found while enabling LTI on this file + expect(fetchFn.mock.calls[0][1]).toEqual({id: '1'}); + expect(renderer?.toJSON()).toBe('Loading'); + + TestRenderer.act(() => { + // This should resolve client-edge query + networkSink.next({ + data: { + node: { + id: '1', + __typename: 'User', + name: 'Alice', + }, + }, + }); + jest.runAllImmediates(); + }); + expect(renderer?.toJSON()).toBe('ALICE'); + }); }, ); diff --git a/packages/react-relay/__tests__/__generated__/ClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge.graphql.js b/packages/react-relay/__tests__/__generated__/ClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge.graphql.js new file mode 100644 index 0000000000000..db94fe9c4c80c --- /dev/null +++ b/packages/react-relay/__tests__/__generated__/ClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge.graphql.js @@ -0,0 +1,157 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @oncall relay + * + * @generated SignedSource<<366f4676acbcbb62dabdb15fc6a1e6c9>> + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { ConcreteRequest, Query } from 'relay-runtime'; +import type { RefetchableClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge$fragmentType } from "./RefetchableClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge.graphql"; +export type ClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge$variables = {| + id: string, +|}; +export type ClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge$data = {| + +node: ?{| + +$fragmentSpreads: RefetchableClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge$fragmentType, + |}, +|}; +export type ClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge = {| + response: ClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge$data, + variables: ClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge$variables, +|}; +*/ + +var node/*: ConcreteRequest*/ = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "id" + } +], +v1 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "id" + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "ClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "RefetchableClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge" + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "ClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "kind": "InlineFragment", + "selections": [ + { + "name": "upper_name", + "args": null, + "fragment": { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } + ], + "type": "User", + "abstractKey": null + }, + "kind": "RelayResolver", + "storageKey": null, + "isOutputType": true + } + ], + "type": "User", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "4b0b7798dd0bfc7d7ff2e24c459cdccc", + "id": null, + "metadata": {}, + "name": "ClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge", + "operationKind": "query", + "text": "query ClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge(\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...RefetchableClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge\n id\n }\n}\n\nfragment ClientEdgesTestUpperName on User {\n name\n}\n\nfragment RefetchableClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge on User {\n ...ClientEdgesTestUpperName\n id\n}\n" + } +}; +})(); + +if (__DEV__) { + (node/*: any*/).hash = "330a0878ce30575d8c36e2fdd626c833"; +} + +module.exports = ((node/*: any*/)/*: Query< + ClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge$variables, + ClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge$data, +>*/); diff --git a/packages/react-relay/__tests__/__generated__/ClientEdgesTest6Query.graphql.js b/packages/react-relay/__tests__/__generated__/ClientEdgesTest6Query.graphql.js new file mode 100644 index 0000000000000..08126407b5d5b --- /dev/null +++ b/packages/react-relay/__tests__/__generated__/ClientEdgesTest6Query.graphql.js @@ -0,0 +1,167 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @oncall relay + * + * @generated SignedSource<<7b8a2fff0ac8a3e5b71333c58dbc823f>> + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { ConcreteRequest, Query } from 'relay-runtime'; +import type { DataID } from "relay-runtime"; +import type { ClientEdgesTestUpperName$key } from "./ClientEdgesTestUpperName.graphql"; +import {same_user_client_edge as userSameUserClientEdgeResolverType} from "../ClientEdges-test.js"; +import type { TestResolverContextType } from "../../../relay-runtime/mutations/__tests__/TestResolverContextType"; +// Type assertion validating that `userSameUserClientEdgeResolverType` resolver is correctly implemented. +// A type error here indicates that the type signature of the resolver module is incorrect. +(userSameUserClientEdgeResolverType: ( + args: void, + context: TestResolverContextType, +) => ?{| + +id: DataID, +|}); +import {upper_name as userUpperNameResolverType} from "../ClientEdges-test.js"; +// Type assertion validating that `userUpperNameResolverType` resolver is correctly implemented. +// A type error here indicates that the type signature of the resolver module is incorrect. +(userUpperNameResolverType: ( + rootKey: ClientEdgesTestUpperName$key, + args: void, + context: TestResolverContextType, +) => ?string); +export type ClientEdgesTest6Query$variables = {||}; +export type ClientEdgesTest6Query$data = {| + +me: ?{| + +same_user_client_edge: ?{| + +upper_name: ?string, + |}, + |}, +|}; +export type ClientEdgesTest6Query = {| + response: ClientEdgesTest6Query$data, + variables: ClientEdgesTest6Query$variables, +|}; +*/ + +var node/*: ConcreteRequest*/ = { + "fragment": { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": { + "hasClientEdges": true + }, + "name": "ClientEdgesTest6Query", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "User", + "kind": "LinkedField", + "name": "me", + "plural": false, + "selections": [ + { + "kind": "ClientEdgeToServerObject", + "operation": require('./ClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge.graphql'), + "backingField": { + "alias": null, + "args": null, + "fragment": null, + "kind": "RelayResolver", + "name": "same_user_client_edge", + "resolverModule": require('../ClientEdges-test').same_user_client_edge, + "path": "me.same_user_client_edge" + }, + "linkedField": { + "alias": null, + "args": null, + "concreteType": "User", + "kind": "LinkedField", + "name": "same_user_client_edge", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "fragment": { + "args": null, + "kind": "FragmentSpread", + "name": "ClientEdgesTestUpperName" + }, + "kind": "RelayResolver", + "name": "upper_name", + "resolverModule": require('../ClientEdges-test').upper_name, + "path": "me.same_user_client_edge.upper_name" + } + ], + "storageKey": null + } + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [], + "kind": "Operation", + "name": "ClientEdgesTest6Query", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "User", + "kind": "LinkedField", + "name": "me", + "plural": false, + "selections": [ + { + "name": "same_user_client_edge", + "args": null, + "fragment": null, + "kind": "RelayResolver", + "storageKey": null, + "isOutputType": false + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "e62271734cfff9eb7b0535fd011e32b3", + "id": null, + "metadata": {}, + "name": "ClientEdgesTest6Query", + "operationKind": "query", + "text": "query ClientEdgesTest6Query {\n me {\n id\n }\n}\n" + } +}; + +if (__DEV__) { + (node/*: any*/).hash = "330a0878ce30575d8c36e2fdd626c833"; +} + +module.exports = ((node/*: any*/)/*: Query< + ClientEdgesTest6Query$variables, + ClientEdgesTest6Query$data, +>*/); diff --git a/packages/react-relay/__tests__/__generated__/ClientEdgesTestUpperName.graphql.js b/packages/react-relay/__tests__/__generated__/ClientEdgesTestUpperName.graphql.js new file mode 100644 index 0000000000000..c7574c6e720fd --- /dev/null +++ b/packages/react-relay/__tests__/__generated__/ClientEdgesTestUpperName.graphql.js @@ -0,0 +1,59 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @oncall relay + * + * @generated 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 ClientEdgesTestUpperName$fragmentType: FragmentType; +export type ClientEdgesTestUpperName$data = {| + +name: ?string, + +$fragmentType: ClientEdgesTestUpperName$fragmentType, +|}; +export type ClientEdgesTestUpperName$key = { + +$data?: ClientEdgesTestUpperName$data, + +$fragmentSpreads: ClientEdgesTestUpperName$fragmentType, + ... +}; +*/ + +var node/*: ReaderFragment*/ = { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "ClientEdgesTestUpperName", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } + ], + "type": "User", + "abstractKey": null +}; + +if (__DEV__) { + (node/*: any*/).hash = "be2c514c21045e5df5e947adccc4f146"; +} + +module.exports = ((node/*: any*/)/*: Fragment< + ClientEdgesTestUpperName$fragmentType, + ClientEdgesTestUpperName$data, +>*/); diff --git a/packages/react-relay/__tests__/__generated__/RefetchableClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge.graphql.js b/packages/react-relay/__tests__/__generated__/RefetchableClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge.graphql.js new file mode 100644 index 0000000000000..02cbf47af8b9a --- /dev/null +++ b/packages/react-relay/__tests__/__generated__/RefetchableClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge.graphql.js @@ -0,0 +1,97 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @oncall relay + * + * @generated SignedSource<<8ea39c0a6070c25a68e02cc8a3820ec5>> + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { ReaderFragment, RefetchableFragment } from 'relay-runtime'; +import type { ClientEdgesTestUpperName$key } from "./ClientEdgesTestUpperName.graphql"; +import type { FragmentType } from "relay-runtime"; +import {upper_name as userUpperNameResolverType} from "../ClientEdges-test.js"; +import type { TestResolverContextType } from "../../../relay-runtime/mutations/__tests__/TestResolverContextType"; +// Type assertion validating that `userUpperNameResolverType` resolver is correctly implemented. +// A type error here indicates that the type signature of the resolver module is incorrect. +(userUpperNameResolverType: ( + rootKey: ClientEdgesTestUpperName$key, + args: void, + context: TestResolverContextType, +) => ?string); +declare export opaque type RefetchableClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge$fragmentType: FragmentType; +type ClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge$variables = any; +export type RefetchableClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge$data = {| + +id: string, + +upper_name: ?string, + +$fragmentType: RefetchableClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge$fragmentType, +|}; +export type RefetchableClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge$key = { + +$data?: RefetchableClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge$data, + +$fragmentSpreads: RefetchableClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge$fragmentType, + ... +}; +*/ + +var node/*: ReaderFragment*/ = { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": { + "refetch": { + "connection": null, + "fragmentPathInResult": [ + "node" + ], + "operation": require('./ClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge.graphql'), + "identifierInfo": { + "identifierField": "id", + "identifierQueryVariableName": "id" + } + } + }, + "name": "RefetchableClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge", + "selections": [ + { + "alias": null, + "args": null, + "fragment": { + "args": null, + "kind": "FragmentSpread", + "name": "ClientEdgesTestUpperName" + }, + "kind": "RelayResolver", + "name": "upper_name", + "resolverModule": require('../ClientEdges-test').upper_name, + "path": "upper_name" + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + } + ], + "type": "User", + "abstractKey": null +}; + +if (__DEV__) { + (node/*: any*/).hash = "330a0878ce30575d8c36e2fdd626c833"; +} + +module.exports = ((node/*: any*/)/*: RefetchableFragment< + RefetchableClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge$fragmentType, + RefetchableClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge$data, + ClientEdgeQuery_ClientEdgesTest6Query_me__same_user_client_edge$variables, +>*/); diff --git a/packages/relay-runtime/store/RelayReader.js b/packages/relay-runtime/store/RelayReader.js index f03c9f6c3d86a..73f6c115a2abe 100644 --- a/packages/relay-runtime/store/RelayReader.js +++ b/packages/relay-runtime/store/RelayReader.js @@ -788,7 +788,7 @@ class RelayReader { // `getResolverValue`) and converted into an error object. const evaluate = (): EvaluationResult => { if (fragment != null) { - const key = { + const key: SelectorData = { __id: parentRecordID, __fragmentOwner: this._owner, __fragments: { @@ -797,6 +797,14 @@ class RelayReader { : {}, }, }; + if ( + this._clientEdgeTraversalPath.length > 0 && + this._clientEdgeTraversalPath[ + this._clientEdgeTraversalPath.length - 1 + ] !== null + ) { + key[CLIENT_EDGE_TRAVERSAL_PATH] = [...this._clientEdgeTraversalPath]; + } const resolverContext = {getDataForResolverFragment}; return withResolverContext(resolverContext, () => { const [resolverResult, resolverError] = getResolverValue(