diff --git a/packages/react-relay/__tests__/RelayResolverNullableModelClientEdge-test.js b/packages/react-relay/__tests__/RelayResolverNullableModelClientEdge-test.js
index ac16fecbe050a..b41686ebb7b23 100644
--- a/packages/react-relay/__tests__/RelayResolverNullableModelClientEdge-test.js
+++ b/packages/react-relay/__tests__/RelayResolverNullableModelClientEdge-test.js
@@ -29,9 +29,34 @@ const {
RelayFeatureFlags,
graphql,
} = require('relay-runtime');
+const {
+ addTodo,
+} = require('relay-runtime/store/__tests__/resolvers/ExampleTodoStore');
const LiveResolverStore = require('relay-runtime/store/experimental-live-resolvers/LiveResolverStore');
const {createMockEnvironment} = require('relay-test-utils');
+/**
+ * CLIENT EDGE TO PLURAL LIVE STRONG CLIENT OBJECT
+ */
+
+/**
+ * @RelayResolver Query.edge_to_plural_live_objects_some_exist: [TodoModel]
+ */
+export function edge_to_plural_live_objects_some_exist(): $ReadOnlyArray<{
+ id: DataID,
+}> {
+ return [{id: 'todo-1'}, {id: 'THERE_IS_NO_TODO_WITH_THIS_ID'}];
+}
+
+/**
+ * @RelayResolver Query.edge_to_plural_live_objects_none_exist: [TodoModel]
+ */
+export function edge_to_plural_live_objects_none_exist(): $ReadOnlyArray<{
+ id: DataID,
+}> {
+ return [{id: 'NO_TODO_1'}, {id: 'NO_TODO_2'}];
+}
+
/**
* CLIENT EDGE TO LIVE STRONG CLIENT OBJECT
*/
@@ -158,6 +183,73 @@ describe.each([
beforeEach(() => {
environment = createEnvironment();
});
+ test('client edge to plural IDs, none have corresponding live object', () => {
+ function TodoNullComponent() {
+ const data = useClientQuery(
+ graphql`
+ query RelayResolverNullableModelClientEdgeTest_PluralLiveModelNoneExist_Query {
+ edge_to_plural_live_objects_none_exist {
+ id
+ description
+ }
+ }
+ `,
+ {},
+ );
+
+ invariant(data != null, 'Query response should be nonnull');
+ expect(data.edge_to_plural_live_objects_none_exist).toHaveLength(2);
+ return data.edge_to_plural_live_objects_none_exist
+ ?.map(item =>
+ item
+ ? `${item.id ?? 'unknown'} - ${item.description ?? 'unknown'}`
+ : 'unknown',
+ )
+ .join(',');
+ }
+
+ const renderer = TestRenderer.create(
+
+
+ ,
+ );
+ expect(renderer.toJSON()).toEqual('unknown,unknown');
+ });
+
+ test('client edge to plural IDs, some with no corresponding live object', () => {
+ function TodoNullComponent() {
+ const data = useClientQuery(
+ graphql`
+ query RelayResolverNullableModelClientEdgeTest_PluralLiveModel_Query {
+ edge_to_plural_live_objects_some_exist {
+ id
+ description
+ }
+ }
+ `,
+ {},
+ );
+
+ invariant(data != null, 'Query response should be nonnull');
+ expect(data.edge_to_plural_live_objects_some_exist).toHaveLength(2);
+ return data.edge_to_plural_live_objects_some_exist
+ ?.map(item =>
+ item
+ ? `${item.id ?? 'unknown'} - ${item.description ?? 'unknown'}`
+ : 'unknown',
+ )
+ .join(',');
+ }
+
+ addTodo('Test todo');
+ const renderer = TestRenderer.create(
+
+
+ ,
+ );
+ expect(renderer.toJSON()).toEqual('todo-1 - Test todo,unknown');
+ });
+
test('client edge to ID with no corresponding live object', () => {
function TodoNullComponent() {
const data = useClientQuery(
diff --git a/packages/react-relay/__tests__/__generated__/RelayResolverNullableModelClientEdgeTest_PluralLiveModelNoneExist_Query.graphql.js b/packages/react-relay/__tests__/__generated__/RelayResolverNullableModelClientEdgeTest_PluralLiveModelNoneExist_Query.graphql.js
new file mode 100644
index 0000000000000..746b929f3284d
--- /dev/null
+++ b/packages/react-relay/__tests__/__generated__/RelayResolverNullableModelClientEdgeTest_PluralLiveModelNoneExist_Query.graphql.js
@@ -0,0 +1,198 @@
+/**
+ * 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<<64c14c21fc90aa6c3436149d7a959674>>
+ * @flow
+ * @lightSyntaxTransform
+ * @nogrep
+ */
+
+/* eslint-disable */
+
+'use strict';
+
+/*::
+import type { ClientRequest, ClientQuery } from 'relay-runtime';
+import type { DataID } from "relay-runtime";
+import type { TodoModel____relay_model_instance$data } from "./../../../relay-runtime/store/__tests__/resolvers/__generated__/TodoModel____relay_model_instance.graphql";
+import {edge_to_plural_live_objects_none_exist as queryEdgeToPluralLiveObjectsNoneExistResolverType} from "../RelayResolverNullableModelClientEdge-test.js";
+// Type assertion validating that `queryEdgeToPluralLiveObjectsNoneExistResolverType` resolver is correctly implemented.
+// A type error here indicates that the type signature of the resolver module is incorrect.
+(queryEdgeToPluralLiveObjectsNoneExistResolverType: () => ?$ReadOnlyArray{|
+ +id: DataID,
+|}>);
+import {description as todoModelDescriptionResolverType} from "../../../relay-runtime/store/__tests__/resolvers/TodoModel.js";
+// Type assertion validating that `todoModelDescriptionResolverType` resolver is correctly implemented.
+// A type error here indicates that the type signature of the resolver module is incorrect.
+(todoModelDescriptionResolverType: (
+ __relay_model_instance: TodoModel____relay_model_instance$data['__relay_model_instance'],
+) => ?string);
+export type RelayResolverNullableModelClientEdgeTest_PluralLiveModelNoneExist_Query$variables = {||};
+export type RelayResolverNullableModelClientEdgeTest_PluralLiveModelNoneExist_Query$data = {|
+ +edge_to_plural_live_objects_none_exist: ?$ReadOnlyArray{|
+ +description: ?string,
+ +id: string,
+ |}>,
+|};
+export type RelayResolverNullableModelClientEdgeTest_PluralLiveModelNoneExist_Query = {|
+ response: RelayResolverNullableModelClientEdgeTest_PluralLiveModelNoneExist_Query$data,
+ variables: RelayResolverNullableModelClientEdgeTest_PluralLiveModelNoneExist_Query$variables,
+|};
+*/
+
+var node/*: ClientRequest*/ = (function(){
+var v0 = {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "id",
+ "storageKey": null
+};
+return {
+ "fragment": {
+ "argumentDefinitions": [],
+ "kind": "Fragment",
+ "metadata": {
+ "hasClientEdges": true
+ },
+ "name": "RelayResolverNullableModelClientEdgeTest_PluralLiveModelNoneExist_Query",
+ "selections": [
+ {
+ "kind": "ClientEdgeToClientObject",
+ "concreteType": "TodoModel",
+ "modelResolver": {
+ "alias": null,
+ "args": null,
+ "fragment": {
+ "args": null,
+ "kind": "FragmentSpread",
+ "name": "TodoModel__id"
+ },
+ "kind": "RelayLiveResolver",
+ "name": "edge_to_plural_live_objects_none_exist",
+ "resolverModule": require('relay-runtime/experimental').resolverDataInjector(require('./../../../relay-runtime/store/__tests__/resolvers/__generated__/TodoModel__id.graphql'), require('./../../../relay-runtime/store/__tests__/resolvers/TodoModel').TodoModel, 'id', true),
+ "path": "edge_to_plural_live_objects_none_exist.__relay_model_instance"
+ },
+ "backingField": {
+ "alias": null,
+ "args": null,
+ "fragment": null,
+ "kind": "RelayResolver",
+ "name": "edge_to_plural_live_objects_none_exist",
+ "resolverModule": require('./../RelayResolverNullableModelClientEdge-test').edge_to_plural_live_objects_none_exist,
+ "path": "edge_to_plural_live_objects_none_exist"
+ },
+ "linkedField": {
+ "alias": null,
+ "args": null,
+ "concreteType": "TodoModel",
+ "kind": "LinkedField",
+ "name": "edge_to_plural_live_objects_none_exist",
+ "plural": true,
+ "selections": [
+ (v0/*: any*/),
+ {
+ "alias": null,
+ "args": null,
+ "fragment": {
+ "args": null,
+ "kind": "FragmentSpread",
+ "name": "TodoModel____relay_model_instance"
+ },
+ "kind": "RelayResolver",
+ "name": "description",
+ "resolverModule": require('relay-runtime/experimental').resolverDataInjector(require('./../../../relay-runtime/store/__tests__/resolvers/__generated__/TodoModel____relay_model_instance.graphql'), require('./../../../relay-runtime/store/__tests__/resolvers/TodoModel').description, '__relay_model_instance', true),
+ "path": "edge_to_plural_live_objects_none_exist.description"
+ }
+ ],
+ "storageKey": null
+ }
+ }
+ ],
+ "type": "Query",
+ "abstractKey": null
+ },
+ "kind": "Request",
+ "operation": {
+ "argumentDefinitions": [],
+ "kind": "Operation",
+ "name": "RelayResolverNullableModelClientEdgeTest_PluralLiveModelNoneExist_Query",
+ "selections": [
+ {
+ "kind": "ClientEdgeToClientObject",
+ "backingField": {
+ "name": "edge_to_plural_live_objects_none_exist",
+ "args": null,
+ "fragment": null,
+ "kind": "RelayResolver",
+ "storageKey": null,
+ "isOutputType": false
+ },
+ "linkedField": {
+ "alias": null,
+ "args": null,
+ "concreteType": "TodoModel",
+ "kind": "LinkedField",
+ "name": "edge_to_plural_live_objects_none_exist",
+ "plural": true,
+ "selections": [
+ (v0/*: any*/),
+ {
+ "name": "description",
+ "args": null,
+ "fragment": {
+ "kind": "InlineFragment",
+ "selections": [
+ {
+ "name": "__relay_model_instance",
+ "args": null,
+ "fragment": {
+ "kind": "InlineFragment",
+ "selections": [
+ (v0/*: any*/)
+ ],
+ "type": "TodoModel",
+ "abstractKey": null
+ },
+ "kind": "RelayResolver",
+ "storageKey": null,
+ "isOutputType": false
+ }
+ ],
+ "type": "TodoModel",
+ "abstractKey": null
+ },
+ "kind": "RelayResolver",
+ "storageKey": null,
+ "isOutputType": true
+ }
+ ],
+ "storageKey": null
+ }
+ }
+ ]
+ },
+ "params": {
+ "cacheID": "2cf8306cf86529bf2fddf425e1816af4",
+ "id": null,
+ "metadata": {},
+ "name": "RelayResolverNullableModelClientEdgeTest_PluralLiveModelNoneExist_Query",
+ "operationKind": "query",
+ "text": null
+ }
+};
+})();
+
+if (__DEV__) {
+ (node/*: any*/).hash = "99ff4eeb2e8eb3dfaed38852f3d2c70f";
+}
+
+module.exports = ((node/*: any*/)/*: ClientQuery<
+ RelayResolverNullableModelClientEdgeTest_PluralLiveModelNoneExist_Query$variables,
+ RelayResolverNullableModelClientEdgeTest_PluralLiveModelNoneExist_Query$data,
+>*/);
diff --git a/packages/react-relay/__tests__/__generated__/RelayResolverNullableModelClientEdgeTest_PluralLiveModel_Query.graphql.js b/packages/react-relay/__tests__/__generated__/RelayResolverNullableModelClientEdgeTest_PluralLiveModel_Query.graphql.js
new file mode 100644
index 0000000000000..5b75ca25b8fcc
--- /dev/null
+++ b/packages/react-relay/__tests__/__generated__/RelayResolverNullableModelClientEdgeTest_PluralLiveModel_Query.graphql.js
@@ -0,0 +1,198 @@
+/**
+ * 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<<068bc2521f50334bee6b072b269cdb55>>
+ * @flow
+ * @lightSyntaxTransform
+ * @nogrep
+ */
+
+/* eslint-disable */
+
+'use strict';
+
+/*::
+import type { ClientRequest, ClientQuery } from 'relay-runtime';
+import type { DataID } from "relay-runtime";
+import type { TodoModel____relay_model_instance$data } from "./../../../relay-runtime/store/__tests__/resolvers/__generated__/TodoModel____relay_model_instance.graphql";
+import {edge_to_plural_live_objects_some_exist as queryEdgeToPluralLiveObjectsSomeExistResolverType} from "../RelayResolverNullableModelClientEdge-test.js";
+// Type assertion validating that `queryEdgeToPluralLiveObjectsSomeExistResolverType` resolver is correctly implemented.
+// A type error here indicates that the type signature of the resolver module is incorrect.
+(queryEdgeToPluralLiveObjectsSomeExistResolverType: () => ?$ReadOnlyArray{|
+ +id: DataID,
+|}>);
+import {description as todoModelDescriptionResolverType} from "../../../relay-runtime/store/__tests__/resolvers/TodoModel.js";
+// Type assertion validating that `todoModelDescriptionResolverType` resolver is correctly implemented.
+// A type error here indicates that the type signature of the resolver module is incorrect.
+(todoModelDescriptionResolverType: (
+ __relay_model_instance: TodoModel____relay_model_instance$data['__relay_model_instance'],
+) => ?string);
+export type RelayResolverNullableModelClientEdgeTest_PluralLiveModel_Query$variables = {||};
+export type RelayResolverNullableModelClientEdgeTest_PluralLiveModel_Query$data = {|
+ +edge_to_plural_live_objects_some_exist: ?$ReadOnlyArray{|
+ +description: ?string,
+ +id: string,
+ |}>,
+|};
+export type RelayResolverNullableModelClientEdgeTest_PluralLiveModel_Query = {|
+ response: RelayResolverNullableModelClientEdgeTest_PluralLiveModel_Query$data,
+ variables: RelayResolverNullableModelClientEdgeTest_PluralLiveModel_Query$variables,
+|};
+*/
+
+var node/*: ClientRequest*/ = (function(){
+var v0 = {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "id",
+ "storageKey": null
+};
+return {
+ "fragment": {
+ "argumentDefinitions": [],
+ "kind": "Fragment",
+ "metadata": {
+ "hasClientEdges": true
+ },
+ "name": "RelayResolverNullableModelClientEdgeTest_PluralLiveModel_Query",
+ "selections": [
+ {
+ "kind": "ClientEdgeToClientObject",
+ "concreteType": "TodoModel",
+ "modelResolver": {
+ "alias": null,
+ "args": null,
+ "fragment": {
+ "args": null,
+ "kind": "FragmentSpread",
+ "name": "TodoModel__id"
+ },
+ "kind": "RelayLiveResolver",
+ "name": "edge_to_plural_live_objects_some_exist",
+ "resolverModule": require('relay-runtime/experimental').resolverDataInjector(require('./../../../relay-runtime/store/__tests__/resolvers/__generated__/TodoModel__id.graphql'), require('./../../../relay-runtime/store/__tests__/resolvers/TodoModel').TodoModel, 'id', true),
+ "path": "edge_to_plural_live_objects_some_exist.__relay_model_instance"
+ },
+ "backingField": {
+ "alias": null,
+ "args": null,
+ "fragment": null,
+ "kind": "RelayResolver",
+ "name": "edge_to_plural_live_objects_some_exist",
+ "resolverModule": require('./../RelayResolverNullableModelClientEdge-test').edge_to_plural_live_objects_some_exist,
+ "path": "edge_to_plural_live_objects_some_exist"
+ },
+ "linkedField": {
+ "alias": null,
+ "args": null,
+ "concreteType": "TodoModel",
+ "kind": "LinkedField",
+ "name": "edge_to_plural_live_objects_some_exist",
+ "plural": true,
+ "selections": [
+ (v0/*: any*/),
+ {
+ "alias": null,
+ "args": null,
+ "fragment": {
+ "args": null,
+ "kind": "FragmentSpread",
+ "name": "TodoModel____relay_model_instance"
+ },
+ "kind": "RelayResolver",
+ "name": "description",
+ "resolverModule": require('relay-runtime/experimental').resolverDataInjector(require('./../../../relay-runtime/store/__tests__/resolvers/__generated__/TodoModel____relay_model_instance.graphql'), require('./../../../relay-runtime/store/__tests__/resolvers/TodoModel').description, '__relay_model_instance', true),
+ "path": "edge_to_plural_live_objects_some_exist.description"
+ }
+ ],
+ "storageKey": null
+ }
+ }
+ ],
+ "type": "Query",
+ "abstractKey": null
+ },
+ "kind": "Request",
+ "operation": {
+ "argumentDefinitions": [],
+ "kind": "Operation",
+ "name": "RelayResolverNullableModelClientEdgeTest_PluralLiveModel_Query",
+ "selections": [
+ {
+ "kind": "ClientEdgeToClientObject",
+ "backingField": {
+ "name": "edge_to_plural_live_objects_some_exist",
+ "args": null,
+ "fragment": null,
+ "kind": "RelayResolver",
+ "storageKey": null,
+ "isOutputType": false
+ },
+ "linkedField": {
+ "alias": null,
+ "args": null,
+ "concreteType": "TodoModel",
+ "kind": "LinkedField",
+ "name": "edge_to_plural_live_objects_some_exist",
+ "plural": true,
+ "selections": [
+ (v0/*: any*/),
+ {
+ "name": "description",
+ "args": null,
+ "fragment": {
+ "kind": "InlineFragment",
+ "selections": [
+ {
+ "name": "__relay_model_instance",
+ "args": null,
+ "fragment": {
+ "kind": "InlineFragment",
+ "selections": [
+ (v0/*: any*/)
+ ],
+ "type": "TodoModel",
+ "abstractKey": null
+ },
+ "kind": "RelayResolver",
+ "storageKey": null,
+ "isOutputType": false
+ }
+ ],
+ "type": "TodoModel",
+ "abstractKey": null
+ },
+ "kind": "RelayResolver",
+ "storageKey": null,
+ "isOutputType": true
+ }
+ ],
+ "storageKey": null
+ }
+ }
+ ]
+ },
+ "params": {
+ "cacheID": "21b0ec4e0d6526708b4a0de91391b7d7",
+ "id": null,
+ "metadata": {},
+ "name": "RelayResolverNullableModelClientEdgeTest_PluralLiveModel_Query",
+ "operationKind": "query",
+ "text": null
+ }
+};
+})();
+
+if (__DEV__) {
+ (node/*: any*/).hash = "abbb7292c9ca7ffab83aec05b278406b";
+}
+
+module.exports = ((node/*: any*/)/*: ClientQuery<
+ RelayResolverNullableModelClientEdgeTest_PluralLiveModel_Query$variables,
+ RelayResolverNullableModelClientEdgeTest_PluralLiveModel_Query$data,
+>*/);
diff --git a/packages/relay-runtime/store/RelayReader.js b/packages/relay-runtime/store/RelayReader.js
index 0414ca6868aeb..9387f2ab0d3aa 100644
--- a/packages/relay-runtime/store/RelayReader.js
+++ b/packages/relay-runtime/store/RelayReader.js
@@ -740,10 +740,18 @@ class RelayReader {
validClientEdgeResolverResponse.ids,
this._resolverCache,
);
+ let validStoreIDs: $ReadOnlyArray = storeIDs;
+ if (field.modelResolver != null) {
+ const modelResolver = field.modelResolver;
+ validStoreIDs = storeIDs.map(storeID => {
+ const model = this._readResolverFieldImpl(modelResolver, storeID);
+ return model != null ? storeID : null;
+ });
+ }
this._clientEdgeTraversalPath.push(null);
const edgeValues = this._readLinkedIds(
field.linkedField,
- storeIDs,
+ validStoreIDs,
record,
data,
);