import * as React from 'react'; import {graphql} from 'react-apollo'; import merge from 'lodash/merge'; import uniq from 'lodash/uniq'; import flatten from 'lodash/flatten'; import isEmpty from 'lodash/isEmpty'; import {getDefinitionName, getResponseErrors} from '../utils'; import PropTypes from 'prop-types'; import t from 'coral-framework/services/i18n'; import hoistStatics from 'recompose/hoistStatics'; import union from 'lodash/union'; class ResponseErrors extends Error { constructor(errors) { super(`Response Errors ${JSON.stringify(errors)}`); this.errors = errors.map((e) => new ResponseError(e)); } } class ResponseError { constructor(error) { Object.assign(this, error); } translate(...args) { return t(`error.${this.translation_key}`, ...args); } } /** * Exports a HOC with the same signature as `graphql`, that will * apply mutation options registered in the graphRegistry. */ export default (document, config = {}) => hoistStatics((WrappedComponent) => { config = { ...config, options: config.options || {}, props: config.props || ((data) => ({mutate: data.mutate()})), }; return class WithMutation extends React.Component { static contextTypes = { eventEmitter: PropTypes.object, store: PropTypes.object, graphqlRegistry: PropTypes.object, }; get graphqlRegistry() { return this.context.graphqlRegistry; } resolveDocument(documentOrCallback) { const document = typeof documentOrCallback === 'function' ? documentOrCallback(this.props, this.context) : documentOrCallback; return this.graphqlRegistry.resolveFragments(document); } // Lazily resolve fragments from graphRegistry to support circular dependencies. memoized = null; // Props as we would pass to the BaseComponent without optimizations. dynamicProps = {}; // Props that are optimized by keeping the identity of function callbacks. staticProps = {}; propsWrapper = (data) => { const name = getDefinitionName(document); const callbacks = this.graphqlRegistry.getMutationOptions(name); const mutate = (base) => { const variables = base.variables || config.options.variables; const configs = callbacks.map((cb) => cb({variables, state: this.context.store.getState()})); const optimisticResponse = merge( base.optimisticResponse || config.options.optimisticResponse, ...configs.map((cfg) => cfg.optimisticResponse), ); const refetchQueries = flatten(uniq([ base.refetchQueries || config.options.refetchQueries, ...configs.map((cfg) => cfg.refetchQueries), ].filter((i) => i))); const updateCallbacks = [base.update || config.options.update] .concat(...configs.map((cfg) => cfg.update)) .filter((i) => i); const update = (proxy, result) => { if (getResponseErrors(result)) { // Do not run updates when we have mutation errors. return; } updateCallbacks.forEach((cb) => cb(proxy, result)); }; const updateQueries = [ base.updateQueries || config.options.updateQueries, ...configs.map((cfg) => cfg.updateQueries) ] .filter((i) => i) .reduce((res, map) => { Object.keys(map).forEach((key) => { if (!(key in res)) { res[key] = (prev, result) => { if (getResponseErrors(result.mutationResult)) { // Do not run updates when we have mutation errors. return prev; } return map[key](prev, result) || prev; }; } else { const existing = res[key]; res[key] = (prev, result) => { if (getResponseErrors(result.mutationResult)) { // Do not run updates when we have mutation errors. return prev; } const next = existing(prev, result); return map[key](next, result) || next; }; } }); return res; }, {}); const wrappedConfig = { variables, optimisticResponse, refetchQueries, updateQueries, update, }; if (isEmpty(wrappedConfig.optimisticResponse)) { delete wrappedConfig.optimisticResponse; } this.context.eventEmitter.emit(`mutation.${name}.begin`, {variables}); return data.mutate(wrappedConfig) .then((res) => { const errors = getResponseErrors(res); if (errors) { throw new ResponseErrors(errors); } this.context.eventEmitter.emit(`mutation.${name}.success`, {variables, data: res.data}); return Promise.resolve(res); }) .catch((error) => { this.context.eventEmitter.emit(`mutation.${name}.error`, {variables, error}); throw error; }); }; // Save current props to `dynamicProps` this.dynamicProps = config.props({...data, mutate}); // Sync props to `staticProps`. // `staticProps` ultimately contains the same props as `dynamicProps` but all callbacks // keep their identity. union(Object.keys(this.dynamicProps), Object.keys(this.staticProps)).forEach((key) => { if (!(key in this.dynamicProps)) { delete this.staticProps[key]; return; } if (typeof this.dynamicProps[key] !== 'function') { this.staticProps[key] = this.dynamicProps[key]; return; } if (!(key in this.staticProps)) { this.staticProps[key] = (...args) => this.dynamicProps[key](...args); return; } }); return this.staticProps; }; getWrapped = () => { if (!this.memoized) { this.memoized = graphql(this.resolveDocument(document), {...config, props: this.propsWrapper})(WrappedComponent); } return this.memoized; }; render() { const Wrapped = this.getWrapped(); return ; } }; });