Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactoring fetchQueryOverInterface within QueryManager #498

Merged
merged 9 commits into from
Aug 3, 2016
317 changes: 204 additions & 113 deletions src/QueryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
createFragmentMap,
getOperationName,
addFragmentsToDocument,
FragmentMap,
} from './queries/getFromAST';

import {
Expand All @@ -38,6 +39,7 @@ import {
import {
GraphQLResult,
Document,
OperationDefinition,
FragmentDefinition,
// We need to import this here to allow TypeScript to include it in the definition file even
// though we don't use it. https://github.com/Microsoft/TypeScript/issues/5711
Expand Down Expand Up @@ -622,33 +624,187 @@ export class QueryManager {
return resultBehaviors;
}

// Takes a set of WatchQueryOptions and transforms the query document
// accordingly. Specifically, it does the following:
// 1. Adds the fragments to the document
// 2. Applies the queryTransformer (if there is one defined)
// 3. Creates a fragment map out of all of the fragment definitions within the query
// document.
// 4. Returns the final query document and the fragment map associated with the
// query.
private transformQueryDocument(options: WatchQueryOptions): {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there some way we can make this return less stuff? For example, document, definition, and selection set seem a bit redundant.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm still working on the refactor. The final stuff that it will return will probably only be the document and nothing else.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I spoke too soon

queryDoc: Document,
fragmentMap: FragmentMap,
} {
const {
query,
fragments = [],
} = options;
let queryDoc = addFragmentsToDocument(query, fragments);

// Apply the query transformer if one has been provided
if (this.queryTransformer) {
queryDoc = applyTransformers(queryDoc, [ this.queryTransformer ]);
}

return {
queryDoc,
fragmentMap: createFragmentMap(getFragmentDefinitions(queryDoc)),
};
}

// Takes a selection set for a query and diffs it against the store.
// Returns a query document of selection sets
// that must be fetched from the server and as well as the data returned from the store.
private handleDiffQuery({
queryDef,
rootId,
variables,
fragmentMap,
noFetch,
}: {
queryDef: OperationDefinition,
rootId: string,
variables: Object,
fragmentMap: FragmentMap,
noFetch: boolean,
}): {
diffedQuery: Document,
initialResult: Object,
} {
const { missingSelectionSets, result } = diffSelectionSetAgainstStore({
selectionSet: queryDef.selectionSet,
store: this.store.getState()[this.reduxRootKey].data,
throwOnMissingField: false,
rootId,
variables,
fragmentMap,
});

const initialResult = result;
let diffedQuery: Document;
if (missingSelectionSets && missingSelectionSets.length && !noFetch) {
diffedQuery = queryDocument({
missingSelectionSets,
variableDefinitions: queryDef.variableDefinitions,
name: queryDef.name,
fragmentMap,
});
}

return {
diffedQuery,
initialResult,
};
}

// Takes a request id, query id, a query document and information asscoaiated with the query
// (e.g. variables, fragment map, etc.) and send it to the network interface. Returns
// a promise for the result associated with that request.
private fetchRequest({
requestId,
queryId,
query,
querySS,
options,
fragmentMap,
networkInterface,
}: {
requestId: number,
queryId: string,
query: Document,
querySS: SelectionSetWithRoot,
options: WatchQueryOptions,
fragmentMap: FragmentMap,
networkInterface: NetworkInterface,
}): Promise<GraphQLResult> {
const {
variables,
noFetch,
returnPartialData,
} = options;
const request: Request = {
query,
variables,
operationName: getOperationName(query),
};

const fetchRequest: QueryFetchRequest = {
options: { query, variables },
queryId,
operationName: request.operationName,
};

const retPromise = new Promise<ApolloQueryResult>((resolve, reject) => {
this.addFetchQueryPromise(requestId, retPromise, resolve, reject);

return this.batcher.enqueueRequest(fetchRequest)
.then((result: GraphQLResult) => {
// XXX handle multiple ApolloQueryResults
this.store.dispatch({
type: 'APOLLO_QUERY_RESULT',
result,
queryId,
requestId,
});

this.removeFetchQueryPromise(requestId);
return result;
}).then(() => {

let resultFromStore;
try {
// ensure result is combined with data already in store
// this will throw an error if there are missing fields in
// the results if returnPartialData is false.
resultFromStore = readSelectionSetFromStore({
store: this.getApolloState().data,
rootId: querySS.id,
selectionSet: querySS.selectionSet,
variables,
returnPartialData: returnPartialData || noFetch,
fragmentMap,
});
// ensure multiple errors don't get thrown
/* tslint:disable */
} catch (e) {}
/* tslint:enable */

// return a chainable promise
this.removeFetchQueryPromise(requestId);
resolve({ data: resultFromStore, loading: false });
}).catch((error: Error) => {
this.store.dispatch({
type: 'APOLLO_QUERY_ERROR',
error,
queryId,
requestId,
});

this.removeFetchQueryPromise(requestId);
});
});
return retPromise;
}

private fetchQueryOverInterface(
queryId: string,
options: WatchQueryOptions,
network: NetworkInterface
networkInterface: NetworkInterface
): Promise<ApolloQueryResult> {
const {
query,
variables,
forceFetch = false,
returnPartialData = false,
noFetch = false,
fragments = [],
} = options;

let queryDoc = addFragmentsToDocument(query, fragments);
// Apply the query transformer if one has been provided.
if (this.queryTransformer) {
queryDoc = applyTransformers(queryDoc, [this.queryTransformer]);
}

// Add the fragments passed in into the query and then create the fragment map
const queryFragmentMap = createFragmentMap(getFragmentDefinitions(queryDoc));
const {
queryDoc,
fragmentMap,
} = this.transformQueryDocument(options);
const queryDef = getQueryDefinition(queryDoc);
const queryString = print(queryDoc);

// Parse the query passed in -- this could also be done by a build plugin or tagged
// template string
const querySS = {
id: 'ROOT_QUERY',
typeName: 'Query',
Expand All @@ -660,44 +816,34 @@ export class QueryManager {
let minimizedQueryString = queryString;
let minimizedQuery = querySS;
let minimizedQueryDoc = queryDoc;
let initialResult;
let storeResult;

// If this is not a force fetch, we want to diff the query against the
// store before we fetch it from the network interface.
if (!forceFetch) {
// If the developer has specified they want to use the existing data in the store for this
// query, use the query diff algorithm to get as much of a result as we can, and identify
// what data is missing from the store
const { missingSelectionSets, result } = diffSelectionSetAgainstStore({
selectionSet: querySS.selectionSet,
store: this.store.getState()[this.reduxRootKey].data,
throwOnMissingField: false,
const {
diffedQuery,
initialResult,
} = this.handleDiffQuery({
queryDef,
rootId: querySS.id,
variables,
fragmentMap: queryFragmentMap,
fragmentMap,
noFetch,
});

initialResult = result;

if (missingSelectionSets && missingSelectionSets.length && !noFetch) {
const diffedQuery = queryDocument({
missingSelectionSets,
variableDefinitions: queryDef.variableDefinitions,
name: queryDef.name,
fragmentMap: queryFragmentMap,
});
const diffedQueryDef = getQueryDefinition(diffedQuery);

storeResult = initialResult;
if (diffedQuery) {
minimizedQueryDoc = diffedQuery;
minimizedQueryString = print(minimizedQueryDoc);
minimizedQuery = {
id: 'ROOT_QUERY',
id: querySS.id,
typeName: 'Query',
selectionSet: diffedQueryDef.selectionSet,
};

minimizedQueryString = print(diffedQuery);
minimizedQueryDoc = diffedQuery;
selectionSet: getQueryDefinition(diffedQuery).selectionSet,
} as SelectionSetWithRoot;
} else {
minimizedQuery = null;
minimizedQueryString = null;
minimizedQueryDoc = null;
minimizedQueryString = null;
minimizedQuery = null;
}
}

Expand All @@ -715,92 +861,37 @@ export class QueryManager {
returnPartialData: returnPartialData || noFetch,
queryId,
requestId,
fragmentMap: queryFragmentMap,
fragmentMap,
});

// If there is no part of the query we need to fetch from the server (or,
// noFetch is turned on), we just write the store result as the final result.
if (! minimizedQuery || returnPartialData || noFetch) {
this.store.dispatch({
type: 'APOLLO_QUERY_RESULT_CLIENT',
result: {
data: initialResult,
},
result: { data: storeResult },
variables,
query: querySS,
complete: !! minimizedQuery,
queryId,
});
}

if (minimizedQuery) {
const request: Request = {
if (minimizedQuery && !noFetch) {
return this.fetchRequest({
requestId,
queryId,
query: minimizedQueryDoc,
variables,
operationName: getOperationName(minimizedQueryDoc),
};

const fetchRequest: QueryFetchRequest = {
options: { query: minimizedQueryDoc, variables },
queryId: queryId,
operationName: request.operationName,
};

const retPromise = new Promise<ApolloQueryResult>((resolve, reject) => {
this.addFetchQueryPromise(requestId, retPromise, resolve, reject);

return this.batcher.enqueueRequest(fetchRequest)
.then((result: GraphQLResult) => {
// XXX handle multiple ApolloQueryResults
this.store.dispatch({
type: 'APOLLO_QUERY_RESULT',
result,
queryId,
requestId,
});

this.removeFetchQueryPromise(requestId);
return result;
}).then(() => {

let resultFromStore;
try {
// ensure result is combined with data already in store
// this will throw an error if there are missing fields in
// the results if returnPartialData is false.
resultFromStore = readSelectionSetFromStore({
store: this.getApolloState().data,
rootId: querySS.id,
selectionSet: querySS.selectionSet,
variables,
returnPartialData: returnPartialData || noFetch,
fragmentMap: queryFragmentMap,
});
// ensure multiple errors don't get thrown
/* tslint:disable */
} catch (e) {}
/* tslint:enable */

// return a chainable promise
this.removeFetchQueryPromise(requestId);
resolve({ data: resultFromStore, loading: false });
}).catch((error: Error) => {
this.store.dispatch({
type: 'APOLLO_QUERY_ERROR',
error,
queryId,
requestId,
});

this.removeFetchQueryPromise(requestId);

});
querySS: minimizedQuery,
options,
fragmentMap,
networkInterface,
});
return retPromise;
}

// return a chainable promise
return new Promise((resolve) => {
resolve({ data: initialResult });
});
// If we have no query to send to the server, we should return the result
// found within the store.
return Promise.resolve({ data: storeResult });
}

// Refetches a query given that query's name. Refetches
Expand Down