1import { createStore, reconcile } from 'solid-js/store';
2import {
3 type AnyVariables,
4 type DocumentInput,
5 type OperationContext,
6 type Operation,
7 type OperationResult,
8 type CombinedError,
9 createRequest,
10} from '@urql/core';
11import { useClient } from './context';
12import { pipe, onPush, filter, take, toPromise } from 'wonka';
13import { batch } from 'solid-js';
14
15export type CreateMutationState<
16 Data = any,
17 Variables extends AnyVariables = AnyVariables,
18> = {
19 /** Indicates whether `createMutation` is currently executing a mutation. */
20 fetching: boolean;
21
22 /** Indicates that the mutation result is not fresh.
23 *
24 * @remarks
25 * The `stale` flag is set to `true` when a new result for the mutation
26 * is expected.
27 * This is mostly unused for mutations and will rarely affect you, and
28 * is more relevant for queries.
29 *
30 * @see {@link OperationResult.stale} for the source of this value.
31 */
32 stale: boolean;
33 /** The {@link OperationResult.data} for the executed mutation. */
34 data?: Data;
35 /** The {@link OperationResult.error} for the executed mutation. */
36 error?: CombinedError;
37 /** The {@link OperationResult.extensions} for the executed mutation. */
38 extensions?: Record<string, any>;
39 /** The {@link Operation} that the current state is for.
40 *
41 * @remarks
42 * This is the mutation {@link Operation} that has last been executed.
43 * When {@link CreateMutationState.fetching} is `true`, this is the
44 * last `Operation` that the current state was for.
45 */
46 operation?: Operation<Data, Variables>;
47 /** The {@link OperationResult.hasNext} for the executed query. */
48 hasNext: boolean;
49};
50
51/** Triggers {@link createMutation} to execute its GraphQL mutation operation.
52 *
53 * @param variables - variables using which the mutation will be executed.
54 * @param context - optionally, context options that will be merged with the hook's
55 * context options and the `Client`’s options.
56 * @returns the {@link OperationResult} of the mutation.
57 *
58 * @remarks
59 * When called, {@link createMutation} will start the GraphQL mutation
60 * it currently holds and use the `variables` passed to it.
61 *
62 * Once the mutation response comes back from the API, its
63 * returned promise will resolve to the mutation’s {@link OperationResult}
64 * and the {@link CreateMutationState} will be updated with the result.
65 *
66 * @example
67 * ```ts
68 * const [result, executeMutation] = createMutation(UpdateTodo);
69 * const start = async ({ id, title }) => {
70 * const result = await executeMutation({ id, title });
71 * };
72 */
73export type CreateMutationExecute<
74 Data = any,
75 Variables extends AnyVariables = AnyVariables,
76> = (
77 variables: Variables,
78 context?: Partial<OperationContext>
79) => Promise<OperationResult<Data, Variables>>;
80
81/** Result tuple returned by the {@link createMutation} hook.
82 *
83 * @remarks
84 * Similarly to a `createSignal` hook’s return value,
85 * the first element is the {@link createMutation}’s state, updated
86 * as mutations are executed with the second value, which is
87 * used to start mutations and is a {@link CreateMutationExecute}
88 * function.
89 */
90export type CreateMutationResult<
91 Data = any,
92 Variables extends AnyVariables = AnyVariables,
93> = [
94 CreateMutationState<Data, Variables>,
95 CreateMutationExecute<Data, Variables>,
96];
97
98/** Hook to create a GraphQL mutation, run by passing variables to the returned execute function.
99 *
100 * @param query - a GraphQL mutation document which `createMutation` will execute.
101 * @returns a {@link CreateMutationResult} tuple of a {@link CreateMutationState} result,
102 * and an execute function to start the mutation.
103 *
104 * @remarks
105 * `createMutation` allows GraphQL mutations to be defined and keeps its state
106 * after the mutation is started with the returned execute function.
107 *
108 * Given a GraphQL mutation document it returns state to keep track of the
109 * mutation state and a {@link CreateMutationExecute} function, which accepts
110 * variables for the mutation to be executed.
111 * Once called, the mutation executes and the state will be updated with
112 * the mutation’s result.
113 *
114 * @example
115 * ```ts
116 * import { gql, createMutation } from '@urql/solid';
117 *
118 * const UpdateTodo = gql`
119 * mutation ($id: ID!, $title: String!) {
120 * updateTodo(id: $id, title: $title) {
121 * id, title
122 * }
123 * }
124 * `;
125 *
126 * const UpdateTodo = () => {
127 * const [result, executeMutation] = createMutation(UpdateTodo);
128 * const start = async ({ id, title }) => {
129 * const result = await executeMutation({ id, title });
130 * };
131 * // ...
132 * };
133 * ```
134 */
135export const createMutation = <
136 Data = any,
137 Variables extends AnyVariables = AnyVariables,
138>(
139 query: DocumentInput<Data, Variables>
140): CreateMutationResult<Data, Variables> => {
141 const client = useClient();
142 const initialResult: CreateMutationState<Data, Variables> = {
143 operation: undefined,
144 fetching: false,
145 hasNext: false,
146 stale: false,
147 data: undefined,
148 error: undefined,
149 extensions: undefined,
150 };
151
152 const [state, setState] =
153 createStore<CreateMutationState<Data, Variables>>(initialResult);
154
155 const execute = (
156 variables: Variables,
157 context?: Partial<OperationContext>
158 ) => {
159 setState({ ...initialResult, fetching: true });
160
161 const request = createRequest(query, variables);
162 return pipe(
163 client.executeMutation(request, context),
164 onPush(result => {
165 batch(() => {
166 setState('data', reconcile(result.data));
167 setState({
168 fetching: false,
169 stale: result.stale,
170 error: result.error,
171 extensions: result.extensions,
172 operation: result.operation,
173 hasNext: result.hasNext,
174 });
175 });
176 }),
177 filter(result => !result.hasNext),
178 take(1),
179 toPromise
180 );
181 };
182
183 return [state, execute];
184};