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 Aug 16, 2016
1 parent 87c2618 commit a7aa785
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 @@ -33,6 +33,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 @@ -60,6 +60,10 @@ import {
removeUnusedVariablesFromQuery,
} from './data/diffAgainstStore';

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

import {
MutationBehavior,
MutationQueryReducersMap,
Expand Down Expand Up @@ -103,6 +107,7 @@ export class QueryManager {
private networkInterface: NetworkInterface;
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 @@ -140,13 +145,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 @@ -156,6 +163,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 @@ -328,6 +336,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 @@ -681,6 +690,7 @@ export class QueryManager {
context: {
store: this.store.getState()[this.reduxRootKey].data,
fragmentMap,
fetchMiddleware: this.storeFetchMiddleware,
},
selectionSet: queryDef.selectionSet,
throwOnMissingField: false,
Expand Down Expand Up @@ -770,6 +780,7 @@ export class QueryManager {
context: {
store: this.getApolloState().data,
fragmentMap,
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 @@ -57,6 +62,7 @@ export interface DiffResult {
export interface StoreContext {
store: NormalizedCache;
fragmentMap: FragmentMap;
fetchMiddleware?: StoreFetchMiddleware;
}

export function diffQueryAgainstStore({
Expand Down Expand Up @@ -326,7 +332,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 (${rootId}) ${JSON.stringify(storeObj, null, 2)}.
Expand All @@ -342,8 +358,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 @@ -167,6 +173,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 @@ -179,6 +186,7 @@ export default class ApolloClient {
initialState,
dataIdFromObject,
queryTransformer,
storeFetchMiddleware,
shouldBatch = false,
ssrMode = false,
ssrForceFetchDelay = 0,
Expand All @@ -190,6 +198,7 @@ export default class ApolloClient {
initialState?: any,
dataIdFromObject?: IdGetter,
queryTransformer?: QueryTransformer,
storeFetchMiddleware?: StoreFetchMiddleware,
shouldBatch?: boolean,
ssrMode?: boolean,
ssrForceFetchDelay?: number
Expand All @@ -201,6 +210,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 @@ -307,6 +317,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 a7aa785

Please sign in to comment.