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(