Files
talk/client/coral-framework/hocs/withQuery.js
T
2017-08-31 02:05:00 +07:00

209 lines
6.3 KiB
JavaScript

import * as React from 'react';
import {graphql} from 'react-apollo';
import {getDefinitionName, separateDataAndRoot, getResponseErrors} from '../utils';
import PropTypes from 'prop-types';
import hoistStatics from 'recompose/hoistStatics';
const withSkipOnErrors = (reducer) => (prev, action, ...rest) => {
if (action.type === 'APOLLO_MUTATION_RESULT' && getResponseErrors(action.result)) {
return prev;
}
return reducer(prev, action, ...rest);
};
function networkStatusToString(networkStatus) {
switch(networkStatus) {
case 1:
return 'loading';
case 2:
return 'setVariables';
case 3:
return 'fetchMore';
case 4:
return 'refetch';
case 6:
return 'poll';
case 7:
return 'ready';
case 8:
return 'error';
}
throw new Error(`Unknown network status ${networkStatus}`);
}
/**
* Exports a HOC with the same signature as `graphql`, that will
* apply query options registered in the graphRegistry.
*/
export default (document, config = {}) => hoistStatics((WrappedComponent) => {
return class WithQuery extends React.Component {
static contextTypes = {
eventEmitter: PropTypes.object,
graphqlRegistry: PropTypes.object,
};
// Lazily resolve fragments from graphRegistry to support circular dependencies.
memoized = null;
lastNetworkStatus = null;
data = null;
name = '';
get graphqlRegistry() {
return this.context.graphqlRegistry;
}
resolveDocument(documentOrCallback) {
const document = typeof documentOrCallback === 'function'
? documentOrCallback(this.props, this.context)
: documentOrCallback;
return this.graphqlRegistry.resolveFragments(document);
}
emitWhenNeeded(data) {
const {variables, networkStatus} = data;
if (this.lastNetworkStatus === networkStatus) {
return;
}
this.lastNetworkStatus = networkStatus;
const status = networkStatusToString(networkStatus);
const {root} = separateDataAndRoot(data);
this.context.eventEmitter.emit(`query.${this.name}.${status}`, {variables, data: root});
}
nextData(data) {
this.emitWhenNeeded(data);
// If data was previously set, we update it in a immutable way.
if (this.data) {
if (this.data.networkStatus !== data.networkStatus ||
this.data.loading !== data.loading ||
this.data.error !== data.error ||
this.data.variables !== data.variables) {
this.data = {
...this.data,
error: data.error,
networkStatus: data.networkStatus,
loading: data.loading,
variables: data.variables,
};
}
}
else {
// Set data for the first time.
this.data = {
error: data.error,
variables: data.variables,
networkStatus: data.networkStatus,
loading: data.loading,
startPolling: data.startPolling,
stopPolling: data.stopPolling,
refetch: data.refetch,
updateQuery: data.updateQuery,
subscribeToMore: (stmArgs) => {
const resolvedDocument = this.resolveDocument(stmArgs.document);
// Resolve document fragments before passing it to `apollo-client`.
return data.subscribeToMore({
...stmArgs,
document: resolvedDocument,
onError: (err) => {
if (stmArgs.onErr) {
return stmArgs.onErr(err);
}
throw err;
},
});
},
fetchMore: (lmArgs) => {
const resolvedDocument = this.resolveDocument(lmArgs.query);
const fetchName = getDefinitionName(resolvedDocument);
this.context.eventEmitter.emit(
`query.${this.name}.fetchMore.${fetchName}.begin`,
{variables: lmArgs.variables});
// Resolve document fragments before passing it to `apollo-client`.
return data.fetchMore({
...lmArgs,
query: resolvedDocument,
})
.then((res) => {
this.context.eventEmitter.emit(
`query.${this.name}.fetchMore.${fetchName}.success`,
{variables: lmArgs.variables, data: res.data});
return Promise.resolve(res);
})
.catch((err) => {
this.context.eventEmitter.emit(
`query.${this.name}.fetchMore.${fetchName}.error`,
{variables: lmArgs.variables, error: err});
throw err;
});
},
};
}
return this.data;
}
wrappedConfig = {
...config,
options: config.options || {},
props: (args) => {
const nextData = this.nextData(args.data);
const {root} = separateDataAndRoot(args.data);
if (config.props) {
// Custom props, in this case we just pass the wrapped args to it.
return config.props({...args, data: {...args.data, ...nextData}});
}
// Return our wrapped data with a separated root.
return {...args, data: nextData, root};
},
};
wrappedOptions = (data) => {
const base = (typeof this.wrappedConfig.options === 'function')
? this.wrappedConfig.options(data)
: this.wrappedConfig.options;
const configs = this.graphqlRegistry.getQueryOptions(this.name);
const reducerCallbacks =
[base.reducer || ((i) => i)]
.concat(...configs.map((cfg) => cfg.reducer))
.filter((i) => i);
const reducer = withSkipOnErrors(
reducerCallbacks.reduce(
(a, b) => (prev, ...rest) => {
const next = a(prev, ...rest);
return b(next, ...rest) || next;
}
));
return {
...base,
reducer,
};
};
getWrapped = () => {
if (!this.memoized) {
const resolvedDocument = this.resolveDocument(document);
this.name = getDefinitionName(resolvedDocument);
this.memoized = graphql(
resolvedDocument,
{...this.wrappedConfig, options: this.wrappedOptions},
)(WrappedComponent);
}
return this.memoized;
};
render() {
const Wrapped = this.getWrapped();
return <Wrapped {...this.props} />;
}
};
});