diff --git a/docs/use-infinite-query.md b/docs/use-infinite-query.md index b20a9bc..1453c34 100644 --- a/docs/use-infinite-query.md +++ b/docs/use-infinite-query.md @@ -71,13 +71,14 @@ export const UserTable: React.FC = () => { The `useInfiniteQuery` hook receives a configuration object with the following properties -| Property | Description | -| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `params` | The params for the API call. In the case of `useInfiniteQuery`, the params must be passed as a function which receives the page size and the previous response data as argument, and returns the params object. This structure should allow for both number & cursor driven paging models (see [paging](paging.md) for more information.) | -| `fetchConfig` | The `AxiosRequest` config for the API call, this will be merged with any global fetch config. (see [here](https://axios-http.com/docs/req_config) for full docs.) | -| `fetchWrapper` | An optional fetch wrapper for this specific hook. NOTE: This will be called in place of any supplied [global fetch wrapper](global-fetch-wrapper.md) for maximum flexibility. If you want to use the global fetch wrapper, you must call it manually within the wrapper passed here. | -| `cacheKey` | The cache key to store the response against, it can be a string param key, an array of param keys, or a function that generates the key from params, see [here](caching.md) for more info | -| `swrConfig` | Additional config to send to SWR (like settings or fallback data for SSR.) This `SWRConfiguration` (see [here](https://swr.vercel.app/docs/api#options) for full docs.) will be merged with any global config | +| Property | Description | +| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `params` | The params for the API call. In the case of `useInfiniteQuery`, the params must be passed as a function which receives the page size and the previous response data as argument, and returns the params object. This structure should allow for both number & cursor driven paging models (see [paging](paging.md) for more information.) | +| `fetchConfig` | The `AxiosRequest` config for the API call, this will be merged with any global fetch config. (see [here](https://axios-http.com/docs/req_config) for full docs.) | +| `fetchWrapper` | An optional fetch wrapper for this specific hook. NOTE: This will be called in place of any supplied [global fetch wrapper](global-fetch-wrapper.md) for maximum flexibility. If you want to use the global fetch wrapper, you must call it manually within the wrapper passed here. | +| `cacheKey` | The cache key to store the response against, it can be a string param key, an array of param keys, or a function that generates the key from params, see [here](caching.md) for more info | +| `swrConfig` | Additional config to send to SWR (like settings or fallback data for SSR.) This `SWRConfiguration` (see [here](https://swr.vercel.app/docs/api#options) for full docs.) will be merged with any global config | +| `enableMocking` | Whether to use the supplied [mocked endpoints](mocking.md) instead of real endpoints for this hook only, resulting in no genuine API calls being made. If this property is `true`, you **must** have a mock endpoint registered for every endpoint used in your app, otherwise an error will be thrown | ## Returns diff --git a/docs/use-mutation.md b/docs/use-mutation.md index be41e0e..d47cc6d 100644 --- a/docs/use-mutation.md +++ b/docs/use-mutation.md @@ -71,11 +71,12 @@ export const UserForm: React.FC = ({ userId }) => { The `useMutation` hook receives a configuration object with the following properties -| Property | Description | -| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `params` | The params for the API call (usually a combination route params, query string params & body). A re-fetch will be triggered automatically if the values in here change | -| `fetchConfig` | The `AxiosRequest` config for the API call, this will be merged with any global fetch config. (see [here](https://axios-http.com/docs/req_config) for full docs.) | -| `fetchWrapper` | An optional fetch wrapper for this specific hook. NOTE: This will be called in place of any supplied [global fetch wrapper](global-fetch-wrapper.md) for maximum flexibility. If you want to use the global fetch wrapper, you must call it manually within the wrapper passed here. | +| Property | Description | +| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `params` | The params for the API call (usually a combination route params, query string params & body). A re-fetch will be triggered automatically if the values in here change | +| `fetchConfig` | The `AxiosRequest` config for the API call, this will be merged with any global fetch config. (see [here](https://axios-http.com/docs/req_config) for full docs.) | +| `fetchWrapper` | An optional fetch wrapper for this specific hook. NOTE: This will be called in place of any supplied [global fetch wrapper](global-fetch-wrapper.md) for maximum flexibility. If you want to use the global fetch wrapper, you must call it manually within the wrapper passed here. | +| `enableMocking` | Whether to use the supplied [mocked endpoints](mocking.md) instead of real endpoints for this hook only, resulting in no genuine API calls being made. If this property is `true`, you **must** have a mock endpoint registered for every endpoint used in your app, otherwise an error will be thrown | ## Returns diff --git a/docs/use-query.md b/docs/use-query.md index cf58db6..bdb9a5e 100644 --- a/docs/use-query.md +++ b/docs/use-query.md @@ -48,14 +48,15 @@ export const UserCard: React.FC = ({ userId }) => { The `useQuery` hook receives a configuration object with the following properties -| Property | Description | -| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `params` | The params for the API call (usually a combination route params, query string params & body). A re-fetch will be triggered automatically if the values in here change | -| `fetchConfig` | The `AxiosRequest` config for the API call, this will be merged with any global fetch config. (see [here](https://axios-http.com/docs/req_config) for full docs.) | -| `fetchWrapper` | An optional fetch wrapper for this specific hook. NOTE: This will be called in place of any supplied [global fetch wrapper](global-fetch-wrapper.md) for maximum flexibility. If you want to use the global fetch wrapper, you must call it manually within the wrapper passed here. | -| `cacheKey` | The cache key to store the response against, it can be a string param key, an array of param keys, or a function that generates the key from params, see [here](caching.md) for more info | -| `swrConfig` | Additional config to send to SWR (like settings or fallback data for SSR.) This `SWRConfiguration` (see [here](https://swr.vercel.app/docs/api#options) for full docs.) will be merged with any global config | -| `waitFor` | An optional boolean. If this property is false, the query fetch will wait until it becomes true or undefined. Useful for holding back queries until conditions are met. | +| Property | Description | +| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `params` | The params for the API call (usually a combination route params, query string params & body). A re-fetch will be triggered automatically if the values in here change | +| `fetchConfig` | The `AxiosRequest` config for the API call, this will be merged with any global fetch config. (see [here](https://axios-http.com/docs/req_config) for full docs.) | +| `fetchWrapper` | An optional fetch wrapper for this specific hook. NOTE: This will be called in place of any supplied [global fetch wrapper](global-fetch-wrapper.md) for maximum flexibility. If you want to use the global fetch wrapper, you must call it manually within the wrapper passed here. | +| `cacheKey` | The cache key to store the response against, it can be a string param key, an array of param keys, or a function that generates the key from params, see [here](caching.md) for more info | +| `swrConfig` | Additional config to send to SWR (like settings or fallback data for SSR.) This `SWRConfiguration` (see [here](https://swr.vercel.app/docs/api#options) for full docs.) will be merged with any global config | +| `waitFor` | An optional boolean. If this property is false, the query fetch will wait until it becomes true or undefined. Useful for holding back queries until conditions are met. | +| `enableMocking` | Whether to use the supplied [mocked endpoints](mocking.md) instead of real endpoints for this hook only, resulting in no genuine API calls being made. If this property is `true`, you **must** have a mock endpoint registered for every endpoint used in your app, otherwise an error will be thrown | ## Returns diff --git a/src/@types/global.ts b/src/@types/global.ts index 9485a12..eb0f0e9 100644 --- a/src/@types/global.ts +++ b/src/@types/global.ts @@ -96,6 +96,8 @@ export interface IHookBaseConfig; + /** Enables mocking for this hook only */ + enableMocking?: boolean; } /** diff --git a/src/factories/axiosOpenApiController.spec.ts b/src/factories/axiosOpenApiController.spec.ts index f50850c..3154f5a 100644 --- a/src/factories/axiosOpenApiController.spec.ts +++ b/src/factories/axiosOpenApiController.spec.ts @@ -1,5 +1,6 @@ import type { AxiosResponse } from 'axios'; +import { renderHook } from '@testing-library/react'; import { fixGeneratedClient } from '../utils/api'; import { cacheKeyConcat } from '../utils/caching'; import { createMockAxiosErrorResponse, createMockAxiosSuccessResponse } from '../utils/mocking'; @@ -27,7 +28,8 @@ describe('axiosOpenApiControllerFactory', () => { const axiosOpenApiControllerFactoryParams: IOpenApiControllerSetup = { basePath }; const controllerKey = 'testControllerKey'; - const controllerHooks = axiosOpenApiControllerFactory(axiosOpenApiControllerFactoryParams).createAxiosOpenApiController(controllerKey, MockApi); + const controller = axiosOpenApiControllerFactory(axiosOpenApiControllerFactoryParams); + const controllerHooks = controller.createAxiosOpenApiController(controllerKey, MockApi); const mockedController = axiosOpenApiControllerFactory({ ...axiosOpenApiControllerFactoryParams, @@ -165,6 +167,42 @@ describe('axiosOpenApiControllerFactory', () => { await expect(mockedControllerHooks.testGetSuccess.fetch()).resolves.toEqual({ ...successResponse, data: { test: 'mocked-test' } }); }); + it('should call mock function when enableMocking is passed through query hook config', async () => { + const localMockControllerHooks = controller.createAxiosOpenApiController(controllerKey, MockApi); + const mockTestSuccess = jest.fn(); + localMockControllerHooks.registerMockEndpoints({ + // eslint-disable-next-line @typescript-eslint/require-await + testGetSuccess: mockTestSuccess, + }); + renderHook(() => localMockControllerHooks.testGetSuccess.useQuery({ enableMocking: true, params: { hello: 'test-query' } })); + expect(mockTestSuccess).toHaveBeenCalledWith({ hello: 'test-query' }, undefined); + }); + + it('should call mock function when enableMocking is passed through infinite query hook config', async () => { + const localMockControllerHooks = controller.createAxiosOpenApiController(controllerKey, MockApi); + const mockTestSuccess = jest.fn(); + localMockControllerHooks.registerMockEndpoints({ + // eslint-disable-next-line @typescript-eslint/require-await + testGetSuccess: mockTestSuccess, + }); + renderHook(() => localMockControllerHooks.testGetSuccess.useInfiniteQuery({ enableMocking: true, params: () => ({ hello: 'test-query' }) })); + expect(mockTestSuccess).toHaveBeenCalledWith({ hello: 'test-query' }, undefined); + }); + + it('should call mock function when enableMocking is passed through mutation hook config', async () => { + const localMockControllerHooks = controller.createAxiosOpenApiController(controllerKey, MockApi); + const mockTestSuccess = jest.fn(); + localMockControllerHooks.registerMockEndpoints({ + // eslint-disable-next-line @typescript-eslint/require-await + testGetSuccess: mockTestSuccess, + }); + const { result } = renderHook(() => + localMockControllerHooks.testGetSuccess.useMutation({ enableMocking: true, params: { hello: 'test-mutation' } }) + ); + await result.current.clientFetch(); + expect(mockTestSuccess).toHaveBeenCalledWith({ hello: 'test-mutation' }, undefined); + }); + it('should throw an error when trying to fetch from an undefined mock endpoint', async () => { const mockedControllerHooks = mockedController.createAxiosOpenApiController(controllerKey, MockApi); diff --git a/src/factories/axiosOpenApiController.ts b/src/factories/axiosOpenApiController.ts index b40f21d..d389535 100644 --- a/src/factories/axiosOpenApiController.ts +++ b/src/factories/axiosOpenApiController.ts @@ -6,6 +6,7 @@ import type { AxiosRequestConfig } from 'axios'; import { Arguments } from 'swr'; +import * as React from 'react'; import { useClientFetch } from '../hooks/useClientFetch'; import { useQuery } from '../hooks/useQuery'; @@ -84,14 +85,16 @@ export const axiosOpenApiControllerFactory = ({ * @param args Whatever args have been passed to the fetch, this function doesn't need to know what they are * @returns The axios response */ - const fetch = async (...args: Array) => { - if (enableMocking) { - const mockFunc = getMockEndpointFunction(endpointKey); - return processAxiosPromise(() => mockFunc(...args)); - } - const func = (client as Record)[endpointKey]; - return processAxiosPromise(() => func(...args)); - }; + const fetchFactory = + (enableMockingViaConfig?: boolean) => + async (...args: Array) => { + if (enableMocking || enableMockingViaConfig) { + const mockFunc = getMockEndpointFunction(endpointKey); + return processAxiosPromise(() => mockFunc(...args)); + } + const func = (client as Record)[endpointKey]; + return processAxiosPromise(() => func(...args)); + }; /** * Retrieves the cache key for this specific endpoint @@ -124,19 +127,21 @@ export const axiosOpenApiControllerFactory = ({ controllerKey, endpointKey, endpointId: cacheKeyConcat(controllerKey, endpointKey), - fetch, + fetch: fetchFactory(), cacheKey: cacheKeyGetter, startsWithInvalidator, useQuery: (config) => { - const { data, ...rest } = useQuery(endpointId, fetch, config, useApiProcessing, useGlobalFetchWrapper, swrConfig); + const fetchOverride = React.useCallback((...args: unknown[]) => fetchFactory(config?.enableMocking)(...args), [fetchFactory]); + const { data, ...rest } = useQuery(endpointId, fetchOverride, config, useApiProcessing, useGlobalFetchWrapper, swrConfig); return { ...rest, data: isAxiosResponse(data) ? data.data : data }; }, useMutation: (config) => { + const fetchOverride = React.useCallback((...args: unknown[]) => fetchFactory(config?.enableMocking)(...args), [fetchFactory]); const { data, ...rest } = useClientFetch( endpointId, 'mutation', config?.fetchConfig, - fetch, + fetchOverride, config?.params, useApiProcessing, useGlobalFetchWrapper, @@ -145,7 +150,8 @@ export const axiosOpenApiControllerFactory = ({ return { ...rest, data: isAxiosResponse(data) ? data.data : data }; }, useInfiniteQuery: (config) => { - const { data, ...rest } = useInfiniteQuery(endpointId, fetch, config, useApiProcessing, useGlobalFetchWrapper, swrInfiniteConfig); + const fetchOverride = React.useCallback((...args: unknown[]) => fetchFactory(config?.enableMocking)(...args), [fetchFactory]); + const { data, ...rest } = useInfiniteQuery(endpointId, fetchOverride, config, useApiProcessing, useGlobalFetchWrapper, swrInfiniteConfig); return { ...rest, data: data?.map((page) => (isAxiosResponse(page) ? page.data : page)) }; }, }; diff --git a/src/factories/genericApiController.ts b/src/factories/genericApiController.ts index 948bcc6..4a0700a 100644 --- a/src/factories/genericApiController.ts +++ b/src/factories/genericApiController.ts @@ -5,6 +5,7 @@ */ import { Arguments } from 'swr'; +import * as React from 'react'; import { useClientFetch } from '../hooks/useClientFetch'; import { useQuery } from '../hooks/useQuery'; import { cacheKeyConcat } from '../utils/caching'; @@ -80,14 +81,16 @@ export const genericApiControllerFactory = ) => { - if (enableMocking) { - const mockFunc = getMockEndpointFunction(endpointKey); - return mockFunc(...args); - } - const func = (controller as Record)[endpointKey]; - return func(...args); - }; + const fetchFactory = + (enableMockingViaConfig?: boolean) => + async (...args: Array) => { + if (enableMocking || enableMockingViaConfig) { + const mockFunc = getMockEndpointFunction(endpointKey); + return mockFunc(...args); + } + const func = (controller as Record)[endpointKey]; + return func(...args); + }; /** * Retrieves the cache key for this specific endpoint @@ -120,38 +123,45 @@ export const genericApiControllerFactory = fetch(params, combineConfigs({ fetchConfig: config }, globalFetchConfig, controllerConfig)?.fetchConfig), + fetch: (params, config) => + fetchFactory()(params, combineConfigs({ fetchConfig: config }, globalFetchConfig, controllerConfig)?.fetchConfig), cacheKey: cacheKeyGetter, startsWithInvalidator, - useQuery: (config) => - useQuery( + useQuery: (config) => { + const fetchOverride = React.useCallback((...args: unknown[]) => fetchFactory(config?.enableMocking)(...args), [fetchFactory]); + return useQuery( endpointId, - fetch, + fetchOverride, combineConfigs(config, globalFetchConfig, controllerConfig), useApiProcessing, useGlobalFetchWrapper, swrConfig - ), - useMutation: (config) => - useClientFetch( + ); + }, + useMutation: (config) => { + const fetchOverride = React.useCallback((...args: unknown[]) => fetchFactory(config?.enableMocking)(...args), [fetchFactory]); + return useClientFetch( endpointId, 'mutation', combineConfigs(config, globalFetchConfig, controllerConfig)?.fetchConfig, - fetch, + fetchOverride, config?.params, useApiProcessing, useGlobalFetchWrapper, config?.fetchWrapper - ), - useInfiniteQuery: (config) => - useInfiniteQuery( + ); + }, + useInfiniteQuery: (config) => { + const fetchOverride = React.useCallback((...args: unknown[]) => fetchFactory(config?.enableMocking)(...args), [fetchFactory]); + return useInfiniteQuery( endpointId, - fetch, + fetchOverride, combineConfigs(config, globalFetchConfig, controllerConfig), useApiProcessing, useGlobalFetchWrapper, swrInfiniteConfig - ), + ); + }, }; return { ...memo, [endpointKey]: endpointTools };