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

Initial implementation of fetchMore #472

Merged
merged 13 commits into from
Aug 1, 2016
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Expect active development and potentially significant breaking changes in the `0
### vNEXT
- Fixed issue with `fragments` array for `updateQueries`. [PR #475](https://github.com/apollostack/apollo-client/pull/475) and [Issue #470](https://github.com/apollostack/apollo-client/issues/470).

- Add a new experimental feature to observable queries called `fetchMore`. It allows application developers to update the results of a query in the store by issuing new queries. We are currently testing this feature internally and we will document it once it is stable.

### v0.4.8

- Add `useAfter` function that accepts `afterwares`. Afterwares run after a request is made (after middlewares). In the afterware function, you get the whole response and request options, so you can handle status codes and errors if you need to. For example, if your requests return a `401` in the case of user logout, you can use this to identify when that starts happening. It can be used just as a `middleware` is used. Just pass an array of afterwares to the `useAfter` function.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"pretest": "npm run compile",
"test": "npm run testonly --",
"posttest": "npm run lint",
"filesize": "npm run compile:browser && ./scripts/filesize.js --file=./dist/index.min.js --maxGzip=36",
"filesize": "npm run compile:browser && ./scripts/filesize.js --file=./dist/index.min.js --maxGzip=37",
"compile": "tsc",
"compile:browser": "rm -rf ./dist && mkdir ./dist && browserify ./lib/src/index.js -o=./dist/index.js && npm run minify:browser",
"minify:browser": "uglifyjs --compress --mangle --screw-ie8 -o=./dist/index.min.js -- ./dist/index.js",
Expand Down
52 changes: 51 additions & 1 deletion src/ObservableQuery.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { WatchQueryOptions } from './watchQueryOptions';
import { WatchQueryOptions, FetchMoreQueryOptions } from './watchQueryOptions';

import { Observable, Observer } from './util/Observable';

Expand All @@ -16,8 +16,16 @@ import {

import assign = require('lodash.assign');

export interface FetchMoreOptions {
updateQuery: (previousQueryResult: Object, options: {
fetchMoreResult: Object,
queryVariables: Object,
}) => Object;
}

export class ObservableQuery extends Observable<ApolloQueryResult> {
public refetch: (variables?: any) => Promise<ApolloQueryResult>;
public fetchMore: (options: FetchMoreQueryOptions & FetchMoreOptions) => Promise<any>;
public stopPolling: () => void;
public startPolling: (p: number) => void;
public options: WatchQueryOptions;
Expand Down Expand Up @@ -90,6 +98,48 @@ export class ObservableQuery extends Observable<ApolloQueryResult> {
}) as WatchQueryOptions);
};

this.fetchMore = (fetchMoreOptions: WatchQueryOptions & FetchMoreOptions) => {
return Promise.resolve()
.then(() => {
const qid = this.queryManager.generateQueryId();
let combinedOptions = null;

if (fetchMoreOptions.query) {
// fetch a new query
combinedOptions = fetchMoreOptions;
} else {
// fetch the same query with a possibly new variables
combinedOptions =
assign({}, this.options, fetchMoreOptions);
}

combinedOptions = assign({}, combinedOptions, {
forceFetch: true,
}) as WatchQueryOptions;
return this.queryManager.fetchQuery(qid, combinedOptions);
})
.then((fetchMoreResult) => {
const reducer = fetchMoreOptions.updateQuery;
const {
previousResult,
queryVariables,
querySelectionSet,
queryFragments = [],
} = this.queryManager.getQueryWithPreviousResult(this.queryId);

this.queryManager.store.dispatch({
type: 'APOLLO_UPDATE_QUERY_RESULT',
newResult: reducer(previousResult, {
fetchMoreResult,
queryVariables,
}),
queryVariables,
querySelectionSet,
queryFragments,
});
});
};

this.stopPolling = () => {
this.queryManager.stopQuery(this.queryId);
if (isPollingQuery) {
Expand Down
116 changes: 72 additions & 44 deletions src/QueryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ import {
GraphQLResult,
Document,
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
// We need to disable the linter here because TSLint rightfully complains that this is unused.
/* tslint:disable */
SelectionSet,
/* tslint:enable */
} from 'graphql';

import { print } from 'graphql-tag/printer';
Expand Down Expand Up @@ -88,9 +94,9 @@ export type QueryListener = (queryStoreValue: QueryStoreValue) => void;
export class QueryManager {
public pollingTimers: {[queryId: string]: NodeJS.Timer | any}; //oddity in Typescript
public scheduler: QueryScheduler;
public store: ApolloStore;

private networkInterface: NetworkInterface;
private store: ApolloStore;
private reduxRootKey: string;
private queryTransformer: QueryTransformer;
private queryListeners: { [queryId: string]: QueryListener };
Expand Down Expand Up @@ -495,6 +501,53 @@ export class QueryManager {
this.stopQueryInStore(queryId);
}

public getQueryWithPreviousResult(queryId: string, isOptimistic = false) {
if (!this.observableQueries[queryId]) {
throw new Error(`ObservableQuery with this id doesn't exist: ${queryId}`);
}

