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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Expect active development and potentially significant breaking changes in the `0.x` track. We'll try to be diligent about releasing a `1.0` version in a timely fashion (ideally within 3 to 6 months), to signal the start of a more stable API.

### vNEXT
- Implement cache redirects with custom resolvers [PR #921](https://github.com/apollostack/apollo-client/pull/921)

### 0.5.4
- Fix a bug that caused apollo-client to catch errors thrown in Observer.next callbacks [PR #910](https://github.com/apollostack/apollo-client/pull/910)
Expand Down
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
8 changes: 8 additions & 0 deletions src/data/storeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,14 @@ export function isIdValue(idObject: StoreValue): idObject is IdValue {
return (isObject(idObject) && (idObject as (IdValue | JsonValue)).type === 'id');
}

export function toIdValue(id: string, generated = false): IdValue {
return {
type: 'id',
id,
generated,
};
}

export function isJsonValue(jsonObject: StoreValue): jsonObject is JsonValue {
return (isObject(jsonObject) && (jsonObject as (IdValue | JsonValue)).type === 'json');
}
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ import {
ApolloQueryResult,
} from './core/QueryManager';

import {
toIdValue,
} from './data/storeUtils';

// We expose the print method from GraphQL so that people that implement
// custom network interfaces can turn query ASTs into query strings as needed.
export {
Expand Down Expand Up @@ -94,6 +98,8 @@ export {

ApolloQueryResult,

toIdValue,

// internal type definitions for export
WatchQueryOptions,
MutationOptions,
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
});

});
62 changes: 62 additions & 0 deletions test/customResolvers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import mockNetworkInterface from './mocks/mockNetworkInterface';
import gql from 'graphql-tag';
import { assert } from 'chai';
import ApolloClient, { toIdValue } 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) => toIdValue(args['id']),
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, let's talk about this.

},
},
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',
});
});
});
Loading