Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
at main 22 kB view raw
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');