UNPKG

canner

Version:

Build CMS in few lines of code for different data sources

443 lines (418 loc) 13.3 kB
/** * @flow */ import * as React from 'react'; import {HOCContext} from '../hocs/context'; import {ApolloProvider} from 'react-apollo'; import isEmpty from 'lodash/isEmpty'; import pluralize from 'pluralize'; import upperFirst from 'lodash/upperFirst'; import {ActionManager, actionToMutation, actionsToVariables, mutate} from '../action'; import {Query} from '../query'; import {OnDeployManager} from '../onDeployManager'; import gql from 'graphql-tag'; import {objectToQueries} from '../query/utils'; import mapValues from 'lodash/mapValues'; import {groupBy, difference} from 'lodash'; import log from '../utils/log'; import type {ProviderProps} from './types'; import type {Action, ActionType} from '../action/types'; type Props = ProviderProps; type State = { dataChanged: Object, }; export default class Provider extends React.PureComponent<Props, State> { actionManager: ActionManager; query: Query; observableQueryMap: {[string]: any} onDeployManager: OnDeployManager; state = { dataChanged: {} }; constructor(props: Props) { super(props); this.actionManager = new ActionManager(); this.genQuery(); this.genObservableQueryMap(); this.onDeployManager = new OnDeployManager(); } UNSAFE_componentWillReceiveProps(nextProps: Props) { const {rootKey, routes, schema, routerParams} = nextProps; const customizedGQL = schema[rootKey] && schema[rootKey].graphql; if (customizedGQL) { this.observableQueryMap[rootKey] = this.getObservable({ routes: routes, operator: routerParams.operator }); } } genQuery = () => { const {schema} = this.props; this.query = new Query({schema}); } genObservableQueryMap = () => { const {schema, routes, routerParams} = this.props; this.observableQueryMap = mapValues(schema, (v, key) => { if (routes[0] === key) { return this.getObservable({ routes: routes, operator: routerParams.operator }) } return this.getObservable({ routes: [key], operator: 'update' }); }); } getObservable = ({ routes, operator }: { routes: Array<string>, operator: string }) => { const {client, schema, beforeFetch} = this.props; const key = routes[0]; let variables = this.query && this.query.getVairables(); const customizedGQL = routes.length === 1 && operator === 'update' && schema[key].graphql; const fetchPolicy = schema[key].fetchPolicy; let query = '' if (customizedGQL) { query = schema[key].graphql; } else { query = this.query.toGQL(key); } if (beforeFetch) { const updated = beforeFetch( key, { query, variables, relation: schema[key].relation } ); query = updated.query; variables = updated.variables; } return client.watchQuery({ query: gql`${query}`, variables, fetchPolicy: routes.length > 1 && operator === 'update' ? fetchPolicy : 'cache-first' }); } updateDataChanged = () => { const {dataDidChange} = this.props; const actions = this.actionManager.getActions(); let dataChanged = groupBy(actions, (action => action.payload.key)); dataChanged = mapValues(dataChanged, value => { if (value[0].type === 'UPDATE_OBJECT') { return true; } return value.map(v => v.payload.id); }); if (dataDidChange) { dataDidChange(dataChanged); } this.setState({dataChanged}); } updateQuery = (paths: Array<string>, args: Object) => { const {routerParams, schema, routes, beforeFetch, rootKey} = this.props; const originVariables = this.query.getVairables(); this.query.updateQueries(paths, 'args', args); let variables = this.query.getVairables(); const reWatchQuery = compareVariables(originVariables, variables); if (reWatchQuery) { this.observableQueryMap[paths[0]] = this.getObservable({ routes: paths, operator: routerParams.operator }); log('updateQuery rewatch', variables, args); return Promise.resolve(reWatchQuery); } else { const refetch = (routes.length > 1 && schema[paths[0]] && schema[paths[0]].refetch); if (beforeFetch) { const updated = beforeFetch( rootKey, { query: '', variables, relation: schema[rootKey].relation } ); variables = updated.variables; } log('updateQuery', variables, args); if (refetch) { return this.observableQueryMap[paths[0]].refetch(variables).then(() => false); } return this.observableQueryMap[paths[0]].setVariables(variables).then(() => false); } } // path: posts/name args: {where, pagination, sort} fetch = (key: string): Promise.resolve<*> => { const {errorHandler} = this.props; const observabale = this.observableQueryMap[key]; const currentResult = observabale.currentResult(); const {loading, error} = currentResult; if (loading) { return observabale.result() .then(result => { log('fetch', 'loading', key, result); return result.data; }).catch(e => { log('fetch', 'network error', key, e); errorHandler && errorHandler(e); }) } else if (error) { const lastResult = observabale.getLastResult(); log('fetch', 'error', key, lastResult); errorHandler && errorHandler(error); return Promise.resolve(lastResult.data); } else { log('fetch', 'loaded', key, currentResult, this.query.getVairables()); return Promise.resolve(currentResult.data); } } subscribe = (key: string, callback: (data: any) => void) => { const observableQuery = this.observableQueryMap[key]; return observableQuery.subscribe({ next: () => { const {loading, errors, data} = observableQuery.currentResult(); if (!loading && !errors && data && !isEmpty(data)) { callback(data); } }, error: () => { // ignore the error here since in fetch method, the error should be handled } }); } executeOnDeploy = (key: string, value: any) => { return this.onDeployManager.execute({ key, value }); } deploy = (key: string, id?: string): Promise<*> => { const {client, afterDeploy, schema, errorHandler, routes, rootKey, routerParams, beforeDeploy, beforeFetch} = this.props; let actions = this.actionManager.getActions(key, id); if (!actions || !actions.length) { return Promise.resolve(); } actions = removeIdInCreateArray(actions); let mutation = objectToQueries(actionToMutation(actions[0]), false); let variables = actionsToVariables(actions, schema); const queryVariables = this.query.getVairables(); let query = null; if (routes.length === 1 && routerParams.operator === 'update' && schema[rootKey].graphql) { query = schema[rootKey].graphql; } else { query = this.query.toGQL(actions[0].payload.key); } if (beforeFetch) { const updated = beforeFetch( rootKey, { query, variables: queryVariables, relation: schema[rootKey].relation } ); variables = updated.variables; query = updated.query; } const cachedData = client.readQuery({query: gql`${query}`, variables: queryVariables}); const mutatedData = cachedData[key]; const {error} = this.executeOnDeploy(key, mutatedData); if (error) { errorHandler && errorHandler(new Error('Invalid field')); return Promise.reject(error); } if (beforeDeploy) { const updated = beforeDeploy(key, { mutation, variables }); mutation = updated.mutation; variables = updated.variables; } return client.mutate({ mutation: gql`${mutation}`, variables }).then(result => { if (actions[0].type === 'CREATE_ARRAY') { this.updateID({ action: actions[0], result }); client.resetStore(); } log('deploy', key, { id, result, mutation, variables }); this.actionManager.removeActions(key, id); return result.data; }).then(result => { this.updateDataChanged(); afterDeploy && afterDeploy({ key, id: id || '', result, actions }); return result; }).catch(e => { errorHandler && errorHandler(e); log('deploy', e, key, { id, mutation, variables }); // to hocs throw e; }); } updateID({ action, result }: { action: Action<ActionType>, result: any }) { const {client} = this.props; const originId = action.payload.id; const key = action.payload.key; const resultKey = `create${upperFirst(pluralize.singular(key))}`; const newId = result.data[resultKey].id; const variables = this.query.getVairables(); const query = gql`${this.query.toGQL(key)}`; const data = client.readQuery({query, variables}); data[key].edges.map(edge => { if (edge.cursor === originId) { edge.cursor = newId; edge.node.id = newId; } return edge; }); client.writeQuery({ query, variables, data }); } reset = (key?: string, id?: string): Promise<*> => { const {rootKey} = this.props; if (this.actionManager.getActions(key, id).length === 0) { return Promise.resolve(); } this.actionManager.removeActions(key, id); this.updateDataChanged(); const variables = this.query.getVairables(); if (this.observableQueryMap[(key || rootKey)]) { return this.observableQueryMap[key || rootKey].refetch(variables); } return Promise.resolve(); } request = (action: Array<Action<ActionType>> | Action<ActionType>, options: {write: boolean} = {write: true}): Promise<*> => { const {write = true} = options; const actions = [].concat(action); if (actions.length === 0) { return Promise.resolve(); } actions.forEach(ac => this.actionManager.addAction(ac)); this.updateDataChanged(); if (write) { const {data, mutatedData} = this.updateCachedData(actions); log('request', action, data, mutatedData); } return Promise.resolve(); } updateCachedData = (actions: Array<Action<ActionType>>) => { const {client, schema, routes, rootKey, routerParams, beforeFetch} = this.props; let variables = this.query.getVairables(); let query = null; if (routes.length === 1 && routerParams.operator === 'update' && schema[rootKey].graphql) { query = schema[rootKey].graphql; } else { query = this.query.toGQL(actions[0].payload.key); } if (beforeFetch) { const updated = beforeFetch( rootKey, { query, variables, relation: schema[rootKey].relation } ); variables = updated.variables; query = updated.query; } const data = client.readQuery({query: gql`${query}`, variables}); const mutatedData = actions.reduce((result: Object, ac: any) => mutate(result, ac), data); client.writeQuery({ query, variables, data: mutatedData }); return {data, mutatedData}; } render() { const {client, beforeFetch} = this.props; return <ApolloProvider client={client}> {/* $FlowFixMe */} <HOCContext.Provider value={{ request: this.request, deploy: this.deploy, fetch: this.fetch, reset: this.reset, updateQuery: this.updateQuery, subscribe: this.subscribe, query: this.query, onDeploy: this.onDeployManager.registerCallback, removeOnDeploy: this.onDeployManager.unregisterCallback, dataChanged: this.state.dataChanged, beforeFetch }}> {/* $FlowFixMe */} {React.cloneElement(this.props.children, { request: this.request, deploy: this.deploy, fetch: this.fetch, reset: this.reset, updateQuery: this.updateQuery, subscribe: this.subscribe, query: this.query, onDeploy: this.onDeployManager.registerCallback, removeOnDeploy: this.onDeployManager.unregisterCallback, beforeFetch })} </HOCContext.Provider> </ApolloProvider>; } } function removeIdInCreateArray(actions: Array<Action<ActionType>>) { return actions.map(action => { if (action.type === 'CREATE_ARRAY') { const newAction = JSON.parse(JSON.stringify(action)); delete newAction.payload.value.id; delete newAction.payload.value.__typename; return newAction; } return action; }); } function compareVariables(originVariables: Object, variables: Object) { const originArr = Object.keys(originVariables).filter(key => originVariables[key]); const varArr = Object.keys(variables).filter(key => variables[key]); const less = difference(originArr, varArr); const more = difference(varArr, originArr); if (less.length === 0 && more.length === 0) { return false; } return true }