Skip to content

Commit

Permalink
Merge pull request #1483 from apollographql/fragment-matcher
Browse files Browse the repository at this point in the history
WIP: Fragment matcher
  • Loading branch information
helfer authored Mar 25, 2017
2 parents dc1ecdb + a811f19 commit 581ef1e
Show file tree
Hide file tree
Showing 17 changed files with 818 additions and 142 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Expect active development and potentially significant breaking changes in the `0
- Fix: `cachePolicy: cache-and-network` queries now dispatch `APOLLO_QUERY_RESULT_CLIENT` [PR #1463](https://github.com/apollographql/apollo-client/pull/1463)
- Fix: query deduplication no longer causes query errors to prevent subsequent successful execution of the same query [PR #1481](https://github.com/apollographql/apollo-client/pull/1481)
- Breaking: change default of notifyOnNetworkStatusChange back to false [PR #1482](https://github.com/apollographql/apollo-client/pull/1482)

- Feature: add fragmentMatcher option to client and implement IntrospectionFragmentMatcher [PR #1483](https://github.com/apollographql/apollo-client/pull/1483)

### 1.0.0-rc.6
- Feature: Default selector for `dataIdFromObject` that tries `id` and falls back to `_id` to reduce configuration requirements whenever `__typename` is present.
Expand Down
17 changes: 17 additions & 0 deletions src/ApolloClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ import {
FragmentDefinitionNode,
} from 'graphql';

import {
HeuristicFragmentMatcher,
FragmentMatcherInterface,
} from './data/fragmentMatcher';

import {
createApolloStore,
ApolloStore,
Expand Down Expand Up @@ -149,6 +154,7 @@ export default class ApolloClient implements DataProxy {

private devToolsHookCb: Function;
private proxy: DataProxy | undefined;
private fragmentMatcher: FragmentMatcherInterface;

/**
* Constructs an instance of {@link ApolloClient}.
Expand Down Expand Up @@ -176,6 +182,7 @@ export default class ApolloClient implements DataProxy {
* @param queryDeduplication If set to false, a query will still be sent to the server even if a query
* with identical parameters (query, variables, operationName) is already in flight.
*
* @param fragmentMatcher A function to use for matching fragment conditions in GraphQL documents
*/

constructor(options: {
Expand All @@ -189,6 +196,7 @@ export default class ApolloClient implements DataProxy {
customResolvers?: CustomResolverMap,
connectToDevTools?: boolean,
queryDeduplication?: boolean,
fragmentMatcher?: FragmentMatcherInterface,
} = {}) {
let {
dataIdFromObject,
Expand All @@ -202,6 +210,7 @@ export default class ApolloClient implements DataProxy {
addTypename = true,
customResolvers,
connectToDevTools,
fragmentMatcher,
queryDeduplication = true,
} = options;

Expand All @@ -211,6 +220,12 @@ export default class ApolloClient implements DataProxy {
throw new Error('"reduxRootSelector" must be a function.');
}

if (typeof fragmentMatcher === 'undefined') {
this.fragmentMatcher = new HeuristicFragmentMatcher();
} else {
this.fragmentMatcher = fragmentMatcher;
}

this.initialState = initialState ? initialState : {};
this.networkInterface = networkInterface ? networkInterface :
createNetworkInterface({ uri: '/graphql' });
Expand Down Expand Up @@ -503,6 +518,7 @@ export default class ApolloClient implements DataProxy {
addTypename: this.addTypename,
reducerConfig: this.reducerConfig,
queryDeduplication: this.queryDeduplication,
fragmentMatcher: this.fragmentMatcher,
});
};

Expand All @@ -517,6 +533,7 @@ export default class ApolloClient implements DataProxy {
this.proxy = new ReduxDataProxy(
this.store,
this.reduxRootSelector || defaultReduxRootSelector,
this.fragmentMatcher,
this.reducerConfig,
);
}
Expand Down
224 changes: 133 additions & 91 deletions src/core/QueryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ import {
DataProxy,
} from '../data/proxy';

import {
FragmentMatcherInterface,
HeuristicFragmentMatcher,
} from '../data/fragmentMatcher';

import {
isProduction,
} from '../util/environment';
Expand Down Expand Up @@ -122,13 +127,14 @@ export class QueryManager {
public pollingTimers: {[queryId: string]: any};
public scheduler: QueryScheduler;
public store: ApolloStore;
public networkInterface: NetworkInterface;

private addTypename: boolean;
private networkInterface: NetworkInterface;
private deduplicator: Deduplicator;
private reduxRootSelector: ApolloStateSelector;
private reducerConfig: ApolloReducerConfig;
private queryDeduplication: boolean;
private fragmentMatcher: FragmentMatcherInterface;

// TODO REFACTOR collect all operation-related info in one place (e.g. all these maps)
// this should be combined with ObservableQuery, but that needs to be expanded to support
Expand Down Expand Up @@ -164,12 +170,14 @@ export class QueryManager {
store,
reduxRootSelector,
reducerConfig = { mutationBehaviorReducers: {} },
fragmentMatcher,
addTypename = true,
queryDeduplication = false,
}: {
networkInterface: NetworkInterface,
store: ApolloStore,
reduxRootSelector: ApolloStateSelector,
fragmentMatcher?: FragmentMatcherInterface,
reducerConfig?: ApolloReducerConfig,
addTypename?: boolean,
queryDeduplication?: boolean,
Expand All @@ -187,6 +195,16 @@ export class QueryManager {
this.addTypename = addTypename;
this.queryDeduplication = queryDeduplication;

// XXX This logic is duplicated in ApolloClient.ts for two reasons:
// 1. we need it in ApolloClient.ts for readQuery and readFragment of the data proxy.
// 2. we need it here so we don't have to rewrite all the tests.
// in the longer term we should remove the need for 2 and move it to ApolloClient.ts only.
if (typeof fragmentMatcher === 'undefined') {
this.fragmentMatcher = new HeuristicFragmentMatcher();
} else {
this.fragmentMatcher = fragmentMatcher;
}

this.scheduler = new QueryScheduler({
queryManager: this,
});
Expand Down Expand Up @@ -401,6 +419,7 @@ export class QueryManager {
query: this.queryDocuments[queryId],
variables: queryStoreValue.previousVariables || queryStoreValue.variables,
config: this.reducerConfig,
fragmentMatcherFunction: this.fragmentMatcher.match,
previousResult: lastResult && lastResult.data,
});

Expand Down Expand Up @@ -490,9 +509,9 @@ export class QueryManager {
}

let transformedOptions = { ...options } as WatchQueryOptions;
if (this.addTypename) {
transformedOptions.query = addTypenameToDocument(transformedOptions.query);
}
// if (this.addTypename) {
// transformedOptions.query = addTypenameToDocument(transformedOptions.query);
// }

let observableQuery = new ObservableQuery<T>({
scheduler: this.scheduler,
Expand Down Expand Up @@ -553,99 +572,18 @@ export class QueryManager {
queryId: string,
options: WatchQueryOptions,
fetchType?: FetchType,

// This allows us to track if this is a query spawned by a `fetchMore`
// call for another query. We need this data to compute the `fetchMore`
// network status for the query this is fetching for.
fetchMoreForQueryId?: string,
): Promise<ApolloQueryResult<T>> {

const {
variables = {},
metadata = null,
fetchPolicy = 'cache-first', // cache-first is the default fetch policy.
} = options;

const {
queryDoc,
} = this.transformQueryDocument(options);

const queryString = print(queryDoc);

let storeResult: any;
let needToFetch: boolean = fetchPolicy === 'network-only';

// If this is not a force fetch, we want to diff the query against the
// store before we fetch it from the network interface.
// TODO we hit the cache even if the policy is network-first. This could be unnecessary if the network is up.
if ( (fetchType !== FetchType.refetch && fetchPolicy !== 'network-only')) {
const { isMissing, result } = diffQueryAgainstStore({
query: queryDoc,
store: this.reduxRootSelector(this.store.getState()).data,
variables,
returnPartialData: true,
config: this.reducerConfig,
});

// If we're in here, only fetch if we have missing fields
needToFetch = isMissing || fetchPolicy === 'cache-and-network';

storeResult = result;
// putting this in front of the real fetchQuery function lets us make sure
// that the fragment matcher is initialized before we try to read from the store
if (this.fragmentMatcher.canBypassInit(options.query)) {
return this.fetchQueryAfterInit(queryId, options, fetchType, fetchMoreForQueryId);
}

const requestId = this.generateRequestId();
const shouldFetch = needToFetch && fetchPolicy !== 'cache-only';

// Initialize query in store with unique requestId
this.queryDocuments[queryId] = queryDoc;
this.store.dispatch({
type: 'APOLLO_QUERY_INIT',
queryString,
document: queryDoc,
variables,
fetchPolicy,
queryId,
requestId,
// we store the old variables in order to trigger "loading new variables"
// state if we know we will go to the server
storePreviousVariables: shouldFetch,
isPoll: fetchType === FetchType.poll,
isRefetch: fetchType === FetchType.refetch,
fetchMoreForQueryId,
metadata,
return this.fragmentMatcher.ensureReady(this).then( () => {
return this.fetchQueryAfterInit(queryId, options, fetchType, fetchMoreForQueryId);
});

// If there is no part of the query we need to fetch from the server (or,
// cachePolicy is cache-only), we just write the store result as the final result.
const shouldDispatchClientResult = !shouldFetch || fetchPolicy === 'cache-and-network';
if (shouldDispatchClientResult) {
this.store.dispatch({
type: 'APOLLO_QUERY_RESULT_CLIENT',
result: { data: storeResult },
variables,
document: queryDoc,
complete: !shouldFetch,
queryId,
requestId,
});
}

if (shouldFetch) {
const networkResult = this.fetchRequest({
requestId,
queryId,
document: queryDoc,
options,
fetchMoreForQueryId,
});

if (fetchPolicy !== 'cache-and-network') {
return networkResult;
}
}
// If we have no query to send to the server, we should return the result
// found within the store.
return Promise.resolve({ data: storeResult });
}

public generateQueryId() {
Expand Down Expand Up @@ -902,6 +840,110 @@ export class QueryManager {
};
}

/** This function runs fetchQuery without initializing the fragment matcher.
* We always want to initialize the fragment matcher, so this function should not be accessible outside.
* The only place we call this function from is within fetchQuery after initializing the fragment matcher.
*/
private fetchQueryAfterInit<T>(
queryId: string,
options: WatchQueryOptions,
fetchType?: FetchType,

// This allows us to track if this is a query spawned by a `fetchMore`
// call for another query. We need this data to compute the `fetchMore`
// network status for the query this is fetching for.
fetchMoreForQueryId?: string,
): Promise<ApolloQueryResult<T>> {

const {
variables = {},
metadata = null,
fetchPolicy = 'cache-first', // cache-first is the default fetch policy.
} = options;

const {
queryDoc,
} = this.transformQueryDocument(options);

const queryString = print(queryDoc);

let storeResult: any;
let needToFetch: boolean = fetchPolicy === 'network-only';

// If this is not a force fetch, we want to diff the query against the
// store before we fetch it from the network interface.
// TODO we hit the cache even if the policy is network-first. This could be unnecessary if the network is up.
if ( (fetchType !== FetchType.refetch && fetchPolicy !== 'network-only')) {
const { isMissing, result } = diffQueryAgainstStore({
query: queryDoc,
store: this.reduxRootSelector(this.store.getState()).data,
variables,
returnPartialData: true,
fragmentMatcherFunction: this.fragmentMatcher.match,
config: this.reducerConfig,
});

// If we're in here, only fetch if we have missing fields
needToFetch = isMissing || fetchPolicy === 'cache-and-network';

storeResult = result;
}

const requestId = this.generateRequestId();
const shouldFetch = needToFetch && fetchPolicy !== 'cache-only';

// Initialize query in store with unique requestId
this.queryDocuments[queryId] = queryDoc;
this.store.dispatch({
type: 'APOLLO_QUERY_INIT',
queryString,
document: queryDoc,
variables,
fetchPolicy,
queryId,
requestId,
// we store the old variables in order to trigger "loading new variables"
// state if we know we will go to the server
storePreviousVariables: shouldFetch,
isPoll: fetchType === FetchType.poll,
isRefetch: fetchType === FetchType.refetch,
fetchMoreForQueryId,
metadata,
});

// If there is no part of the query we need to fetch from the server (or,
// cachePolicy is cache-only), we just write the store result as the final result.
const shouldDispatchClientResult = !shouldFetch || fetchPolicy === 'cache-and-network';
if (shouldDispatchClientResult) {
this.store.dispatch({
type: 'APOLLO_QUERY_RESULT_CLIENT',
result: { data: storeResult },
variables,
document: queryDoc,
complete: !shouldFetch,
queryId,
requestId,
});
}

if (shouldFetch) {
const networkResult = this.fetchRequest({
requestId,
queryId,
document: queryDoc,
options,
fetchMoreForQueryId,
});

if (fetchPolicy !== 'cache-and-network') {
return networkResult;
}
}
// If we have no query to send to the server, we should return the result
// found within the store.
return Promise.resolve({ data: storeResult });
}

// XXX: I think we just store this on the observable query at creation time
// TODO LATER: rename this function. Its main role is to apply the transform, nothing else!
private getQueryParts<T>(observableQuery: ObservableQuery<T>) {
Expand Down
Loading

0 comments on commit 581ef1e

Please sign in to comment.