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

resultTransformer and resultComparator #446

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Expect active development and potentially significant breaking changes in the `0
### vNEXT
- Fixed an issue with named fragments in batched queries. [PR #509](https://github.com/apollostack/apollo-client/pull/509) and [Issue #501](https://github.com/apollostack/apollo-client/issues/501).
- Fixed an issue with unused variables in queries after diffing queries against information available in the store. [PR #518](https://github.com/apollostack/apollo-client/pull/518) and [Issue #496](https://github.com/apollostack/apollo-client/issues/496).
- 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.11

Expand Down
3 changes: 2 additions & 1 deletion src/ObservableQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ export class ObservableQuery extends Observable<ApolloQueryResult> {
return this.queryManager.fetchQuery(this.queryId, assign(this.options, {
forceFetch: true,
variables,
}) as WatchQueryOptions);
}) as WatchQueryOptions)
.then(result => this.queryManager.transformResult(result));
};

this.fetchMore = (fetchMoreOptions: WatchQueryOptions & FetchMoreOptions) => {
Expand Down
40 changes: 37 additions & 3 deletions src/QueryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,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 @@ -103,6 +119,8 @@ export class QueryManager {
private networkInterface: NetworkInterface;
private reduxRootKey: string;
private queryTransformer: QueryTransformer;
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 @@ -140,13 +158,17 @@ export class QueryManager {
store,
reduxRootKey,
queryTransformer,
resultTransformer,
resultComparator,
shouldBatch = false,
batchInterval = 10,
}: {
networkInterface: NetworkInterface,
store: ApolloStore,
reduxRootKey: string,
queryTransformer?: QueryTransformer,
resultTransformer?: ResultTransformer,
resultComparator?: ResultComparator,
shouldBatch?: Boolean,
batchInterval?: number,
}) {
Expand All @@ -156,6 +178,8 @@ export class QueryManager {
this.store = store;
this.reduxRootKey = reduxRootKey;
this.queryTransformer = queryTransformer;
this.resultTransformer = resultTransformer;
this.resultComparator = resultComparator;
this.pollingTimers = {};
this.batchInterval = batchInterval;
this.queryListeners = {};
Expand Down Expand Up @@ -275,7 +299,7 @@ export class QueryManager {
});

refetchQueries.forEach((name) => { this.refetchQueryByName(name); });
resolve(result);
resolve(this.transformResult(<ApolloQueryResult>result));
})
.catch((err) => {
this.store.dispatch({
Expand Down Expand Up @@ -338,7 +362,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 @@ -577,6 +601,15 @@ export class QueryManager {
};
}

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

private collectResultBehaviorsFromUpdateQueries(
updateQueries: MutationQueryReducersMap,
mutationResult: Object,
Expand Down Expand Up @@ -908,7 +941,8 @@ 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);
}

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 @@ -167,6 +169,8 @@ export default class ApolloClient {
public queryManager: QueryManager;
public reducerConfig: ApolloReducerConfig;
public queryTransformer: QueryTransformer;
public resultTransformer: ResultTransformer;
public resultComparator: ResultComparator;
public shouldBatch: boolean;
public shouldForceFetch: boolean;
public dataId: IdGetter;
Expand All @@ -179,6 +183,8 @@ export default class ApolloClient {
initialState,
dataIdFromObject,
queryTransformer,
resultTransformer,
resultComparator,
shouldBatch = false,
ssrMode = false,
ssrForceFetchDelay = 0,
Expand All @@ -190,6 +196,8 @@ export default class ApolloClient {
initialState?: any,
dataIdFromObject?: IdGetter,
queryTransformer?: QueryTransformer,
resultTransformer?: ResultTransformer,
resultComparator?: ResultComparator,
shouldBatch?: boolean,
ssrMode?: boolean,
ssrForceFetchDelay?: number
Expand All @@ -201,6 +209,8 @@ export default class ApolloClient {
this.networkInterface = networkInterface ? networkInterface :
createNetworkInterface('/graphql');
this.queryTransformer = queryTransformer;
this.resultTransformer = resultTransformer;
this.resultComparator = resultComparator;
this.shouldBatch = shouldBatch;
this.shouldForceFetch = !(ssrMode || ssrForceFetchDelay > 0);
this.dataId = dataIdFromObject;
Expand Down Expand Up @@ -307,6 +317,8 @@ export default class ApolloClient {
reduxRootKey: this.reduxRootKey,
store,
queryTransformer: this.queryTransformer,
resultTransformer: this.resultTransformer,
resultComparator: this.resultComparator,
shouldBatch: this.shouldBatch,
batchInterval: this.batchInterval,
});
Expand Down
155 changes: 155 additions & 0 deletions test/QueryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3067,6 +3067,161 @@ describe('QueryManager', () => {
});
});
});

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}),
loading: false,
};
},
});
});

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
2 changes: 1 addition & 1 deletion typings/main/globals/node/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2298,4 +2298,4 @@ declare module "constants" {
export var W_OK: number;
export var X_OK: number;
export var UV_UDP_REUSEADDR: number;
}
}