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

StoreFetchMiddleware #376

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
- Fixed an issue with named fragments in batched queries. [PR #509](https://github.com/apollostack/apollo-client/pull/509) and [Issue #501](https://github.com/apollostack/apollo-client/issues/501).
- Fixed an issue with unused variables in queries after diffing queries against information available in the store. [PR #518](https://github.com/apollostack/apollo-client/pull/518) and [Issue #496](https://github.com/apollostack/apollo-client/issues/496).
- 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.11

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
41 changes: 30 additions & 11 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 @@ -325,12 +333,15 @@ export class QueryManager {
try {
const resultFromStore = {
data: readSelectionSetFromStore({
store: this.getDataWithOptimisticResults(),
context: {
store: this.getDataWithOptimisticResults(),
fragmentMap: queryStoreValue.fragmentMap,
fetchMiddleware: this.storeFetchMiddleware,
},
rootId: queryStoreValue.query.id,
selectionSet: queryStoreValue.query.selectionSet,
variables: queryStoreValue.variables,
returnPartialData: options.returnPartialData || options.noFetch,
fragmentMap: queryStoreValue.fragmentMap,
}),
loading: queryStoreValue.loading,
};
Expand Down Expand Up @@ -558,15 +569,17 @@ export class QueryManager {
}

const previousResult = readSelectionSetFromStore({
// In case of an optimistic change, apply reducer on top of the
// results including previous optimistic updates. Otherwise, apply it
// on top of the real data only.
store: isOptimistic ? this.getDataWithOptimisticResults() : this.getApolloState().data,
context: {
// In case of an optimistic change, apply reducer on top of the
// results including previous optimistic updates. Otherwise, apply it
// on top of the real data only.
store: isOptimistic ? this.getDataWithOptimisticResults() : this.getApolloState().data,
fragmentMap: createFragmentMap(fragments || []),
},
rootId: 'ROOT_QUERY',
selectionSet: queryDefinition.selectionSet,
variables: queryOptions.variables,
returnPartialData: queryOptions.returnPartialData || queryOptions.noFetch,
fragmentMap: createFragmentMap(fragments || []),
});

return {
Expand Down Expand Up @@ -674,12 +687,15 @@ export class QueryManager {
initialResult: Object,
} {
const { missingSelectionSets, result } = diffSelectionSetAgainstStore({
context: {
store: this.store.getState()[this.reduxRootKey].data,
fragmentMap,
fetchMiddleware: this.storeFetchMiddleware,
},
selectionSet: queryDef.selectionSet,
store: this.store.getState()[this.reduxRootKey].data,
throwOnMissingField: false,
rootId,
variables,
fragmentMap,
});

const initialResult = result;
Expand Down Expand Up @@ -761,12 +777,15 @@ export class QueryManager {
// this will throw an error if there are missing fields in
// the results if returnPartialData is false.
resultFromStore = readSelectionSetFromStore({
store: this.getApolloState().data,
context: {
store: this.getApolloState().data,
fragmentMap,
fetchMiddleware: this.storeFetchMiddleware,
},
rootId: querySS.id,
selectionSet: querySS.selectionSet,
variables,
returnPartialData: returnPartialData || noFetch,
fragmentMap,
});
// ensure multiple errors don't get thrown
/* tslint:disable */
Expand Down
66 changes: 37 additions & 29 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,14 @@ export interface DiffResult {
missingSelectionSets?: SelectionSetWithRoot[];
}

// Contexual state and configuration that is used throught a request from the
// store.
export interface StoreContext {
store: NormalizedCache;
fragmentMap: FragmentMap;
fetchMiddleware?: StoreFetchMiddleware;
}

export function diffQueryAgainstStore({
store,
query,
Expand All @@ -64,7 +77,7 @@ export function diffQueryAgainstStore({
const queryDef = getQueryDefinition(query);

return diffSelectionSetAgainstStore({
store,
context: { store, fragmentMap: {} },
rootId: 'ROOT_QUERY',
selectionSet: queryDef.selectionSet,
throwOnMissingField: false,
Expand All @@ -86,7 +99,7 @@ export function diffFragmentAgainstStore({
const fragmentDef = getFragmentDefinition(fragment);

return diffSelectionSetAgainstStore({
store,
context: { store, fragmentMap: {} },
rootId,
selectionSet: fragmentDef.selectionSet,
throwOnMissingField: false,
Expand Down Expand Up @@ -126,28 +139,22 @@ export function handleFragmentErrors(fragmentErrors: { [typename: string]: Error
* @return {result: Object, missingSelectionSets: [SelectionSet]}
*/
export function diffSelectionSetAgainstStore({
context,
selectionSet,
store,
rootId,
throwOnMissingField = false,
variables,
fragmentMap,
}: {
context: StoreContext,
selectionSet: SelectionSet,
store: NormalizedCache,
rootId: string,
throwOnMissingField: boolean,
variables: Object,
fragmentMap?: FragmentMap,
}): DiffResult {
if (selectionSet.kind !== 'SelectionSet') {
throw new Error('Must be a selection set.');
}

if (!fragmentMap) {
fragmentMap = {};
}

const result = {};
const missingFields: Selection[] = [];

Expand Down Expand Up @@ -177,12 +184,11 @@ export function diffSelectionSetAgainstStore({

if (isField(selection)) {
const diffResult = diffFieldAgainstStore({
context,
field: selection,
throwOnMissingField,
variables,
rootId,
store,
fragmentMap,
included,
});
fieldIsMissing = diffResult.isMissing;
Expand All @@ -204,12 +210,11 @@ export function diffSelectionSetAgainstStore({
if (included) {
try {
const diffResult = diffSelectionSetAgainstStore({
context,
selectionSet: selection.selectionSet,
throwOnMissingField,
variables,
rootId,
store,
fragmentMap,
});
fieldIsMissing = diffResult.isMissing;
fieldResult = diffResult.result;
Expand All @@ -232,7 +237,7 @@ export function diffSelectionSetAgainstStore({
}
}
} else {
const fragment = fragmentMap[selection.name.value];
const fragment = context.fragmentMap[selection.name.value];

if (!fragment) {
throw new Error(`No fragment named ${selection.name.value}`);
Expand All @@ -243,12 +248,11 @@ export function diffSelectionSetAgainstStore({
if (included) {
try {
const diffResult = diffSelectionSetAgainstStore({
context,
selectionSet: fragment.selectionSet,
throwOnMissingField,
variables,
rootId,
store,
fragmentMap,
});
fieldIsMissing = diffResult.isMissing;
fieldResult = diffResult.result;
Expand Down Expand Up @@ -311,26 +315,34 @@ export function diffSelectionSetAgainstStore({
}

function diffFieldAgainstStore({
context,
field,
throwOnMissingField,
variables,
rootId,
store,
fragmentMap,
included = true,
}: {
context: StoreContext,
field: Field,
throwOnMissingField: boolean,
variables: Object,
rootId: string,
store: NormalizedCache,
fragmentMap?: FragmentMap,
included?: Boolean,
}): FieldDiffResult {
const storeObj = store[rootId] || {};
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 @@ -346,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 Expand Up @@ -382,12 +392,11 @@ Perhaps you want to use the \`returnPartialData\` option?`,
}

const itemDiffResult = diffSelectionSetAgainstStore({
store,
context,
throwOnMissingField,
rootId: id,
selectionSet: field.selectionSet,
variables,
fragmentMap,
});

if (itemDiffResult.isMissing) {
Expand All @@ -409,12 +418,11 @@ Perhaps you want to use the \`returnPartialData\` option?`,
if (isIdValue(storeValue)) {
const unescapedId = storeValue.id;
return diffSelectionSetAgainstStore({
store,
context,
throwOnMissingField,
rootId: unescapedId,
selectionSet: field.selectionSet,
variables,
fragmentMap,
});
}

Expand Down
Loading