1import {
2 type AnyVariables,
3 type OperationContext,
4 type DocumentInput,
5 type OperationResult,
6 type RequestPolicy,
7 createRequest,
8} from '@urql/core';
9import {
10 batch,
11 createComputed,
12 createMemo,
13 createResource,
14 createSignal,
15 onCleanup,
16} from 'solid-js';
17import { createStore, produce, reconcile } from 'solid-js/store';
18import { useClient } from './context';
19import { type MaybeAccessor, asAccessor } from './utils';
20import type { Source, Subscription } from 'wonka';
21import { onEnd, pipe, subscribe } from 'wonka';
22
23/** Triggers {@link createQuery} to execute a new GraphQL query operation.
24 *
25 * @remarks
26 * When called, {@link createQuery} will re-execute the GraphQL query operation
27 * it currently holds, even if {@link CreateQueryArgs.pause} is set to `true`.
28 *
29 * This is useful for executing a paused query or re-executing a query
30 * and get a new network result, by passing a new request policy.
31 *
32 * ```ts
33 * const [result, reExecuteQuery] = createQuery({ query });
34 *
35 * const refresh = () => {
36 * // Re-execute the query with a network-only policy, skipping the cache
37 * reExecuteQuery({ requestPolicy: 'network-only' });
38 * };
39 * ```
40 *
41 */
42export type CreateQueryExecute = (opts?: Partial<OperationContext>) => void;
43
44/** State of the current query, your {@link createQuery} hook is executing.
45 *
46 * @remarks
47 * `CreateQueryState` is returned (in a tuple) by {@link createQuery} and
48 * gives you the updating {@link OperationResult} of GraphQL queries.
49 *
50 * Even when the query and variables passed to {@link createQuery} change,
51 * this state preserves the prior state and sets the `fetching` flag to
52 * `true`.
53 * This allows you to display the previous state, while implementing
54 * a separate loading indicator separately.
55 */
56export type CreateQueryState<
57 Data = any,
58 Variables extends AnyVariables = AnyVariables,
59> = OperationResult<Data, Variables> & {
60 /** Indicates whether `createQuery` is waiting for a new result.
61 *
62 * @remarks
63 * When `createQuery` is passed a new query and/or variables, it will
64 * start executing the new query operation and `fetching` is set to
65 * `true` until a result arrives.
66 *
67 * Hint: This is subtly different than whether the query is actually
68 * fetching, and doesn’t indicate whether a query is being re-executed
69 * in the background. For this, see {@link CreateQueryState.stale}.
70 */
71 fetching: boolean;
72};
73
74/**
75 * Input arguments for the {@link createQuery} hook.
76 */
77export type CreateQueryArgs<
78 Data = any,
79 Variables extends AnyVariables = AnyVariables,
80> = {
81 /** The GraphQL query that `createQuery` executes. */
82 query: DocumentInput<Data, Variables>;
83
84 /** The variables for the GraphQL {@link CreateQueryArgs.query} that `createQuery` executes. */
85 variables?: MaybeAccessor<Variables>;
86
87 /** Updates the {@link RequestPolicy} for the executed GraphQL query operation.
88 *
89 * @remarks
90 * `requestPolicy` modifies the {@link RequestPolicy} of the GraphQL query operation
91 * that `createQuery` executes, and indicates a caching strategy for cache exchanges.
92 *
93 * For example, when set to `'cache-and-network'`, {@link createQuery} will
94 * receive a cached result with `stale: true` and an API request will be
95 * sent in the background.
96 *
97 * @see {@link OperationContext.requestPolicy} for where this value is set.
98 */
99 requestPolicy?: MaybeAccessor<RequestPolicy>;
100
101 /** Updates the {@link OperationContext} for the executed GraphQL query operation.
102 *
103 * @remarks
104 * `context` may be passed to {@link createQuery}, to update the {@link OperationContext}
105 * of a query operation. This may be used to update the `context` that exchanges
106 * will receive for a single hook.
107 *
108 * In order to re-execute query on context change pass {@link Accessor} instead
109 * of raw value.
110 */
111 context?: MaybeAccessor<Partial<OperationContext>>;
112
113 /** Prevents {@link createQuery} from automatically executing GraphQL query operations.
114 *
115 * @remarks
116 * `pause` may be set to `true` to stop {@link createQuery} from executing
117 * automatically. The hook will stop receiving updates from the {@link Client}
118 * and won’t execute the query operation, until either it’s set to `false`
119 * or the {@link CreateQueryExecute} function is called.
120 */
121 pause?: MaybeAccessor<boolean>;
122};
123
124/** Result tuple returned by the {@link createQuery} hook.
125 *
126 * @remarks
127 * the first element is the {@link createQuery}’s result and state,
128 * a {@link CreateQueryState} object,
129 * and the second is used to imperatively re-execute the query
130 * via a {@link CreateQueryExecute} function.
131 */
132export type CreateQueryResult<
133 Data = any,
134 Variables extends AnyVariables = AnyVariables,
135> = [CreateQueryState<Data, Variables>, CreateQueryExecute];
136
137/** Hook to run a GraphQL query and get updated GraphQL results.
138 *
139 * @param args - a {@link CreateQueryArgs} object, to pass a `query`, `variables`, and options.
140 * @returns a {@link CreateQueryResult} tuple of a {@link CreateQueryState} result, and re-execute function.
141 *
142 * @remarks
143 * `createQuery` allows GraphQL queries to be defined and executed.
144 * Given {@link CreateQueryArgs.query}, it executes the GraphQL query with the
145 * context’s {@link Client}.
146 *
147 * The returned result updates when the `Client` has new results
148 * for the query, and changes when your input `args` change.
149 *
150 * Additionally, if the `suspense` option is enabled on the `Client`,
151 * the `createQuery` hook will suspend instead of indicating that it’s
152 * waiting for a result via {@link CreateQueryState.fetching}.
153 *
154 * @example
155 * ```tsx
156 * import { gql, createQuery } from '@urql/solid';
157 *
158 * const TodosQuery = gql`
159 * query { todos { id, title } }
160 * `;
161 *
162 * const Todos = () => {
163 * const [result, reExecuteQuery] = createQuery({
164 * query: TodosQuery,
165 * });
166 * // ...
167 * };
168 * ```
169 */
170export const createQuery = <
171 Data = any,
172 Variables extends AnyVariables = AnyVariables,
173>(
174 args: CreateQueryArgs<Data, Variables>
175): CreateQueryResult<Data, Variables> => {
176 const client = useClient();
177 const getContext = asAccessor(args.context);
178 const getPause = asAccessor(args.pause);
179 const getRequestPolicy = asAccessor(args.requestPolicy);
180 const getVariables = asAccessor(args.variables);
181
182 const [source, setSource] = createSignal<
183 Source<OperationResult<Data, Variables>> | undefined
184 >(undefined, { equals: false });
185
186 // Combine suspense param coming from context and client with context being priority
187 const isSuspense = createMemo(() => {
188 const ctx = getContext();
189 if (ctx !== undefined && ctx.suspense !== undefined) {
190 return ctx.suspense;
191 }
192
193 return client.suspense;
194 });
195
196 const request = createRequest(args.query, getVariables() as any);
197 const context: Partial<OperationContext> = {
198 requestPolicy: getRequestPolicy(),
199 ...getContext(),
200 };
201 const operation = client.createRequestOperation('query', request, context);
202 const initialResult: CreateQueryState<Data, Variables> = {
203 operation: operation,
204 fetching: false,
205 data: undefined,
206 error: undefined,
207 extensions: undefined,
208 hasNext: false,
209 stale: false,
210 };
211
212 const [result, setResult] =
213 createStore<CreateQueryState<Data, Variables>>(initialResult);
214
215 createComputed(() => {
216 if (getPause() === true) {
217 setSource(undefined);
218 return;
219 }
220
221 const request = createRequest(args.query, getVariables() as any);
222 const context: Partial<OperationContext> = {
223 requestPolicy: getRequestPolicy(),
224 ...getContext(),
225 };
226
227 setSource(() => client.executeQuery(request, context));
228 });
229
230 createComputed(() => {
231 const s = source();
232 if (s === undefined) {
233 setResult(
234 produce(draft => {
235 draft.fetching = false;
236 draft.stale = false;
237 draft.hasNext = false;
238 })
239 );
240
241 return;
242 }
243
244 setResult(
245 produce(draft => {
246 draft.fetching = true;
247 draft.stale = false;
248 draft.hasNext = false;
249 })
250 );
251
252 onCleanup(
253 pipe(
254 s,
255 onEnd(() => {
256 setResult(
257 produce(draft => {
258 draft.fetching = false;
259 draft.stale = false;
260 draft.hasNext = false;
261 })
262 );
263 }),
264 subscribe(res => {
265 batch(() => {
266 setResult('data', reconcile(res.data));
267 setResult(
268 produce(draft => {
269 draft.stale = !!res.stale;
270 draft.fetching = false;
271 draft.error = res.error;
272 draft.operation = res.operation;
273 draft.extensions = res.extensions;
274 draft.hasNext = res.hasNext;
275 })
276 );
277 });
278 })
279 ).unsubscribe
280 );
281 });
282
283 const [dataResource, { refetch }] = createResource<
284 CreateQueryState<Data, Variables>,
285 Source<OperationResult<Data, Variables>> | undefined
286 >(source, source => {
287 let sub: Subscription | void;
288 if (source === undefined) {
289 return Promise.resolve(result);
290 }
291
292 return new Promise<CreateQueryState<Data, Variables>>(resolve => {
293 let hasResult = false;
294 sub = pipe(
295 source,
296 subscribe(() => {
297 if (!result.fetching && !result.stale) {
298 if (sub) sub.unsubscribe();
299 hasResult = true;
300 resolve(result);
301 }
302 })
303 );
304 if (hasResult) {
305 sub.unsubscribe();
306 }
307 });
308 });
309
310 const executeQuery: CreateQueryExecute = opts => {
311 const request = createRequest(args.query, getVariables() as any);
312 const context: Partial<OperationContext> = {
313 requestPolicy: getRequestPolicy(),
314 ...getContext(),
315 ...opts,
316 };
317
318 setSource(() => client.executeQuery(request, context));
319 if (isSuspense()) {
320 refetch();
321 }
322 };
323
324 const handler = {
325 get(
326 target: CreateQueryState<Data, Variables>,
327 prop: keyof CreateQueryState<Data, Variables>
328 ): any {
329 if (isSuspense() && prop === 'data') {
330 const resource = dataResource();
331 if (resource === undefined) return undefined;
332 }
333
334 return Reflect.get(target, prop);
335 },
336 };
337
338 const proxy = new Proxy(result, handler);
339 return [proxy, executeQuery];
340};