1import type { FormattedNode, CombinedError } from '@urql/core';
2import { formatDocument } from '@urql/core';
3
4import type {
5 FieldNode,
6 DocumentNode,
7 FragmentDefinitionNode,
8} from '@0no-co/graphql.web';
9
10import type { SelectionSet } from '../ast';
11import {
12 getSelectionSet,
13 getName,
14 getFragmentTypeName,
15 getFieldAlias,
16 getFragments,
17 getMainOperation,
18 normalizeVariables,
19 getFieldArguments,
20 getDirectives,
21} from '../ast';
22
23import type {
24 Variables,
25 Data,
26 DataField,
27 Link,
28 OperationRequest,
29 Dependencies,
30 Resolver,
31} from '../types';
32
33import { joinKeys, keyOfField } from '../store/keys';
34import type { Store } from '../store/store';
35import * as InMemoryData from '../store/data';
36import { warn, pushDebugNode, popDebugNode } from '../helpers/help';
37
38import type { Context } from './shared';
39import {
40 SelectionIterator,
41 ensureData,
42 makeContext,
43 updateContext,
44 getFieldError,
45 deferRef,
46 optionalRef,
47} from './shared';
48
49import {
50 isFieldAvailableOnType,
51 isFieldNullable,
52 isListNullable,
53} from '../ast';
54
55export interface QueryResult {
56 dependencies: Dependencies;
57 partial: boolean;
58 hasNext: boolean;
59 data: null | Data;
60}
61
62/** Reads a GraphQL query from the cache.
63 * @internal
64 */
65export const __initAnd_query = (
66 store: Store,
67 request: OperationRequest,
68 data?: Data | null | undefined,
69 error?: CombinedError | undefined,
70 key?: number
71): QueryResult => {
72 InMemoryData.initDataState('read', store.data, key);
73 const result = _query(store, request, data, error);
74 InMemoryData.clearDataState();
75 return result;
76};
77
78/** Reads a GraphQL query from the cache.
79 * @internal
80 */
81export const _query = (
82 store: Store,
83 request: OperationRequest,
84 input?: Data | null | undefined,
85 error?: CombinedError | undefined
86): QueryResult => {
87 const query = formatDocument(request.query);
88 const operation = getMainOperation(query);
89 const rootKey = store.rootFields[operation.operation];
90 const rootSelect = getSelectionSet(operation);
91
92 const ctx = makeContext(
93 store,
94 normalizeVariables(operation, request.variables),
95 getFragments(query),
96 rootKey,
97 rootKey,
98 error
99 );
100
101 if (process.env.NODE_ENV !== 'production') {
102 pushDebugNode(rootKey, operation);
103 }
104
105 // NOTE: This may reuse "previous result data" as indicated by the
106 // `originalData` argument in readRoot(). This behaviour isn't used
107 // for readSelection() however, which always produces results from
108 // scratch
109 const data =
110 rootKey !== ctx.store.rootFields['query']
111 ? readRoot(ctx, rootKey, rootSelect, input || InMemoryData.makeData())
112 : readSelection(
113 ctx,
114 rootKey,
115 rootSelect,
116 input || InMemoryData.makeData()
117 );
118
119 if (process.env.NODE_ENV !== 'production') {
120 popDebugNode();
121 InMemoryData.getCurrentDependencies();
122 }
123
124 return {
125 dependencies: InMemoryData.currentDependencies!,
126 partial: ctx.partial || !data,
127 hasNext: ctx.hasNext,
128 data: data || null,
129 };
130};
131
132const readRoot = (
133 ctx: Context,
134 entityKey: string,
135 select: FormattedNode<SelectionSet>,
136 input: Data
137): Data => {
138 const typename = ctx.store.rootNames[entityKey]
139 ? entityKey
140 : input.__typename;
141 if (typeof typename !== 'string') {
142 return input;
143 }
144
145 const selection = new SelectionIterator(
146 entityKey,
147 entityKey,
148 false,
149 undefined,
150 select,
151 ctx
152 );
153
154 let node: FormattedNode<FieldNode> | void;
155 let hasChanged = InMemoryData.currentForeignData;
156 const output = InMemoryData.makeData(input);
157 while ((node = selection.next())) {
158 const fieldAlias = getFieldAlias(node);
159 const fieldValue = input[fieldAlias];
160 // Add the current alias to the walked path before processing the field's value
161 ctx.__internal.path.push(fieldAlias);
162 // We temporarily store the data field in here, but undefined
163 // means that the value is missing from the cache
164 let dataFieldValue: void | DataField;
165 if (node.selectionSet && fieldValue !== null) {
166 dataFieldValue = readRootField(
167 ctx,
168 getSelectionSet(node),
169 ensureData(fieldValue)
170 );
171 } else {
172 dataFieldValue = fieldValue;
173 }
174
175 // Check for any referential changes in the field's value
176 hasChanged = hasChanged || dataFieldValue !== fieldValue;
177 if (dataFieldValue !== undefined) output[fieldAlias] = dataFieldValue!;
178
179 // After processing the field, remove the current alias from the path again
180 ctx.__internal.path.pop();
181 }
182
183 return hasChanged ? output : input;
184};
185
186const readRootField = (
187 ctx: Context,
188 select: FormattedNode<SelectionSet>,
189 originalData: Link<Data>
190): Link<Data> => {
191 if (Array.isArray(originalData)) {
192 const newData = new Array(originalData.length);
193 let hasChanged = InMemoryData.currentForeignData;
194 for (let i = 0, l = originalData.length; i < l; i++) {
195 // Add the current index to the walked path before reading the field's value
196 ctx.__internal.path.push(i);
197 // Recursively read the root field's value
198 newData[i] = readRootField(ctx, select, originalData[i]);
199 hasChanged = hasChanged || newData[i] !== originalData[i];
200 // After processing the field, remove the current index from the path
201 ctx.__internal.path.pop();
202 }
203
204 return hasChanged ? newData : originalData;
205 } else if (originalData === null) {
206 return null;
207 }
208
209 // Write entity to key that falls back to the given parentFieldKey
210 const entityKey = ctx.store.keyOfEntity(originalData);
211 if (entityKey !== null) {
212 // We assume that since this is used for result data this can never be undefined,
213 // since the result data has already been written to the cache
214 return readSelection(ctx, entityKey, select, originalData) || null;
215 } else {
216 return readRoot(ctx, originalData.__typename, select, originalData);
217 }
218};
219
220export const _queryFragment = (
221 store: Store,
222 query: FormattedNode<DocumentNode>,
223 entity: Partial<Data> | string,
224 variables?: Variables,
225 fragmentName?: string
226): Data | null => {
227 const fragments = getFragments(query);
228
229 let fragment: FormattedNode<FragmentDefinitionNode>;
230 if (fragmentName) {
231 fragment = fragments[fragmentName]!;
232 if (!fragment) {
233 warn(
234 'readFragment(...) was called with a fragment name that does not exist.\n' +
235 'You provided ' +
236 fragmentName +
237 ' but could only find ' +
238 Object.keys(fragments).join(', ') +
239 '.',
240 6,
241 store.logger
242 );
243
244 return null;
245 }
246 } else {
247 const names = Object.keys(fragments);
248 fragment = fragments[names[0]]!;
249 if (!fragment) {
250 warn(
251 'readFragment(...) was called with an empty fragment.\n' +
252 'You have to call it with at least one fragment in your GraphQL document.',
253 6,
254 store.logger
255 );
256
257 return null;
258 }
259 }
260
261 const typename = getFragmentTypeName(fragment);
262 if (typeof entity !== 'string' && !entity.__typename)
263 entity.__typename = typename;
264 const entityKey = store.keyOfEntity(entity as Data);
265 if (!entityKey) {
266 warn(
267 "Can't generate a key for readFragment(...).\n" +
268 'You have to pass an `id` or `_id` field or create a custom `keys` config for `' +
269 typename +
270 '`.',
271 7,
272 store.logger
273 );
274
275 return null;
276 }
277
278 if (process.env.NODE_ENV !== 'production') {
279 pushDebugNode(typename, fragment);
280 }
281
282 const ctx = makeContext(
283 store,
284 variables || {},
285 fragments,
286 typename,
287 entityKey,
288 undefined
289 );
290
291 const result =
292 readSelection(
293 ctx,
294 entityKey,
295 getSelectionSet(fragment),
296 InMemoryData.makeData()
297 ) || null;
298
299 if (process.env.NODE_ENV !== 'production') {
300 popDebugNode();
301 }
302
303 return result;
304};
305
306function getFieldResolver(
307 directives: ReturnType<typeof getDirectives>,
308 typename: string,
309 fieldName: string,
310 ctx: Context
311): Resolver | void {
312 const resolvers = ctx.store.resolvers[typename];
313 const fieldResolver = resolvers && resolvers[fieldName];
314
315 let directiveResolver: Resolver | undefined;
316 for (const name in directives) {
317 const directiveNode = directives[name];
318 if (
319 directiveNode &&
320 name !== 'include' &&
321 name !== 'skip' &&
322 ctx.store.directives[name]
323 ) {
324 directiveResolver = ctx.store.directives[name](
325 getFieldArguments(directiveNode, ctx.variables)
326 );
327 if (process.env.NODE_ENV === 'production') return directiveResolver;
328 break;
329 }
330 }
331
332 if (fieldResolver && directiveResolver) {
333 warn(
334 `A resolver and directive is being used at "${typename}.${fieldName}" simultaneously. Only the directive will apply.`,
335 28,
336 ctx.store.logger
337 );
338 }
339
340 return directiveResolver || fieldResolver;
341}
342
343const readSelection = (
344 ctx: Context,
345 key: string,
346 select: FormattedNode<SelectionSet>,
347 input: Data,
348 result?: Data
349): Data | undefined => {
350 const { store } = ctx;
351 const isQuery = key === store.rootFields.query;
352
353 const entityKey = (result && store.keyOfEntity(result)) || key;
354 if (!isQuery && !!ctx.store.rootNames[entityKey]) {
355 warn(
356 'Invalid root traversal: A selection was being read on `' +
357 entityKey +
358 '` which is an uncached root type.\n' +
359 'The `' +
360 ctx.store.rootFields.mutation +
361 '` and `' +
362 ctx.store.rootFields.subscription +
363 '` types are special ' +
364 'Operation Root Types and cannot be read back from the cache.',
365 25,
366 store.logger
367 );
368 }
369
370 const typename = !isQuery
371 ? InMemoryData.readRecord(entityKey, '__typename') ||
372 (result && result.__typename)
373 : key;
374
375 if (typeof typename !== 'string') {
376 return;
377 } else if (result && typename !== result.__typename) {
378 warn(
379 'Invalid resolver data: The resolver at `' +
380 entityKey +
381 '` returned an ' +
382 'invalid typename that could not be reconciled with the cache.',
383 8,
384 store.logger
385 );
386
387 return;
388 }
389
390 const selection = new SelectionIterator(
391 typename,
392 entityKey,
393 false,
394 undefined,
395 select,
396 ctx
397 );
398
399 let hasFields = false;
400 let hasNext = false;
401 let hasChanged = InMemoryData.currentForeignData;
402 let node: FormattedNode<FieldNode> | void;
403 const hasPartials = ctx.partial;
404 const output = InMemoryData.makeData(input);
405 while ((node = selection.next()) !== undefined) {
406 // Derive the needed data from our node.
407 const fieldName = getName(node);
408 const fieldArgs = getFieldArguments(node, ctx.variables);
409 const fieldAlias = getFieldAlias(node);
410 const directives = getDirectives(node);
411 const resolver = getFieldResolver(directives, typename, fieldName, ctx);
412 const fieldKey = keyOfField(fieldName, fieldArgs);
413 const key = joinKeys(entityKey, fieldKey);
414 const fieldValue = InMemoryData.readRecord(entityKey, fieldKey);
415 const resultValue = result ? result[fieldName] : undefined;
416
417 if (process.env.NODE_ENV !== 'production' && store.schema && typename) {
418 isFieldAvailableOnType(
419 store.schema,
420 typename,
421 fieldName,
422 ctx.store.logger
423 );
424 }
425
426 // Add the current alias to the walked path before processing the field's value
427 ctx.__internal.path.push(fieldAlias);
428 // We temporarily store the data field in here, but undefined
429 // means that the value is missing from the cache
430 let dataFieldValue: void | DataField = undefined;
431
432 if (fieldName === '__typename') {
433 // We directly assign the typename as it's already available
434 dataFieldValue = typename;
435 } else if (resultValue !== undefined && node.selectionSet === undefined) {
436 // The field is a scalar and can be retrieved directly from the result
437 dataFieldValue = resultValue;
438 } else if (InMemoryData.currentOperation === 'read' && resolver) {
439 // We have a resolver for this field.
440 // Prepare the actual fieldValue, so that the resolver can use it,
441 // as to avoid the user having to do `cache.resolve(parent, info.fieldKey)`
442 // only to get a scalar value.
443 let parent = output;
444 if (node.selectionSet === undefined && fieldValue !== undefined) {
445 parent = {
446 ...output,
447 [fieldAlias]: fieldValue,
448 [fieldName]: fieldValue,
449 };
450 }
451
452 // We have to update the information in context to reflect the info
453 // that the resolver will receive
454 updateContext(ctx, parent, typename, entityKey, fieldKey, fieldName);
455
456 dataFieldValue = resolver(
457 parent,
458 fieldArgs || ({} as Variables),
459 store,
460 ctx
461 );
462
463 if (node.selectionSet) {
464 // When it has a selection set we are resolving an entity with a
465 // subselection. This can either be a list or an object.
466 dataFieldValue = resolveResolverResult(
467 ctx,
468 typename,
469 fieldName,
470 key,
471 getSelectionSet(node),
472 (output[fieldAlias] !== undefined
473 ? output[fieldAlias]
474 : input[fieldAlias]) as Data,
475 dataFieldValue,
476 InMemoryData.ownsData(input)
477 );
478 }
479
480 if (
481 store.schema &&
482 dataFieldValue === null &&
483 !isFieldNullable(store.schema, typename, fieldName, ctx.store.logger)
484 ) {
485 // Special case for when null is not a valid value for the
486 // current field
487 return undefined;
488 }
489 } else if (!node.selectionSet) {
490 // The field is a scalar but isn't on the result, so it's retrieved from the cache
491 dataFieldValue = fieldValue;
492 } else if (resultValue !== undefined) {
493 // We start walking the nested resolver result here
494 dataFieldValue = resolveResolverResult(
495 ctx,
496 typename,
497 fieldName,
498 key,
499 getSelectionSet(node),
500 (output[fieldAlias] !== undefined
501 ? output[fieldAlias]
502 : input[fieldAlias]) as Data,
503 resultValue,
504 InMemoryData.ownsData(input)
505 );
506 } else {
507 // Otherwise we attempt to get the missing field from the cache
508 const link = InMemoryData.readLink(entityKey, fieldKey);
509
510 if (link !== undefined) {
511 dataFieldValue = resolveLink(
512 ctx,
513 link,
514 typename,
515 fieldName,
516 getSelectionSet(node),
517 (output[fieldAlias] !== undefined
518 ? output[fieldAlias]
519 : input[fieldAlias]) as Data,
520 InMemoryData.ownsData(input)
521 );
522 } else if (typeof fieldValue === 'object' && fieldValue !== null) {
523 // The entity on the field was invalid but can still be recovered
524 dataFieldValue = fieldValue;
525 }
526 }
527
528 // Now that dataFieldValue has been retrieved it'll be set on data
529 // If it's uncached (undefined) but nullable we can continue assembling
530 // a partial query result
531 if (
532 !deferRef &&
533 dataFieldValue === undefined &&
534 (directives.optional ||
535 (optionalRef && !directives.required) ||
536 !!getFieldError(ctx) ||
537 (!directives.required &&
538 store.schema &&
539 isFieldNullable(store.schema, typename, fieldName, ctx.store.logger)))
540 ) {
541 // The field is uncached or has errored, so it'll be set to null and skipped
542 ctx.partial = true;
543 dataFieldValue = null;
544 } else if (
545 dataFieldValue === null &&
546 (directives.required || optionalRef === false)
547 ) {
548 if (
549 ctx.store.logger &&
550 process.env.NODE_ENV !== 'production' &&
551 InMemoryData.currentOperation === 'read'
552 ) {
553 ctx.store.logger(
554 'debug',
555 `Got value "null" for required field "${fieldName}"${
556 fieldArgs ? ` with args ${JSON.stringify(fieldArgs)}` : ''
557 } on entity "${entityKey}"`
558 );
559 }
560 dataFieldValue = undefined;
561 } else {
562 hasFields = hasFields || fieldName !== '__typename';
563 }
564
565 // After processing the field, remove the current alias from the path again
566 ctx.__internal.path.pop();
567 // Check for any referential changes in the field's value
568 hasChanged = hasChanged || dataFieldValue !== input[fieldAlias];
569 if (dataFieldValue !== undefined) {
570 output[fieldAlias] = dataFieldValue;
571 } else if (deferRef) {
572 hasNext = true;
573 } else {
574 if (
575 ctx.store.logger &&
576 process.env.NODE_ENV !== 'production' &&
577 InMemoryData.currentOperation === 'read'
578 ) {
579 ctx.store.logger(
580 'debug',
581 `No value for field "${fieldName}"${
582 fieldArgs ? ` with args ${JSON.stringify(fieldArgs)}` : ''
583 } on entity "${entityKey}"`
584 );
585 }
586 // If the field isn't deferred or partial then we have to abort and also reset
587 // the partial field
588 ctx.partial = hasPartials;
589 return undefined;
590 }
591 }
592
593 ctx.partial = ctx.partial || hasPartials;
594 ctx.hasNext = ctx.hasNext || hasNext;
595 return isQuery && ctx.partial && !hasFields
596 ? undefined
597 : hasChanged
598 ? output
599 : input;
600};
601
602const resolveResolverResult = (
603 ctx: Context,
604 typename: string,
605 fieldName: string,
606 key: string,
607 select: FormattedNode<SelectionSet>,
608 prevData: void | null | Data | Data[],
609 result: void | DataField,
610 isOwnedData: boolean
611): DataField | void => {
612 if (Array.isArray(result)) {
613 const { store } = ctx;
614 // Check whether values of the list may be null; for resolvers we assume
615 // that they can be, since it's user-provided data
616 const _isListNullable = store.schema
617 ? isListNullable(store.schema, typename, fieldName, ctx.store.logger)
618 : false;
619 const hasPartials = ctx.partial;
620 const data = InMemoryData.makeData(prevData, true);
621 let hasChanged =
622 InMemoryData.currentForeignData ||
623 !Array.isArray(prevData) ||
624 result.length !== prevData.length;
625 for (let i = 0, l = result.length; i < l; i++) {
626 // Add the current index to the walked path before reading the field's value
627 ctx.__internal.path.push(i);
628 // Recursively read resolver result
629 const childResult = resolveResolverResult(
630 ctx,
631 typename,
632 fieldName,
633 joinKeys(key, `${i}`),
634 select,
635 prevData != null ? prevData[i] : undefined,
636 result[i],
637 isOwnedData
638 );
639 // After processing the field, remove the current index from the path
640 ctx.__internal.path.pop();
641 // Check the result for cache-missed values
642 if (childResult === undefined && !_isListNullable) {
643 ctx.partial = hasPartials;
644 return undefined;
645 } else {
646 ctx.partial =
647 ctx.partial || (childResult === undefined && _isListNullable);
648 data[i] = childResult != null ? childResult : null;
649 hasChanged = hasChanged || data[i] !== prevData![i];
650 }
651 }
652
653 return hasChanged ? data : prevData;
654 } else if (result === null || result === undefined) {
655 return result;
656 } else if (isOwnedData && prevData === null) {
657 return null;
658 } else if (isDataOrKey(result)) {
659 const data = (prevData || InMemoryData.makeData(prevData)) as Data;
660 return typeof result === 'string'
661 ? readSelection(ctx, result, select, data)
662 : readSelection(ctx, key, select, data, result);
663 } else {
664 warn(
665 'Invalid resolver value: The field at `' +
666 key +
667 '` is a scalar (number, boolean, etc)' +
668 ', but the GraphQL query expects a selection set for this field.',
669 9,
670 ctx.store.logger
671 );
672
673 return undefined;
674 }
675};
676
677const resolveLink = (
678 ctx: Context,
679 link: Link | Link[],
680 typename: string,
681 fieldName: string,
682 select: FormattedNode<SelectionSet>,
683 prevData: void | null | Data | Data[],
684 isOwnedData: boolean
685): DataField | undefined => {
686 if (Array.isArray(link)) {
687 const { store } = ctx;
688 const _isListNullable = store.schema
689 ? isListNullable(store.schema, typename, fieldName, ctx.store.logger)
690 : false;
691 const newLink = InMemoryData.makeData(prevData, true);
692 const hasPartials = ctx.partial;
693 let hasChanged =
694 InMemoryData.currentForeignData ||
695 !Array.isArray(prevData) ||
696 link.length !== prevData.length;
697 for (let i = 0, l = link.length; i < l; i++) {
698 // Add the current index to the walked path before reading the field's value
699 ctx.__internal.path.push(i);
700 // Recursively read the link
701 const childLink = resolveLink(
702 ctx,
703 link[i],
704 typename,
705 fieldName,
706 select,
707 prevData != null ? prevData[i] : undefined,
708 isOwnedData
709 );
710 // After processing the field, remove the current index from the path
711 ctx.__internal.path.pop();
712 // Check the result for cache-missed values
713 if (childLink === undefined && !_isListNullable) {
714 ctx.partial = hasPartials;
715 return undefined;
716 } else {
717 ctx.partial =
718 ctx.partial || (childLink === undefined && _isListNullable);
719 newLink[i] = childLink || null;
720 hasChanged = hasChanged || newLink[i] !== prevData![i];
721 }
722 }
723
724 return hasChanged ? newLink : (prevData as Data[]);
725 } else if (link === null || (prevData === null && isOwnedData)) {
726 return null;
727 }
728
729 return readSelection(
730 ctx,
731 link,
732 select,
733 (prevData || InMemoryData.makeData(prevData)) as Data
734 );
735};
736
737const isDataOrKey = (x: any): x is string | Data =>
738 typeof x === 'string' ||
739 (typeof x === 'object' && typeof (x as any).__typename === 'string');