Skip to content

Commit

Permalink
Added ObservableQuery#setVariables
Browse files Browse the repository at this point in the history
For #554
  • Loading branch information
tmeasday committed Sep 12, 2016
1 parent 10cfa79 commit 3779572
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 23 deletions.
48 changes: 41 additions & 7 deletions src/ObservableQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
import { tryFunctionOrLogError } from './util/errorHandling';

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

export interface FetchMoreOptions {
updateQuery: (previousQueryResult: Object, options: {
Expand All @@ -34,12 +35,28 @@ export interface UpdateQueryOptions {

export class ObservableQuery extends Observable<ApolloQueryResult> {
public refetch: (variables?: any) => Promise<ApolloQueryResult>;
/**
* Update the variables of this observable query, and fetch the new results
* if they've changed. If you want to force new results, use `refetch`.
*
* Note: if the variables have not changed, the promise will return the old
* results immediately, and the `next` callback will *not* fire.
*
* @param variables: The new set of variables. If there are missing variables,
* the previous values of those variables will be used.
*/
public setVariables: (variables: any) => Promise<ApolloQueryResult>;
public fetchMore: (options: FetchMoreQueryOptions & FetchMoreOptions) => Promise<any>;
public updateQuery: (mapFn: (previousQueryResult: any, options: UpdateQueryOptions) => any) => void;
public stopPolling: () => void;
public startPolling: (p: number) => void;
public options: WatchQueryOptions;
public queryId: string;
/**
*
* The current value of the variables for this query. Can change.
*/
public variables: { [key: string]: any };
private scheduler: QueryScheduler;
private queryManager: QueryManager;

Expand Down Expand Up @@ -91,25 +108,42 @@ export class ObservableQuery extends Observable<ApolloQueryResult> {
};
super(subscriberFunction);
this.options = options;
this.variables = this.options.variables || {};
this.scheduler = scheduler;
this.queryManager = queryManager;
this.queryId = queryId;

this.refetch = (variables?: any) => {
// Extend variables if available
variables = variables || this.options.variables ?
assign({}, this.options.variables, variables) : undefined;

this.variables = assign(this.variables, variables);

if (this.options.noFetch) {
throw new Error('noFetch option should not use query refetch.');
}
// Use the same options as before, but with new variables and forceFetch true
return this.queryManager.fetchQuery(this.queryId, assign(this.options, {
forceFetch: true,
variables,
variables: this.variables,
}) as WatchQueryOptions);
};

// There's a subtle difference between setVariables and refetch:
// - setVariables will take results from the store unless the query
// is marked forceFetch (and definitely if the variables haven't changed)
// - refetch will always go to the server
this.setVariables = (variables: any) => {
const newVariables = assign({}, this.variables, variables);

if (isEqual(newVariables, this.variables)) {
return this.result();
} else {
this.variables = newVariables;
// Use the same options as before, but with new variables and forceFetch true
return this.queryManager.fetchQuery(this.queryId, assign(this.options, {
variables: this.variables,
}) as WatchQueryOptions);
}
};

this.fetchMore = (fetchMoreOptions: WatchQueryOptions & FetchMoreOptions) => {
return Promise.resolve()
.then(() => {
Expand All @@ -121,8 +155,8 @@ export class ObservableQuery extends Observable<ApolloQueryResult> {
combinedOptions = fetchMoreOptions;
} else {
// fetch the same query with a possibly new variables
const variables = this.options.variables || fetchMoreOptions.variables ?
assign({}, this.options.variables, fetchMoreOptions.variables) : undefined;
const variables = this.variables || fetchMoreOptions.variables ?
assign({}, this.variables, fetchMoreOptions.variables) : undefined;

combinedOptions = assign({}, this.options, fetchMoreOptions, {
variables,
Expand Down
96 changes: 96 additions & 0 deletions test/ObservableQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import * as chai from 'chai';
const { assert } = chai;

import gql from 'graphql-tag';

import mockWatchQuery from './mocks/mockWatchQuery';
import { ObservableQuery } from '../src/ObservableQuery';

// I'm not sure why mocha doesn't provide something like this, you can't
// always use promises
const wrap = (done: Function, cb: (...args: any[]) => any) => (...args: any[]) => {
try {
return cb(...args);
} catch (e) {
done(e);
}
}

describe('ObservableQuery', () => {
describe('setVariables', () => {
const query = gql`
query($id: ID!){
people_one(id: $id) {
name
}
}
`;
const variables = { id: 1 };
const differentVariables = { id: 2 };
const dataOne = {
people_one: {
name: 'Luke Skywalker',
},
};
const dataTwo = {
people_one: {
name: 'Leia Skywalker',
},
};

it('reruns query if the variables change', (done) => {
const observable : ObservableQuery = mockWatchQuery({
request: { query, variables },
result: { data: dataOne },
}, {
request: { query, variables: differentVariables },
result: { data: dataTwo },
});

let handleCount = 0;
observable.subscribe({
next: wrap(done, result => {
handleCount++;

if (handleCount === 1) {
assert.deepEqual(result.data, dataOne);
observable.setVariables(differentVariables);
} else if (handleCount === 2) {
assert.deepEqual(result.data, dataTwo);
done();
}
}),
});
});


it('does not rerun query if variables do not change', (done) => {
const observable : ObservableQuery = mockWatchQuery({
request: { query, variables },
result: { data: dataOne },
}, {
request: { query, variables },
result: { data: dataTwo },
});

let handleCount = 0;
let errored = false;
observable.subscribe({
next: wrap(done, result => {
handleCount++;

if (handleCount === 1) {
assert.deepEqual(result.data, dataOne);
observable.setVariables(variables);

// Nothing should happen, so we'll wait a moment to check that
setTimeout(() => !errored && done(), 10);
} else if (handleCount === 2) {
errored = true;
throw new Error("Observable callback should not fire twice");
}
}),
});
});
});
});
19 changes: 4 additions & 15 deletions test/QueryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import {
QueryManager,
} from '../src/QueryManager';

import mockQueryManager from './mocks/mockQueryManager';

import mockWatchQuery from './mocks/mockWatchQuery';

import { ObservableQuery } from '../src/ObservableQuery';

import { WatchQueryOptions } from '../src/watchQueryOptions';
Expand Down Expand Up @@ -103,21 +107,6 @@ describe('QueryManager', () => {
});
};

// Helper method for the tests that construct a query manager out of a
// a list of mocked responses for a mocked network interface.
const mockQueryManager = (...mockedResponses: MockedResponse[]) => {
return new QueryManager({
networkInterface: mockNetworkInterface(...mockedResponses),
store: createApolloStore(),
reduxRootKey: 'apollo',
});
};

const mockWatchQuery = (mockedResponse: MockedResponse) => {
const queryManager = mockQueryManager(mockedResponse);
return queryManager.watchQuery({ query: mockedResponse.request.query });
};

// Helper method that sets up a mockQueryManager and then passes on the
// results to an observer.
const assertWithObserver = ({
Expand Down
2 changes: 1 addition & 1 deletion test/mocks/mockNetworkInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ function requestToKey(request: ParsedRequest): string {
const queryString = request.query && print(request.query);

return JSON.stringify({
variables: request.variables,
variables: request.variables || {},
debugName: request.debugName,
query: queryString,
});
Expand Down
22 changes: 22 additions & 0 deletions test/mocks/mockQueryManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {
QueryManager,
} from '../../src/QueryManager';

import mockNetworkInterface, {
MockedResponse,
} from './mockNetworkInterface';

import {
createApolloStore,
} from '../../src/store';


// Helper method for the tests that construct a query manager out of a
// a list of mocked responses for a mocked network interface.
export default (...mockedResponses: MockedResponse[]) => {
return new QueryManager({
networkInterface: mockNetworkInterface(...mockedResponses),
store: createApolloStore(),
reduxRootKey: 'apollo',
});
};
16 changes: 16 additions & 0 deletions test/mocks/mockWatchQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import mockNetworkInterface, {
MockedResponse,
} from './mockNetworkInterface';

import mockQueryManager from './mockQueryManager';

import { ObservableQuery } from '../../src/ObservableQuery';

export default (...mockedResponses: MockedResponse[]) => {
const queryManager = mockQueryManager(...mockedResponses);
const firstRequest = mockedResponses[0].request;
return queryManager.watchQuery({
query: firstRequest.query,
variables: firstRequest.variables,
});
};
1 change: 1 addition & 0 deletions test/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ import './errors';
import './mockNetworkInterface';
import './graphqlSubscriptions';
import './batchedNetworkInterface';
import './ObservableQuery';

0 comments on commit 3779572

Please sign in to comment.