diff --git a/.eslintrc b/.eslintrc index db431f3ab..838eefc52 100644 --- a/.eslintrc +++ b/.eslintrc @@ -62,13 +62,14 @@ "no-sequences": 2, "no-console": 1, + "import/no-unresolved": 2, "import/no-self-import": 2, "import/first": 2, "import/newline-after-import": 2, "import/no-named-default": 2, "import/dynamic-import-chunkname": 2, "import/no-unused-modules": [ - 2, + 0, { "missingExports": false, "unusedExports": true, @@ -76,7 +77,14 @@ } ], "import/no-extraneous-dependencies": 2, - "import/order": [2, { "newlines-between": "always" }], + "import/order": [ + 2, + { + "groups": ["builtin", "external", "parent", "sibling", "index"], + "newlines-between": "always" + } + ], + "import/named": 2, "react/prop-types": 0, "react/no-array-index-key": 0, diff --git a/examples/server-side-rendering/src/store/index.js b/examples/server-side-rendering/src/store/index.js index ee880b93e..a773b69f3 100644 --- a/examples/server-side-rendering/src/store/index.js +++ b/examples/server-side-rendering/src/store/index.js @@ -19,7 +19,6 @@ export const configureStore = (initialState = undefined) => { }), ), ssr: ssr ? 'server' : 'client', - cache: true, }); const reducers = combineReducers({ diff --git a/examples/showcase/src/store/index.js b/examples/showcase/src/store/index.js index cb74bd36f..3c5000568 100644 --- a/examples/showcase/src/store/index.js +++ b/examples/showcase/src/store/index.js @@ -8,7 +8,6 @@ import { fetchBooks, fetchPosts, fetchGroups } from './actions'; export const configureStore = () => { const { requestsReducer, requestsMiddleware } = handleRequests({ driver: createDriver(axios), - cache: true, }); const reducers = combineReducers({ diff --git a/packages/redux-requests-axios/package.json b/packages/redux-requests-axios/package.json index 80098f109..094c7f02e 100644 --- a/packages/redux-requests-axios/package.json +++ b/packages/redux-requests-axios/package.json @@ -56,7 +56,7 @@ "nodemon": "2.0.6", "npm-run-all": "4.1.5", "rimraf": "3.0.2", - "typescript": "4.1.2", + "typescript": "4.2.4", "webpack": "5.9.0", "webpack-cli": "4.2.0" }, diff --git a/packages/redux-requests-fetch/package.json b/packages/redux-requests-fetch/package.json index 4937d3cbb..3e47e0add 100644 --- a/packages/redux-requests-fetch/package.json +++ b/packages/redux-requests-fetch/package.json @@ -58,7 +58,7 @@ "nodemon": "2.0.6", "npm-run-all": "4.1.5", "rimraf": "3.0.2", - "typescript": "4.1.2", + "typescript": "4.2.4", "webpack": "5.9.0", "webpack-cli": "4.2.0", "yet-another-abortcontroller-polyfill": "0.0.4" diff --git a/packages/redux-requests-graphql/package.json b/packages/redux-requests-graphql/package.json index aa7e2e567..8bac966fd 100644 --- a/packages/redux-requests-graphql/package.json +++ b/packages/redux-requests-graphql/package.json @@ -57,7 +57,7 @@ "nodemon": "2.0.6", "npm-run-all": "4.1.5", "rimraf": "3.0.2", - "typescript": "4.1.2", + "typescript": "4.2.4", "webpack": "5.9.0", "webpack-cli": "4.2.0" }, diff --git a/packages/redux-requests-mock/package.json b/packages/redux-requests-mock/package.json index 837a6f822..06d2398a8 100644 --- a/packages/redux-requests-mock/package.json +++ b/packages/redux-requests-mock/package.json @@ -55,7 +55,7 @@ "nodemon": "2.0.6", "npm-run-all": "4.1.5", "rimraf": "3.0.2", - "typescript": "4.1.2", + "typescript": "4.2.4", "webpack": "5.9.0", "webpack-cli": "4.2.0" }, diff --git a/packages/redux-requests-promise/package.json b/packages/redux-requests-promise/package.json index 0a0253e21..33d659876 100644 --- a/packages/redux-requests-promise/package.json +++ b/packages/redux-requests-promise/package.json @@ -54,7 +54,7 @@ "nodemon": "2.0.6", "npm-run-all": "4.1.5", "rimraf": "3.0.2", - "typescript": "4.1.2", + "typescript": "4.2.4", "webpack": "5.9.0", "webpack-cli": "4.2.0" }, diff --git a/packages/redux-requests-react/package.json b/packages/redux-requests-react/package.json index 430b19dea..140dfd6a4 100644 --- a/packages/redux-requests-react/package.json +++ b/packages/redux-requests-react/package.json @@ -22,8 +22,8 @@ "scripts": { "clean": "rimraf es lib dist", "lint": "eslint 'src/**'", - "test": "jest src", - "test:cover": "jest --coverage src", + "test": "jest --passWithNoTests src", + "test:cover": "jest --passWithNoTests --coverage src", "test-types": "tsc types/index.d.spec.tsx --noEmit --strict --lib es2015 --jsx react", "build:commonjs": "cross-env BABEL_ENV=cjs babel src --out-dir lib --ignore 'src/**/*.spec.js'", "build:es": "babel src --out-dir es --ignore 'src/**/*.spec.js'", @@ -40,8 +40,7 @@ "redux": ">=4.0.0" }, "dependencies": { - "prop-types": "^15.5.7", - "react-is": ">=16.13.1" + "prop-types": "^15.5.7" }, "devDependencies": { "@babel/cli": "7.12.8", @@ -66,10 +65,10 @@ "react-dom": "17.0.1", "react-redux": "7.2.2", "react-test-renderer": "17.0.1", - "redux": "4.0.5", + "redux": "4.1.0", "redux-mock-store": "1.5.4", "rimraf": "3.0.2", - "typescript": "4.1.2", + "typescript": "4.2.4", "webpack": "5.9.0", "webpack-cli": "4.2.0" }, diff --git a/packages/redux-requests-react/src/__snapshots__/mutation.spec.jsx.snap b/packages/redux-requests-react/src/__snapshots__/mutation.spec.jsx.snap deleted file mode 100644 index 8b831591c..000000000 --- a/packages/redux-requests-react/src/__snapshots__/mutation.spec.jsx.snap +++ /dev/null @@ -1,35 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Mutation doesnt crush if an mutation with a requestKey doesnt exist 1`] = `
`; - -exports[`Mutation maps type to default mutation when no type found 1`] = `
`; - -exports[`Mutation maps type to mutation 1`] = ` -
- loading - error -
-`; - -exports[`Mutation renders custom component as prop with extra prop 1`] = ` -
- loading - - error - - extra -
-`; - -exports[`Mutation renders loading and error of mutation with requestKey 1`] = ` -
- loading - error -
-`; - -exports[`Mutation supports custom selector 1`] = ` -
- error -
-`; diff --git a/packages/redux-requests-react/src/__snapshots__/query.spec.jsx.snap b/packages/redux-requests-react/src/__snapshots__/query.spec.jsx.snap deleted file mode 100644 index 5fc73f4f0..000000000 --- a/packages/redux-requests-react/src/__snapshots__/query.spec.jsx.snap +++ /dev/null @@ -1,69 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Query allows passing no data message 1`] = `"no data"`; - -exports[`Query allows rendering custom error component with extra props 1`] = ` - - error - - extra - -`; - -exports[`Query allows rendering custom loading component with extra props 1`] = ` - - loading... - extra - -`; - -exports[`Query maps type to default query data when request state not found 1`] = `null`; - -exports[`Query maps type to query 1`] = ` -
- data -
-`; - -exports[`Query renders custom prop component with extra props 1`] = ` - - data - - extra - -`; - -exports[`Query renders data even when pending is positive if showLoaderDuringRefetch is false 1`] = ` -
- data -
-`; - -exports[`Query renders null when custom isDataEmpty returns true 1`] = `null`; - -exports[`Query renders null when data is empty array by default 1`] = `null`; - -exports[`Query renders null when data is falsy by default 1`] = `null`; - -exports[`Query renders null when error is truthy by default 1`] = `null`; - -exports[`Query renders null when pending is positive by default 1`] = `null`; - -exports[`Query supports custom query selector 1`] = ` -
- data -
-`; - -exports[`Query uses array as empty data when multiple is true 1`] = ` -
- array length: - 0 -
-`; - -exports[`Query uses defaultData prop when data is null 1`] = ` -
- 1 -
-`; diff --git a/packages/redux-requests-react/src/index.js b/packages/redux-requests-react/src/index.js index d6b80746c..680864b1e 100644 --- a/packages/redux-requests-react/src/index.js +++ b/packages/redux-requests-react/src/index.js @@ -1,8 +1,6 @@ export { default as useQuery } from './use-query'; -export { default as Query } from './query'; export { default as useMutation } from './use-mutation'; -export { default as Mutation } from './mutation'; export { default as useSubscription } from './use-subscription'; -export { default as useDispatchRequest } from './use-dispatch-request'; +export { default as useDispatch } from './use-dispatch'; export { default as RequestsProvider } from './requests-provider'; export { default as RequestErrorBoundary } from './request-error-boundary'; diff --git a/packages/redux-requests-react/src/mutation.jsx b/packages/redux-requests-react/src/mutation.jsx deleted file mode 100644 index ba03def58..000000000 --- a/packages/redux-requests-react/src/mutation.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { useSelector } from 'react-redux'; -import { getMutationSelector } from '@redux-requests/core'; - -import { reactComponentPropType } from './propTypesValidators'; - -const Mutation = ({ - type, - selector, - requestKey, - children, - component: Component, - ...extraProps -}) => { - const mutation = useSelector( - selector || getMutationSelector({ type, requestKey }), - ); - - if (children) { - return children(mutation); - } - - return ; -}; - -Mutation.propTypes = { - selector: PropTypes.func, - type: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), - requestKey: PropTypes.string, - children: PropTypes.func, - component: reactComponentPropType('Mutation'), -}; - -export default Mutation; diff --git a/packages/redux-requests-react/src/mutation.spec.jsx b/packages/redux-requests-react/src/mutation.spec.jsx deleted file mode 100644 index 629b80466..000000000 --- a/packages/redux-requests-react/src/mutation.spec.jsx +++ /dev/null @@ -1,172 +0,0 @@ -import renderer from 'react-test-renderer'; -import React from 'react'; -import { Provider } from 'react-redux'; -import configureStore from 'redux-mock-store'; - -import Mutation from './mutation'; - -const mockStore = configureStore(); - -describe('Mutation', () => { - const MUTATION_TYPE = 'MUTATION_TYPE'; - - it('supports custom selector', () => { - const component = renderer.create( - - state.request.mutations[MUTATION_TYPE]}> - {({ loading, error }) => ( -
- {loading && 'loading'} - {error} -
- )} -
-
, - ); - expect(component.toJSON()).toMatchSnapshot(); - }); - - it('maps type to mutation', () => { - const component = renderer.create( - - - {({ loading, error }) => ( -
- {loading && 'loading'} - {error} -
- )} -
-
, - ); - expect(component.toJSON()).toMatchSnapshot(); - }); - - it('maps type to default mutation when no type found', () => { - const component = renderer.create( - - - {({ loading, error }) => ( -
- {loading && 'loading'} - {error} -
- )} -
-
, - ); - expect(component.toJSON()).toMatchSnapshot(); - }); - - it('renders loading and error of mutation with requestKey', () => { - const component = renderer.create( - - - {({ loading, error }) => ( -
- {loading && 'loading'} - {error} -
- )} -
-
, - ); - - expect(component.toJSON()).toMatchSnapshot(); - }); - - it('doesnt crush if an mutation with a requestKey doesnt exist', () => { - const component = renderer.create( - - - {({ loading, error }) => ( -
- {loading && 'loading'} - {error} -
- )} -
-
, - ); - - expect(component.toJSON()).toMatchSnapshot(); - }); - - it('renders custom component as prop with extra prop', () => { - const MutationComponent = ({ mutation: { loading, error }, extra }) => ( -
- {loading && 'loading'} {error} {extra} -
- ); - - const component = renderer.create( - - - , - ); - - expect(component.toJSON()).toMatchSnapshot(); - }); -}); diff --git a/packages/redux-requests-react/src/propTypesValidators.js b/packages/redux-requests-react/src/propTypesValidators.js deleted file mode 100644 index 292ab8966..000000000 --- a/packages/redux-requests-react/src/propTypesValidators.js +++ /dev/null @@ -1,11 +0,0 @@ -import { isValidElementType } from 'react-is'; - -export const reactComponentPropType = componentName => (props, propName) => { - if (props[propName] && !isValidElementType(props[propName])) { - return new Error( - `Invalid prop '${propName}' supplied to '${componentName}': the prop is not a valid React component`, - ); - } - - return null; -}; diff --git a/packages/redux-requests-react/src/query.jsx b/packages/redux-requests-react/src/query.jsx deleted file mode 100644 index 847642de8..000000000 --- a/packages/redux-requests-react/src/query.jsx +++ /dev/null @@ -1,83 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { useSelector } from 'react-redux'; -import { getQuerySelector } from '@redux-requests/core'; - -import { reactComponentPropType } from './propTypesValidators'; - -const Query = ({ - type, - requestKey, - selector, - defaultData, - multiple, - children, - component: Component, - isDataEmpty, - showLoaderDuringRefetch, - noDataMessage, - errorComponent: ErrorComponent, - errorComponentProps, - loadingComponent: LoadingComponent, - loadingComponentProps, - ...extraProps -}) => { - const query = useSelector( - selector || getQuerySelector({ type, requestKey, defaultData, multiple }), - ); - - const dataEmpty = isDataEmpty(query); - - if (query.loading && (showLoaderDuringRefetch || dataEmpty)) { - return LoadingComponent ? ( - - ) : null; - } - - if (query.error) { - return ErrorComponent ? ( - - ) : null; - } - - if (dataEmpty) { - return noDataMessage; - } - - if (children) { - return children(query); - } - - return ; -}; - -Query.defaultProps = { - isDataEmpty: query => - Array.isArray(query.data) ? query.data.length === 0 : !query.data, - showLoaderDuringRefetch: true, - noDataMessage: null, - multiple: false, -}; - -Query.propTypes = { - type: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), - requestKey: PropTypes.string, - selector: PropTypes.func, - multiple: PropTypes.bool, - defaultData: PropTypes.any, - children: PropTypes.func, - component: reactComponentPropType('Query'), - isDataEmpty: PropTypes.func, - showLoaderDuringRefetch: PropTypes.bool, - noDataMessage: PropTypes.node, - errorComponent: reactComponentPropType('Query'), - errorComponentProps: PropTypes.objectOf(PropTypes.any), - loadingComponent: reactComponentPropType('Query'), - loadingComponentProps: PropTypes.objectOf(PropTypes.any), -}; - -export default Query; diff --git a/packages/redux-requests-react/src/query.spec.jsx b/packages/redux-requests-react/src/query.spec.jsx deleted file mode 100644 index b1c68ce9b..000000000 --- a/packages/redux-requests-react/src/query.spec.jsx +++ /dev/null @@ -1,429 +0,0 @@ -import renderer from 'react-test-renderer'; -import React from 'react'; -import { Provider } from 'react-redux'; -import configureStore from 'redux-mock-store'; - -import Query from './query'; - -const mockStore = configureStore(); - -describe('Query', () => { - const QUERY_TYPE = 'QUERY_TYPE'; - - it('supports custom query selector', () => { - const component = renderer.create( - - state.request}> - {({ data }) =>
{data}
} -
-
, - ); - expect(component.toJSON()).toMatchSnapshot(); - }); - - it('maps type to query', () => { - const component = renderer.create( - - {({ data }) =>
{data}
}
-
, - ); - expect(component.toJSON()).toMatchSnapshot(); - }); - - it('maps type to default query data when request state not found', () => { - const component = renderer.create( - - {({ data }) =>
{data}
}
-
, - ); - expect(component.toJSON()).toMatchSnapshot(); - }); - - it('renders null when data is falsy by default', () => { - const component = renderer.create( - - {({ data }) =>
{data}
}
-
, - ); - - expect(component.toJSON()).toMatchSnapshot(); - }); - - it('renders null when data is empty array by default', () => { - const component = renderer.create( - - - {({ data }) => ( -
- {data.map(v => ( - {v} - ))} -
- )} -
-
, - ); - expect(component.toJSON()).toMatchSnapshot(); - }); - - it('renders null when custom isDataEmpty returns true', () => { - const component = renderer.create( - - !query.data || query.data === 'empty'} - > - {({ data }) =>
{data}
} -
-
, - ); - - expect(component.toJSON()).toMatchSnapshot(); - }); - - it('uses defaultData prop when data is null', () => { - const component = renderer.create( - - - {({ data }) =>
{data}
} -
-
, - ); - expect(component.toJSON()).toMatchSnapshot(); - }); - - it('uses array as empty data when multiple is true', () => { - const component = renderer.create( - - !query.data}> - {({ data }) =>
array length: {data.length}
} -
-
, - ); - expect(component.toJSON()).toMatchSnapshot(); - }); - - it('allows passing no data message', () => { - const component = renderer.create( - - - {({ data }) =>
{data}
} -
-
, - ); - - expect(component.toJSON()).toMatchSnapshot(); - }); - - it('renders null when pending is positive by default', () => { - const component = renderer.create( - - {({ data }) =>
{data}
}
-
, - ); - - expect(component.toJSON()).toMatchSnapshot(); - }); - - it('allows rendering custom loading component with extra props', () => { - const Spinner = ({ extra }) => loading... {extra}; - - const component = renderer.create( - - - {({ data }) =>
{data}
} -
-
, - ); - - expect(component.toJSON()).toMatchSnapshot(); - }); - - it('throws when passing node as loadingComponent', () => { - const loggerSpy = jest - .spyOn(console, 'error') - .mockImplementation(() => null); - - try { - expect(() => - renderer.create( - - loading
}> - {({ data }) =>
{data}
} - - , - ), - ).toThrow(); - expect(loggerSpy).toBeCalled(); - } finally { - loggerSpy.mockRestore(); - } - }); - - it('renders data even when pending is positive if showLoaderDuringRefetch is false', () => { - const component = renderer.create( - - - {({ data }) =>
{data}
} -
-
, - ); - - expect(component.toJSON()).toMatchSnapshot(); - }); - - it('renders null when error is truthy by default', () => { - const component = renderer.create( - - {({ data }) =>
{data}
}
-
, - ); - - expect(component.toJSON()).toMatchSnapshot(); - }); - - it('allows rendering custom error component with extra props', () => { - const Error = ({ error, extra }) => ( - - {error} {extra} - - ); - - const component = renderer.create( - - - {({ data }) =>
{data}
} -
-
, - ); - - expect(component.toJSON()).toMatchSnapshot(); - }); - - it('throws when passing node as errorComponent', () => { - const loggerSpy = jest - .spyOn(console, 'error') - .mockImplementation(() => null); - - try { - expect(() => - renderer.create( - - error
}> - {({ data }) =>
{data}
} - - , - ), - ).toThrow(); - expect(loggerSpy).toBeCalled(); - } finally { - loggerSpy.mockRestore(); - } - }); - - it('renders custom prop component with extra props', () => { - const CustomComponent = ({ query, extra }) => ( - - {query.data} {extra} - - ); - - const component = renderer.create( - - - , - ); - - expect(component.toJSON()).toMatchSnapshot(); - }); -}); diff --git a/packages/redux-requests-react/src/requests-provider.jsx b/packages/redux-requests-react/src/requests-provider.jsx index 2800e7384..9fb8011f2 100644 --- a/packages/redux-requests-react/src/requests-provider.jsx +++ b/packages/redux-requests-react/src/requests-provider.jsx @@ -2,7 +2,7 @@ import React, { useMemo } from 'react'; import PropTypes from 'prop-types'; import { createStore, applyMiddleware, combineReducers, compose } from 'redux'; import { Provider } from 'react-redux'; -import { handleRequests, createRequestsStore } from '@redux-requests/core'; +import { handleRequests } from '@redux-requests/core'; import RequestsContext from './requests-context'; @@ -35,7 +35,7 @@ const RequestsProvider = ({ const store = useMemo(() => { if (customStore) { - return createRequestsStore(customStore); + return customStore; } const { requestsReducer, requestsMiddleware } = handleRequests( @@ -52,12 +52,10 @@ const RequestsProvider = ({ window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose; - return createRequestsStore( - createStore( - reducers, - initialState, - composeEnhancers(applyMiddleware(...getMiddleware(requestsMiddleware))), - ), + return createStore( + reducers, + initialState, + composeEnhancers(applyMiddleware(...getMiddleware(requestsMiddleware))), ); }, []); diff --git a/packages/redux-requests-react/src/use-dispatch-request.js b/packages/redux-requests-react/src/use-dispatch.js similarity index 100% rename from packages/redux-requests-react/src/use-dispatch-request.js rename to packages/redux-requests-react/src/use-dispatch.js diff --git a/packages/redux-requests-react/src/use-mutation.js b/packages/redux-requests-react/src/use-mutation.js index 5a010a207..267cdec21 100644 --- a/packages/redux-requests-react/src/use-mutation.js +++ b/packages/redux-requests-react/src/use-mutation.js @@ -8,7 +8,7 @@ import { joinRequest, } from '@redux-requests/core'; -import useDispatchRequest from './use-dispatch-request'; +import useDispatch from './use-dispatch'; import RequestsContext from './requests-context'; const emptyVariables = []; @@ -27,27 +27,25 @@ const useMutation = ({ throwError = throwError === undefined ? requestContext.throwError : throwError; - const dispatchRequest = useDispatchRequest(); + const dispatch = useDispatch(); const store = useStore(); const key = `${selectorProps.type}${selectorProps.requestKey || ''}`; const dispatchMutation = useCallback(() => { - return dispatchRequest( - (selectorProps.action || selectorProps.type)(...variables), - ); - }, [selectorProps.action, selectorProps.type, ...variables]); + return dispatch(selectorProps.type(...variables)); + }, [selectorProps.type, ...variables]); const mutation = useSelector(getMutationSelector(selectorProps)); useEffect(() => { - dispatchRequest(addWatcher(key)); + dispatch(addWatcher(key)); return () => { - dispatchRequest(removeWatcher(key)); + dispatch(removeWatcher(key)); if (autoReset && !store.getState().requests.watchers[key]) { - dispatchRequest( + dispatch( resetRequests( [ { @@ -63,7 +61,7 @@ const useMutation = ({ }, [selectorProps.type, selectorProps.requestKey]); if (suspense && mutation.loading) { - throw dispatchRequest(joinRequest(key)); + throw dispatch(joinRequest(key)); } if (throwError && mutation.error) { diff --git a/packages/redux-requests-react/src/use-query.js b/packages/redux-requests-react/src/use-query.js index f1a08304f..1e90e047b 100644 --- a/packages/redux-requests-react/src/use-query.js +++ b/packages/redux-requests-react/src/use-query.js @@ -9,7 +9,7 @@ import { joinRequest, } from '@redux-requests/core'; -import useDispatchRequest from './use-dispatch-request'; +import useDispatch from './use-dispatch'; import RequestsContext from './requests-context'; const emptyVariables = []; @@ -33,19 +33,17 @@ const useQuery = ({ throwError = throwError === undefined ? requestContext.throwError : throwError; - const dispatchRequest = useDispatchRequest(); + const dispatch = useDispatch(); const store = useStore(); const key = `${selectorProps.type}${selectorProps.requestKey || ''}`; const dispatchQuery = useCallback(() => { - return dispatchRequest( - (selectorProps.action || selectorProps.type)(...variables), - ); - }, [selectorProps.action, selectorProps.type, ...variables]); + return dispatch(selectorProps.type(...variables)); + }, [selectorProps.type, ...variables]); const dispatchStopPolling = useCallback(() => { - dispatchRequest( + dispatch( stopPolling([ { requestType: selectorProps.type, @@ -64,13 +62,13 @@ const useQuery = ({ const query = useSelector(getQuerySelector(selectorProps)); useEffect(() => { - dispatchRequest(addWatcher(key)); + dispatch(addWatcher(key)); return () => { - dispatchRequest(removeWatcher(key)); + dispatch(removeWatcher(key)); if (autoReset && !store.getState().requests.watchers[key]) { - dispatchRequest( + dispatch( resetRequests( [ { @@ -91,11 +89,11 @@ const useQuery = ({ throw dispatchQuery(); } - throw dispatchRequest(joinRequest(key, autoLoad)); + throw dispatch(joinRequest(key, autoLoad)); } if (suspense && !suspenseSsr && query.loading) { - throw dispatchRequest(joinRequest(key)); + throw dispatch(joinRequest(key)); } if (throwError && query.error) { diff --git a/packages/redux-requests-react/src/use-subscription.js b/packages/redux-requests-react/src/use-subscription.js index de91c0309..7fffca8eb 100644 --- a/packages/redux-requests-react/src/use-subscription.js +++ b/packages/redux-requests-react/src/use-subscription.js @@ -6,7 +6,7 @@ import { stopSubscriptions, } from '@redux-requests/core'; -import useDispatchRequest from './use-dispatch-request'; +import useDispatch from './use-dispatch'; const emptyVariables = []; @@ -14,28 +14,27 @@ const useSubscriptions = ({ variables = emptyVariables, type, requestKey, - action, active = true, }) => { - const dispatchRequest = useDispatchRequest(); + const dispatch = useDispatch(); const store = useStore(); const key = `${type}${requestKey || ''}`; useEffect(() => { if (active) { - dispatchRequest((action || type)(...variables)); + dispatch(type(...variables)); } - }, [active, action, type, ...variables]); + }, [active, type, ...variables]); useEffect(() => { - dispatchRequest(addWatcher(key)); + dispatch(addWatcher(key)); return () => { - dispatchRequest(removeWatcher(key)); + dispatch(removeWatcher(key)); if (!store.getState().requests.watchers[key]) { - dispatchRequest(stopSubscriptions([key])); + dispatch(stopSubscriptions([key])); } }; }, [type, requestKey]); diff --git a/packages/redux-requests-react/types/index.d.spec.tsx b/packages/redux-requests-react/types/index.d.spec.tsx index 0dee1da82..5f8adb541 100644 --- a/packages/redux-requests-react/types/index.d.spec.tsx +++ b/packages/redux-requests-react/types/index.d.spec.tsx @@ -1,13 +1,7 @@ import * as React from 'react'; import { MutationState, QueryState, RequestAction } from '@redux-requests/core'; -import { - Query, - Mutation, - useQuery, - useMutation, - useDispatchRequest, -} from './index'; +import { useQuery, useMutation, useDispatchRequest } from './index'; function fetchBooks( x: number, @@ -51,8 +45,6 @@ function updateBook(id: string): RequestAction<{ id: string; title: string }> { const query = useQuery({ type: 'Query' }); const query2 = useQuery({ type: 'Query', - multiple: true, - defaultData: {}, variables: [{ x: 1, y: 2 }], }); query2.data; @@ -80,113 +72,6 @@ const mutation3 = useMutation({ }); const r2 = mutation3.mutate(); -function BasicQuery() { - return ( - - selector={state => ({ - data: 'x', - error: null, - loading: true, - pending: 1, - pristine: false, - downloadProgress: null, - uploadProgress: null, - })} - type="TYPE" - > - {({ data }) => data} - - ); -} - -function BasicQuery2() { - return {({ data }) => data}; -} - -function Spinner({ extra }: { extra: any }) { - return loading... {extra}; -} - -function Error({ error, extra }: { error: Error, extra: any }) { - return ( - - {error} {extra} - - ); -} - -function Component({ query, extra }: { query: QueryState, extra: any }) { - return ( -
- {query.data} {extra} -
- ); -} - -function QueryWithComponents() { - return ( - - ); -} - -function BasicMutation() { - return ( - - {({ loading, error }) => ( -
- {loading && 'loading'} - {error} -
- )} -
- ); -} - -function MutationComponent({ mutation, extra }: { mutation: MutationState, extra: any }) { - return ( -
- {mutation.loading && 'loading'} - {mutation.error} - {extra} -
- ); -} - -function MutationWithCustomComponent() { - return ( - - ); -} - -function MutationWithSelector() { - return ( - ({ - error: null, - loading: false, - pending: 1, - downloadProgress: null, - uploadProgress: null, - })} - > - {({ loading, error }) => ( -
- {loading && 'loading'} - {error} -
- )} -
- ); -} - const QueryDispatcher = () => { const dispatch = useDispatchRequest(); diff --git a/packages/redux-requests-react/types/index.d.ts b/packages/redux-requests-react/types/index.d.ts index f6d28cd0a..8ef43e1e3 100644 --- a/packages/redux-requests-react/types/index.d.ts +++ b/packages/redux-requests-react/types/index.d.ts @@ -10,61 +10,6 @@ import { SubscriptionAction, } from '@redux-requests/core'; -interface LoadingProps { - downloadProgress?: number | null; - uploadProgress?: number | null; - [loadingProp: string]: any; -} - -interface ErrorProps { - error?: any; - [errorProp: string]: any; -} - -interface QueryCustomComponentProps { - query: QueryState; - [extraProperty: string]: any; -} - -interface QueryProps { - type?: string | ((...params: any[]) => RequestAction); - action?: (...params: any[]) => RequestAction; - requestKey?: string; - multiple?: boolean; - defaultData?: any; - selector?: (state: any) => QueryState; - children?: (query: QueryState) => React.ReactNode; - component?: CustomComponentProps extends QueryCustomComponentProps ? React.ComponentType : never; - isDataEmpty?: (query: QueryState) => boolean; - showLoaderDuringRefetch?: boolean; - noDataMessage?: React.ReactNode; - errorComponent?: React.ComponentType; - errorComponentProps?: { [errorProp: string]: any }; - loadingComponent?: React.ComponentType; - loadingComponentProps?: { [loadingProp: string]: any }; - [extraProperty: string]: any; -} - -export class Query extends React.Component< - QueryProps -> {} - -interface MutationCustomComponentProps { - mutation: MutationState; - [extraProperty: string]: any; -} - -interface MutationProps { - type?: string | ((...params: any[]) => RequestAction); - requestKey?: string; - selector?: (state: any) => MutationState; - children?: (mutation: MutationState) => React.ReactNode; - component?: CustomComponentProps extends MutationCustomComponentProps ? React.ComponentType : never; - [extraProperty: string]: any; -} - -export class Mutation extends React.Component> {} - interface RequestCreator { (...args: any[]): RequestAction; } @@ -86,8 +31,6 @@ export function useQuery< type: string | QueryCreator; action?: QueryCreator; requestKey?: string; - multiple?: boolean; - defaultData?: any; variables?: Parameters; autoLoad?: boolean; autoReset?: boolean; @@ -140,13 +83,20 @@ export function useSubscription(props: { export function useDispatchRequest(): DispatchRequest; -type RequestsProviderProps = - ({ requestsConfig: HandleRequestConfig; - extraReducers?: Reducer[]; - getMiddleware?: (extraMiddleware: Middleware[]) => Middleware[]; - store?: never; } - | { store: Store; requestsConfig?: never; extraReducers?: never; getMiddleware?: never; }) -& { +type RequestsProviderProps = ( + | { + requestsConfig: HandleRequestConfig; + extraReducers?: Reducer[]; + getMiddleware?: (extraMiddleware: Middleware[]) => Middleware[]; + store?: never; + } + | { + store: Store; + requestsConfig?: never; + extraReducers?: never; + getMiddleware?: never; + } +) & { children: React.ReactNode; autoLoad?: boolean; autoReset?: boolean; diff --git a/packages/redux-requests/package.json b/packages/redux-requests/package.json index fa9349170..d2e8b3f79 100644 --- a/packages/redux-requests/package.json +++ b/packages/redux-requests/package.json @@ -57,11 +57,11 @@ "jest-date-mock": "1.0.8", "nodemon": "2.0.6", "npm-run-all": "4.1.5", - "redux": "4.0.5", + "redux": "4.1.0", "redux-mock-store": "1.5.4", "reselect": "4.0.0", "rimraf": "3.0.2", - "typescript": "4.1.2", + "typescript": "4.2.4", "webpack": "5.9.0", "webpack-cli": "4.2.0" }, diff --git a/packages/redux-requests/src/actions.js b/packages/redux-requests/src/actions.js index 8f8fabaa6..13aa1f7c3 100644 --- a/packages/redux-requests/src/actions.js +++ b/packages/redux-requests/src/actions.js @@ -27,29 +27,22 @@ export const error = getActionWithSuffix(ERROR_SUFFIX); export const abort = getActionWithSuffix(ABORT_SUFFIX); -const isFSA = action => !!action.payload; - export const createSuccessAction = (action, response) => ({ type: success(action.type), - ...(isFSA(action) ? { payload: response } : { response }), + response, meta: { ...action.meta, + requestType: undefined, requestAction: action, }, }); export const createErrorAction = (action, errorData) => ({ type: error(action.type), - ...(isFSA(action) - ? { - payload: errorData, - error: true, - } - : { - error: errorData, - }), + error: errorData, meta: { ...action.meta, + requestType: undefined, requestAction: action, }, }); @@ -58,32 +51,15 @@ export const createAbortAction = action => ({ type: abort(action.type), meta: { ...action.meta, + requestType: undefined, requestAction: action, }, }); -export const getActionPayload = action => - action.payload === undefined ? action : action.payload; - -// eslint-disable-next-line import/no-unused-modules -export const getResponseFromSuccessAction = action => - action.payload ? action.payload : action.response; - export const isRequestAction = action => { - const actionPayload = getActionPayload(action); - return ( - !!actionPayload?.request && - !!( - Array.isArray(actionPayload.request) || - actionPayload.request.url || - actionPayload.request.query || - actionPayload.request.promise || - actionPayload.request.response || - actionPayload.request.error - ) && - !actionPayload.response && - !(actionPayload instanceof Error) + action?.meta?.requestType === 'QUERY' || + action?.meta?.requestType === 'MUTATION' ); }; @@ -100,21 +76,20 @@ export const isErrorAction = action => export const isAbortAction = action => isResponseAction(action) && action.type.endsWith(ABORT_SUFFIX); -const isRequestQuery = request => - (!request.query && - (!request.method || request.method.toLowerCase() === 'get')) || - (request.query && !request.query.trim().startsWith('mutation')); - export const isRequestActionQuery = action => { - const { request } = getActionPayload(action); + return action?.meta?.requestType === 'QUERY'; +}; + +export const isRequestActionMutation = action => { + return action?.meta?.requestType === 'MUTATION'; +}; - if (action.meta?.asMutation !== undefined) { - return !action.meta.asMutation; - } +export const isRequestActionLocalMutation = action => { + return action?.meta?.requestType === 'LOCAL_MUTATION'; +}; - return !!(Array.isArray(request) - ? request.every(isRequestQuery) - : isRequestQuery(request)); +export const isRequestActionSubscription = action => { + return action?.meta?.requestType === 'SUBSCRIPTION'; }; export const clearRequestsCache = (requests = null) => ({ diff --git a/packages/redux-requests/src/actions.spec.js b/packages/redux-requests/src/actions.spec.js index 93bc0ce37..9c2cb7d68 100644 --- a/packages/redux-requests/src/actions.spec.js +++ b/packages/redux-requests/src/actions.spec.js @@ -1,3 +1,4 @@ +import { createQuery } from './requests-creators'; import { success, error, @@ -5,7 +6,6 @@ import { createSuccessAction, createErrorAction, createAbortAction, - getActionPayload, isRequestAction, getRequestActionFromResponse, isSuccessAction, @@ -37,10 +37,7 @@ describe('actions', () => { describe('createSuccessAction', () => { it('should correctly transform action payload', () => { - const requestAction = { - type: 'REQUEST', - request: { url: '/' }, - }; + const requestAction = createQuery('REQUEST', { url: '/' })(); expect(createSuccessAction(requestAction, { data: 'data' })).toEqual({ type: 'REQUEST_SUCCESS', @@ -51,25 +48,6 @@ describe('actions', () => { }); }); - it('handles FSA actions', () => { - const requestAction = { - type: 'REQUEST', - payload: { - request: { url: '/' }, - }, - }; - - expect(createSuccessAction(requestAction, { data: 'data' })).toEqual({ - type: 'REQUEST_SUCCESS', - payload: { - data: 'data', - }, - meta: { - requestAction, - }, - }); - }); - it('should merge request meta', () => { const requestAction = { type: 'REQUEST', @@ -106,24 +84,6 @@ describe('actions', () => { }); }); - it('handles FSA actions', () => { - const requestAction = { - type: 'REQUEST', - payload: { - request: { url: '/' }, - }, - }; - - expect(createErrorAction(requestAction, 'errorData')).toEqual({ - type: 'REQUEST_ERROR', - payload: 'errorData', - error: true, - meta: { - requestAction, - }, - }); - }); - it('should merge request meta', () => { const requestAction = { type: 'REQUEST', @@ -178,50 +138,9 @@ describe('actions', () => { }); }); - describe('getActionPayload', () => { - it('just returns not FSA action', () => { - const action = { type: 'ACTION' }; - expect(getActionPayload(action)).toEqual(action); - }); - - it('returns payload of FSA action', () => { - const action = { type: 'ACTION', payload: 'payload' }; - expect(getActionPayload(action)).toBe('payload'); - }); - }); - describe('isRequestAction', () => { it('recognizes request action', () => { - expect(isRequestAction({ type: 'ACTION', request: { url: '/' } })).toBe( - true, - ); - }); - - it('recognizes request FSA action', () => { - expect( - isRequestAction({ - type: 'ACTION', - payload: { request: { url: '/' } }, - }), - ).toBe(true); - }); - - it('recognizes request action with multiple requests', () => { - expect( - isRequestAction({ - type: 'ACTION', - request: [{ url: '/' }, { url: '/path' }], - }), - ).toBe(true); - }); - - it('recognizes request action with graphql query', () => { - expect( - isRequestAction({ - type: 'ACTION', - request: { query: '{ x }' }, - }), - ).toBe(true); + expect(isRequestAction(createQuery('ACTION', { url: '/' })())).toBe(true); }); it('rejects actions without request object', () => { @@ -232,42 +151,11 @@ describe('actions', () => { }), ).toBe(false); }); - - it('rejects actions with request without url', () => { - expect( - isRequestAction({ - type: 'ACTION', - request: { headers: {} }, - }), - ).toBe(false); - }); - - it('rejects actions with response object', () => { - expect( - isRequestAction({ - type: 'ACTION', - request: { url: '/' }, - response: {}, - }), - ).toBe(false); - }); - - it('rejects actions with payload which is instance of error', () => { - const responseError = new Error(); - responseError.request = { request: { url: '/' } }; - expect( - isRequestAction({ - type: 'ACTION', - payload: responseError, - response: {}, - }), - ).toBe(false); - }); }); describe('getRequestActionFromResponse', () => { it('should return request action', () => { - const requestAction = { type: 'REQUEST', request: { url: '/' } }; + const requestAction = createQuery('REQUEST', { url: '/' })(); const responseAction = { type: success('REQUEST'), data: 'data', @@ -341,35 +229,8 @@ describe('actions', () => { describe('isRequestActionQuery', () => { it('treats request with GET method as queries', () => { - expect(isRequestActionQuery({ request: { url: '/books' } })).toBe(true); expect( - isRequestActionQuery({ request: { url: '/books', method: 'GET' } }), - ).toBe(true); - }); - - it('treats request with POST method as mutations', () => { - expect( - isRequestActionQuery({ request: { url: '/books', method: 'POST' } }), - ).toBe(false); - }); - - it('treats request with GET method as mutation when asMutation is true', () => { - expect(isRequestActionQuery({ request: { url: '/books' } })).toBe(true); - expect( - isRequestActionQuery({ - request: { url: '/books', method: 'GET' }, - meta: { asMutation: true }, - }), - ).toBe(false); - }); - - it('treats request with POST method as query when asMutation is false', () => { - expect(isRequestActionQuery({ request: { url: '/books' } })).toBe(true); - expect( - isRequestActionQuery({ - request: { url: '/books', method: 'POST' }, - meta: { asMutation: false }, - }), + isRequestActionQuery(createQuery('QUERY', { url: '/books' })()), ).toBe(true); }); }); diff --git a/packages/redux-requests/src/default-config.js b/packages/redux-requests/src/default-config.js index 3f9ed2c4d..787c7e850 100644 --- a/packages/redux-requests/src/default-config.js +++ b/packages/redux-requests/src/default-config.js @@ -1,4 +1,4 @@ -import { isRequestActionQuery, isRequestAction } from './actions'; +import { isRequestActionQuery } from './actions'; export default { driver: null, @@ -6,11 +6,8 @@ export default { onSuccess: null, onError: null, onAbort: null, - cache: false, ssr: null, disableRequestsPromise: false, - isRequestAction, - isRequestActionQuery, takeLatest: isRequestActionQuery, normalize: false, getNormalisationObjectKey: obj => obj.id, diff --git a/packages/redux-requests/src/handle-requests.js b/packages/redux-requests/src/handle-requests.js index aca69a734..a12322231 100644 --- a/packages/redux-requests/src/handle-requests.js +++ b/packages/redux-requests/src/handle-requests.js @@ -32,12 +32,12 @@ const handleRequests = userConfig => { config.ssr !== 'server' && config.subscriber && createSubscriptionsMiddleware(config), - config.ssr !== 'server' && createPollingMiddleware(config), + config.ssr !== 'server' && createPollingMiddleware(), config.ssr === 'server' && !config.disableRequestsPromise && - createServerSsrMiddleware(requestsPromise, config), - config.ssr === 'client' && createClientSsrMiddleware(config), - config.cache && createRequestsCacheMiddleware(config), + createServerSsrMiddleware(requestsPromise), + config.ssr === 'client' && createClientSsrMiddleware(), + createRequestsCacheMiddleware(), createSendRequestsMiddleware(config), ].filter(Boolean), requestsPromise, diff --git a/packages/redux-requests/src/index.js b/packages/redux-requests/src/index.js index 813f0c438..c8e5edc71 100644 --- a/packages/redux-requests/src/index.js +++ b/packages/redux-requests/src/index.js @@ -16,6 +16,12 @@ export { openWebsocket, closeWebsocket, } from './actions'; +export { + createQuery, + createMutation, + createLocalMutation, + createSubscription, +} from './requests-creators'; export { default as handleRequests } from './handle-requests'; export { getQuery, @@ -24,4 +30,3 @@ export { getMutationSelector, getWebsocketState, } from './selectors'; -export { createRequestsStore } from './middleware'; diff --git a/packages/redux-requests/src/middleware/create-client-ssr-middleware.js b/packages/redux-requests/src/middleware/create-client-ssr-middleware.js index 43c6ab3c7..6ee4ae852 100644 --- a/packages/redux-requests/src/middleware/create-client-ssr-middleware.js +++ b/packages/redux-requests/src/middleware/create-client-ssr-middleware.js @@ -1,15 +1,15 @@ -import defaultConfig from '../default-config'; import { getQuery } from '../selectors'; +import { isRequestAction } from '../actions'; -export default (config = defaultConfig) => store => next => action => { - if (!config.isRequestAction(action)) { +export default () => store => next => action => { + if (!isRequestAction(action)) { return next(action); } const state = store.getState(); const actionsToIgnore = state.requests.ssr; const actionToIgnore = actionsToIgnore.find( - v => (v.requestType || v) === action.type + (action.meta?.requestKey || ''), + v => (v.requestType || v) === action.type + (action.meta.requestKey || ''), ); if (!actionToIgnore) { @@ -18,7 +18,7 @@ export default (config = defaultConfig) => store => next => action => { const query = getQuery(state, { type: action.type, - requestKey: action.meta?.requestKey, + requestKey: action.meta.requestKey, }); action.meta = actionToIgnore.duplicate diff --git a/packages/redux-requests/src/middleware/create-client-ssr-middleware.spec.js b/packages/redux-requests/src/middleware/create-client-ssr-middleware.spec.js index 7dcb8f2af..e54122e62 100644 --- a/packages/redux-requests/src/middleware/create-client-ssr-middleware.spec.js +++ b/packages/redux-requests/src/middleware/create-client-ssr-middleware.spec.js @@ -1,5 +1,7 @@ import configureStore from 'redux-mock-store'; +import { createQuery } from '../requests-creators'; + import { createClientSsrMiddleware } from '.'; describe('middleware', () => { @@ -28,10 +30,10 @@ describe('middleware', () => { ssr: ['REQUEST'], }, }); - const action = { type: 'REQUEST', request: { url: '/' } }; + const action = createQuery('REQUEST', { url: '/' })(); const actionWithSsrResponse = { ...action, - meta: { ssrResponse: { data: 'data' } }, + meta: { ...action.meta, ssrResponse: { data: 'data' } }, }; expect(store.dispatch(action)).toEqual(actionWithSsrResponse); expect(store.getActions()).toEqual([actionWithSsrResponse]); @@ -53,7 +55,7 @@ describe('middleware', () => { ssr: ['REQUEST'], }, }); - const action = { type: 'REQUEST2', request: { url: '/' } }; + const action = createQuery('REQUEST2', { url: '/' })(); expect(store.dispatch(action)).toEqual(action); expect(store.getActions()).toEqual([action]); }); @@ -74,11 +76,11 @@ describe('middleware', () => { ssr: ['REQUEST1'], }, }); - const action = { - type: 'REQUEST', - request: { url: '/' }, - meta: { requestKey: '1' }, - }; + const action = createQuery( + 'REQUEST', + { url: '/' }, + { requestKey: '1' }, + )(); const actionWithSsrResponse = { ...action, meta: { ...action.meta, ssrResponse: { data: 'data' } }, diff --git a/packages/redux-requests/src/middleware/create-polling-middleware.js b/packages/redux-requests/src/middleware/create-polling-middleware.js index 9ee1ddc46..d997add81 100644 --- a/packages/redux-requests/src/middleware/create-polling-middleware.js +++ b/packages/redux-requests/src/middleware/create-polling-middleware.js @@ -1,7 +1,7 @@ -import defaultConfig from '../default-config'; import { STOP_POLLING, RESET_REQUESTS } from '../constants'; +import { isRequestAction, isRequestActionQuery } from '../actions'; -const getIntervalKey = action => action.type + (action.meta?.requestKey || ''); +const getIntervalKey = action => action.type + (action.meta.requestKey || ''); const getKeys = requests => requests.map(v => @@ -10,7 +10,7 @@ const getKeys = requests => : v.toString(), ); -export default (config = defaultConfig) => { +export default () => { let intervals = {}; return store => next => action => { @@ -30,9 +30,9 @@ export default (config = defaultConfig) => { intervals = intervalsCopy; } } else if ( - config.isRequestAction(action) && - config.isRequestActionQuery(action) && - !action.meta?.polled && + isRequestAction(action) && + isRequestActionQuery(action) && + !action.meta.polled && intervals[getIntervalKey(action)] ) { const intervalsCopy = { ...intervals }; @@ -42,9 +42,9 @@ export default (config = defaultConfig) => { } if ( - config.isRequestAction(action) && - config.isRequestActionQuery(action) && - action.meta?.poll && + isRequestAction(action) && + isRequestActionQuery(action) && + action.meta.poll && !action.meta.polled ) { intervals[getIntervalKey(action)] = setInterval(() => { diff --git a/packages/redux-requests/src/middleware/create-polling-middleware.spec.js b/packages/redux-requests/src/middleware/create-polling-middleware.spec.js index 5bc6b5255..74d0a31e1 100644 --- a/packages/redux-requests/src/middleware/create-polling-middleware.spec.js +++ b/packages/redux-requests/src/middleware/create-polling-middleware.spec.js @@ -1,6 +1,7 @@ import configureStore from 'redux-mock-store'; import { stopPolling, resetRequests } from '../actions'; +import { createQuery } from '../requests-creators'; import createPollingMiddleware from './create-polling-middleware'; @@ -17,7 +18,7 @@ describe('middleware', () => { it('doesnt do anything for not polling requests', () => { const mockStore = configureStore([createPollingMiddleware()]); const store = mockStore({}); - const action = { type: 'REQUEST', request: { url: '/' } }; + const action = createQuery('REQUEST', { url: '/' })(); expect(store.dispatch(action)).toBe(action); expect(store.getActions()).toEqual([action]); }); @@ -25,11 +26,7 @@ describe('middleware', () => { it('repeats queries when meta poll defined', async () => { const mockStore = configureStore([createPollingMiddleware()]); const store = mockStore({}); - const action = { - type: 'REQUEST', - request: { url: '/' }, - meta: { poll: 0.1 }, - }; + const action = createQuery('REQUEST', { url: '/' }, { poll: 0.1 })(); store.dispatch(action); await sleep(0.41); expect(store.getActions()).toEqual([ @@ -44,16 +41,8 @@ describe('middleware', () => { it('works with multiple queries types', async () => { const mockStore = configureStore([createPollingMiddleware()]); const store = mockStore({}); - const action = { - type: 'REQUEST', - request: { url: '/' }, - meta: { poll: 0.1 }, - }; - const action2 = { - type: 'REQUEST2', - request: { url: '/' }, - meta: { poll: 0.2 }, - }; + const action = createQuery('REQUEST', { url: '/' }, { poll: 0.1 })(); + const action2 = createQuery('REQUEST2', { url: '/' }, { poll: 0.2 })(); store.dispatch(action); await sleep(0.01); store.dispatch(action2); @@ -73,11 +62,7 @@ describe('middleware', () => { it('clears interval when query of the same type is dispatched', async () => { const mockStore = configureStore([createPollingMiddleware()]); const store = mockStore({}); - const action = { - type: 'REQUEST', - request: { url: '/' }, - meta: { poll: 0.1 }, - }; + const action = createQuery('REQUEST', { url: '/' }, { poll: 0.1 })(); const action2 = { ...action, meta: { ...action.meta, poll: undefined } }; store.dispatch(action); await sleep(0.11); @@ -93,11 +78,7 @@ describe('middleware', () => { it('clears all intervals on reset action', async () => { const mockStore = configureStore([createPollingMiddleware()]); const store = mockStore({}); - const action = { - type: 'REQUEST', - request: { url: '/' }, - meta: { poll: 0.1 }, - }; + const action = createQuery('REQUEST', { url: '/' }, { poll: 0.1 })(); store.dispatch(action); await sleep(0.01); store.dispatch(resetRequests()); @@ -108,11 +89,7 @@ describe('middleware', () => { it('clears all intervals on stopPolling action', async () => { const mockStore = configureStore([createPollingMiddleware()]); const store = mockStore({}); - const action = { - type: 'REQUEST', - request: { url: '/' }, - meta: { poll: 0.1 }, - }; + const action = createQuery('REQUEST', { url: '/' }, { poll: 0.1 })(); store.dispatch(action); await sleep(0.01); store.dispatch(stopPolling()); @@ -123,16 +100,13 @@ describe('middleware', () => { it('can clear specific intervals only on reset action', async () => { const mockStore = configureStore([createPollingMiddleware()]); const store = mockStore({}); - const action = { - type: 'REQUEST', - request: { url: '/' }, - meta: { poll: 0.1 }, - }; - const action2 = { - type: 'REQUEST2', - request: { url: '/' }, - meta: { poll: 0.1, requestKey: '1' }, - }; + const action = createQuery('REQUEST', { url: '/' }, { poll: 0.1 })(); + const action2 = createQuery( + 'REQUEST2', + { url: '/' }, + { poll: 0.1, requestKey: '1' }, + )(); + store.dispatch(action); store.dispatch(action2); await sleep(0.01); @@ -151,16 +125,12 @@ describe('middleware', () => { it('can clear specific intervals only on stopPolling action', async () => { const mockStore = configureStore([createPollingMiddleware()]); const store = mockStore({}); - const action = { - type: 'REQUEST', - request: { url: '/' }, - meta: { poll: 0.1 }, - }; - const action2 = { - type: 'REQUEST2', - request: { url: '/' }, - meta: { poll: 0.1, requestKey: '1' }, - }; + const action = createQuery('REQUEST', { url: '/' }, { poll: 0.1 })(); + const action2 = createQuery( + 'REQUEST2', + { url: '/' }, + { poll: 0.1, requestKey: '1' }, + )(); store.dispatch(action); store.dispatch(action2); await sleep(0.01); @@ -177,16 +147,12 @@ describe('middleware', () => { it('can clear specific intervals only on stopPolling action', async () => { const mockStore = configureStore([createPollingMiddleware()]); const store = mockStore({}); - const action = { - type: 'REQUEST', - request: { url: '/' }, - meta: { poll: 0.1 }, - }; - const action2 = { - type: 'REQUEST2', - request: { url: '/' }, - meta: { poll: 0.1, requestKey: '1' }, - }; + const action = createQuery('REQUEST', { url: '/' }, { poll: 0.1 })(); + const action2 = createQuery( + 'REQUEST2', + { url: '/' }, + { poll: 0.1, requestKey: '1' }, + )(); store.dispatch(action); store.dispatch(action2); await sleep(0.01); diff --git a/packages/redux-requests/src/middleware/create-requests-cache-middleware.js b/packages/redux-requests/src/middleware/create-requests-cache-middleware.js index 97c7f7662..9d636eb76 100644 --- a/packages/redux-requests/src/middleware/create-requests-cache-middleware.js +++ b/packages/redux-requests/src/middleware/create-requests-cache-middleware.js @@ -1,5 +1,5 @@ -import defaultConfig from '../default-config'; import { getQuery } from '../selectors'; +import { isRequestAction } from '../actions'; const isCacheValid = (cache, action) => cache.cacheKey === action.meta.cacheKey && @@ -7,10 +7,10 @@ const isCacheValid = (cache, action) => const getKey = action => action.type + (action.meta.requestKey || ''); -export default (config = defaultConfig) => store => next => action => { +export default () => store => next => action => { if ( - config.isRequestAction(action) && - action.meta?.cache && + isRequestAction(action) && + action.meta.cache && !action.meta.ssrResponse ) { const key = getKey(action); @@ -20,7 +20,7 @@ export default (config = defaultConfig) => store => next => action => { if (cacheValue !== undefined && isCacheValid(cacheValue, action)) { const query = getQuery(state, { type: action.type, - requestKey: action.meta?.requestKey, + requestKey: action.meta.requestKey, }); return next({ diff --git a/packages/redux-requests/src/middleware/create-requests-cache-middleware.spec.js b/packages/redux-requests/src/middleware/create-requests-cache-middleware.spec.js index c7cbc2cb7..ab988c079 100644 --- a/packages/redux-requests/src/middleware/create-requests-cache-middleware.spec.js +++ b/packages/redux-requests/src/middleware/create-requests-cache-middleware.spec.js @@ -2,6 +2,7 @@ import configureStore from 'redux-mock-store'; import { advanceBy, advanceTo, clear } from 'jest-date-mock'; import { createSuccessAction } from '../actions'; +import { createQuery } from '../requests-creators'; import { createRequestsCacheMiddleware } from '.'; @@ -19,7 +20,7 @@ describe('middleware', () => { it('doesnt affect request actions with no meta cache', () => { const mockStore = configureStore([createRequestsCacheMiddleware()]); const store = mockStore({}); - const action = { type: 'REQUEST', request: { url: '/' } }; + const action = createQuery('REQUEST', { url: '/' })(); const responseAction = createSuccessAction(action, { data: null }); store.dispatch(action); store.dispatch(responseAction); @@ -36,18 +37,14 @@ describe('middleware', () => { uploadProgress: {}, }, }); - const action = { - type: 'REQUEST', - request: { url: '/' }, - meta: { cache: true }, - }; + const action = createQuery('REQUEST', { url: '/' }, { cache: true })(); store.dispatch(action); expect(store.getActions()).toEqual([ - { - type: 'REQUEST', - request: { url: '/' }, - meta: { cache: true, cacheResponse: { data: 'data' } }, - }, + createQuery( + 'REQUEST', + { url: '/' }, + { cache: true, cacheResponse: { data: 'data' } }, + )(), ]); }); @@ -63,18 +60,14 @@ describe('middleware', () => { uploadProgress: {}, }, }); - const action = { - type: 'REQUEST', - request: { url: '/' }, - meta: { cache: 1 }, - }; + const action = createQuery('REQUEST', { url: '/' }, { cache: 1 })(); store.dispatch(action); expect(store.getActions()).toEqual([ - { - type: 'REQUEST', - request: { url: '/' }, - meta: { cache: 1, cacheResponse: { data: 'data' } }, - }, + createQuery( + 'REQUEST', + { url: '/' }, + { cache: 1, cacheResponse: { data: 'data' } }, + )(), ]); } finally { clear(); @@ -91,11 +84,7 @@ describe('middleware', () => { cache: { REQUEST: { cacheKey: undefined, timeout: Date.now() } }, }, }); - const action = { - type: 'REQUEST', - request: { url: '/' }, - meta: { cache: 1 }, - }; + const action = createQuery('REQUEST', { url: '/' }, { cache: 1 })(); advanceBy(1); store.dispatch(action); expect(store.getActions()).toEqual([action]); @@ -112,19 +101,9 @@ describe('middleware', () => { cache: {}, }, }); - const action = { - type: 'REQUEST', - request: { url: '/' }, - meta: { cache: true }, - }; + const action = createQuery('REQUEST', { url: '/' }, { cache: true })(); store.dispatch(action); - expect(store.getActions()).toEqual([ - { - type: 'REQUEST', - request: { url: '/' }, - meta: { cache: true }, - }, - ]); + expect(store.getActions()).toEqual([action]); }); }); }); diff --git a/packages/redux-requests/src/middleware/create-requests-store.js b/packages/redux-requests/src/middleware/create-requests-store.js deleted file mode 100644 index c6f5ab548..000000000 --- a/packages/redux-requests/src/middleware/create-requests-store.js +++ /dev/null @@ -1,6 +0,0 @@ -export default store => { - return { - ...store, - dispatchRequest: store.dispatch, - }; -}; diff --git a/packages/redux-requests/src/middleware/create-send-requests-middleware.js b/packages/redux-requests/src/middleware/create-send-requests-middleware.js index ae996b388..ee45b2e86 100644 --- a/packages/redux-requests/src/middleware/create-send-requests-middleware.js +++ b/packages/redux-requests/src/middleware/create-send-requests-middleware.js @@ -1,48 +1,41 @@ import { - getActionPayload, createSuccessAction, createErrorAction, createAbortAction, setDownloadProgress, setUploadProgress, + isRequestAction, } from '../actions'; import { ABORT_REQUESTS, RESET_REQUESTS, JOIN_REQUEST } from '../constants'; import { getQuery } from '../selectors'; -import createRequestsStore from './create-requests-store'; - -const getRequestTypeString = requestType => - typeof requestType === 'function' ? requestType.toString() : requestType; - const getKeys = requests => requests.map(v => typeof v === 'object' - ? getRequestTypeString(v.requestType) + (v.requestKey || '') - : getRequestTypeString(v), + ? v.requestType.toString() + (v.requestKey || '') + : v.toString(), ); const getDriver = (config, action) => - action.meta?.driver + action.meta.driver ? config.driver[action.meta.driver] : config.driver.default || config.driver; -const getLastActionKey = action => - action.type + (action.meta?.requestKey ? action.meta.requestKey : ''); +const getLastActionKey = action => action.type + (action.meta.requestKey || ''); const isActionRehydrated = action => !!( - action.meta?.cacheResponse || - action.meta?.ssrResponse || - action.meta?.ssrError + action.meta.cacheResponse || + action.meta.ssrResponse || + action.meta.ssrError ); // TODO: remove to more functional style, we need object maps and filters const abortPendingRequests = (action, pendingRequests) => { - const payload = getActionPayload(action); - const clearAll = !payload.requests; - const keys = !clearAll && getKeys(payload.requests); + const clearAll = !action.requests; + const keys = !clearAll && getKeys(action.requests); - if (!payload.requests) { + if (!action.requests) { Object.values(pendingRequests).forEach(requests => requests.forEach(r => r.cancel()), ); @@ -54,33 +47,21 @@ const abortPendingRequests = (action, pendingRequests) => { }; const isTakeLatest = (action, config) => - action.meta?.takeLatest !== undefined + action.meta.takeLatest !== undefined ? action.meta.takeLatest : typeof config.takeLatest === 'function' ? config.takeLatest(action) : config.takeLatest; const maybeCallOnRequestInterceptor = (action, config, store) => { - const payload = getActionPayload(action); - if ( config.onRequest && - (!action.meta || - (action.meta.runOnRequest !== false && !action.meta.ssrDuplicate)) + action.meta.runOnRequest !== false && + !action.meta.ssrDuplicate ) { - if (action.request) { - return { - ...action, - request: config.onRequest(payload.request, action, store), - }; - } - return { ...action, - payload: { - ...action.payload, - request: config.onRequest(payload.request, action, store), - }, + payload: config.onRequest(action.payload, action, store), }; } @@ -88,22 +69,10 @@ const maybeCallOnRequestInterceptor = (action, config, store) => { }; const maybeCallOnRequestMeta = (action, store) => { - const payload = getActionPayload(action); - - if (action.meta?.onRequest && !action.meta.ssrDuplicate) { - if (action.request) { - return { - ...action, - request: action.meta.onRequest(payload.request, action, store), - }; - } - + if (action.meta.onRequest && !action.meta.ssrDuplicate) { return { ...action, - payload: { - ...action.payload, - request: action.meta.onRequest(payload.request, action, store), - }, + payload: action.meta.onRequest(action.payload, action, store), }; } @@ -111,7 +80,7 @@ const maybeCallOnRequestMeta = (action, store) => { }; const maybeDispatchRequestAction = (action, next) => { - if (!action.meta || !action.meta.silent) { + if (!action.meta.silent) { action = next(action); } @@ -121,21 +90,21 @@ const maybeDispatchRequestAction = (action, next) => { const getDriverActions = (action, store) => { const driverActions = {}; - if (action.meta?.measureDownloadProgress) { + if (action.meta.measureDownloadProgress) { driverActions.setDownloadProgress = progress => store.dispatch( setDownloadProgress( - action.type + (action.meta?.requestKey || ''), + action.type + (action.meta.requestKey || ''), progress, ), ); } - if (action.meta?.measureUploadProgress) { + if (action.meta.measureUploadProgress) { driverActions.setUploadProgress = progress => store.dispatch( setUploadProgress( - action.type + (action.meta?.requestKey || ''), + action.type + (action.meta.requestKey || ''), progress, ), ); @@ -158,14 +127,13 @@ const defer = () => { }; const getResponsePromises = (action, config, pendingRequests, store) => { - const actionPayload = getActionPayload(action); - const isBatchedRequest = Array.isArray(actionPayload.request); + const isBatchedRequest = Array.isArray(action.payload); - if (action.meta?.cacheResponse) { + if (action.meta.cacheResponse) { return [Promise.resolve(action.meta.cacheResponse)]; - } else if (action.meta?.ssrResponse) { + } else if (action.meta.ssrResponse) { return [Promise.resolve(action.meta.ssrResponse)]; - } else if (action.meta?.ssrError) { + } else if (action.meta.ssrError) { return [Promise.reject(action.meta.ssrError)]; } @@ -179,8 +147,8 @@ const getResponsePromises = (action, config, pendingRequests, store) => { } const responsePromises = isBatchedRequest - ? actionPayload.request.map(r => driver(r, action, driverActions)) - : [driver(actionPayload.request, action, driverActions)]; + ? action.payload.map(r => driver(r, action, driverActions)) + : [driver(action.payload, action, driverActions)]; if (responsePromises[0].cancel) { pendingRequests[lastActionKey] = responsePromises; @@ -193,7 +161,7 @@ const maybeCallGetError = (action, error) => { if ( error !== 'REQUEST_ABORTED' && !isActionRehydrated(action) && - action.meta?.getError + action.meta.getError ) { throw action.meta.getError(error); } @@ -205,8 +173,8 @@ const maybeCallOnErrorInterceptor = (action, config, store, error) => { if ( error !== 'REQUEST_ABORTED' && config.onError && - (!action.meta || - (action.meta.runOnError !== false && !action.meta.ssrDuplicate)) + action.meta.runOnError !== false && + !action.meta.ssrDuplicate ) { return Promise.all([config.onError(error, action, store)]); } @@ -217,7 +185,7 @@ const maybeCallOnErrorInterceptor = (action, config, store, error) => { const maybeCallOnErrorMeta = (action, store, error) => { if ( error !== 'REQUEST_ABORTED' && - action.meta?.onError && + action.meta.onError && !action.meta.ssrDuplicate ) { return Promise.all([action.meta.onError(error, action, store)]); @@ -230,7 +198,7 @@ const maybeCallOnAbortInterceptor = (action, config, store, error) => { if ( error === 'REQUEST_ABORTED' && config.onAbort && - (!action.meta || action.meta.runOnAbort !== false) + action.meta.runOnAbort !== false ) { config.onAbort(action, store); } @@ -239,7 +207,7 @@ const maybeCallOnAbortInterceptor = (action, config, store, error) => { }; const maybeCallOnAbortMeta = (action, store, error) => { - if (error === 'REQUEST_ABORTED' && action.meta?.onAbort) { + if (error === 'REQUEST_ABORTED' && action.meta.onAbort) { action.meta.onAbort(action, store); } @@ -253,8 +221,7 @@ const getInitialBatchObject = responseKeys => }, {}); const maybeTransformBatchRequestResponse = (action, response) => { - const actionPayload = getActionPayload(action); - const isBatchedRequest = Array.isArray(actionPayload.request); + const isBatchedRequest = Array.isArray(action.payload); const responseKeys = Object.keys(response[0]); return isBatchedRequest && !isActionRehydrated(action) @@ -268,10 +235,10 @@ const maybeTransformBatchRequestResponse = (action, response) => { }; const maybeCallGetData = (action, store, response) => { - if (!isActionRehydrated(action) && action.meta?.getData) { + if (!isActionRehydrated(action) && action.meta.getData) { const query = getQuery(store.getState(), { type: action.type, - requestKey: action.meta?.requestKey, + requestKey: action.meta.requestKey, }); return { @@ -286,8 +253,8 @@ const maybeCallGetData = (action, store, response) => { const maybeCallOnSuccessInterceptor = (action, config, store, response) => { if ( config.onSuccess && - (!action.meta || - (action.meta.runOnSuccess !== false && !action.meta.ssrDuplicate)) + action.meta.runOnSuccess !== false && + !action.meta.ssrDuplicate ) { const result = config.onSuccess(response, action, store); @@ -300,7 +267,7 @@ const maybeCallOnSuccessInterceptor = (action, config, store, response) => { }; const maybeCallOnSuccessMeta = (action, store, response) => { - if (action.meta?.onSuccess && !action.meta.ssrDuplicate) { + if (action.meta.onSuccess && !action.meta.ssrDuplicate) { const result = action.meta.onSuccess(response, action, store); if (!isActionRehydrated(action)) { @@ -319,9 +286,6 @@ const createSendRequestMiddleware = config => { const allPendingRequests = {}; // for joining return store => next => action => { - const payload = getActionPayload(action); - const requestsStore = createRequestsStore(store); - if (action.type === JOIN_REQUEST) { next(action); return allPendingRequests[action.requestType] || sleep(); @@ -329,18 +293,18 @@ const createSendRequestMiddleware = config => { if ( action.type === ABORT_REQUESTS || - (action.type === RESET_REQUESTS && payload.abortPending) + (action.type === RESET_REQUESTS && action.abortPending) ) { abortPendingRequests(action, pendingRequests); return next(action); } - if (config.isRequestAction(action)) { + if (isRequestAction(action)) { const lastActionKey = getLastActionKey(action); allPendingRequests[lastActionKey] = defer(); - action = maybeCallOnRequestInterceptor(action, config, requestsStore); - action = maybeCallOnRequestMeta(action, requestsStore); + action = maybeCallOnRequestInterceptor(action, config, store); + action = maybeCallOnRequestMeta(action, store); action = maybeDispatchRequestAction(action, next); const responsePromises = getResponsePromises( @@ -353,18 +317,18 @@ const createSendRequestMiddleware = config => { return Promise.all(responsePromises) .catch(error => maybeCallGetError(action, error)) .catch(error => - maybeCallOnErrorInterceptor(action, config, requestsStore, error), + maybeCallOnErrorInterceptor(action, config, store, error), ) - .catch(error => maybeCallOnErrorMeta(action, requestsStore, error)) + .catch(error => maybeCallOnErrorMeta(action, store, error)) .catch(error => - maybeCallOnAbortInterceptor(action, config, requestsStore, error), + maybeCallOnAbortInterceptor(action, config, store, error), ) - .catch(error => maybeCallOnAbortMeta(action, requestsStore, error)) + .catch(error => maybeCallOnAbortMeta(action, store, error)) .catch(error => { if (error === 'REQUEST_ABORTED') { const abortAction = createAbortAction(action); - if (!action.meta || !action.meta.silent) { + if (!action.meta.silent) { store.dispatch(abortAction); } @@ -378,7 +342,7 @@ const createSendRequestMiddleware = config => { const errorAction = createErrorAction(action, error); - if (!action.meta || !action.meta.silent) { + if (!action.meta.silent) { store.dispatch(errorAction); } @@ -394,20 +358,13 @@ const createSendRequestMiddleware = config => { return maybeCallGetData(action, store, response); }) .then(response => - maybeCallOnSuccessInterceptor( - action, - config, - requestsStore, - response, - ), - ) - .then(response => - maybeCallOnSuccessMeta(action, requestsStore, response), + maybeCallOnSuccessInterceptor(action, config, store, response), ) + .then(response => maybeCallOnSuccessMeta(action, store, response)) .then(response => { const successAction = createSuccessAction(action, response); - if (!action.meta || !action.meta.silent) { + if (!action.meta.silent) { store.dispatch(successAction); } diff --git a/packages/redux-requests/src/middleware/create-send-requests-middleware.spec.js b/packages/redux-requests/src/middleware/create-send-requests-middleware.spec.js index feb609be6..f7007f7e7 100644 --- a/packages/redux-requests/src/middleware/create-send-requests-middleware.spec.js +++ b/packages/redux-requests/src/middleware/create-send-requests-middleware.spec.js @@ -7,6 +7,7 @@ import { createAbortAction, abortRequests, } from '../actions'; +import { createQuery } from '../requests-creators'; import { createSendRequestsMiddleware } from '.'; @@ -36,7 +37,6 @@ describe('middleware', () => { const testConfig = { ...defaultConfig, driver: dummyDriver, - isRequestAction: action => !!action.request, }; const mockStore = configureStore([createSendRequestsMiddleware(testConfig)]); @@ -50,26 +50,27 @@ describe('middleware', () => { }); it('dispatches requests and resolves on success', async () => { - const requestAction = { - type: 'REQUEST', - request: { response: { data: 'data' } }, - }; + const requestAction = createQuery('REQUEST', { + response: { data: 'data' }, + })(); const { dispatch, getActions } = mockStore({}); const successAction = createSuccessAction(requestAction, { data: 'data', }); + const result = await dispatch(requestAction); + expect(result).toEqual({ action: successAction, data: 'data' }); expect(getActions()).toEqual([requestAction, successAction]); }); it('resolves on success but doesnt dispatch in silent mode', async () => { - const requestAction = { - type: 'REQUEST', - request: { response: { data: 'data' } }, - meta: { silent: true }, - }; + const requestAction = createQuery( + 'REQUEST', + { response: { data: 'data' } }, + { silent: true }, + )(); const { dispatch, getActions } = mockStore({}); const successAction = createSuccessAction(requestAction, { @@ -81,13 +82,10 @@ describe('middleware', () => { }); it('dispatches requests and resolves on success for batch request', async () => { - const requestAction = { - type: 'REQUEST', - request: [ - { response: { data: 'data1' } }, - { response: { data: 'data2' } }, - ], - }; + const requestAction = createQuery('REQUEST', [ + { response: { data: 'data1' } }, + { response: { data: 'data2' } }, + ])(); const { dispatch, getActions } = mockStore({}); const successAction = createSuccessAction(requestAction, { @@ -102,11 +100,11 @@ describe('middleware', () => { }); it('dispatches requests and resolves on success for cache response', async () => { - const requestAction = { - type: 'REQUEST', - request: { response: { data: 'data' } }, - meta: { cacheResponse: { data: 'data cached' } }, - }; + const requestAction = createQuery( + 'REQUEST', + { response: { data: 'data' } }, + { cacheResponse: { data: 'data cached' } }, + )(); const { dispatch, getActions } = mockStore({}); const successAction = createSuccessAction(requestAction, { @@ -118,11 +116,11 @@ describe('middleware', () => { }); it('dispatches requests and resolves on success for ssr response', async () => { - const requestAction = { - type: 'REQUEST', - request: { response: { data: 'data' } }, - meta: { ssrResponse: { data: 'data ssr' } }, - }; + const requestAction = createQuery( + 'REQUEST', + { response: { data: 'data' } }, + { ssrResponse: { data: 'data ssr' } }, + )(); const { dispatch, getActions } = mockStore({}); const successAction = createSuccessAction(requestAction, { @@ -134,10 +132,7 @@ describe('middleware', () => { }); it('dispatches requests and resolves on error', async () => { - const requestAction = { - type: 'REQUEST', - request: { error: 'error' }, - }; + const requestAction = createQuery('REQUEST', { error: 'error' })(); const { dispatch, getActions } = mockStore({}); const errorAction = createErrorAction(requestAction, 'error'); @@ -150,11 +145,11 @@ describe('middleware', () => { }); it('resolves on error but doesnt dispatch in silent mode', async () => { - const requestAction = { - type: 'REQUEST', - request: { error: 'error' }, - meta: { silent: true }, - }; + const requestAction = createQuery( + 'REQUEST', + { error: 'error' }, + { silent: true }, + )(); const { dispatch, getActions } = mockStore({}); const errorAction = createErrorAction(requestAction, 'error'); @@ -167,11 +162,11 @@ describe('middleware', () => { }); it('dispatches requests and resolves on abort', async () => { - const requestAction = { - type: 'REQUEST', - request: { response: { data: 'data' } }, - meta: { takeLatest: true }, - }; + const requestAction = createQuery( + 'REQUEST', + { response: { data: 'data' } }, + { takeLatest: true }, + )(); const { dispatch, getActions } = mockStore({}); const successAction = createSuccessAction(requestAction, { @@ -197,10 +192,9 @@ describe('middleware', () => { }); it('aborts all requests on abortRequests action', async () => { - const requestAction = { - type: 'REQUEST', - request: { response: { data: 'data' } }, - }; + const requestAction = createQuery('REQUEST', { + response: { data: 'data' }, + })(); const { dispatch, getActions } = mockStore({}); const abortAction = createAbortAction(requestAction); @@ -225,19 +219,17 @@ describe('middleware', () => { }); it('aborts specific requests on abortRequests action', async () => { - const requestAction1 = { - type: 'REQUEST1', - request: { response: { data: 'data' } }, - }; - const requestAction2 = { - type: 'REQUEST2', - request: { response: { data: 'data' } }, - }; - const requestAction3 = { - type: 'REQUEST3', - request: { response: { data: 'data' } }, - meta: { requestKey: '1' }, - }; + const requestAction1 = createQuery('REQUEST1', { + response: { data: 'data' }, + })(); + const requestAction2 = createQuery('REQUEST2', { + response: { data: 'data' }, + })(); + const requestAction3 = createQuery( + 'REQUEST3', + { response: { data: 'data' } }, + { requestKey: '1' }, + )(); const { dispatch, getActions } = mockStore({}); const responseAction1 = createSuccessAction(requestAction1, { @@ -286,14 +278,12 @@ describe('middleware', () => { }, }), ]); - const requestAction = { - type: 'REQUEST', - request: { response: { data: 'data' } }, - }; - const requestActionUpdated = { - type: 'REQUEST', - request: { response: { data: 'dataUpdated' } }, - }; + const requestAction = createQuery('REQUEST', { + response: { data: 'data' }, + })(); + const requestActionUpdated = createQuery('REQUEST', { + response: { data: 'dataUpdated' }, + })(); const { dispatch, getActions } = onRequestMockStore({}); const successAction = createSuccessAction(requestActionUpdated, { @@ -318,11 +308,11 @@ describe('middleware', () => { }, }), ]); - const requestAction = { - type: 'REQUEST', - request: { response: { data: 'data' } }, - meta: { runOnRequest: false }, - }; + const requestAction = createQuery( + 'REQUEST', + { response: { data: 'data' } }, + { runOnRequest: false }, + )(); const { dispatch, getActions } = onRequestMockStore({}); const successAction = createSuccessAction(requestAction, { @@ -343,10 +333,9 @@ describe('middleware', () => { }, }), ]); - const requestAction = { - type: 'REQUEST', - request: { response: { data: 'data' } }, - }; + const requestAction = createQuery('REQUEST', { + response: { data: 'data' }, + })(); const { dispatch, getActions } = onSuccessMockStore({}); const successAction = createSuccessAction(requestAction, { data: 'dataUpdated', @@ -370,11 +359,11 @@ describe('middleware', () => { }, }), ]); - const requestAction = { - type: 'REQUEST', - request: { response: { data: 'data' } }, - meta: { runOnSuccess: false }, - }; + const requestAction = createQuery( + 'REQUEST', + { response: { data: 'data' } }, + { runOnSuccess: false }, + )(); const { dispatch, getActions } = onSuccessMockStore({}); const successAction = createSuccessAction(requestAction, { data: 'data', @@ -394,10 +383,7 @@ describe('middleware', () => { }, }), ]); - const requestAction = { - type: 'REQUEST', - request: { error: 'error' }, - }; + const requestAction = createQuery('REQUEST', { error: 'error' })(); const { dispatch, getActions } = onErrorMockStore({}); const errorAction = createErrorAction(requestAction, 'errorUpdated'); const result = dispatch(requestAction); @@ -422,11 +408,11 @@ describe('middleware', () => { }, }), ]); - const requestAction = { - type: 'REQUEST', - request: { error: 'error' }, - meta: { runOnError: false }, - }; + const requestAction = createQuery( + 'REQUEST', + { error: 'error' }, + { runOnError: false }, + )(); const { dispatch, getActions } = onErrorMockStore({}); const errorAction = createErrorAction(requestAction, 'error'); const result = dispatch(requestAction); @@ -442,20 +428,19 @@ describe('middleware', () => { createSendRequestsMiddleware({ ...testConfig, onError: async (error, action, store) => { - const { data } = await store.dispatch({ - type: 'REQUEST', - request: { response: { data: 'data' } }, - meta: { silent: true }, - }); + const { data } = await store.dispatch( + createQuery( + 'REQUEST', + { response: { data: 'data' } }, + { silent: true }, + )(), + ); return { data }; }, }), ]); - const requestAction = { - type: 'REQUEST', - request: { error: 'error' }, - }; + const requestAction = createQuery('REQUEST', { error: 'error' })(); const { dispatch, getActions } = onErrorMockStore({}); const successAction = createSuccessAction(requestAction, { data: 'data', @@ -477,10 +462,7 @@ describe('middleware', () => { }, }), ]); - const requestAction = { - type: 'REQUEST', - request: { error: 'error' }, - }; + const requestAction = createQuery('REQUEST', { error: 'error' })(); const { dispatch, getActions } = onErrorMockStore({}); const abortAction = createAbortAction(requestAction); const result = dispatch(requestAction); @@ -506,11 +488,11 @@ describe('middleware', () => { }, }), ]); - const requestAction = { - type: 'REQUEST', - request: { error: 'error' }, - meta: { runOnAbort: false }, - }; + const requestAction = createQuery( + 'REQUEST', + { error: 'error' }, + { runOnAbort: false }, + )(); const { dispatch, getActions } = onErrorMockStore({}); const abortAction = createAbortAction(requestAction); const result = dispatch(requestAction); @@ -527,13 +509,13 @@ describe('middleware', () => { }); it('dispatches requests and rejects on success but with getData syntax error', async () => { - const requestAction = { - type: 'REQUEST', - request: { response: { data: 'data' } }, - meta: { + const requestAction = createQuery( + 'REQUEST', + { response: { data: 'data' } }, + { getData: data => data.map(v => v), // error }, - }; + )(); const { dispatch, getActions } = mockStore({ requests: { queries: {}, mutations: {} }, diff --git a/packages/redux-requests/src/middleware/create-server-ssr-middleware.js b/packages/redux-requests/src/middleware/create-server-ssr-middleware.js index 23dccb224..446f2ebea 100644 --- a/packages/redux-requests/src/middleware/create-server-ssr-middleware.js +++ b/packages/redux-requests/src/middleware/create-server-ssr-middleware.js @@ -1,15 +1,14 @@ -import defaultConfig from '../default-config'; -import { isResponseAction, isSuccessAction } from '../actions'; +import { isResponseAction, isSuccessAction, isRequestAction } from '../actions'; -export default (requestsPromise, config = defaultConfig) => { +export default requestsPromise => { let index = 0; const successActions = []; const errorActions = []; return () => next => action => { - if (config.isRequestAction(action)) { + if (isRequestAction(action)) { index += - action.meta?.dependentRequestsNumber !== undefined + action.meta.dependentRequestsNumber !== undefined ? action.meta.dependentRequestsNumber + 1 : 1; } else if (isResponseAction(action)) { diff --git a/packages/redux-requests/src/middleware/create-server-ssr-middleware.spec.js b/packages/redux-requests/src/middleware/create-server-ssr-middleware.spec.js index ea364ac2f..7f0599db9 100644 --- a/packages/redux-requests/src/middleware/create-server-ssr-middleware.spec.js +++ b/packages/redux-requests/src/middleware/create-server-ssr-middleware.spec.js @@ -1,6 +1,7 @@ import configureStore from 'redux-mock-store'; import { createSuccessAction, createErrorAction } from '../actions'; +import { createQuery } from '../requests-creators'; import { createServerSsrMiddleware } from '.'; @@ -18,7 +19,7 @@ const defer = () => { describe('middleware', () => { describe('createServerSsrMiddleware', () => { - const requestAction = { type: 'REQUEST', request: { url: '/' } }; + const requestAction = createQuery('REQUEST', { url: '/' })(); const successAction = createSuccessAction(requestAction, 'data'); const errorAction = createErrorAction(requestAction, 'error'); @@ -72,21 +73,21 @@ describe('middleware', () => { createServerSsrMiddleware(requestsPromise), ]); const store = mockStore({}); - const firstRequestAction = { - type: 'REQUEST', - request: { url: '/' }, - meta: { dependentRequestsNumber: 2 }, - }; - const secondRequestAction = { - type: 'REQUEST2', - request: { url: '/' }, - meta: { isDependentRequest: true }, - }; - const thirdRequestAction = { - type: 'REQUEST3', - request: { url: '/' }, - meta: { isDependentRequest: true }, - }; + const firstRequestAction = createQuery( + 'REQUEST', + { url: '/' }, + { dependentRequestsNumber: 2 }, + )(); + const secondRequestAction = createQuery( + 'REQUEST2', + { url: '/' }, + { isDependentRequest: true }, + )(); + const thirdRequestAction = createQuery( + 'REQUEST3', + { url: '/' }, + { isDependentRequest: true }, + )(); const firsResponseAction = createSuccessAction(firstRequestAction); const secondResponseAction = createSuccessAction(secondRequestAction); const thirdResponseAction = createSuccessAction(thirdRequestAction); @@ -117,11 +118,11 @@ describe('middleware', () => { createServerSsrMiddleware(requestsPromise), ]); const store = mockStore({}); - const firstRequestAction = { - type: 'REQUEST', - request: { url: '/' }, - meta: { dependentRequestsNumber: 2 }, - }; + const firstRequestAction = createQuery( + 'REQUEST', + { url: '/' }, + { dependentRequestsNumber: 2 }, + )(); // const secondRequestAction = { // type: 'REQUEST2', // request: { url: '/' }, @@ -161,21 +162,21 @@ describe('middleware', () => { createServerSsrMiddleware(requestsPromise), ]); const store = mockStore({}); - const firstRequestAction = { - type: 'REQUEST', - request: { url: '/' }, - meta: { dependentRequestsNumber: 2 }, - }; - const secondRequestAction = { - type: 'REQUEST2', - request: { url: '/' }, - meta: { isDependentRequest: true }, - }; - const thirdRequestAction = { - type: 'REQUEST3', - request: { url: '/' }, - meta: { isDependentRequest: true }, - }; + const firstRequestAction = createQuery( + 'REQUEST', + { url: '/' }, + { dependentRequestsNumber: 2 }, + )(); + const secondRequestAction = createQuery( + 'REQUEST2', + { url: '/' }, + { isDependentRequest: true }, + )(); + const thirdRequestAction = createQuery( + 'REQUEST3', + { url: '/' }, + { isDependentRequest: true }, + )(); const firsResponseAction = createSuccessAction(firstRequestAction); const secondResponseAction = createSuccessAction(secondRequestAction); const thirdErrorAction = createErrorAction(thirdRequestAction); @@ -205,16 +206,16 @@ describe('middleware', () => { createServerSsrMiddleware(requestsPromise), ]); const store = mockStore({}); - const firstRequestAction = { - type: 'REQUEST', - request: { url: '/' }, - meta: { dependentRequestsNumber: 1 }, - }; - const secondRequestAction = { - type: 'REQUEST2', - request: { url: '/' }, - meta: { isDependentRequest: true, dependentRequestsNumber: 1 }, - }; + const firstRequestAction = createQuery( + 'REQUEST', + { url: '/' }, + { dependentRequestsNumber: 1 }, + )(); + const secondRequestAction = createQuery( + 'REQUEST2', + { url: '/' }, + { isDependentRequest: true, dependentRequestsNumber: 1 }, + )(); // const thirdRequestAction = { // type: 'REQUEST3', // request: { url: '/' }, diff --git a/packages/redux-requests/src/middleware/create-subscriptions-middleware.js b/packages/redux-requests/src/middleware/create-subscriptions-middleware.js index 06a3e4a2d..955f0f4e0 100644 --- a/packages/redux-requests/src/middleware/create-subscriptions-middleware.js +++ b/packages/redux-requests/src/middleware/create-subscriptions-middleware.js @@ -4,7 +4,7 @@ import { websocketClosed, openWebsocket, closeWebsocket, - getActionPayload, + isRequestActionSubscription, } from '../actions'; import { GET_WEBSOCKET, @@ -13,13 +13,10 @@ import { CLOSE_WEBSOCKET, WEBSOCKET_OPENED, } from '../constants'; - -import createRequestsStore from './create-requests-store'; +import { createLocalMutation } from '../requests-creators'; const shouldBeNormalized = (action, globalNormalize) => - action.meta?.normalize !== undefined - ? action.meta.normalize - : globalNormalize; + action.meta.normalize !== undefined ? action.meta.normalize : globalNormalize; const transformIntoLocalMutation = ( subscriptionAction, @@ -34,16 +31,13 @@ const transformIntoLocalMutation = ( } if (subscriptionAction.meta?.mutations) { - meta.mutations = mapObject(subscriptionAction.meta.mutations, (k, v) => ({ - local: true, - updateData: data => v(data, subscriptionData, message), - })); + meta.mutations = mapObject( + subscriptionAction.meta.mutations, + (k, v) => data => v(data, subscriptionData, message), + ); } - return { - type: `${subscriptionAction.type}_MUTATION`, - meta, - }; + return createLocalMutation(`${subscriptionAction.type}_MUTATION`, meta)(); }; /* @@ -178,8 +172,6 @@ export default ({ } if ((!ws && WS && url && !lazy) || action.type === OPEN_WEBSOCKET) { - const requestsStore = createRequestsStore(store); - clearLastReconnectTimeout(); clearLastHeartbeatTimeout(); @@ -197,7 +189,7 @@ export default ({ if (onOpen) { onOpen( - requestsStore, + store, ws, action.type === OPEN_WEBSOCKET ? action.props : null, ); @@ -214,7 +206,7 @@ export default ({ ws.addEventListener('error', e => { if (onError) { - onError(e, requestsStore, ws); + onError(e, store, ws); } }); @@ -225,7 +217,7 @@ export default ({ clearLastHeartbeatTimeout(); if (onClose) { - onClose(e, requestsStore, ws); + onClose(e, store, ws); } if (e.code !== 1000 && reconnectTimeout) { @@ -256,22 +248,22 @@ export default ({ } if (onMessage) { - onMessage(data, message, requestsStore); + onMessage(data, message, store); } const subscription = subscriptions[data.type]; if (subscription) { - if (subscription.meta?.getData) { + if (subscription.meta.getData) { data = subscription.meta.getData(data); } - if (subscription.meta?.onMessage) { - subscription.meta.onMessage(data, message, requestsStore); + if (subscription.meta.onMessage) { + subscription.meta.onMessage(data, message, store); } if ( - subscription.meta?.mutations || + subscription.meta.mutations || shouldBeNormalized(subscription, normalize) ) { store.dispatch( @@ -298,66 +290,52 @@ export default ({ ws.close(action.code); ws = null; return response; - } else if (action.type === WEBSOCKET_OPENED) { - Object.values(subscriptions).forEach(subscriptionAction => { - const actionPayload = getActionPayload(subscriptionAction); + } - if (actionPayload.subscription) { + if (action.type === WEBSOCKET_OPENED) { + Object.values(subscriptions).forEach(subscriptionAction => { + if (subscriptionAction.payload) { ws.send( JSON.stringify( onSend - ? onSend(actionPayload.subscription, subscriptionAction) - : actionPayload.subscription, + ? onSend(subscriptionAction.payload, subscriptionAction) + : subscriptionAction.payload, ), ); } }); } else if (action.type === STOP_SUBSCRIPTIONS) { - const requestsStore = createRequestsStore(store); - if (!action.subscriptions) { if (onStopSubscriptions) { - onStopSubscriptions( - Object.keys(subscriptions), - action, - ws, - requestsStore, - ); + onStopSubscriptions(Object.keys(subscriptions), action, ws, store); } subscriptions = {}; } else { if (onStopSubscriptions) { - onStopSubscriptions(action.subscriptions, action, ws, requestsStore); + onStopSubscriptions(action.subscriptions, action, ws, store); } subscriptions = mapObject(subscriptions, (k, v) => action.subscriptions.includes(k) ? undefined : v, ); } - } else if ( - action.subscription !== undefined || - action.payload?.subscription !== undefined - ) { + } else if (isRequestActionSubscription(action)) { if ( - action.meta?.onMessage || - action.meta?.mutations || + action.meta.onMessage || + action.meta.mutations || shouldBeNormalized(action, normalize) ) { subscriptions = { ...subscriptions, - [action.type + (action.meta?.requestKey || '')]: action, + [action.type + (action.meta.requestKey || '')]: action, }; } - const actionPayload = getActionPayload(action); - - if (actionPayload.subscription && ws && active) { + if (action.payload && ws && active) { ws.send( JSON.stringify( - onSend - ? onSend(actionPayload.subscription, action) - : actionPayload.subscription, + onSend ? onSend(action.payload, action) : action.payload, ), ); } diff --git a/packages/redux-requests/src/middleware/create-subscriptions-middleware.spec.js b/packages/redux-requests/src/middleware/create-subscriptions-middleware.spec.js index 9142895a2..8aabba55b 100644 --- a/packages/redux-requests/src/middleware/create-subscriptions-middleware.spec.js +++ b/packages/redux-requests/src/middleware/create-subscriptions-middleware.spec.js @@ -1,6 +1,7 @@ import configureStore from 'redux-mock-store'; import { websocketOpened, getWebsocket, websocketClosed } from '../actions'; +import { createQuery, createSubscription } from '../requests-creators'; import createSubscriptionsMiddleware from './create-subscriptions-middleware'; @@ -74,7 +75,7 @@ describe('middleware', () => { }), ]); const store = mockStore({}); - const action = { type: 'REQUEST', request: { url: '/' } }; + const action = createQuery('REQUEST', { url: '/' })(); expect(store.dispatch(action)).toBe(action); expect(store.getActions()).toEqual([websocketOpened(), action]); }); @@ -138,13 +139,9 @@ describe('middleware', () => { const store = mockStore({}); const ws = store.dispatch(getWebsocket()); - const subscription = { - type: 'SUBSCRIPTION2', - subscription: null, - meta: { - onMessage: jest.fn(), - }, - }; + const subscription = createSubscription('SUBSCRIPTION2', null, { + onMessage: jest.fn(), + })(); store.dispatch(subscription); ws.sendToClient({ type: 'SUBSCRIPTION' }); @@ -165,13 +162,9 @@ describe('middleware', () => { const store = mockStore({}); const ws = store.dispatch(getWebsocket()); - const subscription = { - type: 'SUBSCRIPTION', - subscription: null, - meta: { - onMessage: jest.fn(), - }, - }; + const subscription = createSubscription('SUBSCRIPTION', null, { + onMessage: jest.fn(), + })(); store.dispatch(subscription); ws.sendToClient({ type: 'SUBSCRIPTION' }); @@ -201,14 +194,10 @@ describe('middleware', () => { const store = mockStore({}); const ws = store.dispatch(getWebsocket()); - const subscription = { - type: 'SUBSCRIPTION', - subscription: null, - meta: { - getData: data => data.type, - onMessage: jest.fn(), - }, - }; + const subscription = createSubscription('SUBSCRIPTION', null, { + getData: data => data.type, + onMessage: jest.fn(), + })(); store.dispatch(subscription); ws.sendToClient({ type: 'SUBSCRIPTION' }); @@ -235,17 +224,13 @@ describe('middleware', () => { const store = mockStore({}); const ws = store.dispatch(getWebsocket()); - const onBookAdded = { - type: 'ON_BOOK_ADDED', - subscription: null, - meta: { - normalize: true, - mutations: { - FETCH_BOOK: (data, subscriptionData) => - data.concat(subscriptionData.newBook), - }, + const onBookAdded = createSubscription('ON_BOOK_ADDED', null, { + normalize: true, + mutations: { + FETCH_BOOK: (data, subscriptionData) => + data.concat(subscriptionData.newBook), }, - }; + })(); store.dispatch(onBookAdded); ws.sendToClient({ type: 'ON_BOOK_ADDED', newBook: 'New book' }); @@ -259,9 +244,9 @@ describe('middleware', () => { type: 'ON_BOOK_ADDED', newBook: 'New book', }); - expect(dispatchedActions[2].meta.mutations.FETCH_BOOK.local).toBe(true); + expect(dispatchedActions[2].meta.requestType).toBe('LOCAL_MUTATION'); expect( - dispatchedActions[2].meta.mutations.FETCH_BOOK.updateData(['Old book']), + dispatchedActions[2].meta.mutations.FETCH_BOOK(['Old book']), ).toEqual(['Old book', 'New book']); }); @@ -277,10 +262,9 @@ describe('middleware', () => { const store = mockStore({}); const ws = store.dispatch(getWebsocket()); - const subscription = { + const subscription = createSubscription('SUBSCRIPTION', { type: 'SUBSCRIPTION', - subscription: { type: 'SUBSCRIPTION' }, - }; + })(); store.dispatch(subscription); ws.sendToClient({ type: 'SUBSCRIPTION' }); diff --git a/packages/redux-requests/src/middleware/index.js b/packages/redux-requests/src/middleware/index.js index 7db814cda..5104426d8 100644 --- a/packages/redux-requests/src/middleware/index.js +++ b/packages/redux-requests/src/middleware/index.js @@ -4,4 +4,3 @@ export { default as createServerSsrMiddleware } from './create-server-ssr-middle export { default as createSendRequestsMiddleware } from './create-send-requests-middleware'; export { default as createPollingMiddleware } from './create-polling-middleware'; export { default as createSubscriptionsMiddleware } from './create-subscriptions-middleware'; -export { default as createRequestsStore } from './create-requests-store'; diff --git a/packages/redux-requests/src/reducers/cache-reducer.js b/packages/redux-requests/src/reducers/cache-reducer.js index b169c61da..b6cae72b0 100644 --- a/packages/redux-requests/src/reducers/cache-reducer.js +++ b/packages/redux-requests/src/reducers/cache-reducer.js @@ -6,14 +6,11 @@ const getNewCacheTimeout = cache => const getRequestKey = action => action.type + (action.meta.requestKey || ''); -const getRequestTypeString = requestType => - typeof requestType === 'function' ? requestType.toString() : requestType; - const getRequestKeys = requests => requests.map(v => typeof v === 'object' - ? getRequestTypeString(v.requestType) + v.requestKey - : getRequestTypeString(v), + ? v.requestType.toString() + v.requestKey + : v.toString(), ); export default (state, action) => { diff --git a/packages/redux-requests/src/reducers/cache-reducer.spec.js b/packages/redux-requests/src/reducers/cache-reducer.spec.js index 5f0fecbf3..d289563d5 100644 --- a/packages/redux-requests/src/reducers/cache-reducer.spec.js +++ b/packages/redux-requests/src/reducers/cache-reducer.spec.js @@ -1,6 +1,7 @@ import { advanceTo, clear } from 'jest-date-mock'; import { clearRequestsCache, createSuccessAction } from '../actions'; +import { createQuery } from '../requests-creators'; import cacheReducer from './cache-reducer'; @@ -34,7 +35,7 @@ describe('reducers', () => { it('doesnt do anything for request action', () => { expect( - cacheReducer(defaultState, { type: 'REQUEST', request: { url: '/' } }), + cacheReducer(defaultState, createQuery('REQUEST', { url: '/' })()), ).toBe(defaultState); }); @@ -42,10 +43,9 @@ describe('reducers', () => { expect( cacheReducer( defaultState, - createSuccessAction( - { type: 'QUERY4', request: { url: '/' } }, - { data: 'data' }, - ), + createSuccessAction(createQuery('QUERY4', { url: '/' })(), { + data: 'data', + }), ), ).toBe(defaultState); }); @@ -55,11 +55,11 @@ describe('reducers', () => { cacheReducer( defaultState, createSuccessAction( - { - type: 'QUERY4', - request: { url: '/' }, - meta: { cache: true, cacheResponse: { data: 'data' } }, - }, + createQuery( + 'QUERY4', + { url: '/' }, + { cache: true, cacheResponse: { data: 'data' } }, + )(), { data: 'data' }, ), ), @@ -71,7 +71,7 @@ describe('reducers', () => { cacheReducer( defaultState, createSuccessAction( - { type: 'QUERY4', request: { url: '/' }, meta: { cache: true } }, + createQuery('QUERY4', { url: '/' }, { cache: true })(), { data: 'data' }, ), ), @@ -88,7 +88,7 @@ describe('reducers', () => { cacheReducer( defaultState, createSuccessAction( - { type: 'QUERY4', request: { url: '/' }, meta: { cache: 1 } }, + createQuery('QUERY4', { url: '/' }, { cache: 1 })(), { data: 'data' }, ), ), diff --git a/packages/redux-requests/src/reducers/mutations-reducer.js b/packages/redux-requests/src/reducers/mutations-reducer.js index fcbd767b0..975b21b9a 100644 --- a/packages/redux-requests/src/reducers/mutations-reducer.js +++ b/packages/redux-requests/src/reducers/mutations-reducer.js @@ -3,12 +3,13 @@ import { isAbortAction, isResponseAction, getRequestActionFromResponse, + isRequestAction, + isRequestActionMutation, } from '../actions'; export default (state, action) => { - if (!isResponseAction(action)) { - const mutationType = - action.type + (action.meta?.requestKey ? action.meta.requestKey : ''); + if (isRequestAction(action) && isRequestActionMutation(action)) { + const mutationType = action.type + (action.meta.requestKey || ''); return { ...state, @@ -20,36 +21,41 @@ export default (state, action) => { }; } - const requestAction = getRequestActionFromResponse(action); - const mutationType = - requestAction.type + - (action.meta?.requestKey ? action.meta.requestKey : ''); + if ( + isResponseAction(action) && + isRequestActionMutation(getRequestActionFromResponse(action)) + ) { + const requestAction = getRequestActionFromResponse(action); + const mutationType = requestAction.type + (action.meta.requestKey || ''); + + if (isErrorAction(action)) { + return { + ...state, + [mutationType]: { + error: action.error, + pending: state[mutationType].pending - 1, + ref: state[mutationType].ref, + }, + }; + } + + if ( + isAbortAction(action) && + state[mutationType].pending === 1 && + state[mutationType].error === null + ) { + return state; + } - if (isErrorAction(action)) { return { ...state, [mutationType]: { - error: action.payload ? action.payload : action.error, + error: null, pending: state[mutationType].pending - 1, ref: state[mutationType].ref, }, }; } - if ( - isAbortAction(action) && - state[mutationType].pending === 1 && - state[mutationType].error === null - ) { - return state; - } - - return { - ...state, - [mutationType]: { - error: null, - pending: state[mutationType].pending - 1, - ref: state[mutationType].ref, - }, - }; + return state; }; diff --git a/packages/redux-requests/src/reducers/progress-reducer.js b/packages/redux-requests/src/reducers/progress-reducer.js index 1c260bae9..dee9b8e53 100644 --- a/packages/redux-requests/src/reducers/progress-reducer.js +++ b/packages/redux-requests/src/reducers/progress-reducer.js @@ -1,6 +1,7 @@ import { SET_DOWNLOAD_PROGRESS, SET_UPLOAD_PROGRESS } from '../constants'; +import { isRequestAction } from '../actions'; -export default (state, action, config) => { +export default (state, action) => { if (action.type === SET_DOWNLOAD_PROGRESS) { return { ...state, @@ -21,22 +22,22 @@ export default (state, action, config) => { }; } - if (config.isRequestAction(action) && action.meta?.measureDownloadProgress) { + if (isRequestAction(action) && action.meta.measureDownloadProgress) { return { ...state, downloadProgress: { ...state.downloadProgress, - [action.type + (action.meta?.requestKey || '')]: 0, + [action.type + (action.meta.requestKey || '')]: 0, }, }; } - if (config.isRequestAction(action) && action.meta?.measureUploadProgress) { + if (isRequestAction(action) && action.meta.measureUploadProgress) { return { ...state, uploadProgress: { ...state.uploadProgress, - [action.type + (action.meta?.requestKey || '')]: 0, + [action.type + (action.meta.requestKey || '')]: 0, }, }; } diff --git a/packages/redux-requests/src/reducers/queries-reducer.js b/packages/redux-requests/src/reducers/queries-reducer.js index c5a359a16..29a11f4fc 100644 --- a/packages/redux-requests/src/reducers/queries-reducer.js +++ b/packages/redux-requests/src/reducers/queries-reducer.js @@ -7,9 +7,13 @@ import { getRequestActionFromResponse, isErrorAction, isSuccessAction, + isRequestAction, + isRequestActionQuery, + isRequestActionLocalMutation, } from '../actions'; import { getQuery } from '../selectors'; -import { normalize, mergeData } from '../normalizers'; +import { normalize, mergeData, getDependentKeys } from '../normalizers'; +import { mapObject } from '../helpers'; import updateData from './update-data'; @@ -21,16 +25,59 @@ const getInitialQuery = normalized => ({ ref: {}, normalized, usedKeys: normalized ? {} : null, + dependencies: normalized ? [] : null, }); -const getDataFromResponseAction = action => - action.payload ? action.payload.data : action.response.data; - const shouldBeNormalized = (action, config) => - action.meta?.normalize !== undefined + action.meta.normalize !== undefined ? action.meta.normalize : config.normalize; +const addQueryAsDependency = (dependentQueries, dependencies, queryType) => { + dependencies.forEach(dependency => { + if (!dependentQueries[dependency]) { + dependentQueries = { ...dependentQueries, [dependency]: [queryType] }; + } + + if (!dependentQueries[dependency].includes(queryType)) { + dependentQueries = { + ...dependentQueries, + [dependency]: [...dependentQueries[dependency], queryType], + }; + } + }); + + return dependentQueries; +}; + +const removeQueryAsDependency = (dependentQueries, dependencies, queryType) => { + dependencies.forEach(dependency => { + if (dependentQueries[dependency].length > 1) { + dependentQueries = { + ...dependentQueries, + [dependency]: dependentQueries[dependency].filter(v => v !== queryType), + }; + } else { + dependentQueries = mapObject(dependentQueries, (k, v) => + k === dependency ? undefined : v, + ); + } + }); + + return dependentQueries; +}; + +const getDependenciesDiff = (oldDependencies, newDependencies) => { + return { + addedDependencies: newDependencies.filter( + v => !oldDependencies.includes(v), + ), + removedDependencies: oldDependencies.filter( + v => !newDependencies.includes(v), + ), + }; +}; + const queryReducer = (state, action, actionType, config, normalizedData) => { if (state === undefined) { state = getInitialQuery(shouldBeNormalized(action, config)); @@ -64,7 +111,7 @@ const queryReducer = (state, action, actionType, config, normalizedData) => { ? state : { ...state, - data: getDataFromResponseAction(action), + data: action.response.data, pending: state.pending - 1, error: null, }; @@ -76,7 +123,7 @@ const queryReducer = (state, action, actionType, config, normalizedData) => { ...state, data: null, pending: state.pending - 1, - error: action.payload ? action.payload : action.error, + error: action.error, }; case abort(actionType): { if (state.pending === 1 && state.data === null && state.error === null) { @@ -102,14 +149,14 @@ const queryReducer = (state, action, actionType, config, normalizedData) => { } }; -const maybeGetQueryActionType = (action, config) => { - if (config.isRequestAction(action) && config.isRequestActionQuery(action)) { +const maybeGetQueryActionType = action => { + if (isRequestAction(action) && isRequestActionQuery(action)) { return action.type; } if ( isResponseAction(action) && - config.isRequestActionQuery(getRequestActionFromResponse(action)) + isRequestActionQuery(getRequestActionFromResponse(action)) ) { return getRequestActionFromResponse(action).type; } @@ -117,10 +164,9 @@ const maybeGetQueryActionType = (action, config) => { return null; }; -const updateNormalizedData = (normalizedData, action, config) => { - if (config.isRequestAction(action) && action.meta?.optimisticData) { - const [, newNormalizedData] = normalize(action.meta.optimisticData, config); - return mergeData(normalizedData, newNormalizedData); +const maybeGetMutationData = (action, config) => { + if (isRequestAction(action) && action.meta.optimisticData) { + return action.meta.optimisticData; } if ( @@ -128,47 +174,125 @@ const updateNormalizedData = (normalizedData, action, config) => { isErrorAction(action) && action.meta.revertedData ) { - const [, newNormalizedData] = normalize(action.meta.revertedData, config); - return mergeData(normalizedData, newNormalizedData); + return action.meta.revertedData; } if ( isResponseAction(action) && isSuccessAction(action) && shouldBeNormalized(action, config) && - !config.isRequestActionQuery(getRequestActionFromResponse(action)) + !isRequestActionQuery(getRequestActionFromResponse(action)) ) { - const [, newNormalizedData] = normalize( - getDataFromResponseAction(action), - config, - ); - return mergeData(normalizedData, newNormalizedData); + return action.response.data; } - if (action.meta?.localData) { - const [, newNormalizedData] = normalize(action.meta.localData, config); - return mergeData(normalizedData, newNormalizedData); + if (isRequestActionLocalMutation(action) && action.meta.localData) { + return action.meta.localData; } - return normalizedData; + return null; +}; + +const updateNormalizedData = (normalizedData, mutationData, config) => { + const [, newNormalizedData] = normalize(mutationData, config); + return mergeData(normalizedData, newNormalizedData); +}; + +const getQueriesDependentOnMutation = ( + dependentQueries, + mutationDependencies, +) => { + const queries = []; + const orphanDependencies = []; + + mutationDependencies.forEach(dependency => { + if (dependentQueries[dependency]) { + queries.push(...dependentQueries[dependency]); + } else { + orphanDependencies.push(dependency); + } + }); + + return { foundQueries: [...new Set(queries)], orphanDependencies }; }; export default (state, action, config = defaultConfig) => { - let normalizedData = updateNormalizedData( - state.normalizedData, - action, - config, - ); + let { normalizedData, queries, dependentQueries } = state; + const mutationDataToNormalize = maybeGetMutationData(action, config); + + if (mutationDataToNormalize) { + const [, mutationNormalizedData] = normalize( + mutationDataToNormalize, + config, + ); + const mutationDependencies = Object.keys(mutationNormalizedData); + const { foundQueries, orphanDependencies } = getQueriesDependentOnMutation( + dependentQueries, + mutationDependencies, + ); + const recalculatedQueries = {}; + normalizedData = updateNormalizedData( + normalizedData, + mutationDataToNormalize, + config, + ); + const potentialDependenciesToRemove = new Set(orphanDependencies); + + foundQueries.forEach(query => { + const dependencies = [ + ...getDependentKeys( + queries[query].data, + normalizedData, + queries[query].usedKeys, + ), + ]; + + const { addedDependencies, removedDependencies } = getDependenciesDiff( + queries[query].dependencies, + dependencies, + ); + + removedDependencies.forEach(v => { + potentialDependenciesToRemove.add(v); + }); + + dependentQueries = addQueryAsDependency( + dependentQueries, + addedDependencies, + query, + ); + + dependentQueries = removeQueryAsDependency( + dependentQueries, + removedDependencies, + query, + ); + + recalculatedQueries[query] = { + ...queries[query], + dependencies, + }; + }); + + queries = { ...queries, ...recalculatedQueries }; + + const reallyRemovedDeps = [...potentialDependenciesToRemove].filter( + v => !dependentQueries[v], + ); + normalizedData = mapObject(normalizedData, (k, v) => + reallyRemovedDeps.includes(k) ? undefined : v, + ); + } if (action.meta?.mutations) { return { queries: { - ...state.queries, + ...queries, ...Object.keys(action.meta.mutations) - .filter(actionType => !!state.queries[actionType]) + .filter(actionType => !!queries[actionType]) .reduce((prev, actionType) => { const updatedQuery = queryReducer( - state.queries[actionType], + queries[actionType], action, actionType, config, @@ -177,14 +301,28 @@ export default (state, action, config = defaultConfig) => { if ( updatedQuery.normalized && - updatedQuery.data !== state.queries[actionType].data + updatedQuery.data !== queries[actionType].data ) { const [newdata, newNormalizedData, usedKeys] = normalize( updatedQuery.data, config, ); + const dependencies = [ + ...getDependentKeys(newdata, newNormalizedData, usedKeys), + ]; normalizedData = mergeData(normalizedData, newNormalizedData); - prev[actionType] = { ...updatedQuery, data: newdata, usedKeys }; + prev[actionType] = { + ...updatedQuery, + data: newdata, + dependencies, + usedKeys, + }; + + dependentQueries = addQueryAsDependency( + dependentQueries, + dependencies, + actionType, + ); } else { prev[actionType] = updatedQuery; } @@ -192,10 +330,11 @@ export default (state, action, config = defaultConfig) => { }, {}), }, normalizedData, + dependentQueries, }; } - const queryActionType = maybeGetQueryActionType(action, config); + const queryActionType = maybeGetQueryActionType(action); if (queryActionType) { const queryType = @@ -203,7 +342,7 @@ export default (state, action, config = defaultConfig) => { ? queryActionType + action.meta.requestKey : queryActionType; const updatedQuery = queryReducer( - state.queries[queryType], + queries[queryType], action, queryActionType, config, @@ -211,37 +350,51 @@ export default (state, action, config = defaultConfig) => { if (updatedQuery === undefined) { // eslint-disable-next-line no-unused-vars - const { [queryType]: toRemove, ...remainingQueries } = state.queries; + const { [queryType]: toRemove, ...remainingQueries } = queries; return { queries: remainingQueries, normalizedData, + dependentQueries, }; } if ( updatedQuery.normalized && updatedQuery.data && - (!state.queries[queryType] || - state.queries[queryType].data !== updatedQuery.data) + (!queries[queryType] || queries[queryType].data !== updatedQuery.data) ) { const [data, newNormalizedData, usedKeys] = normalize( updatedQuery.data, config, ); + const dependencies = [ + ...getDependentKeys(data, newNormalizedData, usedKeys), + ]; + return { queries: { - ...state.queries, - [queryType]: { ...updatedQuery, data, usedKeys }, + ...queries, + [queryType]: { + ...updatedQuery, + data, + usedKeys, + dependencies, + }, }, normalizedData: mergeData(normalizedData, newNormalizedData), + dependentQueries: addQueryAsDependency( + dependentQueries, + dependencies, + queryType, + ), }; } return { queries: { - ...state.queries, + ...queries, [queryType]: updatedQuery, }, normalizedData, @@ -253,5 +406,7 @@ export default (state, action, config = defaultConfig) => { : { ...state, normalizedData, + queries, + dependentQueries, }; }; diff --git a/packages/redux-requests/src/reducers/queries-reducer.simple.spec.js b/packages/redux-requests/src/reducers/queries-reducer.simple.spec.js new file mode 100644 index 000000000..11c93ba63 --- /dev/null +++ b/packages/redux-requests/src/reducers/queries-reducer.simple.spec.js @@ -0,0 +1,109 @@ +import defaultConfig from '../default-config'; +import { + createSuccessAction, + createErrorAction, + createAbortAction, +} from '../actions'; +import { createQuery } from '../requests-creators'; + +import queriesReducer from './queries-reducer'; + +describe('reducers', () => { + describe('queriesReducer', () => { + describe('simple', () => { + const defaultState = { + data: null, + error: null, + pending: 0, + pristine: true, + normalized: false, + ref: {}, + usedKeys: null, + dependencies: null, + }; + const requestAction = createQuery('FETCH_BOOK', { url: '/ ' })(); + + it('returns the same state for not handled action', () => { + const state = { queries: {}, normalizedData: {} }; + expect(queriesReducer(state, { type: 'STH ' }, defaultConfig)).toBe( + state, + ); + }); + + it('handles request query action', () => { + expect( + queriesReducer( + { queries: {}, normalizedData: {} }, + requestAction, + defaultConfig, + ), + ).toEqual({ + queries: { + FETCH_BOOK: { + ...defaultState, + pending: 1, + pristine: false, + }, + }, + normalizedData: {}, + }); + }); + + it('handles success query action', () => { + expect( + queriesReducer( + { queries: {}, normalizedData: {} }, + createSuccessAction(requestAction, { data: 'data' }), + defaultConfig, + ), + ).toEqual({ + queries: { + FETCH_BOOK: { + ...defaultState, + pending: -1, + data: 'data', + }, + }, + normalizedData: {}, + }); + }); + + it('handles error query action', () => { + expect( + queriesReducer( + { queries: {}, normalizedData: {} }, + createErrorAction(requestAction, 'error'), + defaultConfig, + ), + ).toEqual({ + queries: { + FETCH_BOOK: { + ...defaultState, + pending: -1, + error: 'error', + }, + }, + normalizedData: {}, + }); + }); + + it('handles abort query action', () => { + expect( + queriesReducer( + { queries: {}, normalizedData: {} }, + createAbortAction(requestAction), + defaultConfig, + ), + ).toEqual({ + queries: { + FETCH_BOOK: { + ...defaultState, + pending: -1, + }, + }, + normalizedData: {}, + }); + }); + }); + }); +}); diff --git a/packages/redux-requests/src/reducers/queries-reducer.spec.js b/packages/redux-requests/src/reducers/queries-reducer.spec.js deleted file mode 100644 index 2e8c7fd3c..000000000 --- a/packages/redux-requests/src/reducers/queries-reducer.spec.js +++ /dev/null @@ -1,1114 +0,0 @@ -import defaultConfig from '../default-config'; -import { - createSuccessAction, - createErrorAction, - createAbortAction, -} from '../actions'; - -import queriesReducer from './queries-reducer'; - -describe('reducers', () => { - describe('queriesReducer', () => { - describe('simple', () => { - const defaultState = { - data: null, - error: null, - pending: 0, - pristine: true, - normalized: false, - ref: {}, - usedKeys: null, - }; - const requestAction = { - type: 'FETCH_BOOK', - request: { url: '/ ' }, - }; - - it('returns the same state for not handled action', () => { - const state = { queries: {}, normalizedData: {} }; - expect(queriesReducer(state, { type: 'STH ' }, defaultConfig)).toBe( - state, - ); - }); - - it('handles request query action', () => { - expect( - queriesReducer( - { queries: {}, normalizedData: {} }, - requestAction, - defaultConfig, - ), - ).toEqual({ - queries: { - FETCH_BOOK: { - ...defaultState, - pending: 1, - pristine: false, - }, - }, - normalizedData: {}, - }); - }); - - it('handles success query action', () => { - expect( - queriesReducer( - { queries: {}, normalizedData: {} }, - createSuccessAction(requestAction, { data: 'data' }), - defaultConfig, - ), - ).toEqual({ - queries: { - FETCH_BOOK: { - ...defaultState, - pending: -1, - data: 'data', - }, - }, - normalizedData: {}, - }); - }); - - it('handles error query action', () => { - expect( - queriesReducer( - { queries: {}, normalizedData: {} }, - createErrorAction(requestAction, 'error'), - defaultConfig, - ), - ).toEqual({ - queries: { - FETCH_BOOK: { - ...defaultState, - pending: -1, - error: 'error', - }, - }, - normalizedData: {}, - }); - }); - - it('handles abort query action', () => { - expect( - queriesReducer( - { queries: {}, normalizedData: {} }, - createAbortAction(requestAction), - defaultConfig, - ), - ).toEqual({ - queries: { - FETCH_BOOK: { - ...defaultState, - pending: -1, - }, - }, - normalizedData: {}, - }); - }); - - it('supports FSA actions for getting data and error by default', () => { - const action = { - type: 'FETCH_BOOK', - payload: { - request: { - url: '/', - }, - }, - }; - - expect( - queriesReducer( - { queries: {}, normalizedData: {} }, - createSuccessAction(action, { data: 'data' }), - defaultConfig, - ), - ).toEqual({ - queries: { - FETCH_BOOK: { - ...defaultState, - pending: -1, - data: 'data', - }, - }, - normalizedData: {}, - }); - - expect( - queriesReducer( - { queries: {}, normalizedData: {} }, - createErrorAction(action, 'error'), - defaultConfig, - ), - ).toEqual({ - queries: { - FETCH_BOOK: { - ...defaultState, - pending: -1, - error: 'error', - }, - }, - normalizedData: {}, - }); - }); - }); - - describe('with requestKey', () => { - const defaultState = { - data: null, - error: null, - pending: 0, - pristine: true, - normalized: false, - ref: {}, - usedKeys: null, - }; - const requestAction = { - type: 'FETCH_BOOK', - request: { url: '/ ' }, - meta: { - requestKey: 1, - }, - }; - - it('handles request query action', () => { - expect( - queriesReducer( - { queries: {}, normalizedData: {} }, - requestAction, - defaultConfig, - ), - ).toEqual({ - queries: { - FETCH_BOOK1: { - ...defaultState, - pending: 1, - pristine: false, - }, - }, - normalizedData: {}, - }); - }); - - it('handles success query action', () => { - expect( - queriesReducer( - { queries: {}, normalizedData: {} }, - createSuccessAction(requestAction, { data: 'data' }), - defaultConfig, - ), - ).toEqual({ - queries: { - FETCH_BOOK1: { - ...defaultState, - pending: -1, - data: 'data', - }, - }, - normalizedData: {}, - }); - }); - - it('handles error query action', () => { - expect( - queriesReducer( - { queries: {}, normalizedData: {} }, - createErrorAction(requestAction, 'error'), - defaultConfig, - ), - ).toEqual({ - queries: { - FETCH_BOOK1: { - ...defaultState, - pending: -1, - error: 'error', - }, - }, - normalizedData: {}, - }); - }); - - it('handles abort query action', () => { - expect( - queriesReducer( - { queries: {}, normalizedData: {} }, - createAbortAction(requestAction), - defaultConfig, - ), - ).toEqual({ - queries: { - FETCH_BOOK1: { - ...defaultState, - pending: -1, - }, - }, - normalizedData: {}, - }); - }); - }); - - describe('with mutations', () => { - const initialState = { - queries: { - FETCH_BOOK: { - data: 'data', - error: null, - pending: 0, - pristine: false, - normalized: false, - }, - }, - normalizedData: {}, - }; - - const MUTATION_ACTION = 'MUTATION_ACTION'; - - it('can update data optimistic', () => { - expect( - queriesReducer( - initialState, - { - type: MUTATION_ACTION, - request: { url: '/books', method: 'post' }, - meta: { - mutations: { - FETCH_BOOK: { - updateDataOptimistic: data => `${data} suffix`, - }, - }, - }, - }, - defaultConfig, - ), - ).toEqual({ - queries: { - FETCH_BOOK: { - data: 'data suffix', - error: null, - pending: 0, - pristine: false, - normalized: false, - }, - }, - normalizedData: {}, - }); - }); - - it('keeps data updated optimistic on mutation success if updateData undefined', () => { - expect( - queriesReducer( - initialState, - createSuccessAction( - { - type: MUTATION_ACTION, - request: { url: '/books', method: 'post' }, - meta: { - mutations: { - FETCH_BOOK: { - updateDataOptimistic: data => `${data} suffix`, - }, - }, - }, - }, - { data: 'updated data' }, - ), - defaultConfig, - ), - ).toEqual(initialState); - }); - - it('handles updateData customized per mutation', () => { - expect( - queriesReducer( - initialState, - createSuccessAction( - { - type: MUTATION_ACTION, - request: { url: '/books', method: 'post' }, - meta: { - mutations: { - FETCH_BOOK: (data, mutationData) => - data + mutationData.nested, - }, - }, - }, - { data: { nested: 'suffix' } }, - ), - defaultConfig, - ), - ).toEqual({ - queries: { - FETCH_BOOK: { - data: 'datasuffix', - error: null, - pending: 0, - pristine: false, - normalized: false, - }, - }, - normalizedData: {}, - }); - }); - - it('handles updateData customized per mutation in FSA action', () => { - expect( - queriesReducer( - initialState, - createSuccessAction( - { - type: MUTATION_ACTION, - payload: { - request: { url: '/books', method: 'post' }, - }, - meta: { - mutations: { - FETCH_BOOK: (data, mutationData) => - data + mutationData.nested, - }, - }, - }, - { data: { nested: 'suffix' } }, - ), - defaultConfig, - ), - ).toEqual({ - queries: { - FETCH_BOOK: { - data: 'datasuffix', - error: null, - pending: 0, - pristine: false, - normalized: false, - }, - }, - normalizedData: {}, - }); - }); - - it('handles updateData customized per mutation defined in updateData object key', () => { - expect( - queriesReducer( - initialState, - createSuccessAction( - { - type: MUTATION_ACTION, - request: { url: '/books', method: 'post' }, - meta: { - mutations: { - FETCH_BOOK: { - updateData: (data, mutationData) => - data + mutationData.nested, - }, - }, - }, - }, - { data: { nested: 'suffix' } }, - ), - defaultConfig, - ), - ).toEqual({ - queries: { - FETCH_BOOK: { - data: 'datasuffix', - error: null, - pending: 0, - pristine: false, - normalized: false, - }, - }, - normalizedData: {}, - }); - }); - - it('reverts optimistic update on mutation error', () => { - expect( - queriesReducer( - initialState, - createErrorAction( - { - type: MUTATION_ACTION, - request: { url: '/books', method: 'post' }, - meta: { - mutations: { - FETCH_BOOK: { - updateDataOptimistic: () => 'data2', - revertData: data => `${data} reverted`, - }, - }, - }, - }, - 'error', - ), - defaultConfig, - ), - ).toEqual({ - queries: { - FETCH_BOOK: { - data: 'data reverted', - error: null, - pending: 0, - pristine: false, - normalized: false, - }, - }, - normalizedData: {}, - }); - }); - - it('doesnt change data on mutation error without optimistic update revertData', () => { - expect( - queriesReducer( - initialState, - createErrorAction( - { - type: MUTATION_ACTION, - request: { url: '/books', method: 'post' }, - meta: { - mutations: { - FETCH_BOOK: { - updateDataOptimistic: () => 'data2', - }, - }, - }, - }, - 'error', - ), - defaultConfig, - ), - ).toEqual({ - queries: { - FETCH_BOOK: { - data: 'data', - error: null, - pending: 0, - pristine: false, - normalized: false, - }, - }, - normalizedData: {}, - }); - }); - - it('reverts optimistic update on mutation abort', () => { - expect( - queriesReducer( - initialState, - createAbortAction({ - type: MUTATION_ACTION, - request: { url: '/books', method: 'post' }, - meta: { - mutations: { - FETCH_BOOK: { - updateDataOptimistic: () => 'data2', - revertData: data => `${data} reverted`, - }, - }, - }, - }), - defaultConfig, - ), - ).toEqual({ - queries: { - FETCH_BOOK: { - data: 'data reverted', - error: null, - pending: 0, - pristine: false, - normalized: false, - }, - }, - normalizedData: {}, - }); - }); - - it('handles local mutations', () => { - expect( - queriesReducer( - initialState, - { - type: 'LOCAL_MUTATION_ACTION', - meta: { - mutations: { - FETCH_BOOK: { - local: true, - updateData: data => `${data} suffix`, - }, - }, - }, - }, - defaultConfig, - ), - ).toEqual({ - queries: { - FETCH_BOOK: { - data: 'data suffix', - error: null, - pending: 0, - pristine: false, - normalized: false, - }, - }, - normalizedData: {}, - }); - }); - }); - - describe('with mutations with query request key', () => { - const initialState = { - queries: { - FETCH_BOOK: { - data: 'data', - error: null, - pending: 0, - pristine: false, - normalized: false, - }, - FETCH_BOOK1: { - data: 'data', - error: null, - pending: 0, - pristine: false, - normalized: false, - }, - }, - normalizedData: {}, - }; - - const MUTATION_ACTION = 'MUTATION_ACTION'; - - it('can update data optimistic', () => { - expect( - queriesReducer( - initialState, - { - type: MUTATION_ACTION, - request: { url: '/books', method: 'post' }, - meta: { - mutations: { - FETCH_BOOK1: { - updateDataOptimistic: data => `${data} suffix`, - }, - }, - }, - }, - defaultConfig, - ), - ).toEqual({ - queries: { - FETCH_BOOK: { - data: 'data', - error: null, - pending: 0, - pristine: false, - normalized: false, - }, - FETCH_BOOK1: { - data: 'data suffix', - error: null, - pending: 0, - pristine: false, - normalized: false, - }, - }, - normalizedData: {}, - }); - }); - - it('handles updateData customized per mutation defined in updateData object key', () => { - expect( - queriesReducer( - initialState, - createSuccessAction( - { - type: MUTATION_ACTION, - request: { url: '/books', method: 'post' }, - meta: { - mutations: { - FETCH_BOOK1: { - updateData: (data, mutationData) => - data + mutationData.nested, - }, - }, - }, - }, - { data: { nested: 'suffix' } }, - ), - defaultConfig, - ), - ).toEqual({ - queries: { - FETCH_BOOK: { - data: 'data', - error: null, - pending: 0, - pristine: false, - normalized: false, - }, - FETCH_BOOK1: { - data: 'datasuffix', - error: null, - pending: 0, - pristine: false, - normalized: false, - }, - }, - normalizedData: {}, - }); - }); - - it('handles local mutations', () => { - expect( - queriesReducer( - initialState, - { - type: 'LOCAL_MUTATION_ACTION', - meta: { - mutations: { - FETCH_BOOK1: { - local: true, - updateData: data => `${data} suffix`, - }, - }, - }, - }, - defaultConfig, - ), - ).toEqual({ - queries: { - FETCH_BOOK: { - data: 'data', - error: null, - pending: 0, - pristine: false, - normalized: false, - }, - FETCH_BOOK1: { - data: 'data suffix', - error: null, - pending: 0, - pristine: false, - normalized: false, - }, - }, - normalizedData: {}, - }); - }); - }); - - describe('with normalization', () => { - const defaultState = { - data: null, - error: null, - pending: 0, - pristine: true, - normalized: true, - usedKeys: [], - ref: {}, - }; - const requestAction = { - type: 'FETCH_BOOK', - request: { url: '/ ' }, - meta: { - normalize: true, - }, - }; - - it('should normalize data on query success', () => { - expect( - queriesReducer( - { queries: {}, normalizedData: {} }, - createSuccessAction(requestAction, { - data: { id: '1', name: 'name' }, - }), - defaultConfig, - ), - ).toEqual({ - queries: { - FETCH_BOOK: { - ...defaultState, - pending: -1, - data: '@@1', - usedKeys: { '': ['id', 'name'] }, - }, - }, - normalizedData: { '@@1': { id: '1', name: 'name' } }, - }); - }); - - it('should not touch normalized data if query data is the same', () => { - const initialState = { - queries: { - FETCH_BOOK: { - data: 'data', - pending: 0, - error: null, - normalized: true, - usedKeys: { '': ['id', 'name'] }, - ref: {}, - }, - }, - normalizedData: {}, - }; - const state = queriesReducer( - initialState, - createSuccessAction(requestAction, { data: 'data' }), - defaultConfig, - ); - - expect(state.normalizedData).toBe(initialState.normalizedData); - }); - - it('should normalize data with nested ids and arrays on query success', () => { - expect( - queriesReducer( - { queries: {}, normalizedData: {} }, - createSuccessAction(requestAction, { - data: { - root: { - id: '1', - name: 'name', - nested: [ - { id: '2', v: 2 }, - { id: '3', v: 3 }, - ], - }, - }, - }), - defaultConfig, - ), - ).toEqual({ - queries: { - FETCH_BOOK: { - ...defaultState, - pending: -1, - data: { - root: '@@1', - }, - usedKeys: { - '.root': ['id', 'name', 'nested'], - '.root.nested': ['id', 'v'], - }, - }, - }, - normalizedData: { - '@@1': { - id: '1', - name: 'name', - nested: ['@@2', '@@3'], - }, - '@@2': { id: '2', v: 2 }, - '@@3': { id: '3', v: 3 }, - }, - }); - }); - - it('should merge normalized data on query success', () => { - expect( - queriesReducer( - { - queries: {}, - normalizedData: { '@@1': { id: '1', a: 'a', b: 'b' } }, - }, - createSuccessAction(requestAction, { - data: { id: '1', a: 'd', c: 'c' }, - }), - defaultConfig, - ), - ).toEqual({ - queries: { - FETCH_BOOK: { - ...defaultState, - pending: -1, - data: '@@1', - usedKeys: { '': ['id', 'a', 'c'] }, - }, - }, - normalizedData: { '@@1': { id: '1', a: 'd', b: 'b', c: 'c' } }, - }); - }); - - it('should update normalized data on mutation success', () => { - expect( - queriesReducer( - { - queries: {}, - normalizedData: { '@@1': { id: '1', a: 'a', b: 'b' } }, - }, - createSuccessAction( - { - type: 'UPDATE_BOOK', - request: { url: '/', method: 'put' }, - meta: { - normalize: true, - }, - }, - { - data: { id: '1', a: 'd', c: 'c' }, - }, - ), - defaultConfig, - ), - ).toEqual({ - queries: {}, - normalizedData: { '@@1': { id: '1', a: 'd', b: 'b', c: 'c' } }, - }); - }); - - it('should update normalized query data on mutation success if defined in meta', () => { - const updateData = jest.fn((data, mutationData) => [ - ...data, - mutationData, - { id: '3', x: 3 }, - ]); - expect( - queriesReducer( - { - queries: { - FETCH_BOOK: { - data: ['@@1'], - pending: 0, - error: null, - normalized: true, - ref: {}, - usedKeys: { '': ['id', 'x'] }, - }, - }, - normalizedData: { '@@1': { id: '1', x: 1 } }, - }, - createSuccessAction( - { - type: 'ADD_BOOK', - request: { url: '/', method: 'put' }, - meta: { - normalize: true, - mutations: { - FETCH_BOOK: updateData, - }, - }, - }, - { - data: { id: '2', x: 2 }, - }, - ), - defaultConfig, - ), - ).toEqual({ - queries: { - FETCH_BOOK: { - data: ['@@1', '@@2', '@@3'], - pending: 0, - error: null, - normalized: true, - usedKeys: { '': ['id', 'x'] }, - ref: {}, - }, - }, - normalizedData: { - '@@1': { id: '1', x: 1 }, - '@@2': { id: '2', x: 2 }, - '@@3': { id: '3', x: 3 }, - }, - }); - expect(updateData).toBeCalledWith([{ id: '1', x: 1 }], { - id: '2', - x: 2, - }); - }); - - it('should update normalized query data with local mutation', () => { - expect( - queriesReducer( - { - queries: { - FETCH_BOOK: { - data: ['@@1'], - pending: 0, - error: null, - normalized: true, - usedKeys: { - '': ['id', 'x'], - }, - ref: {}, - }, - }, - normalizedData: { '@@1': { id: '1', x: 1 } }, - }, - { - type: 'ADD_BOOK_LOCALLY', - meta: { - // normalize: true, - mutations: { - FETCH_BOOK: { - updateData: data => [...data, { id: '2', x: 2 }], - local: true, - }, - }, - }, - }, - defaultConfig, - ), - ).toEqual({ - queries: { - FETCH_BOOK: { - data: ['@@1', '@@2'], - pending: 0, - error: null, - normalized: true, - usedKeys: { '': ['id', 'x'] }, - ref: {}, - }, - }, - normalizedData: { - '@@1': { id: '1', x: 1 }, - '@@2': { id: '2', x: 2 }, - }, - }); - }); - - it('should update normalized query data with localData', () => { - expect( - queriesReducer( - { - queries: { - FETCH_BOOK: { - data: ['@@1'], - pending: 0, - error: null, - normalized: true, - ref: {}, - }, - }, - normalizedData: { '@@1': { id: '1', x: 1 } }, - }, - { - type: 'UPDATE_BOOK_LOCALLY', - meta: { - localData: { id: '1', x: 2 }, - }, - }, - defaultConfig, - ), - ).toEqual({ - queries: { - FETCH_BOOK: { - data: ['@@1'], - pending: 0, - error: null, - normalized: true, - ref: {}, - }, - }, - normalizedData: { - '@@1': { id: '1', x: 2 }, - }, - }); - }); - - it('should update normalized query data with optimisticData', () => { - expect( - queriesReducer( - { - queries: { - FETCH_BOOK: { - data: ['@@1'], - pending: 0, - error: null, - normalized: true, - ref: {}, - }, - }, - normalizedData: { '@@1': { id: '1', x: 1 } }, - }, - { - type: 'UPDATE_BOOK', - request: { url: '/books', method: 'post' }, - meta: { - optimisticData: { id: '1', x: 2 }, - }, - }, - defaultConfig, - ), - ).toEqual({ - queries: { - FETCH_BOOK: { - data: ['@@1'], - pending: 0, - error: null, - normalized: true, - ref: {}, - }, - }, - normalizedData: { - '@@1': { id: '1', x: 2 }, - }, - }); - }); - - it('should update normalized query data with revertedData on response error', () => { - expect( - queriesReducer( - { - queries: { - FETCH_BOOK: { - data: ['@@1'], - pending: 0, - error: null, - normalized: true, - ref: {}, - }, - }, - normalizedData: { '@@1': { id: '1', x: 2 } }, - }, - createErrorAction({ - type: 'UPDATE_BOOK', - request: { url: '/books', method: 'post' }, - meta: { - optimisticData: { id: '1', x: 2 }, - revertedData: { id: '1', x: 1 }, - }, - }), - defaultConfig, - ), - ).toEqual({ - queries: { - FETCH_BOOK: { - data: ['@@1'], - pending: 0, - error: null, - normalized: true, - ref: {}, - }, - }, - normalizedData: { - '@@1': { id: '1', x: 1 }, - }, - }); - }); - - it('should allow custom shouldObjectBeNormalized and getNormalisationObjectKey', () => { - expect( - queriesReducer( - { queries: {}, normalizedData: {} }, - createSuccessAction(requestAction, { - data: { _id: '1', name: 'name' }, - }), - { - ...defaultConfig, - getNormalisationObjectKey: obj => obj._id, - shouldObjectBeNormalized: obj => !!obj._id, - }, - ), - ).toEqual({ - queries: { - FETCH_BOOK: { - ...defaultState, - pending: -1, - data: '@@1', - usedKeys: { '': ['_id', 'name'] }, - }, - }, - normalizedData: { '@@1': { _id: '1', name: 'name' } }, - }); - }); - }); - }); -}); diff --git a/packages/redux-requests/src/reducers/queries-reducer.with-mutations-with-query-request-key.spec.js b/packages/redux-requests/src/reducers/queries-reducer.with-mutations-with-query-request-key.spec.js new file mode 100644 index 000000000..b9cbbf1c6 --- /dev/null +++ b/packages/redux-requests/src/reducers/queries-reducer.with-mutations-with-query-request-key.spec.js @@ -0,0 +1,145 @@ +import defaultConfig from '../default-config'; +import { createSuccessAction } from '../actions'; +import { createMutation, createLocalMutation } from '../requests-creators'; + +import queriesReducer from './queries-reducer'; + +describe('reducers', () => { + describe('queriesReducer', () => { + describe('with mutations with query request key', () => { + const initialState = { + queries: { + FETCH_BOOK: { + data: 'data', + error: null, + pending: 0, + pristine: false, + normalized: false, + }, + FETCH_BOOK1: { + data: 'data', + error: null, + pending: 0, + pristine: false, + normalized: false, + }, + }, + normalizedData: {}, + }; + + const MUTATION_ACTION = 'MUTATION_ACTION'; + + it('can update data optimistic', () => { + expect( + queriesReducer( + initialState, + createMutation( + MUTATION_ACTION, + { url: '/books', method: 'post' }, + { + mutations: { + FETCH_BOOK1: { + updateDataOptimistic: data => `${data} suffix`, + }, + }, + }, + )(), + defaultConfig, + ), + ).toEqual({ + queries: { + FETCH_BOOK: { + data: 'data', + error: null, + pending: 0, + pristine: false, + normalized: false, + }, + FETCH_BOOK1: { + data: 'data suffix', + error: null, + pending: 0, + pristine: false, + normalized: false, + }, + }, + normalizedData: {}, + }); + }); + + it('handles updateData customized per mutation defined in updateData object key', () => { + expect( + queriesReducer( + initialState, + createSuccessAction( + createMutation( + MUTATION_ACTION, + { url: '/books', method: 'post' }, + { + mutations: { + FETCH_BOOK1: { + updateData: (data, mutationData) => + data + mutationData.nested, + }, + }, + }, + )(), + { data: { nested: 'suffix' } }, + ), + defaultConfig, + ), + ).toEqual({ + queries: { + FETCH_BOOK: { + data: 'data', + error: null, + pending: 0, + pristine: false, + normalized: false, + }, + FETCH_BOOK1: { + data: 'datasuffix', + error: null, + pending: 0, + pristine: false, + normalized: false, + }, + }, + normalizedData: {}, + }); + }); + + it('handles local mutations', () => { + expect( + queriesReducer( + initialState, + createLocalMutation('LOCAL_MUTATION_ACTION', { + mutations: { + FETCH_BOOK1: data => `${data} suffix`, + }, + })(), + defaultConfig, + ), + ).toEqual({ + queries: { + FETCH_BOOK: { + data: 'data', + error: null, + pending: 0, + pristine: false, + normalized: false, + }, + FETCH_BOOK1: { + data: 'data suffix', + error: null, + pending: 0, + pristine: false, + normalized: false, + }, + }, + normalizedData: {}, + }); + }); + }); + }); +}); diff --git a/packages/redux-requests/src/reducers/queries-reducer.with-mutations.spec.js b/packages/redux-requests/src/reducers/queries-reducer.with-mutations.spec.js new file mode 100644 index 000000000..8b3402ff8 --- /dev/null +++ b/packages/redux-requests/src/reducers/queries-reducer.with-mutations.spec.js @@ -0,0 +1,280 @@ +import defaultConfig from '../default-config'; +import { + createSuccessAction, + createErrorAction, + createAbortAction, +} from '../actions'; +import { createMutation, createLocalMutation } from '../requests-creators'; + +import queriesReducer from './queries-reducer'; + +describe('reducers', () => { + describe('queriesReducer', () => { + describe('with mutations', () => { + const initialState = { + queries: { + FETCH_BOOK: { + data: 'data', + error: null, + pending: 0, + pristine: false, + normalized: false, + }, + }, + normalizedData: {}, + }; + + const MUTATION_ACTION = 'MUTATION_ACTION'; + + it('can update data optimistic', () => { + expect( + queriesReducer( + initialState, + createMutation( + MUTATION_ACTION, + { url: '/books', method: 'post' }, + { + mutations: { + FETCH_BOOK: { + updateDataOptimistic: data => `${data} suffix`, + }, + }, + }, + )(), + defaultConfig, + ), + ).toEqual({ + queries: { + FETCH_BOOK: { + data: 'data suffix', + error: null, + pending: 0, + pristine: false, + normalized: false, + }, + }, + normalizedData: {}, + }); + }); + + it('keeps data updated optimistic on mutation success if updateData undefined', () => { + expect( + queriesReducer( + initialState, + createSuccessAction( + createMutation( + MUTATION_ACTION, + { url: '/books', method: 'post' }, + { + mutations: { + FETCH_BOOK: { + updateDataOptimistic: data => `${data} suffix`, + }, + }, + }, + )(), + { data: 'updated data' }, + ), + defaultConfig, + ), + ).toEqual(initialState); + }); + + it('handles updateData customized per mutation', () => { + expect( + queriesReducer( + initialState, + createSuccessAction( + createMutation( + MUTATION_ACTION, + { url: '/books', method: 'post' }, + { + mutations: { + FETCH_BOOK: (data, mutationData) => + data + mutationData.nested, + }, + }, + )(), + { data: { nested: 'suffix' } }, + ), + defaultConfig, + ), + ).toEqual({ + queries: { + FETCH_BOOK: { + data: 'datasuffix', + error: null, + pending: 0, + pristine: false, + normalized: false, + }, + }, + normalizedData: {}, + }); + }); + + it('handles updateData customized per mutation defined in updateData object key', () => { + expect( + queriesReducer( + initialState, + createSuccessAction( + createMutation( + MUTATION_ACTION, + { url: '/books', method: 'post' }, + { + mutations: { + FETCH_BOOK: { + updateData: (data, mutationData) => + data + mutationData.nested, + }, + }, + }, + )(), + { data: { nested: 'suffix' } }, + ), + defaultConfig, + ), + ).toEqual({ + queries: { + FETCH_BOOK: { + data: 'datasuffix', + error: null, + pending: 0, + pristine: false, + normalized: false, + }, + }, + normalizedData: {}, + }); + }); + + it('reverts optimistic update on mutation error', () => { + expect( + queriesReducer( + initialState, + createErrorAction( + createMutation( + MUTATION_ACTION, + { url: '/books', method: 'post' }, + { + mutations: { + FETCH_BOOK: { + updateDataOptimistic: () => 'data2', + revertData: data => `${data} reverted`, + }, + }, + }, + )(), + 'error', + ), + defaultConfig, + ), + ).toEqual({ + queries: { + FETCH_BOOK: { + data: 'data reverted', + error: null, + pending: 0, + pristine: false, + normalized: false, + }, + }, + normalizedData: {}, + }); + }); + + it('doesnt change data on mutation error without optimistic update revertData', () => { + expect( + queriesReducer( + initialState, + createErrorAction( + createMutation( + MUTATION_ACTION, + { url: '/books', method: 'post' }, + { + mutations: { + FETCH_BOOK: { + updateDataOptimistic: () => 'data2', + }, + }, + }, + )(), + 'error', + ), + defaultConfig, + ), + ).toEqual({ + queries: { + FETCH_BOOK: { + data: 'data', + error: null, + pending: 0, + pristine: false, + normalized: false, + }, + }, + normalizedData: {}, + }); + }); + + it('reverts optimistic update on mutation abort', () => { + expect( + queriesReducer( + initialState, + createAbortAction( + createMutation( + MUTATION_ACTION, + { url: '/books', method: 'post' }, + { + mutations: { + FETCH_BOOK: { + updateDataOptimistic: () => 'data2', + revertData: data => `${data} reverted`, + }, + }, + }, + )(), + ), + defaultConfig, + ), + ).toEqual({ + queries: { + FETCH_BOOK: { + data: 'data reverted', + error: null, + pending: 0, + pristine: false, + normalized: false, + }, + }, + normalizedData: {}, + }); + }); + + it('handles local mutations', () => { + expect( + queriesReducer( + initialState, + createLocalMutation('LOCAL_MUTATION_ACTION', { + mutations: { + FETCH_BOOK: data => `${data} suffix`, + }, + })(), + defaultConfig, + ), + ).toEqual({ + queries: { + FETCH_BOOK: { + data: 'data suffix', + error: null, + pending: 0, + pristine: false, + normalized: false, + }, + }, + normalizedData: {}, + }); + }); + }); + }); +}); diff --git a/packages/redux-requests/src/reducers/queries-reducer.with-normalisation.spec.js b/packages/redux-requests/src/reducers/queries-reducer.with-normalisation.spec.js new file mode 100644 index 000000000..1cfb2036c --- /dev/null +++ b/packages/redux-requests/src/reducers/queries-reducer.with-normalisation.spec.js @@ -0,0 +1,1098 @@ +import defaultConfig from '../default-config'; +import { createSuccessAction, createErrorAction } from '../actions'; +import { + createQuery, + createMutation, + createLocalMutation, +} from '../requests-creators'; + +import queriesReducer from './queries-reducer'; + +/* +1) fetchBooks +[ + { id: 1, name: 'Harry', author: { id: 100, surname: 'Harry author' }, likers: [], }, + { id: 2, name: 'Lord', author: { id: 101, surname: 'Lord author' }, likers: [], }, +] + +fetchBooks: ['@@1', '@@2'], +@@1: { id: 1, name: 'Harry', author: '@@100', likers: [] } +@@2: { id: 2, name: 'Lord', author: '@@101', likers: [] } +@@100: { id: 100, surname: 'Harry author' } +@@101: { id: 101, surname: 'Lord author' } + +dependencies: +fetchBooks: [@@1, @@2, @@100, @@101] + +dependents: +@@1: [fetchBooks] +@@2: [fetchBooks] +@@100: [fetchBooks] +@@101: [fetchBooks] + +2) fetchBook 1 + +{ id: 1, name: 'Harry', author: { id: 100, surname: 'Harry author' } } + +fetchBooks: ['@@1', '@@2'] +fetchBook: '@@1' +@@1: { id: 1, name: 'Harry', author: '@@100', likers: [] } +@@2: { id: 2, name: 'Lord', author: '@@101', likers: [] } +@@100: { id: 100, surname: 'Harry author' } +@@101: { id: 101, surname: 'Lord author' } + +dependencies: +fetchBooks: [@@1, @@2, @@100, @@101] +fetchBooks: [@@1, @@100] + +dependents: +@@1: [fetchBooks, fetchBook] +@@2: [fetchBooks] +@@100: [fetchBooks, fetchBook] +@@101: [fetchBooks] + +3) updateBook 1 + +{ id: 1, name: 'Harry 2', author: { id: 100, surname: 'Harry 2 author' }, liker: { id: 1000 } } + +mutation dependencies: [@@1, @@100, @1000] +getting affected queries: [fetchBooks, fetchBook] +recalculate them, nothing changed re dependencies + +!!!!! what to do about this 1000 we must ignore it, not a dependency! + +fetchBooks: ['@@1', '@@2'] +fetchBook: '@@1' +@@1: { id: 1, name: 'Harry 2', author: '@@100' } +@@2: { id: 2, name: 'Lord', author: '@@101' } +@@100: { id: 100, surname: 'Harry 2 author' } +@@101: { id: 101, surname: 'Lord author' } + +dependencies: +fetchBooks: [@@1, @@2, @@100, @@101] +fetchBooks: [@@1, @@100] + +dependents: +@@1: [fetchBooks, fetchBook] +@@2: [fetchBooks] +@@100: [fetchBooks, fetchBook] +@@101: [fetchBooks] + +4) updateBook 1 - change author! + +{ id: 1, author: { id: 102, surname: 'Harry 2 new author' }, liker: { id: 1000 } } + +mutation dependencies: [@@1, @@102, @1000] +@@1 found, getting affected queries: [fetchBooks, fetchBook] +recalculate them, new dependencies: +dependencies: +fetchBooks: [@@1, @@2, @@100, @@101] => [@@1, @@2, @@102, @@101] 100 gone, 102 added +fetchBook: [@@1, @@100] => [@@1, @@102] 100 gone, 102 added + +!!!!! what to do about this 1000 we must ignore it, not a dependency! + +fetchBooks: ['@@1', '@@2'] +fetchBook: '@@1' +@@1: { id: 1, name: 'Harry 2', author: '@@102' } +@@2: { id: 2, name: 'Lord', author: '@@101' } +@@102: { id: 102, surname: 'Harry 2 new author' } +@@101: { id: 101, surname: 'Lord author' } + + +dependents: +@@1: [fetchBooks, fetchBook] +@@2: [fetchBooks] +@@102: [fetchBooks, fetchBook] +@@101: [fetchBooks] +@@100: [] to remove + +5) updateBook 1 - add liker! + +{ id: 1, likers: [{ id: 1000 }] } + +mutation dependencies: [@@1, @@1000] +@@1 found, getting affected queries: [fetchBooks, fetchBook] + +recalculate them, new dependencies: +dependencies: +fetchBooks: [@@1, @@2, @@102, @@101] => [@@1, @@2, @@102, @@101, @@1000] 1000 added! +fetchBook: [@@1, @@102] => [@@1, @@102, @@1000] 1000 added + + +fetchBooks: ['@@1', '@@2'] +fetchBook: '@@1' +@@1: { id: 1, name: 'Harry 2', author: '@@102' } +@@2: { id: 2, name: 'Lord', author: '@@101' } +@@102: { id: 102, surname: 'Harry 2 new author' } +@@101: { id: 101, surname: 'Lord author' } +@@1000: { id: 1000 } + +dependents: +@@1: [fetchBooks, fetchBook] +@@2: [fetchBooks] +@@102: [fetchBooks, fetchBook] +@@101: [fetchBooks] +@@1000: [fetchBooks, fetchBook] + +6) reset fetchBooks + +fetchBooks: null +fetchBook: '@@1' +@@1: { id: 1, name: 'Harry', author: '@@100' } +@@2: { id: 2, name: 'Lord', author: '@@101' } +@@100: { id: 100, surname: 'Harry author' } +@@101: { id: 101, surname: 'Lord author' } + +dependencies: +fetchBooks: [] diff -1, 2, 100, 101 +fetchBooks: [@@1, @@100] + +dependents: +@@1: [fetchBook] +@@2: [] // safe to remove +@@100: [fetchBook] +@@101: [] // safe to remove + +*/ + +describe('reducers', () => { + describe('queriesReducer', () => { + describe('normalisation garbage collecting story', () => { + const defaultState = { + data: null, + error: null, + pending: 0, + pristine: true, + normalized: true, + usedKeys: [], + dependencies: [], + ref: {}, + }; + + it('handles initial books fetch', () => { + const fetchBooks = createQuery( + 'FETCH_BOOKS', + { url: '/books' }, + { normalize: true }, + )(); + + expect( + queriesReducer( + { + queries: { FETCH_BOOKS: defaultState }, + normalizedData: {}, + dependentQueries: {}, + }, + createSuccessAction(fetchBooks, { + data: [ + { + id: 1, + name: 'Harry', + author: { id: 100, surname: 'Harry author' }, + likers: [], + }, + { + id: 2, + name: 'Lord', + author: { id: 101, surname: 'Lord author' }, + likers: [], + }, + ], + }), + defaultConfig, + ), + ).toEqual({ + queries: { + FETCH_BOOKS: { + ...defaultState, + pending: -1, + data: ['@@1', '@@2'], + usedKeys: { + '': ['id', 'name', 'author', 'likers'], + '.author': ['id', 'surname'], + }, + dependencies: ['@@1', '@@100', '@@2', '@@101'], + }, + }, + normalizedData: { + '@@1': { id: 1, name: 'Harry', author: '@@100', likers: [] }, + '@@2': { id: 2, name: 'Lord', author: '@@101', likers: [] }, + '@@100': { id: 100, surname: 'Harry author' }, + '@@101': { id: 101, surname: 'Lord author' }, + }, + dependentQueries: { + '@@1': ['FETCH_BOOKS'], + '@@2': ['FETCH_BOOKS'], + '@@100': ['FETCH_BOOKS'], + '@@101': ['FETCH_BOOKS'], + }, + }); + }); + + it('handles book detail fetch', () => { + const fetchBook = createQuery( + 'FETCH_BOOK', + { url: '/book/1' }, + { normalize: true }, + )(); + + expect( + queriesReducer( + { + queries: { + FETCH_BOOKS: { + ...defaultState, + data: ['@@1', '@@2'], + usedKeys: { + '': ['id', 'name', 'author', 'likers'], + '.author': ['id', 'surname'], + }, + dependencies: ['@@1', '@@100', '@@2', '@@101'], + }, + FETCH_BOOK: defaultState, + }, + normalizedData: { + '@@1': { id: 1, name: 'Harry', author: '@@100', likers: [] }, + '@@2': { id: 2, name: 'Lord', author: '@@101', likers: [] }, + '@@100': { id: 100, surname: 'Harry author' }, + '@@101': { id: 101, surname: 'Lord author' }, + }, + dependentQueries: { + '@@1': ['FETCH_BOOKS'], + '@@2': ['FETCH_BOOKS'], + '@@100': ['FETCH_BOOKS'], + '@@101': ['FETCH_BOOKS'], + }, + }, + createSuccessAction(fetchBook, { + data: { + id: 1, + name: 'Harry', + author: { id: 100, surname: 'Harry author' }, + likers: [], + }, + }), + defaultConfig, + ), + ).toEqual({ + queries: { + FETCH_BOOKS: { + ...defaultState, + data: ['@@1', '@@2'], + usedKeys: { + '': ['id', 'name', 'author', 'likers'], + '.author': ['id', 'surname'], + }, + dependencies: ['@@1', '@@100', '@@2', '@@101'], + }, + FETCH_BOOK: { + ...defaultState, + pending: -1, + data: '@@1', + usedKeys: { + '': ['id', 'name', 'author', 'likers'], + '.author': ['id', 'surname'], + }, + dependencies: ['@@1', '@@100'], + }, + }, + normalizedData: { + '@@1': { id: 1, name: 'Harry', author: '@@100', likers: [] }, + '@@2': { id: 2, name: 'Lord', author: '@@101', likers: [] }, + '@@100': { id: 100, surname: 'Harry author' }, + '@@101': { id: 101, surname: 'Lord author' }, + }, + dependentQueries: { + '@@1': ['FETCH_BOOKS', 'FETCH_BOOK'], + '@@2': ['FETCH_BOOKS'], + '@@100': ['FETCH_BOOKS', 'FETCH_BOOK'], + '@@101': ['FETCH_BOOKS'], + }, + }); + }); + + it('handles book update', () => { + const updateBook = createMutation( + 'UPDATE_BOOK', + { url: '/book/1', method: 'put' }, + { normalize: true }, + )(); + + expect( + queriesReducer( + { + queries: { + FETCH_BOOKS: { + ...defaultState, + data: ['@@1', '@@2'], + usedKeys: { + '': ['id', 'name', 'author', 'likers'], + '.author': ['id', 'surname'], + }, + dependencies: ['@@1', '@@100', '@@2', '@@101'], + }, + FETCH_BOOK: { + ...defaultState, + data: '@@1', + usedKeys: { + '': ['id', 'name', 'author', 'likers'], + '.author': ['id', 'surname'], + }, + dependencies: ['@@1', '@@100'], + }, + }, + normalizedData: { + '@@1': { id: 1, name: 'Harry', author: '@@100', likers: [] }, + '@@2': { id: 2, name: 'Lord', author: '@@101', likers: [] }, + '@@100': { id: 100, surname: 'Harry author' }, + '@@101': { id: 101, surname: 'Lord author' }, + }, + dependentQueries: { + '@@1': ['FETCH_BOOKS', 'FETCH_BOOK'], + '@@2': ['FETCH_BOOKS'], + '@@100': ['FETCH_BOOKS', 'FETCH_BOOK'], + '@@101': ['FETCH_BOOKS'], + }, + }, + createSuccessAction(updateBook, { + data: { + id: 1, + name: 'Harry 2', + author: { id: 100, surname: 'Harry 2 author' }, + }, + }), + defaultConfig, + ), + ).toEqual({ + queries: { + FETCH_BOOKS: { + ...defaultState, + data: ['@@1', '@@2'], + usedKeys: { + '': ['id', 'name', 'author', 'likers'], + '.author': ['id', 'surname'], + }, + dependencies: ['@@1', '@@100', '@@2', '@@101'], + }, + FETCH_BOOK: { + ...defaultState, + data: '@@1', + usedKeys: { + '': ['id', 'name', 'author', 'likers'], + '.author': ['id', 'surname'], + }, + dependencies: ['@@1', '@@100'], + }, + }, + normalizedData: { + '@@1': { id: 1, name: 'Harry 2', author: '@@100', likers: [] }, + '@@2': { id: 2, name: 'Lord', author: '@@101', likers: [] }, + '@@100': { id: 100, surname: 'Harry 2 author' }, + '@@101': { id: 101, surname: 'Lord author' }, + }, + dependentQueries: { + '@@1': ['FETCH_BOOKS', 'FETCH_BOOK'], + '@@2': ['FETCH_BOOKS'], + '@@100': ['FETCH_BOOKS', 'FETCH_BOOK'], + '@@101': ['FETCH_BOOKS'], + }, + }); + }); + + it('handles book author change and orphan object', () => { + const updateBookAuthor = createMutation( + 'UPDATE_BOOK_AUTHOR', + { url: '/book/1/author', method: 'put' }, + { normalize: true }, + )(); + + expect( + queriesReducer( + { + queries: { + FETCH_BOOKS: { + ...defaultState, + data: ['@@1', '@@2'], + usedKeys: { + '': ['id', 'name', 'author', 'likers'], + '.author': ['id', 'surname'], + }, + dependencies: ['@@1', '@@100', '@@2', '@@101'], + }, + FETCH_BOOK: { + ...defaultState, + data: '@@1', + usedKeys: { + '': ['id', 'name', 'author', 'likers'], + '.author': ['id', 'surname'], + }, + dependencies: ['@@1', '@@100'], + }, + }, + normalizedData: { + '@@1': { id: 1, name: 'Harry 2', author: '@@100', likers: [] }, + '@@2': { id: 2, name: 'Lord', author: '@@101', likers: [] }, + '@@100': { id: 100, surname: 'Harry 2 author' }, + '@@101': { id: 101, surname: 'Lord author' }, + }, + dependentQueries: { + '@@1': ['FETCH_BOOKS', 'FETCH_BOOK'], + '@@2': ['FETCH_BOOKS'], + '@@100': ['FETCH_BOOKS', 'FETCH_BOOK'], + '@@101': ['FETCH_BOOKS'], + }, + }, + createSuccessAction(updateBookAuthor, { + data: { + book: { + id: 1, + author: { id: 102, surname: 'Harry 2 new author' }, + }, + orphan: { id: 1000 }, + }, + }), + defaultConfig, + ), + ).toEqual({ + queries: { + FETCH_BOOKS: { + ...defaultState, + data: ['@@1', '@@2'], + usedKeys: { + '': ['id', 'name', 'author', 'likers'], + '.author': ['id', 'surname'], + }, + dependencies: ['@@1', '@@102', '@@2', '@@101'], + }, + FETCH_BOOK: { + ...defaultState, + data: '@@1', + usedKeys: { + '': ['id', 'name', 'author', 'likers'], + '.author': ['id', 'surname'], + }, + dependencies: ['@@1', '@@102'], + }, + }, + normalizedData: { + '@@1': { id: 1, name: 'Harry 2', author: '@@102', likers: [] }, + '@@2': { id: 2, name: 'Lord', author: '@@101', likers: [] }, + '@@101': { id: 101, surname: 'Lord author' }, + '@@102': { id: 102, surname: 'Harry 2 new author' }, + }, + dependentQueries: { + '@@1': ['FETCH_BOOKS', 'FETCH_BOOK'], + '@@2': ['FETCH_BOOKS'], + '@@101': ['FETCH_BOOKS'], + '@@102': ['FETCH_BOOKS', 'FETCH_BOOK'], + }, + }); + }); + + it('handles added liker', () => { + const addBookLiker = createMutation( + 'ADD_BOOK_LIKER', + { url: '/book/1/liker', method: 'put' }, + { normalize: true }, + )(); + + expect( + queriesReducer( + { + queries: { + FETCH_BOOKS: { + ...defaultState, + data: ['@@1', '@@2'], + usedKeys: { + '': ['id', 'name', 'author', 'likers'], + '.author': ['id', 'surname'], + }, + dependencies: ['@@1', '@@102', '@@2', '@@101'], + }, + FETCH_BOOK: { + ...defaultState, + data: '@@1', + usedKeys: { + '': ['id', 'name', 'author', 'likers'], + '.author': ['id', 'surname'], + }, + dependencies: ['@@1', '@@102'], + }, + }, + normalizedData: { + '@@1': { id: 1, name: 'Harry 2', author: '@@102', likers: [] }, + '@@2': { id: 2, name: 'Lord', author: '@@101', likers: [] }, + '@@101': { id: 101, surname: 'Lord author' }, + '@@102': { id: 102, surname: 'Harry 2 new author' }, + }, + dependentQueries: { + '@@1': ['FETCH_BOOKS', 'FETCH_BOOK'], + '@@2': ['FETCH_BOOKS'], + '@@101': ['FETCH_BOOKS'], + '@@102': ['FETCH_BOOKS', 'FETCH_BOOK'], + }, + }, + createSuccessAction(addBookLiker, { + data: { + id: 1, + likers: [{ id: 1000, name: 'Liker 1' }], + }, + }), + defaultConfig, + ), + ).toEqual({ + queries: { + FETCH_BOOKS: { + ...defaultState, + data: ['@@1', '@@2'], + usedKeys: { + '': ['id', 'name', 'author', 'likers'], + '.author': ['id', 'surname'], + }, + dependencies: ['@@1', '@@102', '@@1000', '@@2', '@@101'], + }, + FETCH_BOOK: { + ...defaultState, + data: '@@1', + usedKeys: { + '': ['id', 'name', 'author', 'likers'], + '.author': ['id', 'surname'], + }, + dependencies: ['@@1', '@@102', '@@1000'], + }, + }, + normalizedData: { + '@@1': { + id: 1, + name: 'Harry 2', + author: '@@102', + likers: ['@@1000'], + }, + '@@2': { id: 2, name: 'Lord', author: '@@101', likers: [] }, + '@@101': { id: 101, surname: 'Lord author' }, + '@@102': { id: 102, surname: 'Harry 2 new author' }, + '@@1000': { id: 1000, name: 'Liker 1' }, + }, + dependentQueries: { + '@@1': ['FETCH_BOOKS', 'FETCH_BOOK'], + '@@2': ['FETCH_BOOKS'], + '@@101': ['FETCH_BOOKS'], + '@@102': ['FETCH_BOOKS', 'FETCH_BOOK'], + '@@1000': ['FETCH_BOOKS', 'FETCH_BOOK'], + }, + }); + }); + }); + + describe('with normalization', () => { + const defaultState = { + data: null, + error: null, + pending: 0, + pristine: true, + normalized: true, + usedKeys: [], + dependencies: [], + ref: {}, + }; + const requestAction = createQuery( + 'FETCH_BOOK', + { url: '/ ' }, + { + normalize: true, + }, + )(); + + it('should normalize data on query success', () => { + expect( + queriesReducer( + { queries: {}, normalizedData: {}, dependentQueries: {} }, + createSuccessAction(requestAction, { + data: { id: '1', name: 'name' }, + }), + defaultConfig, + ), + ).toEqual({ + queries: { + FETCH_BOOK: { + ...defaultState, + pending: -1, + data: '@@1', + usedKeys: { '': ['id', 'name'] }, + dependencies: ['@@1'], + }, + }, + normalizedData: { '@@1': { id: '1', name: 'name' } }, + dependentQueries: { + '@@1': ['FETCH_BOOK'], + }, + }); + }); + + it('should not touch normalized data if query data is the same', () => { + const initialState = { + queries: { + FETCH_BOOK: { + data: 'data', + pending: 0, + error: null, + normalized: true, + usedKeys: { '': ['id', 'name'] }, + dependencies: [], + ref: {}, + }, + }, + normalizedData: {}, + dependentQueries: {}, + }; + const state = queriesReducer( + initialState, + createSuccessAction(requestAction, { data: 'data' }), + defaultConfig, + ); + + expect(state.normalizedData).toBe(initialState.normalizedData); + }); + + it('should normalize data with nested ids and arrays on query success', () => { + expect( + queriesReducer( + { queries: {}, normalizedData: {}, dependentQueries: {} }, + createSuccessAction(requestAction, { + data: { + root: { + id: '1', + name: 'name', + nested: [ + { id: '2', v: 2 }, + { id: '3', v: 3 }, + ], + }, + }, + }), + defaultConfig, + ), + ).toEqual({ + queries: { + FETCH_BOOK: { + ...defaultState, + pending: -1, + data: { + root: '@@1', + }, + usedKeys: { + '.root': ['id', 'name', 'nested'], + '.root.nested': ['id', 'v'], + }, + dependencies: ['@@1', '@@2', '@@3'], + }, + }, + normalizedData: { + '@@1': { + id: '1', + name: 'name', + nested: ['@@2', '@@3'], + }, + '@@2': { id: '2', v: 2 }, + '@@3': { id: '3', v: 3 }, + }, + dependentQueries: { + '@@1': ['FETCH_BOOK'], + '@@2': ['FETCH_BOOK'], + '@@3': ['FETCH_BOOK'], + }, + }); + }); + + it('should merge normalized data on query success', () => { + expect( + queriesReducer( + { + queries: {}, + normalizedData: { '@@1': { id: '1', a: 'a', b: 'b' } }, + dependentQueries: {}, + }, + createSuccessAction(requestAction, { + data: { id: '1', a: 'd', c: 'c' }, + }), + defaultConfig, + ), + ).toEqual({ + queries: { + FETCH_BOOK: { + ...defaultState, + pending: -1, + data: '@@1', + usedKeys: { '': ['id', 'a', 'c'] }, + dependencies: ['@@1'], + }, + }, + normalizedData: { '@@1': { id: '1', a: 'd', b: 'b', c: 'c' } }, + dependentQueries: { + '@@1': ['FETCH_BOOK'], + }, + }); + }); + + it('should update normalized data on mutation success', () => { + expect( + queriesReducer( + { + queries: { + FETCH_BOOK: { + ...defaultState, + data: '@@1', + usedKeys: { '': ['id', 'a', 'b'] }, + dependencies: ['@@1'], + }, + }, + normalizedData: { '@@1': { id: '1', a: 'a', b: 'b' } }, + dependentQueries: { + '@@1': ['FETCH_BOOK'], + }, + }, + createSuccessAction( + createMutation( + 'UPDATE_BOOK', + { url: '/', method: 'put' }, + { + normalize: true, + }, + )(), + { + data: { id: '1', a: 'd', c: 'c' }, + }, + ), + defaultConfig, + ), + ).toEqual({ + queries: { + FETCH_BOOK: { + ...defaultState, + data: '@@1', + usedKeys: { '': ['id', 'a', 'b'] }, + dependencies: ['@@1'], + }, + }, + normalizedData: { '@@1': { id: '1', a: 'd', b: 'b', c: 'c' } }, + dependentQueries: { + '@@1': ['FETCH_BOOK'], + }, + }); + }); + + it('should update normalized query data on mutation success if defined in meta', () => { + const updateData = jest.fn((data, mutationData) => [ + ...data, + mutationData, + { id: '3', x: 3 }, + ]); + expect( + queriesReducer( + { + queries: { + FETCH_BOOK: { + data: ['@@1'], + pending: 0, + error: null, + normalized: true, + ref: {}, + usedKeys: { '': ['id', 'x'] }, + dependencies: ['@@1'], + }, + }, + normalizedData: { '@@1': { id: '1', x: 1 } }, + dependentQueries: { '@@1': ['FETCH_BOOK'] }, + }, + createSuccessAction( + createMutation( + 'ADD_BOOK', + { url: '/', method: 'put' }, + { + normalize: true, + mutations: { + FETCH_BOOK: updateData, + }, + }, + )(), + { + data: { id: '2', x: 2 }, + }, + ), + defaultConfig, + ), + ).toEqual({ + queries: { + FETCH_BOOK: { + data: ['@@1', '@@2', '@@3'], + pending: 0, + error: null, + normalized: true, + usedKeys: { '': ['id', 'x'] }, + dependencies: ['@@1', '@@2', '@@3'], + ref: {}, + }, + }, + normalizedData: { + '@@1': { id: '1', x: 1 }, + '@@2': { id: '2', x: 2 }, + '@@3': { id: '3', x: 3 }, + }, + dependentQueries: { + '@@1': ['FETCH_BOOK'], + '@@2': ['FETCH_BOOK'], + '@@3': ['FETCH_BOOK'], + }, + }); + expect(updateData).toBeCalledWith([{ id: '1', x: 1 }], { + id: '2', + x: 2, + }); + }); + + it('should update normalized query data with local mutation', () => { + expect( + queriesReducer( + { + queries: { + FETCH_BOOK: { + data: ['@@1'], + pending: 0, + error: null, + normalized: true, + usedKeys: { + '': ['id', 'x'], + }, + dependencies: ['@@1'], + ref: {}, + }, + }, + normalizedData: { '@@1': { id: '1', x: 1 } }, + dependentQueries: { '@@1': ['FETCH_BOOK'] }, + }, + createLocalMutation('ADD_BOOK_LOCALLY', { + mutations: { + FETCH_BOOK: { + updateData: data => [...data, { id: '2', x: 2 }], + local: true, + }, + }, + })(), + defaultConfig, + ), + ).toEqual({ + queries: { + FETCH_BOOK: { + data: ['@@1', '@@2'], + pending: 0, + error: null, + normalized: true, + usedKeys: { '': ['id', 'x'] }, + dependencies: ['@@1', '@@2'], + ref: {}, + }, + }, + normalizedData: { + '@@1': { id: '1', x: 1 }, + '@@2': { id: '2', x: 2 }, + }, + dependentQueries: { + '@@1': ['FETCH_BOOK'], + '@@2': ['FETCH_BOOK'], + }, + }); + }); + + it('should update normalized query data with localData', () => { + expect( + queriesReducer( + { + queries: { + FETCH_BOOK: { + data: ['@@1'], + pending: 0, + error: null, + normalized: true, + dependencies: ['@@1'], + ref: {}, + usedKeys: { + '': ['id', 'x'], + }, + }, + }, + normalizedData: { '@@1': { id: '1', x: 1 } }, + dependentQueries: { + '@@1': ['FETCH_BOOK'], + }, + }, + createLocalMutation('UPDATE_BOOK_LOCALLY', { + localData: { id: '1', x: 2 }, + })(), + defaultConfig, + ), + ).toEqual({ + queries: { + FETCH_BOOK: { + data: ['@@1'], + pending: 0, + error: null, + normalized: true, + dependencies: ['@@1'], + ref: {}, + usedKeys: { + '': ['id', 'x'], + }, + }, + }, + normalizedData: { + '@@1': { id: '1', x: 2 }, + }, + dependentQueries: { + '@@1': ['FETCH_BOOK'], + }, + }); + }); + + it('should update normalized query data with optimisticData', () => { + expect( + queriesReducer( + { + queries: { + FETCH_BOOK: { + data: ['@@1'], + pending: 0, + error: null, + normalized: true, + dependencies: ['@@1'], + ref: {}, + usedKeys: { + '': ['id', 'x'], + }, + }, + }, + normalizedData: { '@@1': { id: '1', x: 1 } }, + dependentQueries: { + '@@1': ['FETCH_BOOK'], + }, + }, + createMutation( + 'UPDATE_BOOK', + { url: '/books', method: 'post' }, + { + optimisticData: { id: '1', x: 2 }, + }, + )(), + defaultConfig, + ), + ).toEqual({ + queries: { + FETCH_BOOK: { + data: ['@@1'], + pending: 0, + error: null, + normalized: true, + dependencies: ['@@1'], + ref: {}, + usedKeys: { + '': ['id', 'x'], + }, + }, + }, + normalizedData: { + '@@1': { id: '1', x: 2 }, + }, + dependentQueries: { + '@@1': ['FETCH_BOOK'], + }, + }); + }); + + it('should update normalized query data with revertedData on response error', () => { + expect( + queriesReducer( + { + queries: { + FETCH_BOOK: { + data: ['@@1'], + pending: 0, + error: null, + normalized: true, + dependencies: ['@@1'], + ref: {}, + usedKeys: { + '': ['id', 'x'], + }, + }, + }, + normalizedData: { '@@1': { id: '1', x: 2 } }, + dependentQueries: { + '@@1': ['FETCH_BOOK'], + }, + }, + createErrorAction( + createMutation( + 'UPDATE_BOOK', + { url: '/books', method: 'post' }, + { + optimisticData: { id: '1', x: 2 }, + revertedData: { id: '1', x: 1 }, + }, + )(), + ), + defaultConfig, + ), + ).toEqual({ + queries: { + FETCH_BOOK: { + data: ['@@1'], + pending: 0, + error: null, + normalized: true, + dependencies: ['@@1'], + ref: {}, + usedKeys: { + '': ['id', 'x'], + }, + }, + }, + normalizedData: { + '@@1': { id: '1', x: 1 }, + }, + dependentQueries: { + '@@1': ['FETCH_BOOK'], + }, + }); + }); + + it('should allow custom shouldObjectBeNormalized and getNormalisationObjectKey', () => { + expect( + queriesReducer( + { queries: {}, normalizedData: {}, dependentQueries: {} }, + createSuccessAction(requestAction, { + data: { _id: '1', name: 'name' }, + }), + { + ...defaultConfig, + getNormalisationObjectKey: obj => obj._id, + shouldObjectBeNormalized: obj => !!obj._id, + }, + ), + ).toEqual({ + queries: { + FETCH_BOOK: { + ...defaultState, + pending: -1, + data: '@@1', + dependencies: ['@@1'], + usedKeys: { '': ['_id', 'name'] }, + }, + }, + normalizedData: { '@@1': { _id: '1', name: 'name' } }, + dependentQueries: { + '@@1': ['FETCH_BOOK'], + }, + }); + }); + }); + }); +}); diff --git a/packages/redux-requests/src/reducers/queries-reducer.with-request-key.js b/packages/redux-requests/src/reducers/queries-reducer.with-request-key.js new file mode 100644 index 000000000..bfbd2c5a1 --- /dev/null +++ b/packages/redux-requests/src/reducers/queries-reducer.with-request-key.js @@ -0,0 +1,107 @@ +import defaultConfig from '../default-config'; +import { + createSuccessAction, + createErrorAction, + createAbortAction, +} from '../actions'; +import { createQuery } from '../requests-creators'; + +import queriesReducer from './queries-reducer'; + +describe('reducers', () => { + describe('queriesReducer', () => { + describe('with requestKey', () => { + const defaultState = { + data: null, + error: null, + pending: 0, + pristine: true, + normalized: false, + ref: {}, + usedKeys: null, + }; + const requestAction = createQuery( + 'FETCH_BOOK', + { url: '/ ' }, + { + requestKey: 1, + }, + )(); + + it('handles request query action', () => { + expect( + queriesReducer( + { queries: {}, normalizedData: {} }, + requestAction, + defaultConfig, + ), + ).toEqual({ + queries: { + FETCH_BOOK1: { + ...defaultState, + pending: 1, + pristine: false, + }, + }, + normalizedData: {}, + }); + }); + + it('handles success query action', () => { + expect( + queriesReducer( + { queries: {}, normalizedData: {} }, + createSuccessAction(requestAction, { data: 'data' }), + defaultConfig, + ), + ).toEqual({ + queries: { + FETCH_BOOK1: { + ...defaultState, + pending: -1, + data: 'data', + }, + }, + normalizedData: {}, + }); + }); + + it('handles error query action', () => { + expect( + queriesReducer( + { queries: {}, normalizedData: {} }, + createErrorAction(requestAction, 'error'), + defaultConfig, + ), + ).toEqual({ + queries: { + FETCH_BOOK1: { + ...defaultState, + pending: -1, + error: 'error', + }, + }, + normalizedData: {}, + }); + }); + + it('handles abort query action', () => { + expect( + queriesReducer( + { queries: {}, normalizedData: {} }, + createAbortAction(requestAction), + defaultConfig, + ), + ).toEqual({ + queries: { + FETCH_BOOK1: { + ...defaultState, + pending: -1, + }, + }, + normalizedData: {}, + }); + }); + }); + }); +}); diff --git a/packages/redux-requests/src/reducers/requests-keys-reducer.js b/packages/redux-requests/src/reducers/requests-keys-reducer.js index e6b588910..45fb66c7b 100644 --- a/packages/redux-requests/src/reducers/requests-keys-reducer.js +++ b/packages/redux-requests/src/reducers/requests-keys-reducer.js @@ -1,8 +1,8 @@ -import defaultConfig from '../default-config'; +import { isRequestAction, isRequestActionQuery } from '../actions'; // TODO: this should be rewritten to more functional style, we need things like filter object helpers -export default (state, action, config = defaultConfig) => { - if (config.isRequestAction(action) && action.meta?.requestKey !== undefined) { +export default (state, action) => { + if (isRequestAction(action) && action.meta.requestKey) { let { queries, mutations, cache, requestsKeys } = state; if (!requestsKeys[action.type]) { @@ -26,7 +26,7 @@ export default (state, action, config = defaultConfig) => { action.meta.requestsCapacity && requestsKeys[action.type].length > action.meta.requestsCapacity ) { - const isQuery = config.isRequestActionQuery(action); + const isQuery = isRequestActionQuery(action); const requestsStorage = isQuery ? queries : mutations; const numberOfExceedingRequests = diff --git a/packages/redux-requests/src/reducers/requests-keys-reducer.spec.js b/packages/redux-requests/src/reducers/requests-keys-reducer.spec.js index 4a7480731..1d1655a62 100644 --- a/packages/redux-requests/src/reducers/requests-keys-reducer.spec.js +++ b/packages/redux-requests/src/reducers/requests-keys-reducer.spec.js @@ -1,3 +1,5 @@ +import { createQuery, createMutation } from '../requests-creators'; + import requestsKeysReducer from './requests-keys-reducer'; describe('reducers', () => { @@ -17,11 +19,10 @@ describe('reducers', () => { it('appends requestKeys for request actions', () => { expect( - requestsKeysReducer(defaultState, { - type: 'REQUEST', - request: { url: '/' }, - meta: { requestKey: '1' }, - }), + requestsKeysReducer( + defaultState, + createQuery('REQUEST', { url: '/' }, { requestKey: '1' })(), + ), ).toEqual({ ...defaultState, requestsKeys: { REQUEST: ['1'] } }); }); @@ -36,11 +37,11 @@ describe('reducers', () => { REQUEST2: { pending: 1, data: 'data', error: null }, }, }, - { - type: 'REQUEST', - request: { url: '/' }, - meta: { requestKey: '2', requestsCapacity: 1 }, - }, + createQuery( + 'REQUEST', + { url: '/' }, + { requestKey: '2', requestsCapacity: 1 }, + )(), ), ).toEqual({ ...defaultState, @@ -60,11 +61,11 @@ describe('reducers', () => { REQUEST2: { pending: 1, error: null }, }, }, - { - type: 'REQUEST', - request: { url: '/' }, - meta: { requestKey: '2', requestsCapacity: 1, asMutation: true }, - }, + createMutation( + 'REQUEST', + { url: '/' }, + { requestKey: '2', requestsCapacity: 1, asMutation: true }, + )(), ), ).toEqual({ ...defaultState, @@ -86,11 +87,11 @@ describe('reducers', () => { REQUEST2: { pending: 0, data: 'data', error: null }, }, }, - { - type: 'REQUEST', - request: { url: '/' }, - meta: { requestKey: '2', requestsCapacity: 2 }, - }, + createQuery( + 'REQUEST', + { url: '/' }, + { requestKey: '2', requestsCapacity: 2 }, + )(), ), ).toEqual({ ...defaultState, @@ -113,11 +114,11 @@ describe('reducers', () => { REQUEST2: { pending: 0, data: 'data', error: null }, }, }, - { - type: 'REQUEST', - request: { url: '/' }, - meta: { requestKey: '2', requestsCapacity: 1 }, - }, + createQuery( + 'REQUEST', + { url: '/' }, + { requestKey: '2', requestsCapacity: 1 }, + )(), ), ).toEqual({ ...defaultState, diff --git a/packages/redux-requests/src/reducers/requests-reducer.js b/packages/redux-requests/src/reducers/requests-reducer.js index 302ae252d..73412b995 100644 --- a/packages/redux-requests/src/reducers/requests-reducer.js +++ b/packages/redux-requests/src/reducers/requests-reducer.js @@ -1,5 +1,4 @@ import defaultConfig from '../default-config'; -import { isResponseAction, getRequestActionFromResponse } from '../actions'; import queriesReducer from './queries-reducer'; import mutationsReducer from './mutations-reducer'; @@ -30,23 +29,13 @@ export default (config = defaultConfig) => (state = defaultState, action) => { config, ); - let { mutations } = state; - - if ( - (config.isRequestAction(action) && !config.isRequestActionQuery(action)) || - (isResponseAction(action) && - !config.isRequestActionQuery(getRequestActionFromResponse(action))) - ) { - mutations = mutationsReducer(mutations, action); - } - return { ...requestKeysReducer( { ...requestsResetReducer( { queries, - mutations, + mutations: mutationsReducer(state.mutations, action), cache: cacheReducer(state.cache, action), ...progressReducer( { @@ -54,7 +43,6 @@ export default (config = defaultConfig) => (state = defaultState, action) => { uploadProgress: state.uploadProgress, }, action, - config, ), }, action, @@ -62,7 +50,6 @@ export default (config = defaultConfig) => (state = defaultState, action) => { requestsKeys: state.requestsKeys, }, action, - config, ), normalizedData, ssr: config.ssr ? ssrReducer(state.ssr, action, config) : null, diff --git a/packages/redux-requests/src/reducers/requests-reducer.spec.js b/packages/redux-requests/src/reducers/requests-reducer.spec.js index 9c78e17eb..02d28face 100644 --- a/packages/redux-requests/src/reducers/requests-reducer.spec.js +++ b/packages/redux-requests/src/reducers/requests-reducer.spec.js @@ -1,4 +1,9 @@ import { createSuccessAction, createErrorAction } from '../actions'; +import { + createQuery, + createMutation, + createLocalMutation, +} from '../requests-creators'; import { requestsReducer } from '.'; @@ -48,8 +53,8 @@ describe('reducers', () => { it('handles read only requests', () => { const reducer = requestsReducer(); - const firstRequest = { type: 'REQUEST', request: { url: '/' } }; - const secondRequest = { type: 'REQUEST_2', request: { url: '/' } }; + const firstRequest = createQuery('REQUEST', { url: '/' })(); + const secondRequest = createQuery('REQUEST_2', { url: '/' })(); let state = reducer( { @@ -72,6 +77,7 @@ describe('reducers', () => { pristine: false, normalized: false, usedKeys: null, + dependencies: null, ref: {}, }, }); @@ -85,6 +91,7 @@ describe('reducers', () => { pristine: false, normalized: false, usedKeys: null, + dependencies: null, ref: {}, }, REQUEST_2: { @@ -94,6 +101,7 @@ describe('reducers', () => { pristine: false, normalized: false, usedKeys: null, + dependencies: null, ref: {}, }, }); @@ -110,6 +118,7 @@ describe('reducers', () => { pristine: false, normalized: false, usedKeys: null, + dependencies: null, ref: {}, }, REQUEST_2: { @@ -119,6 +128,7 @@ describe('reducers', () => { pristine: false, normalized: false, usedKeys: null, + dependencies: null, ref: {}, }, }); @@ -132,6 +142,7 @@ describe('reducers', () => { pristine: false, normalized: false, usedKeys: null, + dependencies: null, ref: {}, }, REQUEST_2: { @@ -141,6 +152,7 @@ describe('reducers', () => { pristine: false, normalized: false, usedKeys: null, + dependencies: null, ref: {}, }, }); @@ -186,7 +198,7 @@ describe('reducers', () => { requestsKeys: {}, ssr: null, }, - { type: 'REQUEST', request: { url: '/' } }, + createQuery('REQUEST', { url: '/' })(), ); let state = { @@ -198,6 +210,7 @@ describe('reducers', () => { pristine: false, normalized: false, usedKeys: null, + dependencies: null, ref: {}, }, }, @@ -208,17 +221,14 @@ describe('reducers', () => { ssr: null, }; - state = reducer(state, { - type: 'LOCAL_MUTATION', - meta: { + state = reducer( + state, + createLocalMutation('LOCAL_MUTATION', { mutations: { - REQUEST: { - local: true, - updateData: () => 'data', - }, + REQUEST: () => 'data', }, - }, - }); + })(), + ); expect(state).toEqual({ queries: { @@ -229,6 +239,7 @@ describe('reducers', () => { pristine: false, normalized: false, usedKeys: null, + dependencies: null, ref: {}, }, }, @@ -239,10 +250,10 @@ describe('reducers', () => { ssr: null, }); - const mutationWithoutConfig = { - type: 'MUTATION_WITHOUT_CONFIG', - request: { url: '/', method: 'post' }, - }; + const mutationWithoutConfig = createMutation('MUTATION_WITHOUT_CONFIG', { + url: '/', + method: 'post', + })(); state = reducer(state, mutationWithoutConfig); @@ -255,6 +266,7 @@ describe('reducers', () => { pristine: false, normalized: false, usedKeys: null, + dependencies: null, ref: {}, }, }, @@ -282,6 +294,7 @@ describe('reducers', () => { pristine: false, normalized: false, usedKeys: null, + dependencies: null, ref: {}, }, }, @@ -309,6 +322,7 @@ describe('reducers', () => { pristine: false, normalized: false, usedKeys: null, + dependencies: null, ref: {}, }, }, @@ -339,6 +353,7 @@ describe('reducers', () => { pristine: false, normalized: false, usedKeys: null, + dependencies: null, ref: {}, }, }, @@ -355,15 +370,15 @@ describe('reducers', () => { ssr: null, }); - const mutationWithConfig = { - type: 'MUTATION_WITH_CONFIG', - request: { url: '/', method: 'post' }, - meta: { + const mutationWithConfig = createMutation( + 'MUTATION_WITH_CONFIG', + { url: '/', method: 'post' }, + { mutations: { REQUEST: (_, data) => data, }, }, - }; + )(); state = reducer(state, mutationWithConfig); @@ -376,6 +391,7 @@ describe('reducers', () => { pristine: false, normalized: false, usedKeys: null, + dependencies: null, ref: {}, }, }, @@ -411,6 +427,7 @@ describe('reducers', () => { pristine: false, normalized: false, usedKeys: null, + dependencies: null, ref: {}, }, }, @@ -432,16 +449,16 @@ describe('reducers', () => { ssr: null, }); - const mutationWithConfigWithRequestKey = { - type: 'MUTATION_WITH_CONFIG_WITH_REQUEST_KEY', - request: { url: '/', method: 'post' }, - meta: { + const mutationWithConfigWithRequestKey = createMutation( + 'MUTATION_WITH_CONFIG_WITH_REQUEST_KEY', + { url: '/', method: 'post' }, + { requestKey: '1', mutations: { REQUEST: (_, data) => data, }, }, - }; + )(); state = reducer(state, mutationWithConfigWithRequestKey); state = reducer(state, mutationWithConfigWithRequestKey); @@ -455,6 +472,7 @@ describe('reducers', () => { pristine: false, normalized: false, usedKeys: null, + dependencies: null, ref: {}, }, }, @@ -499,6 +517,7 @@ describe('reducers', () => { pristine: false, normalized: false, usedKeys: null, + dependencies: null, ref: {}, }, }, @@ -543,6 +562,7 @@ describe('reducers', () => { pristine: false, normalized: false, usedKeys: null, + dependencies: null, ref: {}, }, }, @@ -571,10 +591,10 @@ describe('reducers', () => { ssr: null, }); - const mutationWithOptimisticUpdate = { - type: 'MUTATION_WITH_OPTIMISTIC_UPDATE', - request: { url: '/', method: 'post' }, - meta: { + const mutationWithOptimisticUpdate = createMutation( + 'MUTATION_WITH_OPTIMISTIC_UPDATE', + { url: '/', method: 'post' }, + { mutations: { REQUEST: { updateData: (data, mutationData) => mutationData, @@ -583,7 +603,7 @@ describe('reducers', () => { }, }, }, - }; + )(); state = reducer(state, mutationWithOptimisticUpdate); @@ -596,6 +616,7 @@ describe('reducers', () => { pristine: false, normalized: false, usedKeys: null, + dependencies: null, ref: {}, }, }, @@ -643,6 +664,7 @@ describe('reducers', () => { pristine: false, normalized: false, usedKeys: null, + dependencies: null, ref: {}, }, }, @@ -692,6 +714,7 @@ describe('reducers', () => { pristine: false, normalized: false, usedKeys: null, + dependencies: null, ref: {}, }, }, @@ -737,6 +760,7 @@ describe('reducers', () => { pristine: false, normalized: false, usedKeys: null, + dependencies: null, ref: {}, }, }, @@ -750,17 +774,14 @@ describe('reducers', () => { let state = reducer(initialState, {}); expect(state).toEqual(initialState); - state = reducer(state, { - type: 'LOCAL_MUTATION', - meta: { + state = reducer( + state, + createLocalMutation('LOCAL_MUTATION', { mutations: { - QUERY: { - updateData: data => [...data, 'data2'], - local: true, - }, + QUERY: data => [...data, 'data2'], }, - }, - }); + })(), + ); expect(state).toEqual({ queries: { QUERY: { @@ -770,6 +791,7 @@ describe('reducers', () => { pristine: false, normalized: false, usedKeys: null, + dependencies: null, ref: {}, }, }, diff --git a/packages/redux-requests/src/reducers/requests-reset-reducer.js b/packages/redux-requests/src/reducers/requests-reset-reducer.js index 67b4cfc75..6e29bc300 100644 --- a/packages/redux-requests/src/reducers/requests-reset-reducer.js +++ b/packages/redux-requests/src/reducers/requests-reset-reducer.js @@ -1,14 +1,11 @@ import { RESET_REQUESTS } from '../constants'; import { mapObject } from '../helpers'; -const getRequestTypeString = requestType => - typeof requestType === 'function' ? requestType.toString() : requestType; - const getKeys = requests => requests.map(v => typeof v === 'object' - ? getRequestTypeString(v.requestType) + (v.requestKey || '') - : getRequestTypeString(v), + ? v.requestType.toString() + (v.requestKey || '') + : v.toString(), ); const resetQuery = query => @@ -20,6 +17,7 @@ const resetQuery = query => error: null, pristine: true, usedKeys: query.normalized ? {} : null, + dependencies: query.normalized ? [] : null, }; const resetMutation = mutation => diff --git a/packages/redux-requests/src/reducers/requests-reset-reducer.spec.js b/packages/redux-requests/src/reducers/requests-reset-reducer.spec.js index 95d8d13be..ba2af6a32 100644 --- a/packages/redux-requests/src/reducers/requests-reset-reducer.spec.js +++ b/packages/redux-requests/src/reducers/requests-reset-reducer.spec.js @@ -21,6 +21,7 @@ describe('reducers', () => { pristine: false, normalized: false, usedKeys: null, + dependencies: null, }, QUERY2: { data: 'data', @@ -29,6 +30,7 @@ describe('reducers', () => { pristine: false, normalized: true, usedKeys: { x: 1 }, + dependencies: [], }, }, mutations: { @@ -49,6 +51,7 @@ describe('reducers', () => { pristine: true, normalized: false, usedKeys: null, + dependencies: null, }, QUERY2: { data: null, @@ -57,6 +60,7 @@ describe('reducers', () => { pristine: true, normalized: true, usedKeys: {}, + dependencies: [], }, }, mutations: { @@ -80,6 +84,7 @@ describe('reducers', () => { pristine: false, normalized: false, usedKeys: null, + dependencies: null, }, QUERY2: { data: 'data', @@ -88,6 +93,7 @@ describe('reducers', () => { pristine: false, normalized: true, usedKeys: { x: 1 }, + dependencies: [], }, QUERY3: { data: 'data', @@ -96,6 +102,7 @@ describe('reducers', () => { pristine: false, normalized: true, usedKeys: { x: 1 }, + dependencies: [], }, }, mutations: { @@ -122,6 +129,7 @@ describe('reducers', () => { pristine: true, normalized: false, usedKeys: null, + dependencies: null, }, QUERY2: { data: null, @@ -130,6 +138,7 @@ describe('reducers', () => { pristine: true, normalized: true, usedKeys: {}, + dependencies: [], }, QUERY3: { data: 'data', @@ -138,6 +147,7 @@ describe('reducers', () => { pristine: false, normalized: true, usedKeys: { x: 1 }, + dependencies: [], }, }, mutations: { @@ -166,6 +176,7 @@ describe('reducers', () => { pristine: false, normalized: false, usedKeys: null, + dependencies: null, }, }, mutations: {}, @@ -184,6 +195,7 @@ describe('reducers', () => { pristine: true, normalized: false, usedKeys: null, + dependencies: null, }, }, mutations: {}, diff --git a/packages/redux-requests/src/reducers/ssr-reducer.js b/packages/redux-requests/src/reducers/ssr-reducer.js index bee181e8e..d0d589356 100644 --- a/packages/redux-requests/src/reducers/ssr-reducer.js +++ b/packages/redux-requests/src/reducers/ssr-reducer.js @@ -1,5 +1,9 @@ import defaultConfig from '../default-config'; -import { getRequestActionFromResponse, isResponseAction } from '../actions'; +import { + getRequestActionFromResponse, + isResponseAction, + isRequestAction, +} from '../actions'; import { JOIN_REQUEST } from '../constants'; export default (state = [], action, config = defaultConfig) => { @@ -21,8 +25,8 @@ export default (state = [], action, config = defaultConfig) => { if ( config.ssr === 'client' && - config.isRequestAction(action) && - (action.meta?.ssrResponse || action.meta?.ssrError) + isRequestAction(action) && + (action.meta.ssrResponse || action.meta.ssrError) ) { const indexToRemove = state.findIndex( v => diff --git a/packages/redux-requests/src/reducers/ssr-reducer.spec.js b/packages/redux-requests/src/reducers/ssr-reducer.spec.js index a8100e310..578799be0 100644 --- a/packages/redux-requests/src/reducers/ssr-reducer.spec.js +++ b/packages/redux-requests/src/reducers/ssr-reducer.spec.js @@ -1,5 +1,6 @@ import { createSuccessAction } from '../actions'; import defaultConfig from '../default-config'; +import { createQuery } from '../requests-creators'; import ssrReducer from './ssr-reducer'; @@ -18,10 +19,7 @@ describe('reducers', () => { expect( ssrReducer( ['REQUEST'], - createSuccessAction( - { type: 'REQUEST', request: { url: '/' } }, - 'data', - ), + createSuccessAction(createQuery('REQUEST', { url: '/' })(), 'data'), { ...defaultConfig, ssr: 'server' }, ), ).toEqual(['REQUEST', 'REQUEST']); @@ -31,12 +29,11 @@ describe('reducers', () => { expect( ssrReducer( ['REQUEST', 'REQUEST'], - - { - type: 'REQUEST', - request: { url: '/' }, - meta: { ssrResponse: { data: 'data' } }, - }, + createQuery( + 'REQUEST', + { url: '/' }, + { ssrResponse: { data: 'data' } }, + )(), { ...defaultConfig, ssr: 'client' }, ), ).toEqual(['REQUEST']); @@ -46,12 +43,11 @@ describe('reducers', () => { expect( ssrReducer( ['REQUEST', 'REQUEST'], - - { - type: 'REQUEST2', - request: { url: '/' }, - meta: { ssrResponse: { data: 'data' } }, - }, + createQuery( + 'REQUEST2', + { url: '/' }, + { ssrResponse: { data: 'data' } }, + )(), { ...defaultConfig, ssr: 'client' }, ), ).toEqual(['REQUEST', 'REQUEST']); diff --git a/packages/redux-requests/src/reducers/update-data.js b/packages/redux-requests/src/reducers/update-data.js index 95fee30e2..7373d7c39 100644 --- a/packages/redux-requests/src/reducers/update-data.js +++ b/packages/redux-requests/src/reducers/update-data.js @@ -1,4 +1,8 @@ -import { isSuccessAction, isResponseAction } from '../actions'; +import { + isSuccessAction, + isResponseAction, + isRequestActionLocalMutation, +} from '../actions'; const getDataUpdater = mutationConfig => { if (typeof mutationConfig === 'function') { @@ -31,7 +35,7 @@ export default (data, action, mutationConfig) => { return mutationConfig.updateDataOptimistic(data); } - if (mutationConfig.local) { + if (isRequestActionLocalMutation(action)) { return getDataUpdater(mutationConfig)(data); } diff --git a/packages/redux-requests/src/requests-creators.js b/packages/redux-requests/src/requests-creators.js new file mode 100644 index 000000000..5eb00f3b0 --- /dev/null +++ b/packages/redux-requests/src/requests-creators.js @@ -0,0 +1,37 @@ +const createRequest = requestType => (type, requestConfig, metaConfig) => { + const actionCreator = (...params) => ({ + type, + payload: + typeof requestConfig === 'function' + ? requestConfig(...params) + : requestConfig, + meta: { + ...(typeof metaConfig === 'function' + ? metaConfig(...params) + : metaConfig), + requestType, + }, + }); + actionCreator.toString = () => type; + return actionCreator; +}; + +export const createQuery = createRequest('QUERY'); + +export const createMutation = createRequest('MUTATION'); + +export const createSubscription = createRequest('SUBSCRIPTION'); + +export const createLocalMutation = (type, metaConfig) => { + const actionCreator = (...params) => ({ + type, + meta: { + ...(typeof metaConfig === 'function' + ? metaConfig(...params) + : metaConfig), + requestType: 'LOCAL_MUTATION', + }, + }); + actionCreator.toString = () => type; + return actionCreator; +}; diff --git a/packages/redux-requests/src/requests-creators.spec.js b/packages/redux-requests/src/requests-creators.spec.js new file mode 100644 index 000000000..92545a6f2 --- /dev/null +++ b/packages/redux-requests/src/requests-creators.spec.js @@ -0,0 +1,45 @@ +import { createQuery } from './requests-creators'; + +describe('requestsCreators', () => { + describe('createQuery', () => { + it('adds toString method', () => { + const queryCreator = createQuery('QUERY', () => ({ url: '/' })); + expect(queryCreator.toString()).toBe('QUERY'); + }); + + it('can create queries with only request config', () => { + const queryCreator = createQuery('QUERY', { url: '/' }); + expect(queryCreator()).toEqual({ + type: 'QUERY', + payload: { url: '/' }, + meta: { requestType: 'QUERY' }, + }); + }); + + it('merges meta properly', () => { + const queryCreator = createQuery( + 'QUERY', + { url: '/' }, + { normalize: true }, + ); + expect(queryCreator()).toEqual({ + type: 'QUERY', + payload: { url: '/' }, + meta: { requestType: 'QUERY', normalize: true }, + }); + }); + + it('allows callbacks configs', () => { + const queryCreator = createQuery( + 'QUERY', + id => ({ url: `/${id}` }), + id => ({ requestKey: id }), + ); + expect(queryCreator('1')).toEqual({ + type: 'QUERY', + payload: { url: '/1' }, + meta: { requestType: 'QUERY', requestKey: '1' }, + }); + }); + }); +}); diff --git a/packages/redux-requests/src/selectors/get-query.js b/packages/redux-requests/src/selectors/get-query.js index 3d5972801..a69d4d0f9 100644 --- a/packages/redux-requests/src/selectors/get-query.js +++ b/packages/redux-requests/src/selectors/get-query.js @@ -14,14 +14,6 @@ const isQueryEqual = (currentVal, previousVal) => { return false; } - if ( - currentVal.data === null && - (currentVal.multiple !== previousVal.multiple || - currentVal.defaultData !== previousVal.defaultData) - ) { - return false; - } - if ( currentVal.normalized && currentVal.normalizedData !== previousVal.normalizedData @@ -59,28 +51,12 @@ const createCustomSelector = createSelectorCreator( isQueryEqual, ); -const getData = (data, multiple, defaultData) => { - if (data !== null) { - return data; - } - - if (defaultData !== undefined) { - return defaultData; - } - - if (multiple) { - return []; - } - - return data; -}; - const getQueryState = (state, type, requestKey = '') => state.requests.queries[type + requestKey]; const createQuerySelector = (type, requestKey) => createCustomSelector( - (state, defaultData, multiple) => { + state => { // in order not to keep queryState.ref reference in selector memoize const { data, @@ -98,8 +74,6 @@ const createQuerySelector = (type, requestKey) => pristine, normalized, usedKeys, - multiple, - defaultData, normalizedData: state.requests.normalizedData, downloadProgress: state.requests.downloadProgress[type + (requestKey || '')] ?? null, @@ -115,18 +89,10 @@ const createQuerySelector = (type, requestKey) => usedKeys, normalized, normalizedData, - defaultData, - multiple, downloadProgress, uploadProgress, }) => ({ - data: normalized - ? denormalize( - getData(data, multiple, defaultData), - normalizedData, - usedKeys, - ) - : getData(data, multiple, defaultData), + data: normalized ? denormalize(data, normalizedData, usedKeys) : data, pending, loading: pending > 0, error, @@ -146,42 +112,18 @@ const defaultQuery = { uploadProgress: null, }; -const defaultQueryMultiple = { - ...defaultQuery, - data: [], -}; - -const defaultQueriesWithCustomData = new Map(); - -const getDefaultQuery = (defaultData, multiple) => { - if ( - defaultData !== undefined && - defaultQueriesWithCustomData.get(defaultData) - ) { - return defaultQueriesWithCustomData.get(defaultData); - } - - if (defaultData !== undefined) { - const query = { ...defaultQuery, data: defaultData }; - defaultQueriesWithCustomData.set(defaultData, query); - return query; - } - - return multiple ? defaultQueryMultiple : defaultQuery; -}; - const querySelectors = new WeakMap(); -export default (state, { type, requestKey, defaultData, multiple = false }) => { +export default (state, { type, requestKey }) => { const queryState = getQueryState(state, type, requestKey); if (!queryState) { - return getDefaultQuery(defaultData, multiple); + return defaultQuery; } if (!querySelectors.get(queryState.ref)) { querySelectors.set(queryState.ref, createQuerySelector(type, requestKey)); } - return querySelectors.get(queryState.ref)(state, defaultData, multiple); + return querySelectors.get(queryState.ref)(state); }; diff --git a/packages/redux-requests/src/selectors/get-query.spec.js b/packages/redux-requests/src/selectors/get-query.spec.js index 0690578dc..b62cf5998 100644 --- a/packages/redux-requests/src/selectors/get-query.spec.js +++ b/packages/redux-requests/src/selectors/get-query.spec.js @@ -89,60 +89,6 @@ describe('selectors', () => { }); }); - it('replaces data as null with [] when multiple true', () => { - expect( - getQuery( - { - requests: { - queries: {}, - mutations: {}, - downloadProgress: {}, - uploadProgress: {}, - }, - }, - { type: 'QUERY', multiple: true }, - ), - ).toEqual({ - data: [], - pending: 0, - loading: false, - error: null, - pristine: true, - downloadProgress: null, - uploadProgress: null, - }); - }); - - it('replaces data as custom object with {} when defaultData defined', () => { - expect( - getQuery( - { - requests: { - queries: {}, - mutations: {}, - downloadProgress: {}, - uploadProgress: {}, - }, - }, - { type: 'QUERY', defaultData: {} }, - ), - ).toEqual({ - data: {}, - loading: false, - pending: 0, - error: null, - pristine: true, - downloadProgress: null, - uploadProgress: null, - }); - }); - - it('doesnt recompute when multiple is changed when data not empty', () => { - expect(getQuery(state, { type: 'QUERY', multiple: true })).toBe( - getQuery(state, { type: 'QUERY', multiple: false }), - ); - }); - it('returns transformed query state if found', () => { expect(getQuery(state, { type: 'QUERY' })).toEqual({ data: 'data', diff --git a/packages/redux-requests/types/index.d.spec.ts b/packages/redux-requests/types/index.d.spec.ts index cff60f65b..97cb14400 100644 --- a/packages/redux-requests/types/index.d.spec.ts +++ b/packages/redux-requests/types/index.d.spec.ts @@ -7,8 +7,6 @@ import { clearRequestsCache, resetRequests, abortRequests, - RequestAction, - LocalMutationAction, ResponseData, handleRequests, getQuery, @@ -18,74 +16,82 @@ import { isRequestAction, isRequestActionQuery, isResponseAction, - createRequestsStore, + createQuery, } from './index'; success('type'); error('type'); abort('type'); -const requestAction: RequestAction = { - type: 'FETCH', - request: { url: '/' }, - meta: { - driver: 'default', - takeLatest: false, - cache: 1, - cacheKey: 'key', - cacheSize: 2, - dependentRequestsNumber: 1, - isDependentRequest: true, - customKey: 'customValue', - requestKey: '1', - asMutation: true, - mutations: { - FETCH: { - updateData: () => 'data', - revertData: () => 'data', - }, - }, +const fetchBook = createQuery( + 'fetchBook', + (id: number) => ({ url: `/books/${id}` }), + { + getData: (data: string) => ({ + title: 'title', + nested: { value: 1, data }, + }), + normalize: true, + requestType: 'QUERY', }, -}; - -const accessRequestActionProps = (requestAction: RequestAction) => { - if (requestAction.request !== undefined) { - // this request action has an existing `request` key - } else if (requestAction.payload !== undefined) { - // this request action has an existing `payload` key - } -} - -const fetchBook: ( - id: string, -) => RequestAction<{ id: string; title: string }> = () => { - return { - type: 'FETCH_BOOK', - request: { - url: '/book', - }, - }; -}; - -const dummyDriver: Driver = ({}, requestAction, {}) => - new Promise((resolve) => { resolve() }); +); + +// const fetchBook: ( +// id: string, +// ) => RequestAction<{ id: string; title: string }> = () => { +// return { +// type: 'FETCH_BOOK', +// request: { +// url: '/book', +// }, +// }; +// }; + +// const dummyDriver: Driver = ({}, requestAction, {}) => +// new Promise(resolve => { +// resolve(); +// }); +const x = fetchBook(1); + +const booksQuery = getQuery({}, { type: fetchBook }); +booksQuery.data.nested.value; + +const booksSelector = getQuerySelector({ type: fetchBook }); +booksSelector({}).data.title; + +let dummyDriver: Driver; +dummyDriver({}, fetchBook, {}) + .then(v => v) + .catch(e => { + throw e; + }); handleRequests({ driver: dummyDriver }); handleRequests({ driver: { default: dummyDriver, anotherDriver: dummyDriver }, onRequest: (request, action) => request, onSuccess: async (response, action, store) => { - const r = await store.dispatchRequest(fetchBook('1')); + const r = await store.dispatch(fetchBook(1)); return response; }, onError: (error, action) => ({ error }), onAbort: action => {}, takeLatest: true, - isRequestActionQuery: () => true, }); -const requestsStore = createRequestsStore(createStore(combineReducers({}))); -const response = requestsStore.dispatchRequest(fetchBook('1')); +const reducer = (state = 0, action) => { + if (action.type === 'KSKS') { + return 1; + } + + return state; +}; + +const requestsStore = createStore(combineReducers({ x: reducer })); + +const ff = fetchBook(1); +const response = requestsStore.dispatch(fetchBook(1)); +const response2 = requestsStore.dispatch({ type: 'LALA' }); clearRequestsCache(); clearRequestsCache(['TYPE']); @@ -99,57 +105,6 @@ resetRequests(); resetRequests(['TYPE']); resetRequests(['TYPE', { requestType: 'ANOTHER_TYPE', requestKey: '1' }]); -getQuery({}, { type: 'Mutation', requestKey: '1' }); - -const querySelector = getQuerySelector({ type: 'Query' }); -querySelector({}); - -const query = getQuery<{ key: string }>({}, { type: 'Query' }); -query.data.key = '1'; - -const querySelector2 = getQuerySelector<{ key: string }>({ type: 'Query' }); -const query2 = querySelector2({}); -query2.data.key = '1'; - -getMutation({}, { type: 'Mutation', requestKey: '1' }); -const mutationSelector = getMutationSelector({ type: 'Mutation' }); -mutationSelector({}); - isRequestAction({ type: 'ACTION' }) === true; isRequestActionQuery({ type: 'ACTION', request: { url: '/' } }) === true; isResponseAction({ type: 'ACTION', request: { url: '/' } }) === true; - -const fetchBooks: () => RequestAction< - { raw: boolean }, - { parsed: boolean } -> = () => { - return { - type: 'FETCH_BOOKS', - request: { - url: '/books', - }, - meta: { - getData: data => ({ parsed: data.raw }), - }, - }; -}; - -const booksQuery = getQuery({}, { type: fetchBooks }); - -const booksSelector = getQuerySelector({ type: fetchBooks }); -booksSelector({}).data.parsed; - -type BooksData = ResponseData; - -const localMutation: () => LocalMutationAction = () => ({ - type: 'LOCAL_MUTATION', - meta: { - localData: { id: '1', title: 'title' }, - mutations: { - FETCH_BOOKS: { - updateData: (data: BooksData) => ({ parsed: data.parsed }), - local: true, - }, - }, - }, -}); diff --git a/packages/redux-requests/types/index.d.ts b/packages/redux-requests/types/index.d.ts index 19c5e1db1..3f7fb2c95 100644 --- a/packages/redux-requests/types/index.d.ts +++ b/packages/redux-requests/types/index.d.ts @@ -1,14 +1,7 @@ import { AnyAction, Reducer, Middleware, Store } from 'redux'; -export interface DispatchRequest { - ( - requestAction: RequestAction, - ): Promise<{ - data?: QueryStateData; - error?: any; - isAborted?: true; - action: any; - }>; +interface Config { + [key: string]: any; } interface FilterActions { @@ -19,14 +12,15 @@ interface ModifyData { (data: any, mutationData: any): any; } -interface RequestsStore extends Store { - dispatchRequest: DispatchRequest; -} +type ActionTypeModifier = (actionType: string) => string; -export const createRequestsStore: (store: Store) => RequestsStore; +export const success: ActionTypeModifier; -interface RequestActionMeta { - asMutation?: boolean; +export const error: ActionTypeModifier; + +export const abort: ActionTypeModifier; + +interface RequestMeta { driver?: string; takeLatest?: boolean; getData?: (data: Data, currentData: TransformedData) => TransformedData; @@ -34,24 +28,6 @@ interface RequestActionMeta { requestKey?: string; requestsCapacity?: number; normalize?: boolean; - mutations?: { - [actionType: string]: - | ModifyData - | { - updateData?: ModifyData; - updateDataOptimistic?: (data: any) => any; - revertData?: (data: any) => any; - local?: boolean; - }; - }; - optimisticData?: any; - revertedData?: any; - localData?: any; - cache?: boolean | number; - cacheKey?: string; - poll?: number; - dependentRequestsNumber?: number; - isDependentRequest?: boolean; silent?: boolean; onRequest?: ( request: any, @@ -71,40 +47,44 @@ interface RequestActionMeta { runOnAbort?: boolean; measureDownloadProgress?: boolean; measureUploadProgress?: boolean; - [extraProperty: string]: any; } -export type RequestAction = - | { - type?: string; - payload?: never; - request: any | any[]; - meta?: RequestActionMeta; - } - | { - type?: string; - payload: { - request: any | any[]; - }; - request?: never; - meta?: RequestActionMeta; - }; - -export type LocalMutationAction = { - type?: string; - meta: { - mutations?: { - [actionType: string]: { - updateData: ModifyData; - local: true; - }; - }; - localData?: any; - [extraProperty: string]: any; +interface QueryMeta + extends RequestMeta { + requestType: 'QUERY'; + cache?: boolean | number; + cacheKey?: string; + poll?: number; + dependentRequestsNumber?: number; + isDependentRequest?: boolean; +} + +interface MutationMeta + extends RequestMeta { + requestType: 'MUTATION'; + mutations?: { + [actionType: string]: + | ModifyData + | { + updateData?: ModifyData; + updateDataOptimistic?: (data: any) => any; + revertData?: (data: any) => any; + }; + }; + optimisticData?: any; + revertedData?: any; +} + +interface LocalMutationMeta { + requestType: 'LOCAL_MUTATION'; + mutations?: { + [actionType: string]: ModifyData; }; -}; + localData?: any; +} -interface SubscriptionActionMeta { +interface SubscriptionMeta { + requestType: 'SUBSCRIPTION'; requestKey?: string; normalize?: boolean; mutations?: { @@ -116,34 +96,86 @@ interface SubscriptionActionMeta { }; getData?: (data: any) => any; onMessage?: (data: any, message: any, store: RequestsStore) => void; - [extraProperty: string]: any; } -export type SubscriptionAction = - | { - type?: string; - subscription: any; - meta?: SubscriptionActionMeta; - } - | { - type?: string; - payload: { - subscription: any; - }; - meta?: SubscriptionActionMeta; - }; +export interface Query { + type: string; + payload: any | any[]; + meta?: QueryMeta; +} + +export interface Mutation { + type: string; + payload: any | any[]; + meta?: MutationMeta; +} -type ResponseData< - Request extends (...args: any[]) => RequestAction -> = ReturnType['meta']>['getData']>; +export interface LocalMutation { + type: string; + meta: LocalMutationMeta; +} -type ActionTypeModifier = (actionType: string) => string; +export interface Subscription { + type: string; + payload: any; + meta?: SubscriptionMeta; +} -export const success: ActionTypeModifier; +export interface Dispatch { + (action: Action): Action extends + | Query + | Mutation + ? Promise<{ + data?: Data; + error?: any; + isAborted?: true; + action: any; + }> + : Action; +} -export const error: ActionTypeModifier; +interface RequestsStore extends Store { + dispatch: Dispatch; +} -export const abort: ActionTypeModifier; +export function createQuery< + Data = any, + TransformedData = Data, + Variables extends any[] = any[] +>( + type: string, + requestConfig: Config | ((...params: Variables) => Config), + metaConfig?: + | QueryMeta + | ((...params: Variables) => QueryMeta), +): (...params: Variables) => Query; + +export function createMutation< + Data = any, + TransformedData = Data, + Variables extends any[] = any[] +>( + type: string, + requestConfig: Config | ((...params: Variables) => Config), + metaConfig?: + | MutationMeta + | ((...params: Variables) => MutationMeta), +): (...params: Variables) => Mutation; + +export function createLocalMutation( + type: string, + metaConfig: LocalMutationMeta | ((...params: Variables) => LocalMutationMeta), +): (...params: Variables) => LocalMutation; + +export function createSubscription( + type: string, + requestConfig: Config | ((...params: Variables) => Config) | null, + metaConfig?: SubscriptionMeta | ((...params: Variables) => SubscriptionMeta), +): (...params: Variables) => Subscription; + +type ResponseData< + Request extends (...args: any[]) => Query | Mutation +> = ReturnType['meta']['getData']>; interface DriverActions { setDownloadProgress?: (downloadProgress: number) => void; @@ -195,11 +227,8 @@ export interface HandleRequestConfig { ) => any; onError?: (error: any, action: RequestAction, store: RequestsStore) => any; onAbort?: (action: RequestAction, store: RequestsStore) => void; - cache?: boolean; ssr?: null | 'client' | 'server'; disableRequestsPromise?: boolean; - isRequestAction?: (action: AnyAction) => boolean; - isRequestActionQuery?: (requestAction: RequestAction) => boolean; takeLatest?: boolean | FilterActions; normalize?: boolean; getNormalisationObjectKey?: (obj: any) => string; @@ -293,8 +322,8 @@ export const joinRequest: ( rehydrate: boolean; }; -export interface QueryState { - data: QueryStateData; +export interface QueryState { + data: Data; error: any; pending: number; loading: boolean; @@ -303,24 +332,18 @@ export interface QueryState { downloadProgress: number | null; } -export function getQuery( +export function getQuery( state: any, props: { - type: string | ((...params: any[]) => RequestAction); - action?: (...params: any[]) => RequestAction; + type: (...params: any[]) => Query; requestKey?: string; - multiple?: boolean; - defaultData?: any; }, -): QueryState; +): QueryState; -export function getQuerySelector(props: { - type: string | ((...params: any[]) => RequestAction); - action?: (...params: any[]) => RequestAction; +export function getQuerySelector(props: { + type: (...params: any[]) => Query; requestKey?: string; - multiple?: boolean; - defaultData?: any; -}): (state: any) => QueryState; +}): (state: any) => QueryState; export interface MutationState { pending: number; @@ -333,13 +356,13 @@ export interface MutationState { export function getMutation( state: any, props: { - type: string | ((...params: any[]) => RequestAction); + type: (...params: any[]) => Mutation; requestKey?: string; }, ): MutationState; export function getMutationSelector(props: { - type: string | ((...params: any[]) => RequestAction); + type: (...params: any[]) => Mutation; requestKey?: string; }): (state: any) => MutationState; diff --git a/yarn.lock b/yarn.lock index c30dd5410..a5c0a40a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1214,6 +1214,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.9.2": + version "7.13.17" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.17.tgz#8966d1fc9593bf848602f0662d6b4d0069e3a7ec" + integrity sha512-NCdgJEelPTSh+FEFylhnP1ylq848l1z9t9N0j1Lfbcw0+KXGjsTvUmkxy+voLLXB5SOKMbLLx4jxYliGrYQseA== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.1.0", "@babel/template@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237" @@ -8722,7 +8729,7 @@ react-dom@17.0.1: object-assign "^4.1.1" scheduler "^0.20.1" -react-is@>=16.13.1, "react-is@^16.12.0 || ^17.0.0", react-is@^17.0.1: +"react-is@^16.12.0 || ^17.0.0", react-is@^17.0.1: version "17.0.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== @@ -8977,13 +8984,12 @@ redux-mock-store@1.5.4: dependencies: lodash.isplainobject "^4.0.6" -redux@4.0.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f" - integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w== +redux@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.0.tgz#eb049679f2f523c379f1aff345c8612f294c88d4" + integrity sha512-uI2dQN43zqLWCt6B/BMGRMY6db7TTY4qeHHfGeKb3EOhmOKjU3KdWvNLJyqaHRksv/ErdNH7cFZWg9jXtewy4g== dependencies: - loose-envify "^1.4.0" - symbol-observable "^1.2.0" + "@babel/runtime" "^7.9.2" regenerate-unicode-properties@^8.1.0: version "8.1.0" @@ -10013,11 +10019,6 @@ supports-hyperlinks@^2.0.0: has-flag "^4.0.0" supports-color "^7.0.0" -symbol-observable@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" - integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== - symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" @@ -10421,10 +10422,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.2.tgz#6369ef22516fe5e10304aae5a5c4862db55380e9" - integrity sha512-thGloWsGH3SOxv1SoY7QojKi0tc+8FnOmiarEGMbd/lar7QOEd3hvlx3Fp5y6FlDUGl9L+pd4n2e+oToGMmhRQ== +typescript@4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.4.tgz#8610b59747de028fda898a8aef0e103f156d0961" + integrity sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg== typical@^5.0.0, typical@^5.2.0: version "5.2.0"