Skip to content
This repository has been archived by the owner on Sep 19, 2024. It is now read-only.

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
igrayson committed Jul 26, 2016
2 parents a31251d + 08e9367 commit a501a7b
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 3 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Expect active development and potentially significant breaking changes in the `0

### vNEXT

- Added `resultTransformer` and `resultComparator` to `ApolloClient`/`QueryManager`, which afford the ability to transform result objects immediately before they are returned to the application. [PR #446](https://github.com/apollostack/apollo-client/pull/446)

### 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
40 changes: 37 additions & 3 deletions src/QueryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,22 @@ import { ObservableQuery } from './ObservableQuery';

export type QueryListener = (queryStoreValue: QueryStoreValue) => void;

// A result transformer is given the data that is to be returned from the store from a query or
// mutation, and can modify or observe it before the value is provided to your application.
//
// For watched queries, the transformer is only called when the data retrieved from the server is
// different from previous.
//
// If the transformer wants to mutate results (say, by setting the prototype of result data), it
// will likely need to be paired with a custom resultComparator. By default, Apollo performs a
// deep equality comparsion on results, and skips those that are considered equal - reducing
// re-renders.
export type ResultTransformer = (resultData: ApolloQueryResult) => ApolloQueryResult;

// Controls how Apollo compares two query results and considers their equality. Two equal results
// will not trigger re-renders.
export type ResultComparator = (result1: ApolloQueryResult, result2: ApolloQueryResult) => boolean;

export class QueryManager {
public pollingTimers: {[queryId: string]: NodeJS.Timer | any}; //oddity in Typescript
public scheduler: QueryScheduler;
Expand All @@ -98,6 +114,8 @@ export class QueryManager {
private reduxRootKey: string;
private queryTransformer: QueryTransformer;
private storeFetchMiddleware: StoreFetchMiddleware;
private resultTransformer: ResultTransformer;
private resultComparator: ResultComparator;
private queryListeners: { [queryId: string]: QueryListener };

// A map going from queryId to the last result/state that the queryListener was told about.
Expand Down Expand Up @@ -131,6 +149,8 @@ export class QueryManager {
reduxRootKey,
queryTransformer,
storeFetchMiddleware,
resultTransformer,
resultComparator,
shouldBatch = false,
batchInterval = 10,
}: {
Expand All @@ -139,6 +159,8 @@ export class QueryManager {
reduxRootKey: string,
queryTransformer?: QueryTransformer,
storeFetchMiddleware?: StoreFetchMiddleware,
resultTransformer?: ResultTransformer,
resultComparator?: ResultComparator,
shouldBatch?: Boolean,
batchInterval?: number,
}) {
Expand All @@ -149,6 +171,8 @@ export class QueryManager {
this.reduxRootKey = reduxRootKey;
this.queryTransformer = queryTransformer;
this.storeFetchMiddleware = storeFetchMiddleware;
this.resultTransformer = resultTransformer;
this.resultComparator = resultComparator;
this.pollingTimers = {};
this.batchInterval = batchInterval;
this.queryListeners = {};
Expand Down Expand Up @@ -264,7 +288,7 @@ export class QueryManager {
],
});

resolve(result);
resolve(this.transformResult(<ApolloQueryResult>result));
})
.catch((err) => {
this.store.dispatch({
Expand Down Expand Up @@ -329,7 +353,7 @@ export class QueryManager {
if (observer.next) {
if (this.isDifferentResult(queryId, resultFromStore )) {
this.queryResults[queryId] = resultFromStore;
observer.next(resultFromStore);
observer.next(this.transformResult(resultFromStore));
}
}
} catch (error) {
Expand Down Expand Up @@ -776,7 +800,17 @@ export class QueryManager {
// Given a query id and a new result, this checks if the old result is
// the same as the last result for that particular query id.
private isDifferentResult(queryId: string, result: ApolloQueryResult): boolean {
return !isEqual(this.queryResults[queryId], result);
const comparator = this.resultComparator || isEqual;
return !comparator(this.queryResults[queryId], result);
}

// Give the result transformer a chance to observe or modify result data before it is passed on.
private transformResult(result: ApolloQueryResult): ApolloQueryResult {
if (!this.resultTransformer) {
return result;
} else {
return this.resultTransformer(result);
}
}

private broadcastQueries() {
Expand Down
12 changes: 12 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import {

import {
QueryManager,
ResultComparator,
ResultTransformer,
} from './QueryManager';

import {
Expand Down Expand Up @@ -172,6 +174,8 @@ export default class ApolloClient {
public reducerConfig: ApolloReducerConfig;
public queryTransformer: QueryTransformer;
public storeFetchMiddleware: StoreFetchMiddleware;
public resultTransformer: ResultTransformer;
public resultComparator: ResultComparator;
public shouldBatch: boolean;
public shouldForceFetch: boolean;
public dataId: IdGetter;
Expand All @@ -185,6 +189,8 @@ export default class ApolloClient {
dataIdFromObject,
queryTransformer,
storeFetchMiddleware,
resultTransformer,
resultComparator,
shouldBatch = false,
ssrMode = false,
ssrForceFetchDelay = 0,
Expand All @@ -197,6 +203,8 @@ export default class ApolloClient {
dataIdFromObject?: IdGetter,
queryTransformer?: QueryTransformer,
storeFetchMiddleware?: StoreFetchMiddleware,
resultTransformer?: ResultTransformer,
resultComparator?: ResultComparator,
shouldBatch?: boolean,
ssrMode?: boolean,
ssrForceFetchDelay?: number
Expand All @@ -209,6 +217,8 @@ export default class ApolloClient {
createNetworkInterface('/graphql');
this.queryTransformer = queryTransformer;
this.storeFetchMiddleware = storeFetchMiddleware;
this.resultTransformer = resultTransformer;
this.resultComparator = resultComparator;
this.shouldBatch = shouldBatch;
this.shouldForceFetch = !(ssrMode || ssrForceFetchDelay > 0);
this.dataId = dataIdFromObject;
Expand Down Expand Up @@ -315,6 +325,8 @@ export default class ApolloClient {
store,
queryTransformer: this.queryTransformer,
storeFetchMiddleware: this.storeFetchMiddleware,
resultTransformer: this.resultTransformer,
resultComparator: this.resultComparator,
shouldBatch: this.shouldBatch,
batchInterval: this.batchInterval,
});
Expand Down
154 changes: 154 additions & 0 deletions test/QueryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3170,6 +3170,160 @@ describe('QueryManager', () => {
done();
}, 120);
});

describe('result transformation', () => {

let client: ApolloClient, response, transformCount;
beforeEach(() => {
transformCount = 0;

const networkInterface: NetworkInterface = {
query(request: Request): Promise<GraphQLResult> {
return Promise.resolve(response);
},
};

client = new ApolloClient({
networkInterface,
resultTransformer(result) {
transformCount++;
return {
data: assign({}, result.data, {transformCount}),
};
},
});
});

it('transforms query() results', () => {
response = {data: {foo: 123}};
return client.query({query: gql`{ foo }`})
.then((result) => {
assert.deepEqual(result.data, {foo: 123, transformCount: 1});
});
});

it('transforms watchQuery() results', (done) => {
response = {data: {foo: 123}};
const handle = client.watchQuery({query: gql`{ foo }`});

let callCount = 0;
handle.subscribe({
error: done,
next(result) {
try {
callCount++;
if (callCount === 1) {
assert.deepEqual(result.data, {foo: 123, transformCount: 1});
response = {data: {foo: 456}};
handle.refetch();
} else {
assert.deepEqual(result.data, {foo: 456, transformCount: 2});
done();
}
} catch (error) {
done(error);
}
},
});
});

it('does not transform identical watchQuery() results', (done) => {
response = {data: {foo: 123}};
const handle = client.watchQuery({query: gql`{ foo }`});

let callCount = 0;
handle.subscribe({
error: done,
next(result) {
callCount++;
try {
assert.equal(callCount, 1, 'observer should only fire once');
assert.deepEqual(result.data, {foo: 123, transformCount: 1});
} catch (error) {
done(error);
return;
}

response = {data: {foo: 123}}; // Ensure we have new response objects.
handle.refetch().then(() => {
// Skip done() on subsequent calls, because that means we've already failed. This gives
// us better test failure output.
if (callCount === 1) {
done();
}
}).catch(done);
},
});
});

it('transforms mutate() results', () => {
response = {data: {foo: 123}};
return client.mutate({mutation: gql`mutation makeChanges { foo }`})
.then((result) => {
assert.deepEqual(result.data, {foo: 123, transformCount: 1});
});
});

});

describe('result transformation with custom equality', () => {

class Model {}

let client: ApolloClient, response;
beforeEach(() => {
const networkInterface: NetworkInterface = {
query(request: Request): Promise<GraphQLResult> {
return Promise.resolve(response);
},
};

client = new ApolloClient({
networkInterface,
resultTransformer(result) {
result.data.__proto__ = Model.prototype;
return result;
},
resultComparator(result1, result2) {
// A real example would, say, deep compare the two while ignoring prototypes.
const foo1 = result1 && result1.data && result1.data.foo;
const foo2 = result2 && result2.data && result2.data.foo;
return foo1 === foo2;
},
});
});

it('does not transform identical watchQuery() results, according to the comparator', (done) => {
response = {data: {foo: 123}};
const handle = client.watchQuery({query: gql`{ foo }`});

let callCount = 0;
handle.subscribe({
error: done,
next(result) {
callCount++;
try {
assert.equal(callCount, 1, 'observer should only fire once');
assert.instanceOf(result.data, Model);
} catch (error) {
done(error);
return;
}

response = {data: {foo: 123}}; // Ensure we have new response objects.
handle.refetch().then(() => {
// Skip done() on subsequent calls, because that means we've already failed. This gives
// us better test failure output.
if (callCount === 1) {
done();
}
}).catch(done);
},
});
});

});

});

function testDiffing(
Expand Down

0 comments on commit a501a7b

Please sign in to comment.