diff --git a/src/core/index.ts b/src/core/index.ts index 2621a96..f1ab8b4 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -14,6 +14,7 @@ export type { DataSourceFetchContext, ActualParams, ActualData, + ActualResponse, } from './types/DataSource'; export type {DataManager} from './types/DataManger'; export type {DataLoaderStatus} from './types/DataLoaderStatus'; diff --git a/src/core/types/DataSource.ts b/src/core/types/DataSource.ts index 574fd21..a9ff78a 100644 --- a/src/core/types/DataSource.ts +++ b/src/core/types/DataSource.ts @@ -13,6 +13,7 @@ export interface DataSource< TResponse, TData, TError, + TErrorResponse, TOptions, TState, TFetchContext, @@ -27,7 +28,13 @@ export interface DataSource< tags?: (params: ActualParams) => DataSourceTag[]; transformParams?: (params: TParams) => TRequest; - transformResponse?: (response: TResponse) => TData; + + /** + * When set, the `fetch` errors will be transformed into data without changing the state to error. + * @returns NonNullable + */ + transformError?: (error: TError) => TErrorResponse; + transformResponse?: (response: ActualResponse) => TData; [errorHintSymbol]?: TError; @@ -36,7 +43,7 @@ export interface DataSource< } // eslint-disable-next-line @typescript-eslint/no-explicit-any -export type AnyDataSource = DataSource; +export type AnyDataSource = DataSource; export type DataSourceContext = TDataSource extends DataSource< @@ -46,6 +53,7 @@ export type DataSourceContext = infer _TResponse, infer _TData, infer _TError, + infer _TErrorResponse, infer _TOptions, infer _TState, infer _TFetchContext @@ -61,6 +69,7 @@ export type DataSourceParams = infer _TResponse, infer _TData, infer _TError, + infer _TErrorResponse, infer _TOptions, infer _TState, infer _TFetchContext @@ -76,6 +85,7 @@ export type DataSourceRequest = infer _TResponse, infer _TData, infer _TError, + infer _TErrorResponse, infer _TOptions, infer _TState, infer _TFetchContext @@ -91,11 +101,12 @@ export type DataSourceResponse = infer TResponse, infer _TData, infer _TError, + infer TErrorResponse, infer _TOptions, infer _TState, infer _TFetchContext > - ? TResponse + ? ActualResponse : never; export type DataSourceData = @@ -106,11 +117,12 @@ export type DataSourceData = infer TResponse, infer TData, infer _TError, + infer TErrorResponse, infer _TOptions, infer _TState, infer _TFetchContext > - ? ActualData + ? ActualData : never; export type DataSourceError = @@ -121,6 +133,7 @@ export type DataSourceError = infer _TResponse, infer _TData, infer TError, + infer _TErrorResponse, infer _TOptions, infer _TState, infer _TFetchContext @@ -128,6 +141,22 @@ export type DataSourceError = ? TError : never; +export type DataSourceErrorResponse = + TDataSource extends DataSource< + infer _TContenxt, + infer _TParams, + infer _TRequest, + infer _TResponse, + infer _TData, + infer _TError, + infer TErrorResponse, + infer _TOptions, + infer _TState, + infer _TFetchContext + > + ? TErrorResponse + : never; + export type DataSourceOptions = TDataSource extends DataSource< infer _TContenxt, @@ -136,6 +165,7 @@ export type DataSourceOptions = infer _TResponse, infer _TData, infer _TError, + infer _TErrorResponse, infer TOptions, infer _TState, infer _TFetchContext @@ -151,6 +181,7 @@ export type DataSourceState = infer _TResponse, infer _TData, infer _TError, + infer _TErrorResponse, infer _TOptions, infer TState, infer _TFetchContext @@ -166,6 +197,7 @@ export type DataSourceFetchContext = infer _TResponse, infer _TData, infer _TError, + infer _TErrorResponse, infer _TOptions, infer _TState, infer TFetchContext @@ -177,4 +209,10 @@ export type ActualParams = | (unknown extends TParams ? TRequest : TParams) | typeof idle; -export type ActualData = unknown extends TData ? TResponse : TData; +export type ActualResponse = unknown extends TErrorResponse + ? TResponse + : TResponse | TErrorResponse; + +export type ActualData = unknown extends TData + ? ActualResponse + : TData; diff --git a/src/core/utils/skipContext.ts b/src/core/utils/skipContext.ts index d96cfbd..ffeab76 100644 --- a/src/core/utils/skipContext.ts +++ b/src/core/utils/skipContext.ts @@ -1,5 +1,27 @@ import type {AnyFunction} from '../types/utils'; +/** + * @param fetch fetch function + * @returns fetch function with the ignored query data source context and the query function context. + * + * @description + * The function is intended to be used with the `fetch` parameter of the query data source config. + * It is used to ignore the query data context and the query function context parameters. + * + * NOTE + * When passed directly to the `fetch` parameter, typescript fails to infer the types of the `DataSource` fields. + * It is better to define a constant first. + * @example + * function someFetchFunction(request: TRequest) {...} + * + * const fetchData = skipContext(someFetchFunction); + * + * const dataSource = makePlainQueryDataSource({ + * ... + * fetch: fetchData, + * ... + * }) + */ export const skipContext = (fetch: TFunc) => { return (_0: unknown, _1: unknown, ...args: Parameters): ReturnType => { return fetch(...args); diff --git a/src/react-query/impl/infinite/factory.ts b/src/react-query/impl/infinite/factory.ts index 7a2cec9..55c6514 100644 --- a/src/react-query/impl/infinite/factory.ts +++ b/src/react-query/impl/infinite/factory.ts @@ -1,8 +1,18 @@ import type {InfiniteQueryDataSource} from './types'; -export const makeInfiniteQueryDataSource = ( - config: Omit, 'type'>, -): InfiniteQueryDataSource => ({ +export const makeInfiniteQueryDataSource = < + TParams, + TRequest, + TResponse, + TData, + TError, + TErrorResponse, +>( + config: Omit< + InfiniteQueryDataSource, + 'type' + >, +): InfiniteQueryDataSource => ({ ...config, type: 'infinite', }); diff --git a/src/react-query/impl/infinite/types.ts b/src/react-query/impl/infinite/types.ts index 9a0642c..2774a2c 100644 --- a/src/react-query/impl/infinite/types.ts +++ b/src/react-query/impl/infinite/types.ts @@ -8,7 +8,13 @@ import type { } from '@tanstack/react-query'; import type {Overwrite} from 'utility-types'; -import type {ActualData, DataLoaderStatus, DataSource, DataSourceKey} from '../../../core'; +import type { + ActualData, + ActualResponse, + DataLoaderStatus, + DataSource, + DataSourceKey, +} from '../../../core'; import type {QueryDataSourceContext} from '../../types/base'; import type {QueryDataAdditionalOptions} from '../../types/options'; @@ -29,55 +35,64 @@ export type InfiniteQueryObserverExtendedOptions< > >; -export type InfiniteQueryDataSource = DataSource< - QueryDataSourceContext, - TParams, - TRequest, - TResponse, - TData, - TError, - InfiniteQueryObserverExtendedOptions< - TResponse, - TError, - InfiniteData, Partial>, - TResponse, - DataSourceKey, - Partial - >, - ResultWrapper< - InfiniteQueryObserverResult< - InfiniteData, Partial>, - TError - >, +export type InfiniteQueryDataSource = + DataSource< + QueryDataSourceContext, + TParams, TRequest, TResponse, TData, - TError - >, - QueryFunctionContext> -> & { - type: 'infinite'; - next: (lastPage: TResponse, allPages: TResponse[]) => Partial | null | undefined; - prev?: (firstPage: TResponse, allPages: TResponse[]) => Partial | null | undefined; -}; + TError, + TErrorResponse, + InfiniteQueryObserverExtendedOptions< + ActualResponse, + TError, + InfiniteData, Partial>, + ActualResponse, + DataSourceKey, + Partial + >, + ResultWrapper< + InfiniteQueryObserverResult< + InfiniteData, Partial>, + TError + >, + TRequest, + TResponse, + TData, + TError, + TErrorResponse + >, + QueryFunctionContext> + > & { + type: 'infinite'; + next: ( + lastPage: ActualResponse, + allPages: ActualResponse[], + ) => Partial | null | undefined; + prev?: ( + firstPage: ActualResponse, + allPages: ActualResponse[], + ) => Partial | null | undefined; + }; // eslint-disable-next-line @typescript-eslint/no-explicit-any -export type AnyInfiniteQueryDataSource = InfiniteQueryDataSource; +export type AnyInfiniteQueryDataSource = InfiniteQueryDataSource; // It is used instead of `Partial>` because TS can't calculate type // eslint-disable-next-line @typescript-eslint/no-explicit-any export type AnyPageParam = Partial; -type ResultWrapper = +type ResultWrapper = TResult extends InfiniteQueryObserverResult< - InfiniteData, Partial>, + InfiniteData, Partial>, TError > ? Overwrite< TResult, { status: DataLoaderStatus; - data: Array>, 1>>; + data: Array>, 1>>; } > & { originalStatus: TResult['status']; diff --git a/src/react-query/impl/infinite/utils.ts b/src/react-query/impl/infinite/utils.ts index f0b0163..93d08d8 100644 --- a/src/react-query/impl/infinite/utils.ts +++ b/src/react-query/impl/infinite/utils.ts @@ -11,6 +11,7 @@ import type { DataSourceParams, DataSourceResponse, } from '../../../core'; +import {formatNullableValue, parseNullableValue} from '../utils'; import type { AnyInfiniteQueryDataSource, @@ -33,23 +34,35 @@ export const composeOptions = ( DataSourceKey, AnyPageParam > => { - const {transformParams, transformResponse, next, prev} = dataSource; + const {transformParams, transformError, transformResponse, next, prev} = dataSource; - const queryFn = ( + const queryFn = async ( fetchContext: QueryFunctionContext, - ): DataSourceResponse | Promise> => { + ): Promise> => { const request = transformParams ? transformParams(params) : params; const paginatedRequest = {...request, ...fetchContext.pageParam}; - return dataSource.fetch(context, fetchContext, paginatedRequest); + try { + const fetchResult = await dataSource.fetch(context, fetchContext, paginatedRequest); + + return formatNullableValue(fetchResult); + } catch (error) { + if (!transformError) throw error; + + return formatNullableValue(transformError(error)); + } + }; + + const innerTransform = (response: any): any => { + const actualResponse = parseNullableValue(response); + + return transformResponse ? transformResponse(actualResponse) : actualResponse; }; return { queryKey: composeFullKey(dataSource, params), queryFn: params === idle ? skipToken : queryFn, - select: transformResponse - ? (data) => ({...data, pages: data.pages.map(transformResponse)}) - : undefined, + select: (data) => ({...data, pages: data.pages.map(innerTransform)}), initialPageParam: EMPTY_OBJECT, getNextPageParam: next, getPreviousPageParam: prev, diff --git a/src/react-query/impl/plain/factory.ts b/src/react-query/impl/plain/factory.ts index 44254a6..15547d8 100644 --- a/src/react-query/impl/plain/factory.ts +++ b/src/react-query/impl/plain/factory.ts @@ -1,8 +1,18 @@ import type {PlainQueryDataSource} from './types'; -export const makePlainQueryDataSource = ( - config: Omit, 'type'>, -): PlainQueryDataSource => ({ +export const makePlainQueryDataSource = < + TParams, + TRequest, + TResponse, + TData, + TError, + TErrorResponse, +>( + config: Omit< + PlainQueryDataSource, + 'type' + >, +): PlainQueryDataSource => ({ ...config, type: 'plain', }); diff --git a/src/react-query/impl/plain/types.ts b/src/react-query/impl/plain/types.ts index 3c9b8a8..98fee34 100644 --- a/src/react-query/impl/plain/types.ts +++ b/src/react-query/impl/plain/types.ts @@ -7,7 +7,13 @@ import type { } from '@tanstack/react-query'; import type {Overwrite} from 'utility-types'; -import type {ActualData, DataLoaderStatus, DataSource, DataSourceKey} from '../../../core'; +import type { + ActualData, + ActualResponse, + DataLoaderStatus, + DataSource, + DataSourceKey, +} from '../../../core'; import type {QueryDataSourceContext} from '../../types/base'; import type {QueryDataAdditionalOptions} from '../../types/options'; @@ -23,35 +29,38 @@ export type QueryObserverExtendedOptions< QueryDataAdditionalOptions >; -export type PlainQueryDataSource = DataSource< - QueryDataSourceContext, - TParams, - TRequest, - TResponse, - TData, - TError, - QueryObserverExtendedOptions< - TResponse, - TError, - ActualData, - TResponse, - DataSourceKey - >, - ResultWrapper< - QueryObserverResult, TError>, +export type PlainQueryDataSource = + DataSource< + QueryDataSourceContext, + TParams, + TRequest, TResponse, TData, - TError - >, - QueryFunctionContext -> & { - type: 'plain'; -}; + TError, + TErrorResponse, + QueryObserverExtendedOptions< + ActualResponse, + TError, + ActualData, + ActualResponse, + DataSourceKey + >, + ResultWrapper< + QueryObserverResult, TError>, + TResponse, + TData, + TError, + TErrorResponse + >, + QueryFunctionContext + > & { + type: 'plain'; + }; // eslint-disable-next-line @typescript-eslint/no-explicit-any -export type AnyPlainQueryDataSource = PlainQueryDataSource; +export type AnyPlainQueryDataSource = PlainQueryDataSource; -type ResultWrapper = - TResult extends QueryObserverResult, TError> +type ResultWrapper = + TResult extends QueryObserverResult, TError> ? Overwrite & {originalStatus: TResult['status']} : never; diff --git a/src/react-query/impl/plain/utils.ts b/src/react-query/impl/plain/utils.ts index a5c3b4e..7ece537 100644 --- a/src/react-query/impl/plain/utils.ts +++ b/src/react-query/impl/plain/utils.ts @@ -10,6 +10,7 @@ import type { DataSourceParams, DataSourceResponse, } from '../../../core'; +import {formatNullableValue, parseNullableValue} from '../utils'; import type {AnyPlainQueryDataSource, QueryObserverExtendedOptions} from './types'; @@ -25,22 +26,36 @@ export const composeOptions = ( DataSourceResponse, DataSourceKey > => { - const {transformParams} = dataSource; + const {transformParams, transformError, transformResponse} = dataSource; - const queryFn = ( + const queryFn = async ( fetchContext: QueryFunctionContext, - ): DataSourceResponse | Promise> => { - return dataSource.fetch( - context, - fetchContext, - transformParams ? transformParams(params) : params, - ); + ): Promise> => { + try { + const fetchResult = await dataSource.fetch( + context, + fetchContext, + transformParams ? transformParams(params) : params, + ); + + return formatNullableValue(fetchResult); + } catch (error) { + if (!transformError) throw error; + + return formatNullableValue(transformError(error)); + } + }; + + const innerTransform = (response: any): any => { + const actualResponse = parseNullableValue(response); + + return transformResponse ? transformResponse(actualResponse) : actualResponse; }; return { queryKey: composeFullKey(dataSource, params), queryFn: params === idle ? skipToken : queryFn, - select: dataSource.transformResponse, + select: innerTransform, ...dataSource.options, ...options, }; diff --git a/src/react-query/impl/utils.ts b/src/react-query/impl/utils.ts new file mode 100644 index 0000000..290acdb --- /dev/null +++ b/src/react-query/impl/utils.ts @@ -0,0 +1,16 @@ +const undefinedSymbol = Symbol('undefined'); +const nullSymbol = Symbol('null'); + +export function parseNullableValue(value: any): any { + if (value === undefinedSymbol) return undefined; + if (value === nullSymbol) return null; + + return value; +} + +export function formatNullableValue(value: any): any { + if (value === undefined) return undefinedSymbol; + if (value === null) return nullSymbol; + + return value; +}