From 7d5cc34318916dde0d2e79a72986122dff59bfc8 Mon Sep 17 00:00:00 2001 From: Andrey Lunyov Date: Tue, 14 Jun 2022 11:55:42 -0700 Subject: [PATCH] Add support for resolvers without fragments Summary: Remove validation for `rootFragment` on the resolver definition. - With this we can expose plain functions as GraphQL fields - With this we can extend object with client fields that may return Client or Server Objects. Reviewed By: josephsavona Differential Revision: D37119681 fbshipit-source-id: 203fd3e99d68096532b9eb3e45f661766da2402a --- compiler/crates/relay-docblock/src/lib.rs | 8 - ...r-missing-multiple-fields.invalid.expected | 10 -- .../__tests__/ClientOnlyQueries-test.js | 127 +++++++++++++++- ...nlyQueriesTest3Query_hello_user.graphql.js | 141 ++++++++++++++++++ .../ClientOnlyQueriesTest2Query.graphql.js | 105 +++++++++++++ .../ClientOnlyQueriesTest3Query.graphql.js | 121 +++++++++++++++ ...nlyQueriesTest3Query_hello_user.graphql.js | 76 ++++++++++ .../__tests__/resolvers/HelloUserResolver.js | 28 ++++ .../__tests__/resolvers/HelloWorldResolver.js | 25 ++++ packages/relay-runtime/util/ReaderNode.js | 2 +- 10 files changed, 623 insertions(+), 20 deletions(-) create mode 100644 packages/react-relay/__tests__/__generated__/ClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user.graphql.js create mode 100644 packages/react-relay/__tests__/__generated__/ClientOnlyQueriesTest2Query.graphql.js create mode 100644 packages/react-relay/__tests__/__generated__/ClientOnlyQueriesTest3Query.graphql.js create mode 100644 packages/react-relay/__tests__/__generated__/RefetchableClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user.graphql.js create mode 100644 packages/relay-runtime/store/__tests__/resolvers/HelloUserResolver.js create mode 100644 packages/relay-runtime/store/__tests__/resolvers/HelloWorldResolver.js diff --git a/compiler/crates/relay-docblock/src/lib.rs b/compiler/crates/relay-docblock/src/lib.rs index 67456be2e31d1..a4d8e20af837a 100644 --- a/compiler/crates/relay-docblock/src/lib.rs +++ b/compiler/crates/relay-docblock/src/lib.rs @@ -116,14 +116,6 @@ impl RelayResolverParser { } let live = self.fields.get(&LIVE_FIELD).copied(); let root_fragment = self.get_field_with_value(*ROOT_FRAGMENT_FIELD)?; - if live.is_none() && root_fragment.is_none() { - self.errors.push(Diagnostic::error( - ErrorMessages::MissingField { - field_name: *ROOT_FRAGMENT_FIELD, - }, - ast.location, - )); - } let fragment_definition = root_fragment .map(|root_fragment| { self.assert_fragment_definition(root_fragment.value, definitions_in_file) diff --git a/compiler/crates/relay-docblock/tests/parse/fixtures/relay-resolver-missing-multiple-fields.invalid.expected b/compiler/crates/relay-docblock/tests/parse/fixtures/relay-resolver-missing-multiple-fields.invalid.expected index f2d6f36a7663e..bfeda514c3fba 100644 --- a/compiler/crates/relay-docblock/tests/parse/fixtures/relay-resolver-missing-multiple-fields.invalid.expected +++ b/compiler/crates/relay-docblock/tests/parse/fixtures/relay-resolver-missing-multiple-fields.invalid.expected @@ -30,16 +30,6 @@ graphql` ✖︎ Missing docblock field "@fieldName" - /path/to/test/fixture/relay-resolver-missing-multiple-fields.invalid.js:10:3 - 10 │ * - │ ^ - 11 │ * @RelayResolver - │ ^^^^^^^^^^^^^^^^^ - 12 │ - - -✖︎ Missing docblock field "@rootFragment" - /path/to/test/fixture/relay-resolver-missing-multiple-fields.invalid.js:10:3 10 │ * │ ^ diff --git a/packages/react-relay/__tests__/ClientOnlyQueries-test.js b/packages/react-relay/__tests__/ClientOnlyQueries-test.js index eb6c2efdd3c95..2201d0fd67662 100644 --- a/packages/react-relay/__tests__/ClientOnlyQueries-test.js +++ b/packages/react-relay/__tests__/ClientOnlyQueries-test.js @@ -25,6 +25,7 @@ const { Environment, Network, RecordSource, + RelayFeatureFlags, Store, commitLocalUpdate, createOperationDescriptor, @@ -48,7 +49,17 @@ function createEnvironment( }); } -describe('Client Only Queries', () => { +beforeEach(() => { + RelayFeatureFlags.ENABLE_RELAY_RESOLVERS = true; + RelayFeatureFlags.ENABLE_CLIENT_EDGES = true; +}); + +afterEach(() => { + RelayFeatureFlags.ENABLE_RELAY_RESOLVERS = false; + RelayFeatureFlags.ENABLE_CLIENT_EDGES = false; +}); + +describe('Client-only queries', () => { let renderer; let environment: IEnvironment; @@ -217,3 +228,117 @@ describe('Client Only Queries', () => { ); }); }); + +test('hello-world query', () => { + const environment = createEnvironment(RecordSource.create(), () => { + const error = new Error('Unexpected Network Error'); + throw error; + }); + + function InnerTestComponent(props: {|fetchPolicy?: FetchPolicy|}) { + const data = useLazyLoadQuery( + graphql` + query ClientOnlyQueriesTest2Query { + hello(world: "World") + } + `, + {}, + { + fetchPolicy: props.fetchPolicy ?? 'store-only', + }, + ); + return data.hello ?? 'MISSING'; + } + + function TestComponent({ + environment: relayEnvironment, + ...rest + }: {| + environment: IEnvironment, + fetchPolicy?: FetchPolicy, + |}) { + return ( + + + + + + ); + } + let renderer; + TestRenderer.act(() => { + renderer = TestRenderer.create( + , + ); + }); + + expect(renderer?.toJSON()).toBe('Hello, World!'); +}); + +test('hello user query with client-edge query', () => { + const environment = createEnvironment( + RecordSource.create({ + 'client:root': { + __id: 'client:root', + __typename: '__Root', + 'node(id:"4")': {__ref: '4'}, + }, + '4': { + id: '4', + __id: '4', + name: 'Alice', + }, + }), + () => { + const error = new Error('Unexpected Network Error'); + throw error; + }, + ); + + function InnerTestComponent(props: {|fetchPolicy?: FetchPolicy|}) { + const data = useLazyLoadQuery( + graphql` + query ClientOnlyQueriesTest3Query { + hello_user(id: "4") @waterfall { + name + } + } + `, + {}, + { + fetchPolicy: props.fetchPolicy ?? 'store-only', + }, + ); + return `Hello, ${data.hello_user?.name ?? 'MISSING'}!`; + } + + function TestComponent({ + environment: relayEnvironment, + ...rest + }: {| + environment: IEnvironment, + fetchPolicy?: FetchPolicy, + |}) { + return ( + + + + + + ); + } + let renderer; + TestRenderer.act(() => { + renderer = TestRenderer.create( + , + ); + }); + + expect(renderer?.toJSON()).toBe('Hello, Alice!'); +}); diff --git a/packages/react-relay/__tests__/__generated__/ClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user.graphql.js b/packages/react-relay/__tests__/__generated__/ClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user.graphql.js new file mode 100644 index 0000000000000..d379c8c708044 --- /dev/null +++ b/packages/react-relay/__tests__/__generated__/ClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user.graphql.js @@ -0,0 +1,141 @@ +/** + * 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. + * + * @generated SignedSource<<3ab248e5fc14790dea4cb7c10d20f29c>> + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { ConcreteRequest, Query } from 'relay-runtime'; +type RefetchableClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user$fragmentType = any; +export type ClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user$variables = {| + id: string, +|}; +export type ClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user$data = {| + +node: ?{| + +$fragmentSpreads: RefetchableClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user$fragmentType, + |}, +|}; +export type ClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user = {| + response: ClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user$data, + variables: ClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user$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_ClientOnlyQueriesTest3Query_hello_user", + "selections": [ + { + "alias": null, + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "RefetchableClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user" + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "ClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user", + "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": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } + ], + "type": "User", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "81c68d70c6784d71b29ff3f553692c95", + "id": null, + "metadata": {}, + "name": "ClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user", + "operationKind": "query", + "text": "query ClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user(\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...RefetchableClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user\n id\n }\n}\n\nfragment RefetchableClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user on User {\n name\n id\n}\n" + } +}; +})(); + +if (__DEV__) { + (node/*: any*/).hash = "f39e561157fa607bf64e77aad228aa05"; +} + +module.exports = ((node/*: any*/)/*: Query< + ClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user$variables, + ClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user$data, +>*/); diff --git a/packages/react-relay/__tests__/__generated__/ClientOnlyQueriesTest2Query.graphql.js b/packages/react-relay/__tests__/__generated__/ClientOnlyQueriesTest2Query.graphql.js new file mode 100644 index 0000000000000..838a8242eb8e7 --- /dev/null +++ b/packages/react-relay/__tests__/__generated__/ClientOnlyQueriesTest2Query.graphql.js @@ -0,0 +1,105 @@ +/** + * 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. + * + * @generated SignedSource<> + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { ClientRequest, ClientQuery } from 'relay-runtime'; +import queryHelloResolver from "../../../relay-runtime/store/__tests__/resolvers/HelloWorldResolver.js"; +// Type assertion validating that `queryHelloResolver` resolver is correctly implemented. +// A type error here indicates that the type signature of the resolver module is incorrect. +(queryHelloResolver: ( + args: {| + world: string, + |}, +) => mixed); +export type ClientOnlyQueriesTest2Query$variables = {||}; +export type ClientOnlyQueriesTest2Query$data = {| + +hello: ?$Call<((...empty[]) => R) => R, typeof queryHelloResolver>, +|}; +export type ClientOnlyQueriesTest2Query = {| + response: ClientOnlyQueriesTest2Query$data, + variables: ClientOnlyQueriesTest2Query$variables, +|}; +*/ + +var node/*: ClientRequest*/ = { + "fragment": { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "ClientOnlyQueriesTest2Query", + "selections": [ + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": [ + { + "kind": "Literal", + "name": "world", + "value": "World" + } + ], + "fragment": null, + "kind": "RelayResolver", + "name": "hello", + "resolverModule": require('./../../../relay-runtime/store/__tests__/resolvers/HelloWorldResolver.js'), + "path": "hello" + } + ] + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [], + "kind": "Operation", + "name": "ClientOnlyQueriesTest2Query", + "selections": [ + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] + } + ] + }, + "params": { + "cacheID": "9db9f9cb546151c1f888e0e64e9d23b4", + "id": null, + "metadata": {}, + "name": "ClientOnlyQueriesTest2Query", + "operationKind": "query", + "text": null + } +}; + +if (__DEV__) { + (node/*: any*/).hash = "42a67a5a5af91776ffbd2e05505b5001"; +} + +module.exports = ((node/*: any*/)/*: ClientQuery< + ClientOnlyQueriesTest2Query$variables, + ClientOnlyQueriesTest2Query$data, +>*/); diff --git a/packages/react-relay/__tests__/__generated__/ClientOnlyQueriesTest3Query.graphql.js b/packages/react-relay/__tests__/__generated__/ClientOnlyQueriesTest3Query.graphql.js new file mode 100644 index 0000000000000..464780b51ee3f --- /dev/null +++ b/packages/react-relay/__tests__/__generated__/ClientOnlyQueriesTest3Query.graphql.js @@ -0,0 +1,121 @@ +/** + * 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. + * + * @generated SignedSource<<22e88ae935bf1f8281ac4b90105cfccc>> + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { ClientRequest, ClientQuery } from 'relay-runtime'; +export type ClientOnlyQueriesTest3Query$variables = {||}; +export type ClientOnlyQueriesTest3Query$data = {| + +hello_user: ?{| + +name: ?string, + |}, +|}; +export type ClientOnlyQueriesTest3Query = {| + response: ClientOnlyQueriesTest3Query$data, + variables: ClientOnlyQueriesTest3Query$variables, +|}; +*/ + +var node/*: ClientRequest*/ = (function(){ +var v0 = [ + { + "kind": "Literal", + "name": "id", + "value": "4" + } +]; +return { + "fragment": { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": { + "hasClientEdges": true + }, + "name": "ClientOnlyQueriesTest3Query", + "selections": [ + { + "kind": "ClientEdgeToServerObject", + "operation": require('./ClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user.graphql'), + "backingField": { + "alias": null, + "args": (v0/*: any*/), + "fragment": null, + "kind": "RelayResolver", + "name": "hello_user", + "resolverModule": require('./../../../relay-runtime/store/__tests__/resolvers/HelloUserResolver.js'), + "path": "hello_user" + }, + "linkedField": { + "alias": null, + "args": (v0/*: any*/), + "concreteType": "User", + "kind": "LinkedField", + "name": "hello_user", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } + ], + "storageKey": "hello_user(id:\"4\")" + } + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [], + "kind": "Operation", + "name": "ClientOnlyQueriesTest3Query", + "selections": [ + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__id", + "storageKey": null + } + ] + } + ] + }, + "params": { + "cacheID": "c0b1f496196ba70c283176143dc0780d", + "id": null, + "metadata": {}, + "name": "ClientOnlyQueriesTest3Query", + "operationKind": "query", + "text": null + } +}; +})(); + +if (__DEV__) { + (node/*: any*/).hash = "f39e561157fa607bf64e77aad228aa05"; +} + +module.exports = ((node/*: any*/)/*: ClientQuery< + ClientOnlyQueriesTest3Query$variables, + ClientOnlyQueriesTest3Query$data, +>*/); diff --git a/packages/react-relay/__tests__/__generated__/RefetchableClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user.graphql.js b/packages/react-relay/__tests__/__generated__/RefetchableClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user.graphql.js new file mode 100644 index 0000000000000..19cccd82aeebe --- /dev/null +++ b/packages/react-relay/__tests__/__generated__/RefetchableClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user.graphql.js @@ -0,0 +1,76 @@ +/** + * 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. + * + * @generated SignedSource<<647c59e735d028c3ea3aa62f1941e1e0>> + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { ReaderFragment, RefetchableFragment } from 'relay-runtime'; +import type { FragmentType } from "relay-runtime"; +declare export opaque type RefetchableClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user$fragmentType: FragmentType; +type ClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user$variables = any; +export type RefetchableClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user$data = {| + +id: string, + +name: ?string, + +$fragmentType: RefetchableClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user$fragmentType, +|}; +export type RefetchableClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user$key = { + +$data?: RefetchableClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user$data, + +$fragmentSpreads: RefetchableClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user$fragmentType, + ... +}; +*/ + +var node/*: ReaderFragment*/ = { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": { + "refetch": { + "connection": null, + "fragmentPathInResult": [ + "node" + ], + "operation": require('./ClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user.graphql'), + "identifierField": "id" + } + }, + "name": "RefetchableClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + } + ], + "type": "User", + "abstractKey": null +}; + +if (__DEV__) { + (node/*: any*/).hash = "f39e561157fa607bf64e77aad228aa05"; +} + +module.exports = ((node/*: any*/)/*: RefetchableFragment< + RefetchableClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user$fragmentType, + RefetchableClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user$data, + ClientEdgeQuery_ClientOnlyQueriesTest3Query_hello_user$variables, +>*/); diff --git a/packages/relay-runtime/store/__tests__/resolvers/HelloUserResolver.js b/packages/relay-runtime/store/__tests__/resolvers/HelloUserResolver.js new file mode 100644 index 0000000000000..5f4cb1d733303 --- /dev/null +++ b/packages/relay-runtime/store/__tests__/resolvers/HelloUserResolver.js @@ -0,0 +1,28 @@ +/** + * 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. + * + * @format + * @flow strict-local + * @emails oncall+relay + */ + +'use strict'; + +import type {DataID} from '../../../util/RelayRuntimeTypes'; + +/** + * @RelayResolver + * @fieldName hello_user(id: ID!) + * @edgeTo User + * @onType Query + * + * This should return the User + */ +function helloUserResolver(args: {id: string}): DataID { + return args.id; +} + +module.exports = helloUserResolver; diff --git a/packages/relay-runtime/store/__tests__/resolvers/HelloWorldResolver.js b/packages/relay-runtime/store/__tests__/resolvers/HelloWorldResolver.js new file mode 100644 index 0000000000000..5a004e19953c4 --- /dev/null +++ b/packages/relay-runtime/store/__tests__/resolvers/HelloWorldResolver.js @@ -0,0 +1,25 @@ +/** + * 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. + * + * @format + * @flow strict-local + * @emails oncall+relay + */ + +'use strict'; + +/** + * @RelayResolver + * @fieldName hello(world: String!) + * @onType Query + * + * Say `Hello ${world}!` + */ +function helloWorldResolver(args: {world: string}): string { + return `Hello, ${args.world}!`; +} + +module.exports = helloWorldResolver; diff --git a/packages/relay-runtime/util/ReaderNode.js b/packages/relay-runtime/util/ReaderNode.js index 28bfeba624ea6..e9f34d765e0fb 100644 --- a/packages/relay-runtime/util/ReaderNode.js +++ b/packages/relay-runtime/util/ReaderNode.js @@ -246,7 +246,7 @@ export type ReaderRelayResolver = {| +alias: ?string, +name: string, +args: ?$ReadOnlyArray, - +fragment: ReaderFragmentSpread, + +fragment: ?ReaderFragmentSpread, +path: string, +resolverModule: ResolverModule, |};