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

Commit

Permalink
Introducing StoreFetchMiddleware
Browse files Browse the repository at this point in the history
By allowing users to rewrite data lookups from the store, they can satisfy behavior like apollographql#332
  • Loading branch information
nevir committed Jul 11, 2016
1 parent cd3b248 commit 33d6f9d
Show file tree
Hide file tree
Showing 10 changed files with 240 additions and 3 deletions.
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

- Don't throw on unknown directives, instead just pass them through. This can open the door to implementing `@live`, `@defer`, and `@stream`, if coupled with some changes in the network layer. [PR #372](https://github.com/apollostack/apollo-client/pull/372)
- Added a `storeFetchMiddleware` option to `ApolloClient` that allows transformation of values returned from the store. Also exposes a `cachedFetchById` middleware to handle the common case of fetching cached resources by id.

### v0.3.29

Expand Down
5 changes: 5 additions & 0 deletions ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ declare module 'lodash.identity' {
export = main.identity;
}

declare module 'lodash.every' {
import main = require('~lodash/index');
export = main.every;
}

/*
GRAPHQL
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"lodash.assign": "^4.0.8",
"lodash.clonedeep": "^4.3.2",
"lodash.countby": "^4.4.0",
"lodash.every": "^4.4.0",
"lodash.forown": "^4.1.0",
"lodash.has": "^4.3.1",
"lodash.identity": "^3.0.0",
Expand Down
12 changes: 12 additions & 0 deletions src/QueryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ import {
diffSelectionSetAgainstStore,
} from './data/diffAgainstStore';

import {
StoreFetchMiddleware,
} from './data/fetchMiddleware';

import {
MutationBehavior,
} from './data/mutationResults';
Expand Down Expand Up @@ -113,6 +117,7 @@ export class QueryManager {
private reduxRootKey: string;
private pollingTimers: {[queryId: string]: NodeJS.Timer | any}; //oddity in Typescript
private queryTransformer: QueryTransformer;
private storeFetchMiddleware: StoreFetchMiddleware;
private queryListeners: { [queryId: string]: QueryListener };

private idCounter = 0;
Expand Down Expand Up @@ -144,12 +149,14 @@ export class QueryManager {
store,
reduxRootKey,
queryTransformer,
storeFetchMiddleware,
shouldBatch = false,
}: {
networkInterface: NetworkInterface,
store: ApolloStore,
reduxRootKey: string,
queryTransformer?: QueryTransformer,
storeFetchMiddleware?: StoreFetchMiddleware,
shouldBatch?: Boolean,
}) {
// XXX this might be the place to do introspection for inserting the `id` into the query? or
Expand All @@ -158,6 +165,7 @@ export class QueryManager {
this.store = store;
this.reduxRootKey = reduxRootKey;
this.queryTransformer = queryTransformer;
this.storeFetchMiddleware = storeFetchMiddleware;
this.pollingTimers = {};

this.queryListeners = {};
Expand Down Expand Up @@ -283,6 +291,7 @@ export class QueryManager {
context: {
store: this.getApolloState().data,
fragmentMap: queryStoreValue.fragmentMap,
fetchMiddleware: this.storeFetchMiddleware,
},
rootId: queryStoreValue.query.id,
selectionSet: queryStoreValue.query.selectionSet,
Expand Down Expand Up @@ -377,6 +386,7 @@ export class QueryManager {
context: {
store: this.getApolloState().data,
fragmentMap: queryStoreValue.fragmentMap,
fetchMiddleware: this.storeFetchMiddleware,
},
rootId: queryStoreValue.query.id,
selectionSet: queryStoreValue.query.selectionSet,
Expand Down Expand Up @@ -564,6 +574,7 @@ export class QueryManager {
context: {
store: this.store.getState()[this.reduxRootKey].data,
fragmentMap: queryFragmentMap,
fetchMiddleware: this.storeFetchMiddleware,
},
selectionSet: querySS.selectionSet,
throwOnMissingField: false,
Expand Down Expand Up @@ -666,6 +677,7 @@ export class QueryManager {
context: {
store: this.getApolloState().data,
fragmentMap: queryFragmentMap,
fetchMiddleware: this.storeFetchMiddleware,
},
rootId: querySS.id,
selectionSet: querySS.selectionSet,
Expand Down
20 changes: 17 additions & 3 deletions src/data/diffAgainstStore.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import isArray = require('lodash.isarray');
import isNull = require('lodash.isnull');
import isString = require('lodash.isstring');
import isUndefined = require('lodash.isundefined');
import has = require('lodash.has');
import assign = require('lodash.assign');

Expand All @@ -15,6 +16,10 @@ import {
NormalizedCache,
} from './store';

import {
StoreFetchMiddleware,
} from './fetchMiddleware';

import {
SelectionSetWithRoot,
} from '../queries/store';
Expand Down Expand Up @@ -47,6 +52,7 @@ export interface DiffResult {
export interface StoreContext {
store: NormalizedCache;
fragmentMap: FragmentMap;
fetchMiddleware?: StoreFetchMiddleware;
}

export function diffQueryAgainstStore({
Expand Down Expand Up @@ -250,7 +256,17 @@ function diffFieldAgainstStore({
const storeObj = context.store[rootId] || {};
const storeFieldKey = storeKeyNameFromField(field, variables);

if (! has(storeObj, storeFieldKey)) {
let storeValue;
// Give the transformer a chance to yield a rewritten result.
if (context.fetchMiddleware) {
storeValue = context.fetchMiddleware(field, variables, context.store, () => storeObj[storeFieldKey]);
} else {
storeValue = storeObj[storeFieldKey];
}

// This may seem crazy, but we care about the difference between the cases
// where a value is undefined vs when it is not present in the store.
if (isUndefined(storeValue) && !has(storeObj, storeFieldKey)) {
if (throwOnMissingField && included) {
throw new Error(`Can't find field ${storeFieldKey} on object ${storeObj}.`);
}
Expand All @@ -260,8 +276,6 @@ function diffFieldAgainstStore({
};
}

const storeValue = storeObj[storeFieldKey];

// Handle all scalar types here
if (! field.selectionSet) {
return {
Expand Down
55 changes: 55 additions & 0 deletions src/data/fetchMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import every = require('lodash.every');
import has = require('lodash.has');

import {
Field,
} from 'graphql';

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

// Middleware that is given an opportunity to rewrite results from the store.
// It should call `next()` to look up the default value.
export type StoreFetchMiddleware = (
field: Field,
variables: {},
store: NormalizedCache,
next: () => any
) => any;

// StoreFetchMiddleware that special cases all parameterized queries containing
// either `id` or `ids` to retrieve nodes by those ids directly from the store.
//
// This allows the client to avoid an extra round trip when it is fetching a
// node by id that was previously fetched by a different query.
//
// NOTE: This middleware assumes that you are mapping data ids to the id of
// your nodes. E.g. `dataIdFromObject: value => value.id`.
export function cachedFetchById(
field: Field,
variables: {},
store: NormalizedCache,
next: () => any
): any {
// Note that we are careful to _not_ return an id if it doesn't exist in the
// store! apollo-client assumes that if an id exists in the store, the node
// referenced must also exist.
if (field.arguments && field.arguments.length === 1) {
const onlyArg = field.arguments[0];
if (onlyArg.name.value === 'id') {
const id = variables['id'];
if (has(store, id)) {
return id;
}
} else if (onlyArg.name.value === 'ids') {
const ids = variables['ids'];
if (every(ids, id => has(store, id))) {
return ids;
}
}
}

// Otherwise, fall back to the regular behavior.
return next();
}
11 changes: 11 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ import {
addTypenameToSelectionSet,
} from './queries/queryTransform';

import {
cachedFetchById,
StoreFetchMiddleware,
} from './data/fetchMiddleware';

import {
MutationBehavior,
MutationBehaviorReducerMap,
Expand All @@ -72,6 +77,7 @@ export {
readQueryFromStore,
readFragmentFromStore,
addTypenameToSelectionSet as addTypename,
cachedFetchById,
writeQueryToStore,
writeFragmentToStore,
print as printAST,
Expand Down Expand Up @@ -141,6 +147,7 @@ export default class ApolloClient {
public queryManager: QueryManager;
public reducerConfig: ApolloReducerConfig;
public queryTransformer: QueryTransformer;
public storeFetchMiddleware: StoreFetchMiddleware;
public shouldBatch: boolean;
public shouldForceFetch: boolean;
public dataId: IdGetter;
Expand All @@ -152,6 +159,7 @@ export default class ApolloClient {
initialState,
dataIdFromObject,
queryTransformer,
storeFetchMiddleware,
shouldBatch = false,
ssrMode = false,
ssrForceFetchDelay = 0,
Expand All @@ -162,6 +170,7 @@ export default class ApolloClient {
initialState?: any,
dataIdFromObject?: IdGetter,
queryTransformer?: QueryTransformer,
storeFetchMiddleware?: StoreFetchMiddleware,
shouldBatch?: boolean,
ssrMode?: boolean,
ssrForceFetchDelay?: number
Expand All @@ -172,6 +181,7 @@ export default class ApolloClient {
this.networkInterface = networkInterface ? networkInterface :
createNetworkInterface('/graphql');
this.queryTransformer = queryTransformer;
this.storeFetchMiddleware = storeFetchMiddleware;
this.shouldBatch = shouldBatch;
this.shouldForceFetch = !(ssrMode || ssrForceFetchDelay > 0);
this.dataId = dataIdFromObject;
Expand Down Expand Up @@ -274,6 +284,7 @@ export default class ApolloClient {
reduxRootKey: this.reduxRootKey,
store,
queryTransformer: this.queryTransformer,
storeFetchMiddleware: this.storeFetchMiddleware,
shouldBatch: this.shouldBatch,
});
};
Expand Down
Loading

0 comments on commit 33d6f9d

Please sign in to comment.