···11+import type { Client } from '@urql/core';
22+import { createContext, useContext } from 'solid-js';
33+44+export const Context = createContext<Client>();
55+export const Provider = Context.Provider;
66+77+export type UseClient = () => Client;
88+export const useClient: UseClient = () => {
99+ const client = useContext(Context);
1010+1111+ if (process.env.NODE_ENV !== 'production' && client === undefined) {
1212+ const error =
1313+ "No client has been specified using urql's Provider. please create a client and add a Provider.";
1414+1515+ console.error(error);
1616+ throw new Error(error);
1717+ }
1818+1919+ return client!;
2020+};
+113
packages/solid-urql/src/createMutation.test.ts
···11+// @vitest-environment jsdom
22+33+import { testEffect } from '@solidjs/testing-library';
44+import { expect, it, describe, vi } from 'vitest';
55+import { CreateMutationState, createMutation } from './createMutation';
66+import {
77+ OperationResult,
88+ OperationResultSource,
99+ createClient,
1010+ gql,
1111+} from '@urql/core';
1212+import { makeSubject } from 'wonka';
1313+import { createEffect } from 'solid-js';
1414+1515+const QUERY = gql`
1616+ mutation {
1717+ test
1818+ }
1919+`;
2020+2121+const client = createClient({
2222+ url: '/graphql',
2323+ exchanges: [],
2424+ suspense: false,
2525+});
2626+2727+vi.mock('./context', () => {
2828+ const useClient = () => {
2929+ return client!;
3030+ };
3131+3232+ return { useClient };
3333+});
3434+3535+// Given that it is not possible to directly listen to all store changes it is necessary
3636+// to access all relevant parts on which `createEffect` should listen on
3737+const markStateDependencies = (state: CreateMutationState<any, any>) => {
3838+ state.data;
3939+ state.error;
4040+ state.extensions;
4141+ state.fetching;
4242+ state.operation;
4343+ state.stale;
4444+};
4545+4646+describe('createMutation', () => {
4747+ it('should have expected state before and after finish', () => {
4848+ const subject = makeSubject<any>();
4949+ const clientMutation = vi
5050+ .spyOn(client, 'executeMutation')
5151+ .mockImplementation(
5252+ () => subject.source as OperationResultSource<OperationResult>
5353+ );
5454+5555+ return testEffect(done => {
5656+ const [state, execute] = createMutation<
5757+ { test: boolean },
5858+ { variable: number }
5959+ >(QUERY);
6060+6161+ createEffect((run: number = 0) => {
6262+ markStateDependencies(state);
6363+6464+ switch (run) {
6565+ case 0: {
6666+ expect(state).toMatchObject({
6767+ data: undefined,
6868+ stale: false,
6969+ fetching: false,
7070+ error: undefined,
7171+ extensions: undefined,
7272+ operation: undefined,
7373+ });
7474+7575+ execute({ variable: 1 });
7676+ break;
7777+ }
7878+7979+ case 1: {
8080+ expect(state).toMatchObject({
8181+ data: undefined,
8282+ stale: false,
8383+ fetching: true,
8484+ error: undefined,
8585+ extensions: undefined,
8686+ operation: undefined,
8787+ });
8888+8989+ expect(clientMutation).toHaveBeenCalledTimes(1);
9090+ subject.next({ data: { test: true }, stale: false });
9191+9292+ break;
9393+ }
9494+9595+ case 2: {
9696+ expect(state).toMatchObject({
9797+ data: { test: true },
9898+ stale: false,
9999+ fetching: false,
100100+ error: undefined,
101101+ extensions: undefined,
102102+ });
103103+104104+ done();
105105+ break;
106106+ }
107107+ }
108108+109109+ return run + 1;
110110+ });
111111+ });
112112+ });
113113+});
+177
packages/solid-urql/src/createMutation.ts
···11+import { createStore } from 'solid-js/store';
22+import {
33+ type AnyVariables,
44+ type DocumentInput,
55+ type OperationContext,
66+ type Operation,
77+ type OperationResult,
88+ type CombinedError,
99+ createRequest,
1010+} from '@urql/core';
1111+import { useClient } from './context';
1212+import { pipe, onPush, filter, take, toPromise } from 'wonka';
1313+1414+export type CreateMutationState<
1515+ Data = any,
1616+ Variables extends AnyVariables = AnyVariables,
1717+> = {
1818+ /** Indicates whether `createMutation` is currently executing a mutation. */
1919+ fetching: boolean;
2020+2121+ /** Indicates that the mutation result is not fresh.
2222+ *
2323+ * @remarks
2424+ * The `stale` flag is set to `true` when a new result for the mutation
2525+ * is expected.
2626+ * This is mostly unused for mutations and will rarely affect you, and
2727+ * is more relevant for queries.
2828+ *
2929+ * @see {@link OperationResult.stale} for the source of this value.
3030+ */
3131+ stale: boolean;
3232+ /** The {@link OperationResult.data} for the executed mutation. */
3333+ data?: Data;
3434+ /** The {@link OperationResult.error} for the executed mutation. */
3535+ error?: CombinedError;
3636+ /** The {@link OperationResult.extensions} for the executed mutation. */
3737+ extensions?: Record<string, any>;
3838+ /** The {@link Operation} that the current state is for.
3939+ *
4040+ * @remarks
4141+ * This is the mutation {@link Operation} that has last been executed.
4242+ * When {@link CreateMutationState.fetching} is `true`, this is the
4343+ * last `Operation` that the current state was for.
4444+ */
4545+ operation?: Operation<Data, Variables>;
4646+};
4747+4848+/** Triggers {@link createMutation} to execute its GraphQL mutation operation.
4949+ *
5050+ * @param variables - variables using which the mutation will be executed.
5151+ * @param context - optionally, context options that will be merged with the hook's
5252+ * context options and the `Client`’s options.
5353+ * @returns the {@link OperationResult} of the mutation.
5454+ *
5555+ * @remarks
5656+ * When called, {@link createMutation} will start the GraphQL mutation
5757+ * it currently holds and use the `variables` passed to it.
5858+ *
5959+ * Once the mutation response comes back from the API, its
6060+ * returned promise will resolve to the mutation’s {@link OperationResult}
6161+ * and the {@link CreateMutationState} will be updated with the result.
6262+ *
6363+ * @example
6464+ * ```ts
6565+ * const [result, executeMutation] = createMutation(UpdateTodo);
6666+ * const start = async ({ id, title }) => {
6767+ * const result = await executeMutation({ id, title });
6868+ * };
6969+ */
7070+export type CreateMutationExecute<
7171+ Data = any,
7272+ Variables extends AnyVariables = AnyVariables,
7373+> = (
7474+ variables: Variables,
7575+ context?: Partial<OperationContext>
7676+) => Promise<OperationResult<Data, Variables>>;
7777+7878+/** Result tuple returned by the {@link createMutation} hook.
7979+ *
8080+ * @remarks
8181+ * Similarly to a `createSignal` hook’s return value,
8282+ * the first element is the {@link createMutation}’s state, updated
8383+ * as mutations are executed with the second value, which is
8484+ * used to start mutations and is a {@link CreateMutationExecute}
8585+ * function.
8686+ */
8787+export type CreateMutationResult<
8888+ Data = any,
8989+ Variables extends AnyVariables = AnyVariables,
9090+> = [
9191+ CreateMutationState<Data, Variables>,
9292+ CreateMutationExecute<Data, Variables>,
9393+];
9494+9595+/** Hook to create a GraphQL mutation, run by passing variables to the returned execute function.
9696+ *
9797+ * @param query - a GraphQL mutation document which `createMutation` will execute.
9898+ * @returns a {@link CreateMutationResult} tuple of a {@link CreateMutationState} result,
9999+ * and an execute function to start the mutation.
100100+ *
101101+ * @remarks
102102+ * `createMutation` allows GraphQL mutations to be defined and keeps its state
103103+ * after the mutation is started with the returned execute function.
104104+ *
105105+ * Given a GraphQL mutation document it returns state to keep track of the
106106+ * mutation state and a {@link CreateMutationExecute} function, which accepts
107107+ * variables for the mutation to be executed.
108108+ * Once called, the mutation executes and the state will be updated with
109109+ * the mutation’s result.
110110+ *
111111+ * @example
112112+ * ```ts
113113+ * import { gql, createMutation } from '@urql/solid';
114114+ *
115115+ * const UpdateTodo = gql`
116116+ * mutation ($id: ID!, $title: String!) {
117117+ * updateTodo(id: $id, title: $title) {
118118+ * id, title
119119+ * }
120120+ * }
121121+ * `;
122122+ *
123123+ * const UpdateTodo = () => {
124124+ * const [result, executeMutation] = createMutation(UpdateTodo);
125125+ * const start = async ({ id, title }) => {
126126+ * const result = await executeMutation({ id, title });
127127+ * };
128128+ * // ...
129129+ * };
130130+ * ```
131131+ */
132132+export const createMutation = <
133133+ Data = any,
134134+ Variables extends AnyVariables = AnyVariables,
135135+>(
136136+ query: DocumentInput<Data, Variables>
137137+): CreateMutationResult<Data, Variables> => {
138138+ const client = useClient();
139139+ const initialResult: CreateMutationState<Data, Variables> = {
140140+ operation: undefined,
141141+ fetching: false,
142142+ stale: false,
143143+ data: undefined,
144144+ error: undefined,
145145+ extensions: undefined,
146146+ };
147147+148148+ const [state, setState] =
149149+ createStore<CreateMutationState<Data, Variables>>(initialResult);
150150+151151+ const execute = (
152152+ variables: Variables,
153153+ context?: Partial<OperationContext>
154154+ ) => {
155155+ setState({ ...initialResult, fetching: true });
156156+157157+ const request = createRequest(query, variables);
158158+ return pipe(
159159+ client.executeMutation(request, context),
160160+ onPush(result => {
161161+ setState({
162162+ fetching: false,
163163+ stale: result.stale,
164164+ data: result.data,
165165+ error: result.error,
166166+ extensions: result.extensions,
167167+ operation: result.operation,
168168+ });
169169+ }),
170170+ filter(result => !result.hasNext),
171171+ take(1),
172172+ toPromise
173173+ );
174174+ };
175175+176176+ return [state, execute];
177177+};
···11+import { type MaybeAccessor, asAccessor } from './utils';
22+import {
33+ type AnyVariables,
44+ type DocumentInput,
55+ type Operation,
66+ type OperationContext,
77+ type OperationResult,
88+ type CombinedError,
99+ createRequest,
1010+} from '@urql/core';
1111+import { useClient } from './context';
1212+import { createStore, produce } from 'solid-js/store';
1313+import { createComputed, createSignal, onCleanup } from 'solid-js';
1414+import { type Source, onEnd, pipe, subscribe } from 'wonka';
1515+1616+/** Triggers {@link createSubscription} to re-execute a GraphQL subscription operation.
1717+ *
1818+ * @param opts - optionally, context options that will be merged with the hook's
1919+ * {@link CreateSubscriptionArgs.context} options and the `Client`’s options.
2020+ *
2121+ * @remarks
2222+ * When called, {@link createSubscription} will restart the GraphQL subscription
2323+ * operation it currently holds. If {@link CreateSubscriptionArgs.pause} is set
2424+ * to `true`, it will start executing the subscription.
2525+ *
2626+ * ```ts
2727+ * const [result, executeSubscription] = createSubscription({
2828+ * query,
2929+ * pause: true,
3030+ * });
3131+ *
3232+ * const start = () => {
3333+ * executeSubscription();
3434+ * };
3535+ * ```
3636+ */
3737+export type CreateSubscriptionExecute = (
3838+ opts?: Partial<OperationContext>
3939+) => void;
4040+4141+/** Input arguments for the {@link createSubscription} hook. */
4242+export type CreateSubscriptionArgs<
4343+ Data,
4444+ Variables extends AnyVariables = AnyVariables,
4545+> = {
4646+ /** The GraphQL subscription document that `createSubscription` executes. */
4747+ query: DocumentInput<Data, Variables>;
4848+4949+ /** The variables for the GraphQL subscription that `createSubscription` executes. */
5050+ variables?: MaybeAccessor<Variables>;
5151+5252+ /** Updates the {@link OperationContext} for the executed GraphQL subscription operation.
5353+ *
5454+ * @remarks
5555+ * `context` may be passed to {@link createSubscription}, to update the {@link OperationContext}
5656+ * of a subscription operation. This may be used to update the `context` that exchanges
5757+ * will receive for a single hook.
5858+ */
5959+ context?: MaybeAccessor<Partial<OperationContext>>;
6060+6161+ /** Prevents {@link createSubscription} from automatically starting GraphQL subscriptions.
6262+ *
6363+ * @remarks
6464+ * `pause` may be set to `true` to stop {@link createSubscription} from starting its subscription
6565+ * automatically. The hook will stop receiving updates from the {@link Client}
6666+ * and won’t start the subscription operation, until either it’s set to `false`
6767+ * or the {@link CreateSubscriptionExecute} function is called.
6868+ */
6969+ pause?: MaybeAccessor<boolean>;
7070+};
7171+7272+export type CreateSubscriptionState<
7373+ Data = any,
7474+ Variables extends AnyVariables = AnyVariables,
7575+> = {
7676+ /** Indicates whether `createSubscription`’s subscription is active.
7777+ *
7878+ * @remarks
7979+ * When `createSubscription` starts a subscription, the `fetching` flag
8080+ * is set to `true` and will remain `true` until the subscription
8181+ * completes on the API, or the {@link CreateSubscriptionArgs.pause}
8282+ * flag is set to `true`.
8383+ */
8484+ fetching: boolean;
8585+8686+ /** Indicates that the subscription result is not fresh.
8787+ *
8888+ * @remarks
8989+ * This is mostly unused for subscriptions and will rarely affect you, and
9090+ * is more relevant for queries.
9191+ *
9292+ * @see {@link OperationResult.stale} for the source of this value.
9393+ */
9494+ stale: boolean;
9595+9696+ /** The {@link OperationResult.data} for the executed subscription, or data returned by a handler.
9797+ *
9898+ * @remarks
9999+ * `data` will be set to the last {@link OperationResult.data} value
100100+ * received for the subscription.
101101+ *
102102+ * It will instead be set to the values that {@link SubscriptionHandler}
103103+ * returned, if a handler has been passed to {@link CreateSubscription}.
104104+ */
105105+ data?: Data;
106106+107107+ /** The {@link OperationResult.error} for the executed subscription. */
108108+ error?: CombinedError;
109109+110110+ /** The {@link OperationResult.extensions} for the executed mutation. */
111111+ extensions?: Record<string, any>;
112112+113113+ /** The {@link Operation} that the current state is for.
114114+ *
115115+ * @remarks
116116+ * This is the subscription {@link Operation} that is currently active.
117117+ * When {@link CreateSubscriptionState.fetching} is `true`, this is the
118118+ * last `Operation` that the current state was for.
119119+ */
120120+ operation?: Operation<Data, Variables>;
121121+};
122122+123123+/** Combines previous data with an incoming subscription result’s data.
124124+ *
125125+ * @remarks
126126+ * A `SubscriptionHandler` may be passed to {@link createSubscription} to
127127+ * aggregate subscription results into a combined {@link CreateSubscriptionState.data}
128128+ * value.
129129+ *
130130+ * This is useful when a subscription event delivers a single item, while
131131+ * you’d like to display a list of events.
132132+ *
133133+ * @example
134134+ * ```ts
135135+ * const NotificationsSubscription = gql`
136136+ * subscription { newNotification { id, text } }
137137+ * `;
138138+ *
139139+ * const combineNotifications = (notifications = [], data) => {
140140+ * return [...notifications, data.newNotification];
141141+ * };
142142+ *
143143+ * const [result, executeSubscription] = createSubscription(
144144+ * { query: NotificationsSubscription },
145145+ * combineNotifications,
146146+ * );
147147+ * ```
148148+ */
149149+export type SubscriptionHandler<T, R> = (prev: R | undefined, data: T) => R;
150150+151151+/** Result tuple returned by the {@link createSubscription} hook.
152152+ *
153153+ * @remarks
154154+ * Similarly to a `createSignal` hook’s return value,
155155+ * the first element is the {@link createSubscription}’s state,
156156+ * a {@link CreateSubscriptionState} object,
157157+ * and the second is used to imperatively re-execute or start the subscription
158158+ * via a {@link CreateMutationExecute} function.
159159+ */
160160+export type CreateSubscriptionResult<
161161+ Data,
162162+ Variables extends AnyVariables = AnyVariables,
163163+> = [CreateSubscriptionState<Data, Variables>, CreateSubscriptionExecute];
164164+165165+/** Hook to run a GraphQL subscription and get updated GraphQL results.
166166+ *
167167+ * @param args - a {@link CreateSubscriptionArgs} object, to pass a `query`, `variables`, and options.
168168+ * @param handler - optionally, a {@link SubscriptionHandler} function to combine multiple subscription results.
169169+ * @returns a {@link CreateSubscriptionResponse} tuple of a {@link CreateSubscriptionState} result,
170170+ * and an execute function.
171171+ *
172172+ * @remarks
173173+ * `createSubscription` allows GraphQL subscriptions to be defined and executed.
174174+ * Given {@link CreateSubscriptionArgs.query}, it executes the GraphQL subscription with the
175175+ * context’s {@link Client}.
176176+ *
177177+ * The returned result updates when the `Client` has new results
178178+ * for the subscription, and `data` is updated with the result’s data
179179+ * or with the `data` that a `handler` returns.
180180+ *
181181+ * @example
182182+ * ```ts
183183+ * import { gql, createSubscription } from '@urql/solid';
184184+ *
185185+ * const NotificationsSubscription = gql`
186186+ * subscription { newNotification { id, text } }
187187+ * `;
188188+ *
189189+ * const combineNotifications = (notifications = [], data) => {
190190+ * return [...notifications, data.newNotification];
191191+ * };
192192+ *
193193+ * const Notifications = () => {
194194+ * const [result, executeSubscription] = createSubscription(
195195+ * { query: NotificationsSubscription },
196196+ * combineNotifications,
197197+ * );
198198+ * // ...
199199+ * };
200200+ * ```
201201+ */
202202+export const createSubscription = <
203203+ Data,
204204+ Result = Data,
205205+ Variables extends AnyVariables = AnyVariables,
206206+>(
207207+ args: CreateSubscriptionArgs<Data, Variables>,
208208+ handler?: SubscriptionHandler<Data, Result>
209209+): CreateSubscriptionResult<Result, Variables> => {
210210+ const getContext = asAccessor(args.context);
211211+ const getPause = asAccessor(args.pause);
212212+ const getVariables = asAccessor(args.variables);
213213+214214+ const client = useClient();
215215+216216+ const request = createRequest(args.query, getVariables() as Variables);
217217+ const operation = client.createRequestOperation(
218218+ 'subscription',
219219+ request,
220220+ getContext()
221221+ );
222222+ const initialState: CreateSubscriptionState<Result, Variables> = {
223223+ operation,
224224+ fetching: false,
225225+ data: undefined,
226226+ error: undefined,
227227+ extensions: undefined,
228228+ stale: false,
229229+ };
230230+231231+ const [source, setSource] = createSignal<
232232+ Source<OperationResult<Data, Variables>> | undefined
233233+ >(undefined, { equals: false });
234234+235235+ const [state, setState] =
236236+ createStore<CreateSubscriptionState<Result, Variables>>(initialState);
237237+238238+ createComputed(() => {
239239+ if (getPause() === true) {
240240+ setSource(undefined);
241241+ return;
242242+ }
243243+244244+ const context = getContext();
245245+ const request = createRequest(args.query, getVariables() as Variables);
246246+ setSource(() => client.executeSubscription(request, context));
247247+ });
248248+249249+ createComputed(() => {
250250+ const s = source();
251251+ if (s === undefined) {
252252+ setState('fetching', false);
253253+254254+ return;
255255+ }
256256+257257+ setState('fetching', true);
258258+ onCleanup(
259259+ pipe(
260260+ s,
261261+ onEnd(() => {
262262+ setState(
263263+ produce(draft => {
264264+ draft.fetching = false;
265265+ })
266266+ );
267267+ }),
268268+ subscribe(res => {
269269+ setState(
270270+ produce(draft => {
271271+ draft.data =
272272+ res.data !== undefined
273273+ ? typeof handler === 'function'
274274+ ? handler(draft.data, res.data)
275275+ : res.data
276276+ : (draft.data as any);
277277+ draft.stale = !!res.stale;
278278+ draft.fetching = true;
279279+ draft.error = res.error;
280280+ draft.operation = res.operation;
281281+ draft.extensions = res.extensions;
282282+ })
283283+ );
284284+ })
285285+ ).unsubscribe
286286+ );
287287+ });
288288+289289+ const executeSubscription = (opts?: Partial<OperationContext>) => {
290290+ const context: Partial<OperationContext> = {
291291+ ...getContext(),
292292+ ...opts,
293293+ };
294294+ const request = createRequest(args.query, getVariables() as Variables);
295295+296296+ setSource(() => client.executeSubscription(request, context));
297297+ };
298298+299299+ return [state, executeSubscription];
300300+};
+29
packages/solid-urql/src/index.ts
···11+export * from '@urql/core';
22+33+export { type UseClient } from './context';
44+export { useClient } from './context';
55+66+export {
77+ type CreateMutationState,
88+ type CreateMutationExecute,
99+ type CreateMutationResult,
1010+} from './createMutation';
1111+export { createMutation } from './createMutation';
1212+1313+export {
1414+ type CreateQueryArgs,
1515+ type CreateQueryState,
1616+ type CreateQueryExecute,
1717+ type CreateQueryResult,
1818+} from './createQuery';
1919+export { createQuery } from './createQuery';
2020+2121+export {
2222+ type CreateSubscriptionArgs,
2323+ type CreateSubscriptionState,
2424+ type CreateSubscriptionExecute,
2525+ type CreateSubscriptionResult,
2626+ type SubscriptionHandler,
2727+} from './createSubscription';
2828+2929+export { createSubscription } from './createSubscription';