Skip to content

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 #332
  • Loading branch information
nevir committed Jul 26, 2016
1 parent 7bcb0b8 commit a31251d
Show file tree
Hide file tree
Showing 10 changed files with 256 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Expect active development and potentially significant breaking changes in the `0
- Fix repeat calls to a query that includes fragments [PR #447](https://github.com/apollostack/apollo-client/pull/447).
- GraphQL errors on mutation results now result in a rejected promise and are no longer a part of returned results. [PR #465](https://github.com/apollostack/apollo-client/pull/465) and [Issue #458](https://github.com/apollostack/apollo-client/issues/458).
- Don't add fields to root mutations and root queries [PR #463](https://github.com/apollostack/apollo-client/pull/463) and [Issue #413](https://github.com/apollostack/apollo-client/issues/413).
- 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 @@ -309,6 +317,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 @@ -625,6 +634,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 @@ -727,6 +737,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 @@ -52,6 +57,7 @@ export interface DiffResult {
export interface StoreContext {
store: NormalizedCache;
fragmentMap: FragmentMap;
fetchMiddleware?: StoreFetchMiddleware;
}

export function diffQueryAgainstStore({
Expand Down Expand Up @@ -321,7 +327,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 ApolloError({
errorMessage: `Can't find field ${storeFieldKey} on object ${JSON.stringify(storeObj)}.
Expand All @@ -337,8 +353,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
70 changes: 70 additions & 0 deletions src/data/fetchMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import every = require('lodash.every');
import has = require('lodash.has');

import {
Field,
Variable,
} 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];
// Only supports variables, for now.
if (onlyArg.value.kind === 'Variable') {
const variable = <Variable>onlyArg.value;
if (onlyArg.name.value === 'id') {
const id = variables[variable.name.value];
if (has(store, id)) {
return toIdValue(id);
}
} else if (onlyArg.name.value === 'ids') {
const ids = variables[variable.name.value];
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 a31251d

Please sign in to comment.