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

Cache redirects #921

Merged
merged 13 commits into from
Nov 16, 2016
7 changes: 7 additions & 0 deletions src/ApolloClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ import {
Store,
} from './store';

import {
CustomResolverMap,
} from './data/readFromStore';

import {
QueryManager,
ApolloQueryResult,
Expand Down Expand Up @@ -145,6 +149,7 @@ export default class ApolloClient {
mutationBehaviorReducers = {} as MutationBehaviorReducerMap,
addTypename = true,
queryTransformer,
customResolvers,
}: {
networkInterface?: NetworkInterface,
reduxRootKey?: string,
Expand All @@ -158,6 +163,7 @@ export default class ApolloClient {
mutationBehaviorReducers?: MutationBehaviorReducerMap,
addTypename?: boolean,
queryTransformer?: any,
customResolvers?: CustomResolverMap,
} = {}) {
if (reduxRootKey && reduxRootSelector) {
throw new Error('Both "reduxRootKey" and "reduxRootSelector" are configured, but only one of two is allowed.');
Expand Down Expand Up @@ -206,6 +212,7 @@ export default class ApolloClient {
this.reducerConfig = {
dataIdFromObject,
mutationBehaviorReducers,
customResolvers,
};

this.watchQuery = this.watchQuery.bind(this);
Expand Down
5 changes: 5 additions & 0 deletions src/core/QueryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ export class QueryManager {
private resultTransformer: ResultTransformer;
private resultComparator: ResultComparator;
private reducerConfig: ApolloReducerConfig;

// TODO REFACTOR collect all operation-related info in one place (e.g. all these maps)
// this should be combined with ObservableQuery, but that needs to be expanded to support
// mutations and subscriptions as well.
Expand Down Expand Up @@ -381,6 +382,7 @@ export class QueryManager {
query: this.queryDocuments[queryId],
variables: queryStoreValue.previousVariables || queryStoreValue.variables,
returnPartialData: options.returnPartialData || options.noFetch,
config: this.reducerConfig,
}),
loading: queryStoreValue.loading,
networkStatus: queryStoreValue.networkStatus,
Expand Down Expand Up @@ -479,6 +481,7 @@ export class QueryManager {
store: this.reduxRootSelector(this.store.getState()).data,
returnPartialData: true,
variables,
config: this.reducerConfig,
});

// If we're in here, only fetch if we have missing fields
Expand Down Expand Up @@ -733,6 +736,7 @@ export class QueryManager {
query: document,
variables,
returnPartialData: false,
config: this.reducerConfig,
};

try {
Expand Down Expand Up @@ -951,6 +955,7 @@ export class QueryManager {
variables,
returnPartialData: returnPartialData || noFetch,
query: document,
config: this.reducerConfig,
});
// ensure multiple errors don't get thrown
/* tslint:disable */
Expand Down
35 changes: 34 additions & 1 deletion src/data/readFromStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ import {
getQueryDefinition,
} from '../queries/getFromAST';

import {
ApolloReducerConfig,
} from '../store';

export type DiffResult = {
result?: any;
isMissing?: boolean;
Expand All @@ -32,8 +36,17 @@ export type ReadQueryOptions = {
query: Document,
variables?: Object,
returnPartialData?: boolean,
config?: ApolloReducerConfig,
}

export type CustomResolver = (rootValue: any, args: { [argName: string]: any }) => any;

export type CustomResolverMap = {
[typeName: string]: {
[fieldName: string]: CustomResolver
}
};

/**
* Resolves the result of a query solely from the store (i.e. never hits the server).
*
Expand All @@ -54,12 +67,14 @@ export function readQueryFromStore({
query,
variables,
returnPartialData = false,
config,
}: ReadQueryOptions): Object {
const { result } = diffQueryAgainstStore({
query,
store,
returnPartialData,
variables,
config,
});

return result;
Expand All @@ -69,6 +84,7 @@ type ReadStoreContext = {
store: NormalizedCache;
returnPartialData: boolean;
hasMissingField: boolean;
customResolvers: CustomResolverMap;
}

let haveWarned = false;
Expand Down Expand Up @@ -130,6 +146,20 @@ const readStoreResolver: Resolver = (
const fieldValue = (obj || {})[storeKeyName];

if (typeof fieldValue === 'undefined') {
if (context.customResolvers && obj && (obj.__typename || objId === 'ROOT_QUERY')) {
const typename = obj.__typename || 'Query';

// Look for the type in the custom resolver map
const type = context.customResolvers[typename];
if (type) {
// Look for the field in the custom resolver map
const resolver = type[fieldName];
if (resolver) {
return resolver(obj, args);
}
}
}

if (! context.returnPartialData) {
throw new Error(`Can't find field ${storeKeyName} on object (${objId}) ${JSON.stringify(obj, null, 2)}.
Perhaps you want to use the \`returnPartialData\` option?`);
Expand Down Expand Up @@ -161,15 +191,18 @@ export function diffQueryAgainstStore({
query,
variables,
returnPartialData = true,
config,
}: ReadQueryOptions): DiffResult {
// Throw the right validation error by trying to find a query in the document
getQueryDefinition(query);

const context: ReadStoreContext = {
// Global settings
store,
returnPartialData,
customResolvers: config && config.customResolvers,

// Filled in during execution
// Flag set during execution
hasMissingField: false,
};

Expand Down
3 changes: 3 additions & 0 deletions src/data/resultReducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,12 @@ export function createStoreReducer(
query: document,
variables,
returnPartialData: true,
config,
});
// TODO add info about networkStatus

const nextResult = resultReducer(currentResult, action); // action should include operation name

if (currentResult !== nextResult) {
return writeResultToStore({
dataId: 'ROOT_QUERY',
Expand Down
7 changes: 6 additions & 1 deletion src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ import {
MutationBehaviorReducerMap,
} from './data/mutationResults';

import {
CustomResolverMap,
} from './data/readFromStore';

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

export interface Store {
Expand Down Expand Up @@ -151,9 +155,10 @@ export function createApolloStore({
}


export interface ApolloReducerConfig {
export type ApolloReducerConfig = {
dataIdFromObject?: IdGetter;
mutationBehaviorReducers?: MutationBehaviorReducerMap;
customResolvers?: CustomResolverMap;
}

export function getDataWithOptimisticResults(store: Store): NormalizedCache {
Expand Down
1 change: 0 additions & 1 deletion test/QueryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3166,5 +3166,4 @@ describe('QueryManager', () => {

// We have an unhandled error warning from the `subscribe` above, which has no `error` cb
});

});
68 changes: 68 additions & 0 deletions test/customResolvers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import mockNetworkInterface from './mocks/mockNetworkInterface';
import gql from 'graphql-tag';
import { assert } from 'chai';
import ApolloClient from '../src';

import { NetworkStatus } from '../src/queries/store';

describe('custom resolvers', () => {
it(`works for cache redirection`, () => {
const dataIdFromObject = (obj: any) => {
return obj.id;
};

const listQuery = gql`{ people { id name } }`;

const listData = {
people: [
{
id: '4',
name: 'Luke Skywalker',
__typename: 'Person',
},
],
};

const netListQuery = gql`{ people { id name __typename } }`;

const itemQuery = gql`{ person(id: 4) { id name } }`;

// We don't expect the item query to go to the server at all
const networkInterface = mockNetworkInterface({
request: { query: netListQuery },
result: { data: listData },
});

const client = new ApolloClient({
networkInterface,
customResolvers: {
Query: {
person: (_, args) => {
return {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, this is probably not the optimal API right? Perhaps we should change this so that people can just return the ID?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha, yes, good catch! I patched the tests to get things working and then forgot to build that in.

type: 'id',
id: args['id'],
generated: false,
};
},
},
},
dataIdFromObject,
});

return client.query({ query: listQuery }).then(() => {
return client.query({ query: itemQuery });
}).then((itemResult) => {
assert.deepEqual(itemResult, {
loading: false,
networkStatus: NetworkStatus.ready,
data: {
person: {
__typename: 'Person',
id: '4',
name: 'Luke Skywalker',
},
},
});
});
});
});
77 changes: 77 additions & 0 deletions test/readFromStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -569,4 +569,81 @@ describe('reading from the store', () => {
simpleArray: [null, 'two', 'three'],
});
});

it('runs a query with custom resolvers for a computed field', () => {
const result = {
__typename: 'Thing',
id: 'abcd',
stringField: 'This is a string!',
numberField: 5,
nullField: null,
} as StoreObject;

const store = {
'ROOT_QUERY': result,
} as NormalizedCache;

const queryResult = readQueryFromStore({
store,
query: gql`
query {
stringField
numberField
computedField(extra: "bit") @client
}
`,
config: {
customResolvers: {
Thing: {
computedField: (obj, args) => obj.stringField + obj.numberField + args['extra'],
},
},
},
});

// The result of the query shouldn't contain __data_id fields
assert.deepEqual(queryResult, {
stringField: result['stringField'],
numberField: result['numberField'],
computedField: 'This is a string!5bit',
});
});

it('runs a query with custom resolvers for a computed field on root Query', () => {
const result = {
id: 'abcd',
stringField: 'This is a string!',
numberField: 5,
nullField: null,
} as StoreObject;

const store = {
'ROOT_QUERY': result,
} as NormalizedCache;

const queryResult = readQueryFromStore({
store,
query: gql`
query {
stringField
numberField
computedField(extra: "bit") @client
}
`,
config: {
customResolvers: {
Query: {
computedField: (obj, args) => obj.stringField + obj.numberField + args['extra'],
},
},
},
});

// The result of the query shouldn't contain __data_id fields
assert.deepEqual(queryResult, {
stringField: result['stringField'],
numberField: result['numberField'],
computedField: 'This is a string!5bit',
});
});
});
1 change: 1 addition & 0 deletions test/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,4 @@ import './graphqlSubscriptions';
import './batchedNetworkInterface';
import './ObservableQuery';
import './subscribeToMore';
import './customResolvers';