diff --git a/packages/react-relay/__tests__/RelayResolverModel-test.js b/packages/react-relay/__tests__/RelayResolverModel-test.js index aad8600fbed9c..74c3b5a8db72d 100644 --- a/packages/react-relay/__tests__/RelayResolverModel-test.js +++ b/packages/react-relay/__tests__/RelayResolverModel-test.js @@ -31,6 +31,7 @@ const RelayNetwork = require('relay-runtime/network/RelayNetwork'); const {graphql} = require('relay-runtime/query/GraphQLTag'); const { addTodo, + changeDescription, completeTodo, resetStore, } = require('relay-runtime/store/__tests__/resolvers/ExampleTodoStore'); @@ -279,6 +280,41 @@ describe.each([ expect(renderer.toJSON()).toEqual('Test todo - green'); }); + test('read a field with arguments', () => { + function TodoComponentWithFieldWithArgumentsComponent(props: { + todoID: string, + }) { + const data = useClientQuery( + graphql` + query RelayResolverModelTestFieldWithArgumentsQuery($id: ID!) { + todo_model(todoID: $id) { + fancy_description { + text_with_prefix(prefix: "[x]") + } + } + } + `, + {id: props.todoID}, + ); + return data?.todo_model?.fancy_description?.text_with_prefix; + } + + addTodo('Test todo'); + + const renderer = TestRenderer.create( + + + , + ); + expect(renderer.toJSON()).toEqual('[x] Test todo'); + + TestRenderer.act(() => { + changeDescription('todo-1', 'Changed todo description text'); + jest.runAllImmediates(); + }); + expect(renderer.toJSON()).toEqual('[x] Changed todo description text'); + }); + test('read interface field', () => { function TodoComponentWithInterfaceComponent(props: {todoID: string}) { const data = useClientQuery( diff --git a/packages/react-relay/__tests__/__generated__/RelayResolverModelTestFieldWithArgumentsQuery.graphql.js b/packages/react-relay/__tests__/__generated__/RelayResolverModelTestFieldWithArgumentsQuery.graphql.js new file mode 100644 index 0000000000000..bc36a7870c421 --- /dev/null +++ b/packages/react-relay/__tests__/__generated__/RelayResolverModelTestFieldWithArgumentsQuery.graphql.js @@ -0,0 +1,292 @@ +/** + * 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 { ClientRequest, ClientQuery } from 'relay-runtime'; +import type { DataID } from "relay-runtime"; +import type { TodoDescription____relay_model_instance$data } from "./../../../relay-runtime/store/__tests__/resolvers/__generated__/TodoDescription____relay_model_instance.graphql"; +import type { TodoModel____relay_model_instance$data } from "./../../../relay-runtime/store/__tests__/resolvers/__generated__/TodoModel____relay_model_instance.graphql"; +import {todo_model as queryTodoModelResolverType} from "../../../relay-runtime/store/__tests__/resolvers/QueryTodoModel.js"; +// Type assertion validating that `queryTodoModelResolverType` resolver is correctly implemented. +// A type error here indicates that the type signature of the resolver module is incorrect. +(queryTodoModelResolverType: ( + args: {| + todoID: string, + |}, +) => ?{| + +id: DataID, +|}); +import {text_with_prefix as todoDescriptionTextWithPrefixResolverType} from "../../../relay-runtime/store/__tests__/resolvers/TodoDescription.js"; +// Type assertion validating that `todoDescriptionTextWithPrefixResolverType` resolver is correctly implemented. +// A type error here indicates that the type signature of the resolver module is incorrect. +(todoDescriptionTextWithPrefixResolverType: ( + __relay_model_instance: TodoDescription____relay_model_instance$data['__relay_model_instance'], + args: {| + prefix: string, + |}, +) => ?string); +import {fancy_description as todoModelFancyDescriptionResolverType} from "../../../relay-runtime/store/__tests__/resolvers/TodoModel.js"; +// Type assertion validating that `todoModelFancyDescriptionResolverType` resolver is correctly implemented. +// A type error here indicates that the type signature of the resolver module is incorrect. +(todoModelFancyDescriptionResolverType: ( + __relay_model_instance: TodoModel____relay_model_instance$data['__relay_model_instance'], +) => ?TodoDescription); +import type { TodoModel__fancy_description$normalization } from "./../../../relay-runtime/store/__tests__/resolvers/__generated__/TodoModel__fancy_description$normalization.graphql"; +import type { TodoDescription } from "../../../relay-runtime/store/__tests__/resolvers/TodoDescription.js"; +export type RelayResolverModelTestFieldWithArgumentsQuery$variables = {| + id: string, +|}; +export type RelayResolverModelTestFieldWithArgumentsQuery$data = {| + +todo_model: ?{| + +fancy_description: ?{| + +text_with_prefix: ?string, + |}, + |}, +|}; +export type RelayResolverModelTestFieldWithArgumentsQuery = {| + response: RelayResolverModelTestFieldWithArgumentsQuery$data, + variables: RelayResolverModelTestFieldWithArgumentsQuery$variables, +|}; +*/ + +var node/*: ClientRequest*/ = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "id" + } +], +v1 = [ + { + "kind": "Variable", + "name": "todoID", + "variableName": "id" + } +], +v2 = [ + { + "kind": "Literal", + "name": "prefix", + "value": "[x]" + } +], +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": { + "hasClientEdges": true + }, + "name": "RelayResolverModelTestFieldWithArgumentsQuery", + "selections": [ + { + "kind": "ClientEdgeToClientObject", + "concreteType": "TodoModel", + "backingField": { + "alias": null, + "args": (v1/*: any*/), + "fragment": null, + "kind": "RelayResolver", + "name": "todo_model", + "resolverModule": require('./../../../relay-runtime/store/__tests__/resolvers/QueryTodoModel').todo_model, + "path": "todo_model" + }, + "linkedField": { + "alias": null, + "args": (v1/*: any*/), + "concreteType": "TodoModel", + "kind": "LinkedField", + "name": "todo_model", + "plural": false, + "selections": [ + { + "kind": "ClientEdgeToClientObject", + "concreteType": "TodoDescription", + "backingField": { + "alias": null, + "args": null, + "fragment": { + "args": null, + "kind": "FragmentSpread", + "name": "TodoModel____relay_model_instance" + }, + "kind": "RelayResolver", + "name": "fancy_description", + "resolverModule": require('relay-runtime/experimental').weakObjectWrapper(require('relay-runtime/experimental').resolverDataInjector(require('./../../../relay-runtime/store/__tests__/resolvers/__generated__/TodoModel____relay_model_instance.graphql'), require('./../../../relay-runtime/store/__tests__/resolvers/TodoModel').fancy_description, '__relay_model_instance', false), '__relay_model_instance', false), + "path": "fancy_description", + "normalizationInfo": { + "concreteType": "TodoDescription", + "plural": false, + "normalizationNode": require('./../../../relay-runtime/store/__tests__/resolvers/__generated__/TodoModel__fancy_description$normalization.graphql') + } + }, + "linkedField": { + "alias": null, + "args": null, + "concreteType": "TodoDescription", + "kind": "LinkedField", + "name": "fancy_description", + "plural": false, + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "fragment": { + "args": null, + "kind": "FragmentSpread", + "name": "TodoDescription____relay_model_instance" + }, + "kind": "RelayResolver", + "name": "text_with_prefix", + "resolverModule": require('relay-runtime/experimental').resolverDataInjector(require('./../../../relay-runtime/store/__tests__/resolvers/__generated__/TodoDescription____relay_model_instance.graphql'), require('./../../../relay-runtime/store/__tests__/resolvers/TodoDescription').text_with_prefix, '__relay_model_instance', false), + "path": "text_with_prefix" + } + ], + "storageKey": null + } + } + ], + "storageKey": null + } + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "RelayResolverModelTestFieldWithArgumentsQuery", + "selections": [ + { + "kind": "ClientEdgeToClientObject", + "backingField": { + "name": "todo_model", + "args": (v1/*: any*/), + "fragment": null, + "kind": "RelayResolver", + "storageKey": null, + "isOutputType": false + }, + "linkedField": { + "alias": null, + "args": (v1/*: any*/), + "concreteType": "TodoModel", + "kind": "LinkedField", + "name": "todo_model", + "plural": false, + "selections": [ + { + "kind": "ClientEdgeToClientObject", + "backingField": { + "name": "fancy_description", + "args": null, + "fragment": { + "kind": "InlineFragment", + "selections": [ + { + "name": "__relay_model_instance", + "args": null, + "fragment": { + "kind": "InlineFragment", + "selections": [ + (v3/*: any*/) + ], + "type": "TodoModel", + "abstractKey": null + }, + "kind": "RelayResolver", + "storageKey": null, + "isOutputType": false + } + ], + "type": "TodoModel", + "abstractKey": null + }, + "kind": "RelayResolver", + "storageKey": null, + "isOutputType": true + }, + "linkedField": { + "alias": null, + "args": null, + "concreteType": "TodoDescription", + "kind": "LinkedField", + "name": "fancy_description", + "plural": false, + "selections": [ + { + "name": "text_with_prefix", + "args": (v2/*: any*/), + "fragment": { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__relay_model_instance", + "storageKey": null + } + ], + "type": "TodoDescription", + "abstractKey": null + }, + "kind": "RelayResolver", + "storageKey": "text_with_prefix(prefix:\"[x]\")", + "isOutputType": true + } + ], + "storageKey": null + } + }, + (v3/*: any*/) + ], + "storageKey": null + } + } + ] + }, + "params": { + "cacheID": "b10312a5cde1a4ef8e38b4b474a45635", + "id": null, + "metadata": {}, + "name": "RelayResolverModelTestFieldWithArgumentsQuery", + "operationKind": "query", + "text": null + } +}; +})(); + +if (__DEV__) { + (node/*: any*/).hash = "d0daaf4fcd4eaf3bce605aa4775fff4a"; +} + +module.exports = ((node/*: any*/)/*: ClientQuery< + RelayResolverModelTestFieldWithArgumentsQuery$variables, + RelayResolverModelTestFieldWithArgumentsQuery$data, +>*/); diff --git a/packages/relay-runtime/store/__tests__/resolvers/ExampleTodoStore.js b/packages/relay-runtime/store/__tests__/resolvers/ExampleTodoStore.js index e963885befd33..691dd61b1cfb7 100644 --- a/packages/relay-runtime/store/__tests__/resolvers/ExampleTodoStore.js +++ b/packages/relay-runtime/store/__tests__/resolvers/ExampleTodoStore.js @@ -39,6 +39,13 @@ type ACTION = type: 'REMOVE_TODO', payload: TodoID, } + | { + type: 'CHANGE_TODO_DESCRIPTION', + payload: { + todoID: TodoID, + description: string, + }, + } | { type: 'BLOCKED_BY', payload: { @@ -130,6 +137,20 @@ class TodoStore { this._notify([action.payload.todoID]); break; } + case 'CHANGE_TODO_DESCRIPTION': { + this._state = this._state.map(todo => { + if (todo.todoID === action.payload.todoID) { + return { + ...todo, + description: action.payload.description, + }; + } else { + return todo; + } + }); + this._notify([action.payload.todoID]); + break; + } default: (action.type: empty); } @@ -217,6 +238,16 @@ function blockedBy(todoID: string, blockedBy: string) { }); } +function changeDescription(todoID: string, description: string) { + TODO_STORE.dispatch({ + type: 'CHANGE_TODO_DESCRIPTION', + payload: { + todoID, + description, + }, + }); +} + module.exports = { TODO_STORE, Selectors, @@ -225,4 +256,5 @@ module.exports = { completeTodo, removeTodo, blockedBy, + changeDescription, }; diff --git a/packages/relay-runtime/store/__tests__/resolvers/TodoDescription.js b/packages/relay-runtime/store/__tests__/resolvers/TodoDescription.js index 3dc51c3ed2ce9..8bc37399c426f 100644 --- a/packages/relay-runtime/store/__tests__/resolvers/TodoDescription.js +++ b/packages/relay-runtime/store/__tests__/resolvers/TodoDescription.js @@ -9,6 +9,9 @@ * @oncall relay */ +import type {TodoDescription__some_client_type_with_interface$normalization} from './__generated__/TodoDescription__some_client_type_with_interface$normalization.graphql'; +import type {TodoDescription__some_interface$normalization} from './__generated__/TodoDescription__some_interface$normalization.graphql'; + /** * @RelayResolver TodoDescription * @weak @@ -18,9 +21,6 @@ export opaque type TodoDescription = { color: string, }; -import type {TodoDescription__some_client_type_with_interface$normalization} from './__generated__/TodoDescription__some_client_type_with_interface$normalization.graphql'; -import type {TodoDescription__some_interface$normalization} from './__generated__/TodoDescription__some_interface$normalization.graphql'; - // Public constructor for opaque `TodoDescription`. // Other resolvers have to call this function to // create an instance of `TodoDescription`. @@ -41,6 +41,19 @@ function text(instance: ?TodoDescription): ?string { return instance?.text; } +/** + * @RelayResolver TodoDescription.text_with_prefix(prefix: String!): String + */ +function text_with_prefix( + instance: ?TodoDescription, + args: {prefix: string}, +): ?string { + if (instance == null) { + return null; + } + return `${args.prefix} ${instance.text}`; +} + /** * @RelayResolver TodoDescription.color: RelayResolverValue */ @@ -75,6 +88,7 @@ function some_client_type_with_interface( } module.exports = { + text_with_prefix, createTodoDescription, text, color, diff --git a/packages/relay-runtime/store/experimental-live-resolvers/resolverDataInjector.js b/packages/relay-runtime/store/experimental-live-resolvers/resolverDataInjector.js index 1ea8b4c9b57a6..0d161cb2c1980 100644 --- a/packages/relay-runtime/store/experimental-live-resolvers/resolverDataInjector.js +++ b/packages/relay-runtime/store/experimental-live-resolvers/resolverDataInjector.js @@ -16,9 +16,11 @@ import type {FragmentType} from '../RelayStoreTypes'; const {readFragment} = require('../ResolverFragments'); const invariant = require('invariant'); +type ResolverFn = ($FlowFixMe, ?$FlowFixMe) => mixed; + /** * - * This a High order function that returns a relay resolver that can read the data for + * This a higher order function that returns a relay resolver that can read the data for * the fragment`. * * - fragment: contains fragment Reader AST with resolver's data dependencies. @@ -28,16 +30,15 @@ const invariant = require('invariant'); * This will not call the `resolverFn` if the fragment data for it is null/undefined. * The the compiler generates calls to this function, ensuring the correct set of arguments. */ -function resolverDataInjector< - TFragmentType: FragmentType, - TData: ?{...}, - TResolverFn: ($FlowFixMe, ?$FlowFixMe) => mixed, ->( +function resolverDataInjector( fragment: Fragment, - resolverFn: TResolverFn, + // Resolvers have their own type assertions, we don't want to confuse users + // with a type error in their generated code at this point. + _resolverFn: $FlowFixMe, fieldName?: string, isRequiredField?: boolean, ): (fragmentKey: TFragmentType, args: mixed) => mixed { + const resolverFn: ResolverFn = _resolverFn; return (fragmentKey: TFragmentType, args: mixed): mixed => { const data = readFragment(fragment, fragmentKey); if (fieldName != null) {