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

Commit

Permalink
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 21, 2016
1 parent ed0ded8 commit ab54ca2
Show file tree
Hide file tree
Showing 10 changed files with 251 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Expect active development and potentially significant breaking changes in the `0

### vNEXT
- 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.
- 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. [PR #376](https://github.com/apollostack/apollo-client/pull/376)

### v0.4.7

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

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.flatten": "^4.2.0",
"lodash.forown": "^4.1.0",
"lodash.has": "^4.3.1",
Expand Down
11 changes: 11 additions & 0 deletions src/QueryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ import {
diffSelectionSetAgainstStore,
} from './data/diffAgainstStore';

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

import {
MutationBehavior,
MutationQueryReducersMap,
Expand Down Expand Up @@ -93,6 +97,7 @@ export class QueryManager {
private store: ApolloStore;
private reduxRootKey: string;
private queryTransformer: QueryTransformer;
private storeFetchMiddleware: StoreFetchMiddleware;
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 @@ -125,13 +130,15 @@ export class QueryManager {
store,
reduxRootKey,
queryTransformer,
storeFetchMiddleware,
shouldBatch = false,
batchInterval = 10,
}: {
networkInterface: NetworkInterface,
store: ApolloStore,
reduxRootKey: string,
queryTransformer?: QueryTransformer,
storeFetchMiddleware?: StoreFetchMiddleware,
shouldBatch?: Boolean,
batchInterval?: number,
}) {
Expand All @@ -141,6 +148,7 @@ export class QueryManager {
this.store = store;
this.reduxRootKey = reduxRootKey;
this.queryTransformer = queryTransformer;
this.storeFetchMiddleware = storeFetchMiddleware;
this.pollingTimers = {};
this.batchInterval = batchInterval;
this.queryListeners = {};
Expand Down Expand Up @@ -299,6 +307,7 @@ export class QueryManager {
context: {
store: this.getDataWithOptimisticResults(),
fragmentMap: queryStoreValue.fragmentMap,
fetchMiddleware: this.storeFetchMiddleware,
},
rootId: queryStoreValue.query.id,
selectionSet: queryStoreValue.query.selectionSet,
Expand Down Expand Up @@ -615,6 +624,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 @@ -722,6 +732,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,5 +1,6 @@
import isArray = require('lodash.isarray');
import isNull = require('lodash.isnull');
import isUndefined = require('lodash.isundefined');
import has = require('lodash.has');
import assign = require('lodash.assign');

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

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

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

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

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

if (fieldMissing) {
if (throwOnMissingField && included) {
throw new Error(`Can't find field ${storeFieldKey} on object ${JSON.stringify(storeObj)}.
Perhaps you want to use the \`returnPartialData\` option?`);
Expand All @@ -262,8 +278,6 @@ Perhaps you want to use the \`returnPartialData\` option?`);
};
}

const storeValue = storeObj[storeFieldKey];

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

import {
Field,
} from 'graphql';

import {
NormalizedCache,
StoreValue,
IdValue,
} 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: () => StoreValue
) => StoreValue;

// 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: () => StoreValue
): StoreValue {
// 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 toIdValue(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();
}

function toIdValue(id): IdValue {
return {
type: 'id',
id,
generated: false,
};
}
11 changes: 11 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ import {
addTypenameToSelectionSet,
} from './queries/queryTransform';

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

import {
MutationBehavior,
MutationBehaviorReducerMap,
Expand Down Expand Up @@ -87,6 +92,7 @@ export {
readQueryFromStore,
readFragmentFromStore,
addTypenameToSelectionSet as addTypename,
cachedFetchById,
writeQueryToStore,
writeFragmentToStore,
print as printAST,
Expand Down Expand Up @@ -165,6 +171,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 @@ -177,6 +184,7 @@ export default class ApolloClient {
initialState,
dataIdFromObject,
queryTransformer,
storeFetchMiddleware,
shouldBatch = false,
ssrMode = false,
ssrForceFetchDelay = 0,
Expand All @@ -188,6 +196,7 @@ export default class ApolloClient {
initialState?: any,
dataIdFromObject?: IdGetter,
queryTransformer?: QueryTransformer,
storeFetchMiddleware?: StoreFetchMiddleware,
shouldBatch?: boolean,
ssrMode?: boolean,
ssrForceFetchDelay?: number
Expand All @@ -199,6 +208,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 @@ -304,6 +314,7 @@ export default class ApolloClient {
reduxRootKey: this.reduxRootKey,
store,
queryTransformer: this.queryTransformer,
storeFetchMiddleware: this.storeFetchMiddleware,
shouldBatch: this.shouldBatch,
batchInterval: this.batchInterval,
});
Expand Down
Loading

0 comments on commit ab54ca2

Please sign in to comment.