UNPKG

@apollo/client

Version:

A fully-featured caching GraphQL client.

289 lines (288 loc) 12.5 kB
import { Kind } from "graphql"; import { wrap } from "optimism"; import { addTypenameToDocument, cacheSizes, canonicalStringify, isReference, } from "@apollo/client/utilities"; import { __DEV__ } from "@apollo/client/utilities/environment"; import { DeepMerger, getDefaultValues, getFragmentFromSelection, getMainDefinition, getQueryDefinition, isArray, isField, isNonNullObject, makeReference, maybeDeepFreeze, mergeDeepArray, resultKeyNameFromField, shouldInclude, } from "@apollo/client/utilities/internal"; import { invariant, newInvariantError, } from "@apollo/client/utilities/invariant"; import { MissingFieldError } from "../core/types/common.js"; import { maybeDependOnExistenceOfEntity, supportsResultCaching, } from "./entityStore.js"; import { extractFragmentContext, getTypenameFromStoreObject, } from "./helpers.js"; function execSelectionSetKeyArgs(options) { return [options.selectionSet, options.objectOrReference, options.context]; } export class StoreReader { // cached version of executeSelectionSet executeSelectionSet; // cached version of executeSubSelectedArray executeSubSelectedArray; config; knownResults = new WeakMap(); constructor(config) { this.config = config; // memoized functions in this class will be "garbage-collected" // by recreating the whole `StoreReader` in // `InMemoryCache.resetResultsCache` // (triggered from `InMemoryCache.gc` with `resetResultCache: true`) this.executeSelectionSet = wrap((options) => { const peekArgs = execSelectionSetKeyArgs(options); const other = this.executeSelectionSet.peek(...peekArgs); if (other) { // If we previously read this result with canonization enabled, we can // return that canonized result as-is. return other; } maybeDependOnExistenceOfEntity(options.context.store, options.enclosingRef.__ref); // Finally, if we didn't find any useful previous results, run the real // execSelectionSetImpl method with the given options. return this.execSelectionSetImpl(options); }, { max: cacheSizes["inMemoryCache.executeSelectionSet"] || 50000 /* defaultCacheSizes["inMemoryCache.executeSelectionSet"] */, keyArgs: execSelectionSetKeyArgs, // Note that the parameters of makeCacheKey are determined by the // array returned by keyArgs. makeCacheKey(selectionSet, parent, context) { if (supportsResultCaching(context.store)) { return context.store.makeCacheKey(selectionSet, isReference(parent) ? parent.__ref : parent, context.varString); } }, }); this.executeSubSelectedArray = wrap((options) => { maybeDependOnExistenceOfEntity(options.context.store, options.enclosingRef.__ref); return this.execSubSelectedArrayImpl(options); }, { max: cacheSizes["inMemoryCache.executeSubSelectedArray"] || 10000 /* defaultCacheSizes["inMemoryCache.executeSubSelectedArray"] */, makeCacheKey({ field, array, context }) { if (supportsResultCaching(context.store)) { return context.store.makeCacheKey(field, array, context.varString); } }, }); } /** * Given a store and a query, return as much of the result as possible and * identify if any data was missing from the store. */ diffQueryAgainstStore({ store, query, rootId = "ROOT_QUERY", variables, returnPartialData = true, }) { const policies = this.config.cache.policies; variables = { ...getDefaultValues(getQueryDefinition(query)), ...variables, }; const rootRef = makeReference(rootId); const execResult = this.executeSelectionSet({ selectionSet: getMainDefinition(query).selectionSet, objectOrReference: rootRef, enclosingRef: rootRef, context: { store, query, policies, variables, varString: canonicalStringify(variables), ...extractFragmentContext(query, this.config.fragments), }, }); let missing; if (execResult.missing) { missing = new MissingFieldError(firstMissing(execResult.missing), execResult.missing, query, variables); } const complete = !missing; const { result } = execResult; return { result: complete || returnPartialData ? Object.keys(result).length === 0 ? null : result : null, complete, missing, }; } isFresh(result, parent, selectionSet, context) { if (supportsResultCaching(context.store) && this.knownResults.get(result) === selectionSet) { const latest = this.executeSelectionSet.peek(selectionSet, parent, context); if (latest && result === latest.result) { return true; } } return false; } // Uncached version of executeSelectionSet. execSelectionSetImpl({ selectionSet, objectOrReference, enclosingRef, context, }) { if (isReference(objectOrReference) && !context.policies.rootTypenamesById[objectOrReference.__ref] && !context.store.has(objectOrReference.__ref)) { return { result: {}, missing: `Dangling reference to missing ${objectOrReference.__ref} object`, }; } const { variables, policies, store } = context; const typename = store.getFieldValue(objectOrReference, "__typename"); const objectsToMerge = []; let missing; const missingMerger = new DeepMerger(); if (typeof typename === "string" && !policies.rootIdsByTypename[typename]) { // Ensure we always include a default value for the __typename // field, if we have one. Note that this field can be overridden by other // merged objects. objectsToMerge.push({ __typename: typename }); } function handleMissing(result, resultName) { if (result.missing) { missing = missingMerger.merge(missing, { [resultName]: result.missing, }); } return result.result; } const workSet = new Set(selectionSet.selections); workSet.forEach((selection) => { // Omit fields with directives @skip(if: <truthy value>) or // @include(if: <falsy value>). if (!shouldInclude(selection, variables)) return; if (isField(selection)) { let fieldValue = policies.readField({ fieldName: selection.name.value, field: selection, variables: context.variables, from: objectOrReference, }, context); const resultName = resultKeyNameFromField(selection); if (fieldValue === void 0) { if (!addTypenameToDocument.added(selection)) { missing = missingMerger.merge(missing, { [resultName]: `Can't find field '${selection.name.value}' on ${isReference(objectOrReference) ? objectOrReference.__ref + " object" : "object " + JSON.stringify(objectOrReference, null, 2)}`, }); } } else if (isArray(fieldValue)) { if (fieldValue.length > 0) { fieldValue = handleMissing(this.executeSubSelectedArray({ field: selection, array: fieldValue, enclosingRef, context, }), resultName); } } else if (!selection.selectionSet) { // do nothing } else if (fieldValue != null) { // In this case, because we know the field has a selection set, // it must be trying to query a GraphQLObjectType, which is why // fieldValue must be != null. fieldValue = handleMissing(this.executeSelectionSet({ selectionSet: selection.selectionSet, objectOrReference: fieldValue, enclosingRef: isReference(fieldValue) ? fieldValue : enclosingRef, context, }), resultName); } if (fieldValue !== void 0) { objectsToMerge.push({ [resultName]: fieldValue }); } } else { const fragment = getFragmentFromSelection(selection, context.lookupFragment); if (!fragment && selection.kind === Kind.FRAGMENT_SPREAD) { throw newInvariantError(104, selection.name.value); } if (fragment && policies.fragmentMatches(fragment, typename)) { fragment.selectionSet.selections.forEach(workSet.add, workSet); } } }); const result = mergeDeepArray(objectsToMerge); const finalResult = { result, missing }; const frozen = maybeDeepFreeze(finalResult); // Store this result with its selection set so that we can quickly // recognize it again in the StoreReader#isFresh method. if (frozen.result) { this.knownResults.set(frozen.result, selectionSet); } return frozen; } // Uncached version of executeSubSelectedArray. execSubSelectedArrayImpl({ field, array, enclosingRef, context, }) { let missing; let missingMerger = new DeepMerger(); function handleMissing(childResult, i) { if (childResult.missing) { missing = missingMerger.merge(missing, { [i]: childResult.missing }); } return childResult.result; } if (field.selectionSet) { array = array.filter(context.store.canRead); } array = array.map((item, i) => { // null value in array if (item === null) { return null; } // This is a nested array, recurse if (isArray(item)) { return handleMissing(this.executeSubSelectedArray({ field, array: item, enclosingRef, context, }), i); } // This is an object, run the selection set on it if (field.selectionSet) { return handleMissing(this.executeSelectionSet({ selectionSet: field.selectionSet, objectOrReference: item, enclosingRef: isReference(item) ? item : enclosingRef, context, }), i); } if (__DEV__) { assertSelectionSetForIdValue(context.store, field, item); } return item; }); return { result: array, missing, }; } } function firstMissing(tree) { try { JSON.stringify(tree, (_, value) => { if (typeof value === "string") throw value; return value; }); } catch (result) { return result; } } function assertSelectionSetForIdValue(store, field, fieldValue) { if (!field.selectionSet) { const workSet = new Set([fieldValue]); workSet.forEach((value) => { if (isNonNullObject(value)) { invariant( !isReference(value), 105, getTypenameFromStoreObject(store, value), field.name.value ); Object.values(value).forEach(workSet.add, workSet); } }); } } //# sourceMappingURL=readFromStore.js.map