UNPKG

@apollo/client

Version:

A fully-featured caching GraphQL client.

293 lines (292 loc) 13 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.StoreReader = void 0; const graphql_1 = require("graphql"); const optimism_1 = require("optimism"); const utilities_1 = require("@apollo/client/utilities"); const environment_1 = require("@apollo/client/utilities/environment"); const internal_1 = require("@apollo/client/utilities/internal"); const invariant_1 = require("@apollo/client/utilities/invariant"); const common_js_1 = require("../core/types/common.cjs"); const entityStore_js_1 = require("./entityStore.cjs"); const helpers_js_1 = require("./helpers.cjs"); function execSelectionSetKeyArgs(options) { return [options.selectionSet, options.objectOrReference, options.context]; } 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 = (0, optimism_1.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; } (0, entityStore_js_1.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: utilities_1.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 ((0, entityStore_js_1.supportsResultCaching)(context.store)) { return context.store.makeCacheKey(selectionSet, (0, utilities_1.isReference)(parent) ? parent.__ref : parent, context.varString); } }, }); this.executeSubSelectedArray = (0, optimism_1.wrap)((options) => { (0, entityStore_js_1.maybeDependOnExistenceOfEntity)(options.context.store, options.enclosingRef.__ref); return this.execSubSelectedArrayImpl(options); }, { max: utilities_1.cacheSizes["inMemoryCache.executeSubSelectedArray"] || 10000 /* defaultCacheSizes["inMemoryCache.executeSubSelectedArray"] */, makeCacheKey({ field, array, context }) { if ((0, entityStore_js_1.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 = { ...(0, internal_1.getDefaultValues)((0, internal_1.getQueryDefinition)(query)), ...variables, }; const rootRef = (0, internal_1.makeReference)(rootId); const execResult = this.executeSelectionSet({ selectionSet: (0, internal_1.getMainDefinition)(query).selectionSet, objectOrReference: rootRef, enclosingRef: rootRef, context: { store, query, policies, variables, varString: (0, utilities_1.canonicalStringify)(variables), ...(0, helpers_js_1.extractFragmentContext)(query, this.config.fragments), }, }); let missing; if (execResult.missing) { missing = new common_js_1.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 ((0, entityStore_js_1.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 ((0, utilities_1.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 internal_1.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 (!(0, internal_1.shouldInclude)(selection, variables)) return; if ((0, internal_1.isField)(selection)) { let fieldValue = policies.readField({ fieldName: selection.name.value, field: selection, variables: context.variables, from: objectOrReference, }, context); const resultName = (0, internal_1.resultKeyNameFromField)(selection); if (fieldValue === void 0) { if (!utilities_1.addTypenameToDocument.added(selection)) { missing = missingMerger.merge(missing, { [resultName]: `Can't find field '${selection.name.value}' on ${(0, utilities_1.isReference)(objectOrReference) ? objectOrReference.__ref + " object" : "object " + JSON.stringify(objectOrReference, null, 2)}`, }); } } else if ((0, internal_1.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: (0, utilities_1.isReference)(fieldValue) ? fieldValue : enclosingRef, context, }), resultName); } if (fieldValue !== void 0) { objectsToMerge.push({ [resultName]: fieldValue }); } } else { const fragment = (0, internal_1.getFragmentFromSelection)(selection, context.lookupFragment); if (!fragment && selection.kind === graphql_1.Kind.FRAGMENT_SPREAD) { throw (0, invariant_1.newInvariantError)(104, selection.name.value); } if (fragment && policies.fragmentMatches(fragment, typename)) { fragment.selectionSet.selections.forEach(workSet.add, workSet); } } }); const result = (0, internal_1.mergeDeepArray)(objectsToMerge); const finalResult = { result, missing }; const frozen = (0, internal_1.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 internal_1.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 ((0, internal_1.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: (0, utilities_1.isReference)(item) ? item : enclosingRef, context, }), i); } if (environment_1.__DEV__) { assertSelectionSetForIdValue(context.store, field, item); } return item; }); return { result: array, missing, }; } } exports.StoreReader = StoreReader; 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 ((0, internal_1.isNonNullObject)(value)) { (0, invariant_1.invariant)( !(0, utilities_1.isReference)(value), 105, (0, helpers_js_1.getTypenameFromStoreObject)(store, value), field.name.value ); Object.values(value).forEach(workSet.add, workSet); } }); } } //# sourceMappingURL=readFromStore.cjs.map