diff --git a/packages/apollo-cache-inmemory/src/__tests__/diffAgainstStore.ts b/packages/apollo-cache-inmemory/src/__tests__/diffAgainstStore.ts index 03c95f6756d..8dd3b3028dd 100644 --- a/packages/apollo-cache-inmemory/src/__tests__/diffAgainstStore.ts +++ b/packages/apollo-cache-inmemory/src/__tests__/diffAgainstStore.ts @@ -4,14 +4,12 @@ import { toIdValue } from 'apollo-utilities'; import { defaultNormalizedCacheFactory } from '../objectCache'; import { StoreReader } from '../readFromStore'; import { StoreWriter } from '../writeToStore'; -import { HeuristicFragmentMatcher } from '../fragmentMatcher'; import { defaultDataIdFromObject } from '../inMemoryCache'; import { NormalizedCache } from '../types'; -const fragmentMatcherFunction = new HeuristicFragmentMatcher().match; - disableFragmentWarnings(); -export function withError(func: Function, regex: RegExp) { + +export function withError(func: Function, regex?: RegExp) { let message: string = null as never; const { error } = console; console.error = (m: any) => { @@ -20,7 +18,9 @@ export function withError(func: Function, regex: RegExp) { try { const result = func(); - expect(message).toMatch(regex); + if (regex) { + expect(message).toMatch(regex); + } return result; } finally { console.error = error; @@ -53,7 +53,6 @@ describe('diffing queries against the store', () => { } } `, - fragmentMatcherFunction, config: { dataIdFromObject: defaultDataIdFromObject, }, @@ -99,7 +98,6 @@ describe('diffing queries against the store', () => { } } `, - fragmentMatcherFunction, config: { dataIdFromObject: defaultDataIdFromObject, }, @@ -253,11 +251,10 @@ describe('diffing queries against the store', () => { store, query: unionQuery, returnPartialData: false, - fragmentMatcherFunction, }); expect(complete).toBe(false); - }, /IntrospectionFragmentMatcher/); + }); }); it('does not error on a query with fields missing from all but one named fragment', () => { diff --git a/packages/apollo-cache-inmemory/src/__tests__/fragmentMatcher.ts b/packages/apollo-cache-inmemory/src/__tests__/fragmentMatcher.ts index a3f74d3fc07..988fb9fd38e 100644 --- a/packages/apollo-cache-inmemory/src/__tests__/fragmentMatcher.ts +++ b/packages/apollo-cache-inmemory/src/__tests__/fragmentMatcher.ts @@ -1,10 +1,139 @@ -import { IntrospectionFragmentMatcher } from '../fragmentMatcher'; -import { defaultNormalizedCacheFactory } from '../objectCache'; -import { ReadStoreContext } from '../types'; import { InMemoryCache } from '../inMemoryCache'; import gql from 'graphql-tag'; -describe('FragmentMatcher', () => { +describe('fragment matching', () => { + it('can match exact types with or without possibleTypes', () => { + const cacheWithoutPossibleTypes = new InMemoryCache({ + addTypename: true, + }); + + const cacheWithPossibleTypes = new InMemoryCache({ + addTypename: true, + possibleTypes: { + Animal: ['Cat', 'Dog'], + }, + }); + + const query = gql` + query AnimalNames { + animals { + id + name + ...CatDetails + } + } + fragment CatDetails on Cat { + livesLeft + killsToday + } + `; + + const data = { + animals: [ + { + __typename: 'Cat', + id: 1, + name: 'Felix', + livesLeft: 8, + killsToday: 2, + }, + { + __typename: 'Dog', + id: 2, + name: 'Baxter', + }, + ], + }; + + cacheWithoutPossibleTypes.writeQuery({ query, data }); + expect(cacheWithoutPossibleTypes.readQuery({ query })).toEqual(data); + + cacheWithPossibleTypes.writeQuery({ query, data }); + expect(cacheWithPossibleTypes.readQuery({ query })).toEqual(data); + }); + + it('can match interface subtypes', () => { + const cache = new InMemoryCache({ + addTypename: true, + possibleTypes: { + Animal: ['Cat', 'Dog'], + }, + }); + + const query = gql` + query BestFriend { + bestFriend { + id + ...AnimalName + } + } + fragment AnimalName on Animal { + name + } + `; + + const data = { + bestFriend: { + __typename: 'Dog', + id: 2, + name: 'Beckett', + }, + }; + + cache.writeQuery({ query, data }); + expect(cache.readQuery({ query })).toEqual(data); + }); + + it('can match union member types', () => { + const cache = new InMemoryCache({ + addTypename: true, + possibleTypes: { + Status: ['PASSING', 'FAILING', 'SKIPPED'], + }, + }); + + const query = gql` + query { + testResults { + id + output { + ... on Status { + stdout + } + ... on FAILING { + stderr + } + } + } + } + `; + + const data = { + testResults: [ + { + __typename: 'TestResult', + id: 123, + output: { + __typename: 'PASSING', + stdout: 'ok!', + }, + }, + { + __typename: 'TestResult', + id: 456, + output: { + __typename: 'FAILING', + stdout: '', + stderr: 'oh no', + }, + }, + ], + }; + + cache.writeQuery({ query, data }); + expect(cache.readQuery({ query })).toEqual(data); + }); + it('can match against the root Query', () => { const cache = new InMemoryCache({ addTypename: true, @@ -45,57 +174,3 @@ describe('FragmentMatcher', () => { expect(cache.readQuery({ query })).toEqual(data); }); }); - -describe('IntrospectionFragmentMatcher', () => { - it('will throw an error if match is called if it is not ready', () => { - const ifm = new IntrospectionFragmentMatcher(); - expect(() => (ifm.match as any)()).toThrowError(/called before/); - }); - - it('can be seeded with an introspection query result', () => { - const ifm = new IntrospectionFragmentMatcher({ - introspectionQueryResultData: { - __schema: { - types: [ - { - kind: 'UNION', - name: 'Item', - possibleTypes: [ - { - name: 'ItemA', - }, - { - name: 'ItemB', - }, - ], - }, - ], - }, - }, - }); - - const store = defaultNormalizedCacheFactory({ - a: { - __typename: 'ItemB', - }, - }); - - const idValue = { - type: 'id', - id: 'a', - generated: false, - }; - - const readStoreContext = { - store, - returnPartialData: false, - hasMissingField: false, - cacheRedirects: {}, - } as ReadStoreContext; - - expect(ifm.match(idValue as any, 'Item', readStoreContext)).toBe(true); - expect(ifm.match(idValue as any, 'NotAnItem', readStoreContext)).toBe( - false, - ); - }); -}); diff --git a/packages/apollo-cache-inmemory/src/__tests__/readFromStore.ts b/packages/apollo-cache-inmemory/src/__tests__/readFromStore.ts index fa11aaad512..a5e15db8943 100644 --- a/packages/apollo-cache-inmemory/src/__tests__/readFromStore.ts +++ b/packages/apollo-cache-inmemory/src/__tests__/readFromStore.ts @@ -3,11 +3,9 @@ import { IdValue, JsonValue } from 'apollo-utilities'; import gql from 'graphql-tag'; import { stripSymbols } from 'apollo-utilities'; -import { StoreObject, HeuristicFragmentMatcher } from '../'; +import { StoreObject } from '../'; import { StoreReader } from '../readFromStore'; import { defaultNormalizedCacheFactory } from '../objectCache'; - -const fragmentMatcherFunction = new HeuristicFragmentMatcher().match; import { withError } from './diffAgainstStore'; describe('reading from the store', () => { @@ -62,7 +60,6 @@ describe('reading from the store', () => { } } `, - fragmentMatcherFunction, }); expect(stripSymbols(queryResult)).toEqual({ @@ -70,7 +67,7 @@ describe('reading from the store', () => { innerArray: [{ id: 'abcdef', someField: 3 }], }, }); - }, /queries contain union or interface types/); + }); }); it('rejects malformed queries', () => { diff --git a/packages/apollo-cache-inmemory/src/__tests__/roundtrip.ts b/packages/apollo-cache-inmemory/src/__tests__/roundtrip.ts index 7f83b711f8c..8e6f018d93c 100644 --- a/packages/apollo-cache-inmemory/src/__tests__/roundtrip.ts +++ b/packages/apollo-cache-inmemory/src/__tests__/roundtrip.ts @@ -6,9 +6,7 @@ import { withWarning } from './writeToStore'; import { DepTrackingCache } from '../depTrackingCache'; -import { HeuristicFragmentMatcher, StoreReader, StoreWriter } from '../'; - -const fragmentMatcherFunction = new HeuristicFragmentMatcher().match; +import { StoreReader, StoreWriter } from '../'; function assertDeeplyFrozen(value: any, stack: any[] = []) { if (value !== null && typeof value === 'object' && stack.indexOf(value) < 0) { @@ -37,7 +35,6 @@ function storeRoundtrip(query: DocumentNode, result: any, variables = {}) { store, query, variables, - fragmentMatcherFunction, }; const reconstructedResult = reader.readQueryFromStore(readOptions); @@ -335,7 +332,7 @@ describe('roundtrip', () => { ], }, ); - }, /using fragments/); + }); }); // XXX this test is weird because it assumes the server returned an incorrect result @@ -398,7 +395,7 @@ describe('roundtrip', () => { ], }, ); - }, /IntrospectionFragmentMatcher/); + }); }); it('should resolve on union types with spread fragments', () => { @@ -437,7 +434,7 @@ describe('roundtrip', () => { ], }, ); - }, /IntrospectionFragmentMatcher/); + }); }); it('should work with a fragment on the actual interface or union', () => { @@ -476,7 +473,7 @@ describe('roundtrip', () => { ], }, ); - }, /IntrospectionFragmentMatcher/); + }); }); it('should throw on error on two of the same spread fragment types', () => { diff --git a/packages/apollo-cache-inmemory/src/__tests__/writeToStore.ts b/packages/apollo-cache-inmemory/src/__tests__/writeToStore.ts index bf70cbce2ea..e1902272bba 100644 --- a/packages/apollo-cache-inmemory/src/__tests__/writeToStore.ts +++ b/packages/apollo-cache-inmemory/src/__tests__/writeToStore.ts @@ -19,20 +19,18 @@ import { StoreWriter } from '../writeToStore'; import { defaultNormalizedCacheFactory } from '../objectCache'; -import { - HeuristicFragmentMatcher, - IntrospectionFragmentMatcher, - StoreObject, -} from '../'; +import { StoreObject } from '../'; -export function withWarning(func: Function, regex: RegExp) { +export function withWarning(func: Function, regex?: RegExp) { let message: string = null as never; const oldWarn = console.warn; console.warn = (m: string) => (message = m); return Promise.resolve(func()).then(val => { - expect(message).toMatch(regex); + if (regex) { + expect(message).toMatch(regex); + } console.warn = oldWarn; return val; }); @@ -1669,8 +1667,6 @@ describe('writing to the store', () => { }); it('should warn when it receives the wrong data with non-union fragments (using an heuristic matcher)', () => { - const fragmentMatcherFunction = new HeuristicFragmentMatcher().match; - const result = { todos: [ { @@ -1686,7 +1682,7 @@ describe('writing to the store', () => { result, document: query, dataIdFromObject: getIdField, - fragmentMatcherFunction, + possibleTypes: {}, }); expect(newStore.get('1')).toEqual(result.todos[0]); @@ -1694,23 +1690,6 @@ describe('writing to the store', () => { }); it('should warn when it receives the wrong data inside a fragment (using an introspection matcher)', () => { - const fragmentMatcherFunction = new IntrospectionFragmentMatcher({ - introspectionQueryResultData: { - __schema: { - types: [ - { - kind: 'UNION', - name: 'Todo', - possibleTypes: [ - { name: 'ShoppingCartItem' }, - { name: 'TaskItem' }, - ], - }, - ], - }, - }, - }).match; - const queryWithInterface = gql` query { todos { @@ -1751,7 +1730,9 @@ describe('writing to the store', () => { result, document: queryWithInterface, dataIdFromObject: getIdField, - fragmentMatcherFunction, + possibleTypes: { + Todo: ['ShoppingCartItem', 'TaskItem'], + }, }); expect(newStore.get('1')).toEqual(result.todos[0]); @@ -1759,8 +1740,6 @@ describe('writing to the store', () => { }); it('should warn if a result is missing __typename when required (using an heuristic matcher)', () => { - const fragmentMatcherFunction = new HeuristicFragmentMatcher().match; - const result: any = { todos: [ { @@ -1777,7 +1756,7 @@ describe('writing to the store', () => { result, document: addTypenameToDocument(query), dataIdFromObject: getIdField, - fragmentMatcherFunction, + possibleTypes: {}, }); expect(newStore.get('1')).toEqual(result.todos[0]); @@ -1811,13 +1790,11 @@ describe('writing to the store', () => { id: 1, }; - const fragmentMatcherFunction = new HeuristicFragmentMatcher().match; const newStore = writer.writeResultToStore({ dataId: 'ROOT_QUERY', result, document: defered, dataIdFromObject: getIdField, - fragmentMatcherFunction, }); expect(newStore.get('ROOT_QUERY')).toEqual({ id: 1 }); diff --git a/packages/apollo-cache-inmemory/src/fragmentMatcher.ts b/packages/apollo-cache-inmemory/src/fragmentMatcher.ts deleted file mode 100644 index 51b7b1539de..00000000000 --- a/packages/apollo-cache-inmemory/src/fragmentMatcher.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { isTest, IdValue } from 'apollo-utilities'; -import { invariant } from 'ts-invariant'; - -import { - ReadStoreContext, - FragmentMatcherInterface, - PossibleTypesMap, - IntrospectionResultData, -} from './types'; - -let haveWarned = false; - -function shouldWarn() { - const answer = !haveWarned; - /* istanbul ignore if */ - if (!isTest()) { - haveWarned = true; - } - return answer; -} - -/** - * This fragment matcher is very basic and unable to match union or interface type conditions - */ -export class HeuristicFragmentMatcher implements FragmentMatcherInterface { - constructor() { - // do nothing - } - - public ensureReady() { - return Promise.resolve(); - } - - public canBypassInit() { - return true; // we don't need to initialize this fragment matcher. - } - - public match( - idValue: IdValue, - typeCondition: string, - context: ReadStoreContext, - ): boolean | 'heuristic' { - const obj = context.store.get(idValue.id); - const isRootQuery = idValue.id === 'ROOT_QUERY'; - - if (!obj) { - // https://github.com/apollographql/apollo-client/pull/3507 - return isRootQuery; - } - - const { __typename = isRootQuery && 'Query' } = obj; - - if (!__typename) { - if (shouldWarn()) { - invariant.warn(`You're using fragments in your queries, but either don't have the addTypename: - true option set in Apollo Client, or you are trying to write a fragment to the store without the __typename. - Please turn on the addTypename option and include __typename when writing fragments so that Apollo Client - can accurately match fragments.`); - invariant.warn( - 'Could not find __typename on Fragment ', - typeCondition, - obj, - ); - invariant.warn( - `DEPRECATION WARNING: using fragments without __typename is unsupported behavior ` + - `and will be removed in future versions of Apollo client. You should fix this and set addTypename to true now.`, - ); - } - - return 'heuristic'; - } - - if (__typename === typeCondition) { - return true; - } - - // At this point we don't know if this fragment should match or not. It's - // either: - // - // 1. (GOOD) A fragment on a matching interface or union. - // 2. (BAD) A fragment on a non-matching concrete type or interface or union. - // - // If it's 2, we don't want it to match. If it's 1, we want it to match. We - // can't tell the difference, so we warn the user, but still try to match - // it (for backwards compatibility reasons). This unfortunately means that - // using the `HeuristicFragmentMatcher` with unions and interfaces is - // very unreliable. This will be addressed in a future major version of - // Apollo Client, but for now the recommendation is to use the - // `IntrospectionFragmentMatcher` when working with unions/interfaces. - - if (shouldWarn()) { - invariant.error( - 'You are using the simple (heuristic) fragment matcher, but your ' + - 'queries contain union or interface types. Apollo Client will not be ' + - 'able to accurately map fragments. To make this error go away, use ' + - 'the `IntrospectionFragmentMatcher` as described in the docs: ' + - 'https://www.apollographql.com/docs/react/advanced/fragments.html#fragment-matcher', - ); - } - - return 'heuristic'; - } -} - -export class IntrospectionFragmentMatcher implements FragmentMatcherInterface { - private isReady: boolean; - private possibleTypesMap: PossibleTypesMap; - - constructor(options?: { - introspectionQueryResultData?: IntrospectionResultData; - }) { - if (options && options.introspectionQueryResultData) { - this.possibleTypesMap = this.parseIntrospectionResult( - options.introspectionQueryResultData, - ); - this.isReady = true; - } else { - this.isReady = false; - } - - this.match = this.match.bind(this); - } - - public match( - idValue: IdValue, - typeCondition: string, - context: ReadStoreContext, - ) { - invariant( - this.isReady, - 'FragmentMatcher.match() was called before FragmentMatcher.init()', - ); - - const obj = context.store.get(idValue.id); - const isRootQuery = idValue.id === 'ROOT_QUERY'; - - if (!obj) { - // https://github.com/apollographql/apollo-client/pull/4620 - return isRootQuery; - } - - const { __typename = isRootQuery && 'Query' } = obj; - - invariant( - __typename, - `Cannot match fragment because __typename property is missing: ${JSON.stringify( - obj, - )}`, - ); - - if (__typename === typeCondition) { - return true; - } - - const implementingTypes = this.possibleTypesMap[typeCondition]; - if ( - __typename && - implementingTypes && - implementingTypes.indexOf(__typename) > -1 - ) { - return true; - } - - return false; - } - - private parseIntrospectionResult( - introspectionResultData: IntrospectionResultData, - ): PossibleTypesMap { - const typeMap: PossibleTypesMap = {}; - introspectionResultData.__schema.types.forEach(type => { - if (type.kind === 'UNION' || type.kind === 'INTERFACE') { - typeMap[type.name] = type.possibleTypes.map( - implementingType => implementingType.name, - ); - } - }); - return typeMap; - } -} diff --git a/packages/apollo-cache-inmemory/src/fragmentMatcherIntrospectionQuery.ts b/packages/apollo-cache-inmemory/src/fragmentMatcherIntrospectionQuery.ts deleted file mode 100644 index e5b58344125..00000000000 --- a/packages/apollo-cache-inmemory/src/fragmentMatcherIntrospectionQuery.ts +++ /dev/null @@ -1,97 +0,0 @@ -const query: any = { - kind: 'Document', - definitions: [ - { - kind: 'OperationDefinition', - operation: 'query', - name: null, - variableDefinitions: null, - directives: [], - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - alias: null, - name: { - kind: 'Name', - value: '__schema', - }, - arguments: [], - directives: [], - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - alias: null, - name: { - kind: 'Name', - value: 'types', - }, - arguments: [], - directives: [], - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - alias: null, - name: { - kind: 'Name', - value: 'kind', - }, - arguments: [], - directives: [], - selectionSet: null, - }, - { - kind: 'Field', - alias: null, - name: { - kind: 'Name', - value: 'name', - }, - arguments: [], - directives: [], - selectionSet: null, - }, - { - kind: 'Field', - alias: null, - name: { - kind: 'Name', - value: 'possibleTypes', - }, - arguments: [], - directives: [], - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - alias: null, - name: { - kind: 'Name', - value: 'name', - }, - arguments: [], - directives: [], - selectionSet: null, - }, - ], - }, - }, - ], - }, - }, - ], - }, - }, - ], - }, - }, - ], -}; - -export default query; diff --git a/packages/apollo-cache-inmemory/src/fragments.ts b/packages/apollo-cache-inmemory/src/fragments.ts new file mode 100644 index 00000000000..511e32a9944 --- /dev/null +++ b/packages/apollo-cache-inmemory/src/fragments.ts @@ -0,0 +1,24 @@ +import { InlineFragmentNode, FragmentDefinitionNode } from 'graphql'; +import { PossibleTypesMap } from './types'; + +export function fragmentMatches( + fragment: InlineFragmentNode | FragmentDefinitionNode, + typename: string, + possibleTypes?: PossibleTypesMap, +) { + if (!fragment.typeCondition) { + return true; + } + + const typeCondition = fragment.typeCondition.name.value; + if (typename === typeCondition) { + return true; + } + + if (possibleTypes) { + const subtypes = possibleTypes[typeCondition]; + return !!subtypes && subtypes.indexOf(typename) >= 0; + } + + return 'heuristic'; +} diff --git a/packages/apollo-cache-inmemory/src/inMemoryCache.ts b/packages/apollo-cache-inmemory/src/inMemoryCache.ts index 312a03a090d..d0680ea92dd 100644 --- a/packages/apollo-cache-inmemory/src/inMemoryCache.ts +++ b/packages/apollo-cache-inmemory/src/inMemoryCache.ts @@ -11,7 +11,6 @@ import { wrap } from 'optimism'; import { invariant, InvariantError } from 'ts-invariant'; -import { HeuristicFragmentMatcher } from './fragmentMatcher'; import { ApolloReducerConfig, NormalizedCache, @@ -30,7 +29,6 @@ export interface InMemoryCacheConfig extends ApolloReducerConfig { } const defaultConfig: InMemoryCacheConfig = { - fragmentMatcher: new HeuristicFragmentMatcher(), dataIdFromObject: defaultDataIdFromObject, addTypename: true, resultCaching: true, @@ -183,24 +181,17 @@ export class InMemoryCache extends ApolloCache { return null; } - const { fragmentMatcher } = this.config; - const fragmentMatcherFunction = fragmentMatcher && fragmentMatcher.match; - return this.storeReader.readQueryFromStore({ store: options.optimistic ? this.optimisticData : this.data, query: this.transformDocument(options.query), variables: options.variables, rootId: options.rootId, - fragmentMatcherFunction, previousResult: options.previousResult, config: this.config, }) || null; } public write(write: Cache.WriteOptions): void { - const { fragmentMatcher } = this.config; - const fragmentMatcherFunction = fragmentMatcher && fragmentMatcher.match; - this.storeWriter.writeResultToStore({ dataId: write.dataId, result: write.result, @@ -208,23 +199,19 @@ export class InMemoryCache extends ApolloCache { document: this.transformDocument(write.query), store: this.data, dataIdFromObject: this.config.dataIdFromObject, - fragmentMatcherFunction, + possibleTypes: this.config.possibleTypes, }); this.broadcastWatches(); } public diff(query: Cache.DiffOptions): Cache.DiffResult { - const { fragmentMatcher } = this.config; - const fragmentMatcherFunction = fragmentMatcher && fragmentMatcher.match; - return this.storeReader.diffQueryAgainstStore({ store: query.optimistic ? this.optimisticData : this.data, query: this.transformDocument(query.query), variables: query.variables, returnPartialData: query.returnPartialData, previousResult: query.previousResult, - fragmentMatcherFunction, config: this.config, }); } diff --git a/packages/apollo-cache-inmemory/src/index.ts b/packages/apollo-cache-inmemory/src/index.ts index 429817f40ac..29b3035b80c 100644 --- a/packages/apollo-cache-inmemory/src/index.ts +++ b/packages/apollo-cache-inmemory/src/index.ts @@ -6,6 +6,5 @@ export { export * from './readFromStore'; export * from './writeToStore'; -export * from './fragmentMatcher'; export * from './objectCache'; export * from './types'; diff --git a/packages/apollo-cache-inmemory/src/readFromStore.ts b/packages/apollo-cache-inmemory/src/readFromStore.ts index eb2bb1ea122..77c60ef9f1b 100644 --- a/packages/apollo-cache-inmemory/src/readFromStore.ts +++ b/packages/apollo-cache-inmemory/src/readFromStore.ts @@ -45,21 +45,15 @@ import { import { wrap, KeyTrie } from 'optimism'; import { DepTrackingCache } from './depTrackingCache'; import { invariant, InvariantError } from 'ts-invariant'; +import { fragmentMatches } from './fragments'; export type VariableMap = { [name: string]: any }; -export type FragmentMatcher = ( - rootValue: any, - typeCondition: string, - context: ReadStoreContext, -) => boolean | 'heuristic'; - type ExecContext = { query: DocumentNode; fragmentMap: FragmentMap; contextValue: ReadStoreContext; variableValues: VariableMap; - fragmentMatcher: FragmentMatcher; }; type ExecInfo = { @@ -84,8 +78,6 @@ type ExecStoreQueryOptions = { rootValue: IdValue; contextValue: ReadStoreContext; variableValues: VariableMap; - // Default matcher always matches all fragments - fragmentMatcher?: FragmentMatcher; }; type ExecSelectionSetOptions = { @@ -128,7 +120,6 @@ export class StoreReader { rootValue, contextValue, variableValues, - fragmentMatcher, }: ExecStoreQueryOptions) { // The result of executeStoreQuery can be safely cached only if the // underlying store is capable of tracking dependencies and invalidating @@ -137,7 +128,6 @@ export class StoreReader { return cacheKeyRoot.lookup( contextValue.store, query, - fragmentMatcher, JSON.stringify(variableValues), rootValue.id, ); @@ -157,7 +147,6 @@ export class StoreReader { return cacheKeyRoot.lookup( execContext.contextValue.store, selectionSet, - execContext.fragmentMatcher, JSON.stringify(execContext.variableValues), rootValue.id, ); @@ -220,7 +209,6 @@ export class StoreReader { previousResult, returnPartialData = true, rootId = 'ROOT_QUERY', - fragmentMatcherFunction, config, }: DiffQueryAgainstStoreOptions): Cache.DiffResult { // Throw the right validation error by trying to find a query in the document @@ -233,6 +221,7 @@ export class StoreReader { store, dataIdFromObject: config && config.dataIdFromObject, cacheRedirects: (config && config.cacheRedirects) || {}, + possibleTypes: config && config.possibleTypes, }; const execResult = this.executeStoreQuery({ @@ -245,7 +234,6 @@ export class StoreReader { }, contextValue: context, variableValues: variables, - fragmentMatcher: fragmentMatcherFunction, }); const hasMissingFields = @@ -299,8 +287,6 @@ export class StoreReader { rootValue, contextValue, variableValues, - // Default matcher always matches all fragments - fragmentMatcher = defaultFragmentMatcher, }: ExecStoreQueryOptions): ExecResult { const mainDefinition = getMainDefinition(query); const fragments = getFragmentDefinitions(query); @@ -310,7 +296,6 @@ export class StoreReader { fragmentMap, contextValue, variableValues, - fragmentMatcher, }; return this.executeSelectionSet({ @@ -376,14 +361,13 @@ export class StoreReader { } } - const typeCondition = - fragment.typeCondition && fragment.typeCondition.name.value; - - const match = - !typeCondition || - execContext.fragmentMatcher(rootValue, typeCondition, contextValue); + const match = fragmentMatches( + fragment, + typename, + execContext.contextValue.possibleTypes, + ); - if (match) { + if (match && (object || typename === 'Query')) { let fragmentExecResult = this.executeSelectionSet({ selectionSet: fragment.selectionSet, rootValue, @@ -559,10 +543,6 @@ function assertSelectionSetForIdValue( } } -function defaultFragmentMatcher() { - return true; -} - export function assertIdValue(idValue: IdValue) { invariant(isIdValue(idValue), `\ Encountered a sub-selection on the query, but the store doesn't have \ diff --git a/packages/apollo-cache-inmemory/src/types.ts b/packages/apollo-cache-inmemory/src/types.ts index f45b8a6dfd5..b50fe780f52 100644 --- a/packages/apollo-cache-inmemory/src/types.ts +++ b/packages/apollo-cache-inmemory/src/types.ts @@ -1,7 +1,6 @@ import { DocumentNode } from 'graphql'; -import { FragmentMatcher } from './readFromStore'; import { Transaction } from 'apollo-cache'; -import { IdValue, StoreValue } from 'apollo-utilities'; +import { StoreValue } from 'apollo-utilities'; export interface IdGetterObj extends Object { __typename?: string; @@ -54,7 +53,6 @@ export type OptimisticStoreItem = { export type ReadQueryOptions = { store: NormalizedCache; query: DocumentNode; - fragmentMatcherFunction?: FragmentMatcher; variables?: Object; previousResult?: any; rootId?: string; @@ -67,39 +65,20 @@ export type DiffQueryAgainstStoreOptions = ReadQueryOptions & { export type ApolloReducerConfig = { dataIdFromObject?: IdGetter; - fragmentMatcher?: FragmentMatcherInterface; addTypename?: boolean; cacheRedirects?: CacheResolverMap; + possibleTypes?: PossibleTypesMap; }; export type ReadStoreContext = { readonly store: NormalizedCache; readonly cacheRedirects: CacheResolverMap; readonly dataIdFromObject?: IdGetter; + readonly possibleTypes?: PossibleTypesMap; }; -export interface FragmentMatcherInterface { - match( - idValue: IdValue, - typeCondition: string, - context: ReadStoreContext, - ): boolean | 'heuristic'; -} - export type PossibleTypesMap = { [key: string]: string[] }; -export type IntrospectionResultData = { - __schema: { - types: { - kind: string; - name: string; - possibleTypes: { - name: string; - }[]; - }[]; - }; -}; - export type CacheResolver = ( rootValue: any, args: { [argName: string]: any }, diff --git a/packages/apollo-cache-inmemory/src/writeToStore.ts b/packages/apollo-cache-inmemory/src/writeToStore.ts index c71d21aefcc..ed96f591fba 100644 --- a/packages/apollo-cache-inmemory/src/writeToStore.ts +++ b/packages/apollo-cache-inmemory/src/writeToStore.ts @@ -5,7 +5,6 @@ import { InlineFragmentNode, FragmentDefinitionNode, } from 'graphql'; -import { FragmentMatcher } from './readFromStore'; import { assign, @@ -18,7 +17,6 @@ import { isField, isIdValue, isInlineFragment, - isProduction, resultKeyNameFromField, shouldInclude, storeKeyNameFromField, @@ -29,15 +27,15 @@ import { import { invariant } from 'ts-invariant'; -import { ObjectCache } from './objectCache'; import { defaultNormalizedCacheFactory } from './depTrackingCache'; import { IdGetter, NormalizedCache, - ReadStoreContext, StoreObject, + PossibleTypesMap, } from './types'; +import { fragmentMatches } from './fragments'; export class WriteError extends Error { public type = 'WriteError'; @@ -59,7 +57,7 @@ export type WriteContext = { readonly variables?: any; readonly dataIdFromObject?: IdGetter; readonly fragmentMap?: FragmentMap; - readonly fragmentMatcherFunction?: FragmentMatcher; + readonly possibleTypes?: PossibleTypesMap; }; export class StoreWriter { @@ -86,14 +84,14 @@ export class StoreWriter { store = defaultNormalizedCacheFactory(), variables, dataIdFromObject, - fragmentMatcherFunction, + possibleTypes, }: { query: DocumentNode; result: Object; store?: NormalizedCache; variables?: Object; dataIdFromObject?: IdGetter; - fragmentMatcherFunction?: FragmentMatcher; + possibleTypes?: PossibleTypesMap; }): NormalizedCache { return this.writeResultToStore({ dataId: 'ROOT_QUERY', @@ -102,7 +100,7 @@ export class StoreWriter { store, variables, dataIdFromObject, - fragmentMatcherFunction, + possibleTypes, }); } @@ -113,7 +111,7 @@ export class StoreWriter { store = defaultNormalizedCacheFactory(), variables, dataIdFromObject, - fragmentMatcherFunction, + possibleTypes, }: { dataId: string; result: any; @@ -121,7 +119,7 @@ export class StoreWriter { store?: NormalizedCache; variables?: Object; dataIdFromObject?: IdGetter; - fragmentMatcherFunction?: FragmentMatcher; + possibleTypes?: PossibleTypesMap; }): NormalizedCache { // XXX TODO REFACTOR: this is a temporary workaround until query normalization is made to work with documents. const operationDefinition = getOperationDefinition(document)!; @@ -141,7 +139,7 @@ export class StoreWriter { ), dataIdFromObject, fragmentMap: createFragmentMap(getFragmentDefinitions(document)), - fragmentMatcherFunction, + possibleTypes, }, }); } catch (e) { @@ -178,40 +176,26 @@ export class StoreWriter { field: selection, context, }); - } else { - let isDefered = false; - let isClient = false; - if (selection.directives && selection.directives.length) { - // If this is a defered field we don't need to throw / warn. - isDefered = selection.directives.some( - directive => directive.name && directive.name.value === 'defer', - ); - - // When using the @client directive, it might be desirable in - // some cases to want to write a selection set to the store, - // without having all of the selection set values available. - // This is because the @client field values might have already - // been written to the cache separately (e.g. via Apollo - // Cache's `writeData` capabilities). Because of this, we'll - // skip the missing field warning for fields with @client - // directives. - isClient = selection.directives.some( - directive => directive.name && directive.name.value === 'client', - ); - } - - if (!isDefered && !isClient && context.fragmentMatcherFunction) { - // XXX We'd like to throw an error, but for backwards compatibility's sake - // we just print a warning for the time being. - //throw new WriteError(`Missing field ${resultFieldKey} in ${JSON.stringify(result, null, 2).substring(0, 100)}`); - invariant.warn( - `Missing field ${resultFieldKey} in ${JSON.stringify( - result, - null, - 2, - ).substring(0, 100)}`, - ); - } + } else if ( + context.possibleTypes && + !( + selection.directives && + selection.directives.some( + ({ name }) => + name && (name.value === 'defer' || name.value === 'client'), + ) + ) + ) { + // XXX We'd like to throw an error, but for backwards compatibility's sake + // we just print a warning for the time being. + //throw new WriteError(`Missing field ${resultFieldKey} in ${JSON.stringify(result, null, 2).substring(0, 100)}`); + invariant.warn( + `Missing field ${resultFieldKey} in ${JSON.stringify( + result, + null, + 2, + ).substring(0, 100)}`, + ); } } else { // This is not a field, so it must be a fragment, either inline or named @@ -225,31 +209,18 @@ export class StoreWriter { invariant(fragment, `No fragment named ${selection.name.value}.`); } - let matches = true; - if (context.fragmentMatcherFunction && fragment.typeCondition) { - // TODO we need to rewrite the fragment matchers for this to work properly and efficiently - // Right now we have to pretend that we're passing in an idValue and that there's a store - // on the context. - const id = dataId || 'self'; - const idValue = toIdValue({ id, typename: undefined }); - const fakeContext: ReadStoreContext = { - // NOTE: fakeContext always uses ObjectCache - // since this is only to ensure the return value of 'matches' - store: new ObjectCache({ [id]: result }), - cacheRedirects: {}, - }; - const match = context.fragmentMatcherFunction( - idValue, - fragment.typeCondition.name.value, - fakeContext, - ); - if (!isProduction() && match === 'heuristic') { - invariant.error('WARNING: heuristic fragment matching going on!'); - } - matches = !!match; - } + const typename = + (result && result.__typename) || + (dataId === 'ROOT_QUERY' && 'Query') || + void 0; + + const match = fragmentMatches( + fragment, + typename, + context.possibleTypes, + ); - if (matches) { + if (match && (result || typename === 'Query')) { this.writeSelectionSetToStore({ result, selectionSet: fragment.selectionSet, diff --git a/packages/apollo-client/src/__tests__/ApolloClient.ts b/packages/apollo-client/src/__tests__/ApolloClient.ts index c1782dd2dcb..3c90c53e9b5 100644 --- a/packages/apollo-client/src/__tests__/ApolloClient.ts +++ b/packages/apollo-client/src/__tests__/ApolloClient.ts @@ -794,7 +794,10 @@ describe('ApolloClient', () => { it('should warn when the data provided does not match the query shape', () => { const client = new ApolloClient({ link: ApolloLink.empty(), - cache: new InMemoryCache(), + cache: new InMemoryCache({ + // Passing an empty map enables the warning: + possibleTypes: {}, + }), }); return withWarning(() => { @@ -1073,7 +1076,10 @@ describe('ApolloClient', () => { it('should warn when the data provided does not match the fragment shape', () => { const client = new ApolloClient({ link: ApolloLink.empty(), - cache: new InMemoryCache(), + cache: new InMemoryCache({ + // Passing an empty map enables the warning: + possibleTypes: {}, + }), }); return withWarning(() => { diff --git a/packages/apollo-client/src/__tests__/client.ts b/packages/apollo-client/src/__tests__/client.ts index 3f849aae3c2..47fe6d2148d 100644 --- a/packages/apollo-client/src/__tests__/client.ts +++ b/packages/apollo-client/src/__tests__/client.ts @@ -2,11 +2,7 @@ import { cloneDeep, assign } from 'lodash'; import { GraphQLError, ExecutionResult, DocumentNode } from 'graphql'; import gql from 'graphql-tag'; import { ApolloLink, Observable } from 'apollo-link'; -import { - InMemoryCache, - IntrospectionFragmentMatcher, - FragmentMatcherInterface, -} from 'apollo-cache-inmemory'; +import { InMemoryCache, PossibleTypesMap } from 'apollo-cache-inmemory'; import { stripSymbols } from 'apollo-utilities'; import { QueryManager } from '../core/QueryManager'; @@ -327,25 +323,9 @@ describe('client', () => { __typename: 'Query', }; - const ifm = new IntrospectionFragmentMatcher({ - introspectionQueryResultData: { - __schema: { - types: [ - { - kind: 'UNION', - name: 'Query', - possibleTypes: [ - { - name: 'Record', - }, - ], - }, - ], - }, - }, + return clientRoundtrip(query, { data }, null, { + Query: ['Record'], }); - - return clientRoundtrip(query, { data }, null, ifm); }); it('should merge fragments on root query', () => { @@ -378,25 +358,9 @@ describe('client', () => { __typename: 'Query', }; - const ifm = new IntrospectionFragmentMatcher({ - introspectionQueryResultData: { - __schema: { - types: [ - { - kind: 'UNION', - name: 'Query', - possibleTypes: [ - { - name: 'Record', - }, - ], - }, - ], - }, - }, + return clientRoundtrip(query, { data }, null, { + Query: ['Record'], }); - - return clientRoundtrip(query, { data }, null, ifm); }); it('store can be rehydrated from the server', () => { @@ -1218,33 +1182,6 @@ describe('client', () => { ], }; - const fancyFragmentMatcher = ( - idValue: any, // TODO types, please. - typeCondition: string, - context: any, - ): boolean => { - const obj = context.store.get(idValue.id); - - if (!obj) { - return false; - } - - const implementingTypesMap: { [key: string]: string[] } = { - Item: ['ColorItem', 'MonochromeItem'], - }; - - if (obj.__typename === typeCondition) { - return true; - } - - const implementingTypes = implementingTypesMap[typeCondition]; - if (implementingTypes && implementingTypes.indexOf(obj.__typename) > -1) { - return true; - } - - return false; - }; - const link = mockSingleLink({ request: { query }, result: { data: result }, @@ -1252,7 +1189,9 @@ describe('client', () => { const client = new ApolloClient({ link, cache: new InMemoryCache({ - fragmentMatcher: { match: fancyFragmentMatcher }, + possibleTypes: { + Item: ['ColorItem', 'MonochromeItem'], + }, }), }); return client.query({ query }).then((actualResult: any) => { @@ -1297,30 +1236,13 @@ describe('client', () => { result: { data: result }, }); - const ifm = new IntrospectionFragmentMatcher({ - introspectionQueryResultData: { - __schema: { - types: [ - { - kind: 'UNION', - name: 'Item', - possibleTypes: [ - { - name: 'ColorItem', - }, - { - name: 'MonochromeItem', - }, - ], - }, - ], - }, - }, - }); - const client = new ApolloClient({ link, - cache: new InMemoryCache({ fragmentMatcher: ifm }), + cache: new InMemoryCache({ + possibleTypes: { + Item: ['ColorItem', 'MonochromeItem'], + }, + }), }); return client.query({ query }).then(actualResult => { @@ -1380,30 +1302,13 @@ describe('client', () => { }, ); - const ifm = new IntrospectionFragmentMatcher({ - introspectionQueryResultData: { - __schema: { - types: [ - { - kind: 'UNION', - name: 'Item', - possibleTypes: [ - { - name: 'ColorItem', - }, - { - name: 'MonochromeItem', - }, - ], - }, - ], - }, - }, - }); - const client = new ApolloClient({ link, - cache: new InMemoryCache({ fragmentMatcher: ifm }), + cache: new InMemoryCache({ + possibleTypes: { + Item: ['ColorItem', 'MonochromeItem'], + }, + }), }); const queryUpdaterSpy = jest.fn(); @@ -2663,7 +2568,10 @@ describe('client', () => { }); const client = new ApolloClient({ link, - cache: new InMemoryCache(), + cache: new InMemoryCache({ + // Passing an empty map enables the warning: + possibleTypes: {}, + }), }); return withWarning( @@ -2952,19 +2860,18 @@ function clientRoundtrip( query: DocumentNode, data: ExecutionResult, variables?: any, - fragmentMatcher?: FragmentMatcherInterface, + possibleTypes?: PossibleTypesMap, ) { const link = mockSingleLink({ request: { query: cloneDeep(query) }, result: data, }); - const config = {}; - if (fragmentMatcher) config.fragmentMatcher = fragmentMatcher; - const client = new ApolloClient({ link, - cache: new InMemoryCache(config), + cache: new InMemoryCache({ + possibleTypes, + }), }); return client.query({ query, variables }).then(result => { diff --git a/packages/apollo-client/src/__tests__/local-state/general.ts b/packages/apollo-client/src/__tests__/local-state/general.ts index 69e72ec4a58..a67e574cccb 100644 --- a/packages/apollo-client/src/__tests__/local-state/general.ts +++ b/packages/apollo-client/src/__tests__/local-state/general.ts @@ -6,7 +6,6 @@ import ApolloClient from '../..'; import { ApolloCache } from 'apollo-cache'; import { InMemoryCache, - IntrospectionFragmentMatcher, } from 'apollo-cache-inmemory'; import { ApolloLink, Observable, Operation } from 'apollo-link'; import { hasDirectives } from 'apollo-utilities'; @@ -227,19 +226,9 @@ describe('General functionality', () => { const client = new ApolloClient({ cache: new InMemoryCache({ - fragmentMatcher: new IntrospectionFragmentMatcher({ - introspectionQueryResultData: { - __schema: { - types: [ - { - kind: 'UnionTypeDefinition', - name: 'Foo', - possibleTypes: [{ name: 'Bar' }, { name: 'Baz' }], - }, - ], - }, - }, - }), + possibleTypes: { + Foo: ['Bar', 'Baz'], + }, }), link, resolvers, diff --git a/packages/apollo-client/src/__tests__/mutationResults.ts b/packages/apollo-client/src/__tests__/mutationResults.ts index 64590d537e5..f6d7494aada 100644 --- a/packages/apollo-client/src/__tests__/mutationResults.ts +++ b/packages/apollo-client/src/__tests__/mutationResults.ts @@ -138,6 +138,8 @@ describe('mutation results', () => { } return null; }, + // Passing an empty map enables warnings about missing fields: + possibleTypes: {}, }), }); @@ -166,6 +168,8 @@ describe('mutation results', () => { } return null; }, + // Passing an empty map enables warnings about missing fields: + possibleTypes: {}, }), }); diff --git a/packages/apollo-client/src/core/__tests__/ObservableQuery.ts b/packages/apollo-client/src/core/__tests__/ObservableQuery.ts index b6a70dedcd8..76be63f86c5 100644 --- a/packages/apollo-client/src/core/__tests__/ObservableQuery.ts +++ b/packages/apollo-client/src/core/__tests__/ObservableQuery.ts @@ -2,7 +2,6 @@ import gql from 'graphql-tag'; import { ApolloLink, Observable } from 'apollo-link'; import { InMemoryCache, - IntrospectionFragmentMatcher, } from 'apollo-cache-inmemory'; import { GraphQLError } from 'graphql'; @@ -1411,19 +1410,9 @@ describe('ObservableQuery', () => { const client = new ApolloClient({ link: ni, cache: new InMemoryCache({ - fragmentMatcher: new IntrospectionFragmentMatcher({ - introspectionQueryResultData: { - __schema: { - types: [ - { - kind: 'UNION', - name: 'Creature', - possibleTypes: [{ name: 'Pet' }], - }, - ], - }, - }, - }), + possibleTypes: { + Creature: ['Pet'], + }, }), }); diff --git a/packages/apollo-client/src/core/__tests__/fetchPolicies.ts b/packages/apollo-client/src/core/__tests__/fetchPolicies.ts index 4a47c701c26..d17b14a5005 100644 --- a/packages/apollo-client/src/core/__tests__/fetchPolicies.ts +++ b/packages/apollo-client/src/core/__tests__/fetchPolicies.ts @@ -5,8 +5,6 @@ import { print } from 'graphql/language/printer'; import { ApolloLink, Observable } from 'apollo-link'; import { InMemoryCache, - IntrospectionFragmentMatcher, - FragmentMatcherInterface, } from 'apollo-cache-inmemory'; import { stripSymbols } from 'apollo-utilities';