const observableQuery = this.observableQueries[queryId].observableQuery;

const queryOptions = observableQuery.options;

let fragments = queryOptions.fragments;
let queryDefinition = getQueryDefinition(queryOptions.query);

if (this.queryTransformer) {
const doc = {
kind: 'Document',
definitions: [
queryDefinition,
...(fragments || []),
],
};

const transformedDoc = applyTransformers(doc, [this.queryTransformer]);

queryDefinition = getQueryDefinition(transformedDoc);
fragments = getFragmentDefinitions(transformedDoc);
}

const previousResult = readSelectionSetFromStore({
// In case of an optimistic change, apply reducer on top of the
// results including previous optimistic updates. Otherwise, apply it
// on top of the real data only.
store: isOptimistic ? this.getDataWithOptimisticResults() : this.getApolloState().data,
rootId: 'ROOT_QUERY',
selectionSet: queryDefinition.selectionSet,
variables: queryOptions.variables,
returnPartialData: queryOptions.returnPartialData || queryOptions.noFetch,
fragmentMap: createFragmentMap(fragments || []),
});

return {
previousResult,
queryVariables: queryOptions.variables,
querySelectionSet: queryDefinition.selectionSet,
queryFragments: fragments,
};
}

private collectResultBehaviorsFromUpdateQueries(
updateQueries: MutationQueryReducersMap,
mutationResult: Object,
Expand All @@ -505,67 +558,42 @@ export class QueryManager {
}
const resultBehaviors = [];

const observableQueriesByName: { [name: string]: ObservableQuery[] } = {};
Object.keys(this.observableQueries).forEach((key) => {
const observableQuery = this.observableQueries[key].observableQuery;
const queryIdsByName: { [name: string]: string[] } = {};
Object.keys(this.observableQueries).forEach((queryId) => {
const observableQuery = this.observableQueries[queryId].observableQuery;
const queryName = getQueryDefinition(observableQuery.options.query).name.value;

observableQueriesByName[queryName] =
observableQueriesByName[queryName] || [];
observableQueriesByName[queryName].push(observableQuery);
queryIdsByName[queryName] =
queryIdsByName[queryName] || [];
queryIdsByName[queryName].push(queryId);
});

Object.keys(updateQueries).forEach((queryName) => {
const reducer = updateQueries[queryName];
const queries = observableQueriesByName[queryName];
const queries = queryIdsByName[queryName];
if (!queries) {
// XXX should throw an error?
return;
}

queries.forEach((observableQuery) => {
const queryOptions = observableQuery.options;

let fragments = queryOptions.fragments;
let queryDefinition = getQueryDefinition(queryOptions.query);

if (this.queryTransformer) {
const doc = {
kind: 'Document',
definitions: [
queryDefinition,
...(fragments || []),
],
};

const transformedDoc = applyTransformers(doc, [this.queryTransformer]);

queryDefinition = getQueryDefinition(transformedDoc);
fragments = getFragmentDefinitions(transformedDoc);
}

const previousResult = readSelectionSetFromStore({
// In case of an optimistic change, apply reducer on top of the
// results including previous optimistic updates. Otherwise, apply it
// on top of the real data only.
store: isOptimistic ? this.getDataWithOptimisticResults() : this.getApolloState().data,
rootId: 'ROOT_QUERY',
selectionSet: queryDefinition.selectionSet,
variables: queryOptions.variables,
returnPartialData: queryOptions.returnPartialData || queryOptions.noFetch,
fragmentMap: createFragmentMap(fragments),
});
queries.forEach((queryId) => {
const {
previousResult,
queryVariables,
querySelectionSet,
queryFragments,
} = this.getQueryWithPreviousResult(queryId, isOptimistic);

resultBehaviors.push({
type: 'QUERY_RESULT',
newResult: reducer(previousResult, {
mutationResult,
queryName,
queryVariables: queryOptions.variables,
queryVariables,
}),
queryVariables: queryOptions.variables,
querySelectionSet: queryDefinition.selectionSet,
queryFragments: fragments,
queryVariables,
querySelectionSet,
queryFragments,
});
});
});
Expand Down
15 changes: 15 additions & 0 deletions src/actions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {
GraphQLResult,
SelectionSet,
FragmentDefinition,
} from 'graphql';

import {
Expand Down Expand Up @@ -108,6 +110,18 @@ export function isMutationErrorAction(action: ApolloAction): action is MutationE
return action.type === 'APOLLO_MUTATION_ERROR';
}

export interface UpdateQueryResultAction {
type: 'APOLLO_UPDATE_QUERY_RESULT';
queryVariables: any;
querySelectionSet: SelectionSet;
queryFragments: FragmentDefinition[];
newResult: Object;
}

export function isUpdateQueryResultAction(action: ApolloAction): action is UpdateQueryResultAction {
return action.type === 'APOLLO_UPDATE_QUERY_RESULT';
}

export interface StoreResetAction {
type: 'APOLLO_STORE_RESET';
observableQueryIds: string[];
Expand All @@ -126,4 +140,5 @@ export type ApolloAction =
MutationInitAction |
MutationResultAction |
MutationErrorAction |
UpdateQueryResultAction |
StoreResetAction;
2 changes: 1 addition & 1 deletion src/data/diffAgainstStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ function diffFieldAgainstStore({
if (! has(storeObj, storeFieldKey)) {
if (throwOnMissingField && included) {
throw new ApolloError({
errorMessage: `Can't find field ${storeFieldKey} on object ${JSON.stringify(storeObj)}.
errorMessage: `Can't find field ${storeFieldKey} on object (${rootId}) ${JSON.stringify(storeObj, null, 2)}.
Perhaps you want to use the \`returnPartialData\` option?`,
extraInfo: {
isFieldError: true,
Expand Down
32 changes: 8 additions & 24 deletions src/data/mutationResults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,14 @@ import isArray = require('lodash.isarray');
import cloneDeep = require('lodash.clonedeep');
import assign = require('lodash.assign');

import { replaceQueryResults } from './replaceQueryResults';

import {
writeSelectionSetToStore,
} from './writeToStore';

import {
FragmentMap,
createFragmentMap,
} from '../queries/getFromAST';

import {
Expand All @@ -28,10 +33,6 @@ import {
ApolloReducerConfig,
} from '../store';

import {
writeSelectionSetToStore,
} from './writeToStore';

// Mutation behavior types, these can be used in the `resultBehaviors` argument to client.mutate

export type MutationBehavior =
Expand Down Expand Up @@ -265,28 +266,11 @@ function mutationResultArrayDeleteReducer(state: NormalizedCache, {
}) as NormalizedCache;
}

function mutationResultQueryResultReducer(state: NormalizedCache, {
export function mutationResultQueryResultReducer(state: NormalizedCache, {
behavior,
config,
}: MutationBehaviorReducerArgs) {
const {
queryVariables,
newResult,
queryFragments,
querySelectionSet,
} = behavior as MutationQueryResultBehavior;

const clonedState = assign({}, state) as NormalizedCache;

return writeSelectionSetToStore({
result: newResult,
dataId: 'ROOT_QUERY',
selectionSet: querySelectionSet,
variables: queryVariables,
store: clonedState,
dataIdFromObject: config.dataIdFromObject,
fragmentMap: createFragmentMap(queryFragments),
});
return replaceQueryResults(state, behavior as MutationQueryResultBehavior, config);
}

export type MutationQueryReducer = (previousResult: Object, options: {
Expand Down
Loading