Skip to content

Commit

Permalink
Merge branch 'master' into loading_state
Browse files Browse the repository at this point in the history
  • Loading branch information
Sashko Stubailo authored Aug 1, 2016
2 parents cc88578 + 0c91cb1 commit b5ae4f9
Show file tree
Hide file tree
Showing 16 changed files with 378 additions and 76 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ Expect active development and potentially significant breaking changes in the `0
### vNEXT
- Add loading state tracking within Apollo Client in order to simplify the handling of loading state within the view layers. [Issue #342](https://github.com/apollostack/apollo-client/issues/342) and [PR #467](https://github.com/apollostack/apollo-client/pull/467)

### v0.4.9

- 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
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "apollo-client",
"version": "0.4.8",
"version": "0.4.9",
"description": "A simple yet functional GraphQL client.",
"main": "./lib/src/index.js",
"typings": "./lib/src/index.d.ts",
Expand All @@ -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 Expand Up @@ -80,15 +80,15 @@
"lodash.mapvalues": "^4.3.0",
"lodash.omit": "^4.2.1",
"minimist": "^1.2.0",
"mocha": "^2.3.3",
"mocha": "^3.0.0",
"nodemon": "^1.9.2",
"pretty-bytes": "^3.0.1",
"remap-istanbul": "^0.5.1",
"request-promise": "^4.0.1",
"rxjs": "^5.0.0-beta.7",
"sinon": "^1.17.4",
"source-map-support": "^0.4.0",
"tslint": "3.13.0",
"tslint": "3.14.0",
"typescript": "^2.0.0",
"typings": "^1.0.0",
"uglify-js": "^2.6.2"
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 @@ -496,6 +502,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 @@ -506,67 +559,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

0 comments on commit b5ae4f9

Please sign in to comment.