diff --git a/change/@nova-examples-4a9eba03-93cc-41c9-92fd-316b80aa0767.json b/change/@nova-examples-4a9eba03-93cc-41c9-92fd-316b80aa0767.json new file mode 100644 index 00000000..1f38918b --- /dev/null +++ b/change/@nova-examples-4a9eba03-93cc-41c9-92fd-316b80aa0767.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Use useMutation_deprecated in FeedbackComponent", + "packageName": "@nova/examples", + "email": "132382266+DarynaAkhmedova@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/@nova-react-c9299b3a-611a-4555-ad5a-a1c2cf647f6f.json b/change/@nova-react-c9299b3a-611a-4555-ad5a-a1c2cf647f6f.json new file mode 100644 index 00000000..3552a700 --- /dev/null +++ b/change/@nova-react-c9299b3a-611a-4555-ad5a-a1c2cf647f6f.json @@ -0,0 +1,7 @@ +{ + "type": "major", + "comment": "Align useMutation with Relay", + "packageName": "@nova/react", + "email": "132382266+DarynaAkhmedova@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/@nova-react-test-utils-c16efa64-6fae-4431-8824-1491ad927285.json b/change/@nova-react-test-utils-c16efa64-6fae-4431-8824-1491ad927285.json new file mode 100644 index 00000000..bb99ef77 --- /dev/null +++ b/change/@nova-react-test-utils-c16efa64-6fae-4431-8824-1491ad927285.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Update @graphitation/apollo-react-relay-duct-tape", + "packageName": "@nova/react-test-utils", + "email": "132382266+DarynaAkhmedova@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/@nova-types-f14269f8-868b-49cf-9c08-777b5e415c9c.json b/change/@nova-types-f14269f8-868b-49cf-9c08-777b5e415c9c.json new file mode 100644 index 00000000..937932b9 --- /dev/null +++ b/change/@nova-types-f14269f8-868b-49cf-9c08-777b5e415c9c.json @@ -0,0 +1,7 @@ +{ + "type": "major", + "comment": "Align useMutation with Relay", + "packageName": "@nova/types", + "email": "132382266+DarynaAkhmedova@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/packages/examples/src/Feedback/Feedback.tsx b/packages/examples/src/Feedback/Feedback.tsx index bfd244dd..0e85e2bd 100644 --- a/packages/examples/src/Feedback/Feedback.tsx +++ b/packages/examples/src/Feedback/Feedback.tsx @@ -1,4 +1,4 @@ -import { graphql, useFragment, useMutation } from "@nova/react"; +import { graphql, useFragment, useMutation_deprecated } from "@nova/react"; import * as React from "react"; import type { FeedbackComponent_LikeMutation } from "./__generated__/FeedbackComponent_LikeMutation.graphql"; import type { Feedback_feedbackFragment$key } from "./__generated__/Feedback_feedbackFragment.graphql"; @@ -20,7 +20,7 @@ export const Feedback_feedbackFragment = graphql` export const FeedbackComponent = (props: Props) => { const [errorMessage, setErrorMessage] = React.useState(null); const feedback = useFragment(Feedback_feedbackFragment, props.feedback); - const [like, isPending] = useMutation( + const [like, isPending] = useMutation_deprecated( graphql` mutation FeedbackComponent_LikeMutation($input: FeedbackLikeInput!) { feedbackLike(input: $input) { diff --git a/packages/nova-react-test-utils/src/storybook-nova-decorator.tsx b/packages/nova-react-test-utils/src/storybook-nova-decorator.tsx index 2f2f7366..e0f7367b 100644 --- a/packages/nova-react-test-utils/src/storybook-nova-decorator.tsx +++ b/packages/nova-react-test-utils/src/storybook-nova-decorator.tsx @@ -122,7 +122,11 @@ function createNovaEnvironment( const client = createMockClient(schema, options); const env: NovaMockEnvironment<"storybook"> = { graphql: { - ...(GraphQLHooks as NovaGraphQL), + ...(GraphQLHooks as unknown as NovaGraphQL), + useMutation_deprecated: GraphQLHooks.useMutation, + useMutation() { + throw new Error("Not supported yet in Apollo"); + }, mock: client.mock as MockFunctions, }, providerWrapper: ({ children }) => ( diff --git a/packages/nova-react-test-utils/src/test-utils.tsx b/packages/nova-react-test-utils/src/test-utils.tsx index a235771c..78e0fc6b 100644 --- a/packages/nova-react-test-utils/src/test-utils.tsx +++ b/packages/nova-react-test-utils/src/test-utils.tsx @@ -47,7 +47,11 @@ export function createMockEnvironment( bubble: jest.fn(), }, graphql: { - ...(GraphQLHooks as NovaGraphQL), + ...(GraphQLHooks as unknown as NovaGraphQL), + useMutation_deprecated: GraphQLHooks.useMutation, + useMutation() { + throw new Error("Not supported yet in Apollo"); + }, mock: client.mock as MockFunctions, }, providerWrapper: ({ children }) => ( diff --git a/packages/nova-react/package.json b/packages/nova-react/package.json index 861113ad..1df5c260 100644 --- a/packages/nova-react/package.json +++ b/packages/nova-react/package.json @@ -27,6 +27,7 @@ "@types/jest": "^29.2.0", "@types/react": "^18.3.1", "@types/react-dom": "^18.3.0", + "@types/react-relay": "^16.0.6", "monorepo-scripts": "*", "react": "^18.3.1", "react-dom": "^18.3.1" diff --git a/packages/nova-react/src/graphql/hooks.test.tsx b/packages/nova-react/src/graphql/hooks.test.tsx index dfb303fd..faa23995 100644 --- a/packages/nova-react/src/graphql/hooks.test.tsx +++ b/packages/nova-react/src/graphql/hooks.test.tsx @@ -14,13 +14,19 @@ import { usePaginationFragment, useRefetchableFragment, useSubscription, + useMutation, } from "./hooks"; import type { GraphQLTaggedNode } from "./taggedNode"; import type { FragmentRefs } from "./types"; +import type { useMutation as relayUseMutation, GraphQLTaggedNode as RelayGraphQLTaggedNode } from "react-relay"; type IsNotNull = null extends T ? false : true; type IsNotUndefined = undefined extends T ? false : true; +// This is used to verify type compatibility with Relay useMutation hook +declare function useMutationRelay(mutation: RelayGraphQLTaggedNode): ReturnType; +export const typeVerification: typeof relayUseMutation = useMutationRelay; + describe(useLazyLoadQuery, () => { it("ensures an implementation is supplied", () => { const graphql: NovaGraphQL = {}; diff --git a/packages/nova-react/src/graphql/hooks.ts b/packages/nova-react/src/graphql/hooks.ts index dc1cee90..2217725d 100644 --- a/packages/nova-react/src/graphql/hooks.ts +++ b/packages/nova-react/src/graphql/hooks.ts @@ -7,9 +7,11 @@ import type { KeyTypeData, OperationType, PaginationFn, + PayloadError, RefetchFn, FetchPolicy } from "./types"; +import type { Disposable } from "@nova/types"; /** * Executes a GraphQL query. @@ -331,15 +333,24 @@ interface MutationCommitterOptions { */ context?: TMutationPayload["context"]; optimisticResponse?: Partial | null; + onError?: (error: Error) => void; + onCompleted?: ((response: TMutationPayload["response"], errors: PayloadError[] | null) => void | null) | undefined; +} + +interface MutationCommitterOptions_deprecated { + variables: TMutationPayload["variables"]; + /** + * Should be avoided when possible as it will not be compatible with Relay APIs. + */ + context?: TMutationPayload["context"]; + optimisticResponse?: Partial | null; + onError?: (error: Error) => void; onCompleted?: (response: TMutationPayload["response"]) => void; } type MutationCommitter = ( options: MutationCommitterOptions, -) => Promise<{ - errors?: readonly Error[]; - data?: TMutationPayload["response"]; -}>; +) => Disposable; export function useMutation( mutation: GraphQLTaggedNode, @@ -348,3 +359,18 @@ export function useMutation( invariant(graphql.useMutation, "Expected host to provide a useMutation hook"); return graphql.useMutation(mutation); } + +type MutationCommitter_deprecated = ( + options: MutationCommitterOptions_deprecated, +) => Promise<{ + errors?: readonly Error[]; + data?: TMutationPayload["response"]; +}>; + +export function useMutation_deprecated( + mutation: GraphQLTaggedNode, +): [MutationCommitter_deprecated, boolean] { + const graphql = useNovaGraphQL(); + invariant(graphql.useMutation_deprecated, "Expected host to provide a useMutation_deprecated hook"); + return graphql.useMutation_deprecated(mutation); +} diff --git a/packages/nova-react/src/graphql/types.ts b/packages/nova-react/src/graphql/types.ts index fabe3f07..cfd451e1 100644 --- a/packages/nova-react/src/graphql/types.ts +++ b/packages/nova-react/src/graphql/types.ts @@ -1,21 +1,8 @@ -export interface Variables { - [name: string]: any; -} - -export interface Context { - [name: string]: any; -} - -export interface OperationType { - readonly variables: Variables; - readonly context?: Context; - readonly response: unknown; - readonly rawResponse?: unknown; -} - /** * relay-compiler-language-typescript support for fragment references - */ +*/ + +export type { OperationType, PayloadError, Disposable } from "@nova/types"; export interface _RefType { " $refType": Ref; @@ -95,4 +82,4 @@ type RefetchOptions = { type Disposable = { dispose(): void; -}; +}; \ No newline at end of file diff --git a/packages/nova-types/src/nova-graphql.interface.ts b/packages/nova-types/src/nova-graphql.interface.ts index 900376c9..5618ffab 100644 --- a/packages/nova-types/src/nova-graphql.interface.ts +++ b/packages/nova-types/src/nova-graphql.interface.ts @@ -9,7 +9,6 @@ * In case the host application uses Apollo Client, these hooks can be provided by using the * `@graphitation/apollo-react-relay-duct-tape` package. See https://github.com/microsoft/graphitation for details. */ - export interface NovaGraphQL { useFragment?: ( fragmentInput: GraphQLDocument, @@ -64,15 +63,51 @@ export interface NovaGraphQL { onError?: (error: Error) => void; }) => void; - useMutation?: ( + useMutation?: ( mutation: GraphQLDocument, ) => [ (options: { variables: { [name: string]: unknown }; context?: { [name: string]: unknown }; optimisticResponse?: unknown | null; + onCompleted?: ((response: TMutationPayload["response"], errors: PayloadError[] | null) => void | null) | undefined; + onError?: (error: Error) => void; + }) => Disposable, + boolean, + ]; + + useMutation_deprecated?: ( + mutation: GraphQLDocument, + ) => [ + (options: { + variables: { [name: string]: unknown }; + context?: { [name: string]: unknown }; + optimisticResponse?: Partial | null; onCompleted?: (response: unknown) => void; }) => Promise<{ errors?: readonly Error[]; data?: unknown }>, boolean, ]; } + +export type Disposable = { + dispose(): void; +}; + +export interface OperationType { + readonly variables: { [name: string]: unknown }; + readonly context?: { [name: string]: unknown }; + readonly response: unknown; + readonly rawResponse?: unknown | undefined; +} + +export interface PayloadError { + message: string; + locations?: + | Array<{ + line: number; + column: number; + }> + | undefined; + path?: Array; + severity?: "CRITICAL" | "ERROR" | "WARNING" | undefined; +} diff --git a/yarn.lock b/yarn.lock index b99e5822..fc3d4f88 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3841,6 +3841,14 @@ dependencies: "@types/react" "*" +"@types/react-relay@^16.0.6": + version "16.0.6" + resolved "https://registry.yarnpkg.com/@types/react-relay/-/react-relay-16.0.6.tgz#afc467fab89dc4c96fb1424f84b869750f5c42f2" + integrity sha512-VTntVQJhlwQYNUlbNgGf8RYy7EtQPRZqsD/w2Si0ygZspJXuNlVdRkklWMFN99EMRhHDpqlNHD8i3wIs7QRz9g== + dependencies: + "@types/react" "*" + "@types/relay-runtime" "*" + "@types/react-test-renderer@^18.3.0": version "18.3.0" resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-18.3.0.tgz#839502eae70058a4ae161f63385a8e7929cef4c0" @@ -3856,6 +3864,11 @@ "@types/prop-types" "*" csstype "^3.0.2" +"@types/relay-runtime@*": + version "17.0.4" + resolved "https://registry.yarnpkg.com/@types/relay-runtime/-/relay-runtime-17.0.4.tgz#428526fc3e6dfb6e0a5730c38ad521cb1eea189b" + integrity sha512-fB77br4lXlBYM/HpI6VI6KCrj5pw0LiAnkZOkffjirNYso+dzXGWkeIm0G0MGszD8WY1et+r1Uj2TA6rscBXNQ== + "@types/resolve@^1.20.2": version "1.20.6" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.6.tgz#e6e60dad29c2c8c206c026e6dd8d6d1bdda850b8" @@ -10446,7 +10459,16 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -10478,7 +10500,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -11440,7 +11469,7 @@ workspace-tools@^0.30.0: js-yaml "^4.1.0" micromatch "^4.0.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -11458,6 +11487,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"