Skip to content

feat(DataSource): add the transformError data-source function #24

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
48 changes: 43 additions & 5 deletions src/core/types/DataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface DataSource<
TResponse,
TData,
TError,
TErrorResponse,
TOptions,
TState,
TFetchContext,
Expand All @@ -27,7 +28,13 @@ export interface DataSource<
tags?: (params: ActualParams<TParams, TRequest>) => 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<TResponse, TErrorResponse>) => TData;

[errorHintSymbol]?: TError;

Expand All @@ -36,7 +43,7 @@ export interface DataSource<
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyDataSource = DataSource<any, any, any, any, any, any, any, any, any>;
export type AnyDataSource = DataSource<any, any, any, any, any, any, any, any, any, any>;

export type DataSourceContext<TDataSource> =
TDataSource extends DataSource<
Expand All @@ -46,6 +53,7 @@ export type DataSourceContext<TDataSource> =
infer _TResponse,
infer _TData,
infer _TError,
infer _TErrorResponse,
infer _TOptions,
infer _TState,
infer _TFetchContext
Expand All @@ -61,6 +69,7 @@ export type DataSourceParams<TDataSource> =
infer _TResponse,
infer _TData,
infer _TError,
infer _TErrorResponse,
infer _TOptions,
infer _TState,
infer _TFetchContext
Expand All @@ -76,6 +85,7 @@ export type DataSourceRequest<TDataSource> =
infer _TResponse,
infer _TData,
infer _TError,
infer _TErrorResponse,
infer _TOptions,
infer _TState,
infer _TFetchContext
Expand All @@ -91,11 +101,12 @@ export type DataSourceResponse<TDataSource> =
infer TResponse,
infer _TData,
infer _TError,
infer TErrorResponse,
infer _TOptions,
infer _TState,
infer _TFetchContext
>
? TResponse
? ActualResponse<TResponse, TErrorResponse>
: never;

export type DataSourceData<TDataSource> =
Expand All @@ -106,11 +117,12 @@ export type DataSourceData<TDataSource> =
infer TResponse,
infer TData,
infer _TError,
infer TErrorResponse,
infer _TOptions,
infer _TState,
infer _TFetchContext
>
? ActualData<TData, TResponse>
? ActualData<TResponse, TErrorResponse, TData>
: never;

export type DataSourceError<TDataSource> =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion(blocking): Add DataSourceErrorResponse for inferring like DataSourceError

Expand All @@ -121,13 +133,30 @@ export type DataSourceError<TDataSource> =
infer _TResponse,
infer _TData,
infer TError,
infer _TErrorResponse,
infer _TOptions,
infer _TState,
infer _TFetchContext
>
? TError
: never;

export type DataSourceErrorResponse<TDataSource> =
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> =
TDataSource extends DataSource<
infer _TContenxt,
Expand All @@ -136,6 +165,7 @@ export type DataSourceOptions<TDataSource> =
infer _TResponse,
infer _TData,
infer _TError,
infer _TErrorResponse,
infer TOptions,
infer _TState,
infer _TFetchContext
Expand All @@ -151,6 +181,7 @@ export type DataSourceState<TDataSource> =
infer _TResponse,
infer _TData,
infer _TError,
infer _TErrorResponse,
infer _TOptions,
infer TState,
infer _TFetchContext
Expand All @@ -166,6 +197,7 @@ export type DataSourceFetchContext<TDataSource> =
infer _TResponse,
infer _TData,
infer _TError,
infer _TErrorResponse,
infer _TOptions,
infer _TState,
infer TFetchContext
Expand All @@ -177,4 +209,10 @@ export type ActualParams<TParams, TRequest> =
| (unknown extends TParams ? TRequest : TParams)
| typeof idle;

export type ActualData<TData, TResponse> = unknown extends TData ? TResponse : TData;
export type ActualResponse<TResponse, TErrorResponse> = unknown extends TErrorResponse
? TResponse
: TResponse | TErrorResponse;

export type ActualData<TResponse, TErrorResponse, TData> = unknown extends TData
? ActualResponse<TResponse, TErrorResponse>
: TData;
22 changes: 22 additions & 0 deletions src/core/utils/skipContext.ts
Original file line number Diff line number Diff line change
@@ -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 = <TFunc extends AnyFunction>(fetch: TFunc) => {
return (_0: unknown, _1: unknown, ...args: Parameters<TFunc>): ReturnType<TFunc> => {
return fetch(...args);
Expand Down
16 changes: 13 additions & 3 deletions src/react-query/impl/infinite/factory.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import type {InfiniteQueryDataSource} from './types';

export const makeInfiniteQueryDataSource = <TParams, TRequest, TResponse, TData, TError>(
config: Omit<InfiniteQueryDataSource<TParams, TRequest, TResponse, TData, TError>, 'type'>,
): InfiniteQueryDataSource<TParams, TRequest, TResponse, TData, TError> => ({
export const makeInfiniteQueryDataSource = <
TParams,
TRequest,
TResponse,
TData,
TError,
TErrorResponse,
>(
config: Omit<
InfiniteQueryDataSource<TParams, TRequest, TResponse, TData, TError, TErrorResponse>,
'type'
>,
): InfiniteQueryDataSource<TParams, TRequest, TResponse, TData, TError, TErrorResponse> => ({
...config,
type: 'infinite',
});
81 changes: 48 additions & 33 deletions src/react-query/impl/infinite/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -29,55 +35,64 @@ export type InfiniteQueryObserverExtendedOptions<
>
>;

export type InfiniteQueryDataSource<TParams, TRequest, TResponse, TData, TError> = DataSource<
QueryDataSourceContext,
TParams,
TRequest,
TResponse,
TData,
TError,
InfiniteQueryObserverExtendedOptions<
TResponse,
TError,
InfiniteData<ActualData<TData, TResponse>, Partial<TRequest>>,
TResponse,
DataSourceKey,
Partial<TRequest>
>,
ResultWrapper<
InfiniteQueryObserverResult<
InfiniteData<ActualData<TData, TResponse>, Partial<TRequest>>,
TError
>,
export type InfiniteQueryDataSource<TParams, TRequest, TResponse, TData, TError, TErrorResponse> =
DataSource<
QueryDataSourceContext,
TParams,
TRequest,
TResponse,
TData,
TError
>,
QueryFunctionContext<DataSourceKey, Partial<TRequest>>
> & {
type: 'infinite';
next: (lastPage: TResponse, allPages: TResponse[]) => Partial<TRequest> | null | undefined;
prev?: (firstPage: TResponse, allPages: TResponse[]) => Partial<TRequest> | null | undefined;
};
TError,
TErrorResponse,
InfiniteQueryObserverExtendedOptions<
ActualResponse<TResponse, TErrorResponse>,
TError,
InfiniteData<ActualData<TResponse, TErrorResponse, TData>, Partial<TRequest>>,
ActualResponse<TResponse, TErrorResponse>,
DataSourceKey,
Partial<TRequest>
>,
ResultWrapper<
InfiniteQueryObserverResult<
InfiniteData<ActualData<TResponse, TErrorResponse, TData>, Partial<TRequest>>,
TError
>,
TRequest,
TResponse,
TData,
TError,
TErrorResponse
>,
QueryFunctionContext<DataSourceKey, Partial<TRequest>>
> & {
type: 'infinite';
next: (
lastPage: ActualResponse<TResponse, TErrorResponse>,
allPages: ActualResponse<TResponse, TErrorResponse>[],
) => Partial<TRequest> | null | undefined;
prev?: (
firstPage: ActualResponse<TResponse, TErrorResponse>,
allPages: ActualResponse<TResponse, TErrorResponse>[],
) => Partial<TRequest> | null | undefined;
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyInfiniteQueryDataSource = InfiniteQueryDataSource<any, any, any, any, any>;
export type AnyInfiniteQueryDataSource = InfiniteQueryDataSource<any, any, any, any, any, any>;

// It is used instead of `Partial<DataSourceRequest<TDataSource>>` because TS can't calculate type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyPageParam = Partial<any>;

type ResultWrapper<TResult, TRequest, TResponse, TData, TError> =
type ResultWrapper<TResult, TRequest, TResponse, TData, TError, TErrorResponse> =
TResult extends InfiniteQueryObserverResult<
InfiniteData<ActualData<TData, TResponse>, Partial<TRequest>>,
InfiniteData<ActualData<TResponse, TErrorResponse, TData>, Partial<TRequest>>,
TError
>
? Overwrite<
TResult,
{
status: DataLoaderStatus;
data: Array<FlatArray<Array<ActualData<TData, TResponse>>, 1>>;
data: Array<FlatArray<Array<ActualData<TResponse, TErrorResponse, TData>>, 1>>;
}
> & {
originalStatus: TResult['status'];
Expand Down
27 changes: 20 additions & 7 deletions src/react-query/impl/infinite/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
DataSourceParams,
DataSourceResponse,
} from '../../../core';
import {formatNullableValue, parseNullableValue} from '../utils';

import type {
AnyInfiniteQueryDataSource,
Expand All @@ -33,23 +34,35 @@
DataSourceKey,
AnyPageParam
> => {
const {transformParams, transformResponse, next, prev} = dataSource;
const {transformParams, transformError, transformResponse, next, prev} = dataSource;

const queryFn = (
const queryFn = async (
fetchContext: QueryFunctionContext<DataSourceKey, AnyPageParam>,
): DataSourceResponse<TDataSource> | Promise<DataSourceResponse<TDataSource>> => {
): Promise<DataSourceResponse<TDataSource>> => {
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));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion(blocking): Use formatNullableValue for fetch result too

}
};

const innerTransform = (response: any): any => {

Check warning on line 56 in src/react-query/impl/infinite/utils.ts

View workflow job for this annotation

GitHub Actions / Lint & Typecheck

Unexpected any. Specify a different type

Check warning on line 56 in src/react-query/impl/infinite/utils.ts

View workflow job for this annotation

GitHub Actions / Lint & Typecheck

Unexpected any. Specify a different type
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,
Expand Down
16 changes: 13 additions & 3 deletions src/react-query/impl/plain/factory.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import type {PlainQueryDataSource} from './types';

export const makePlainQueryDataSource = <TParams, TRequest, TResponse, TData, TError>(
config: Omit<PlainQueryDataSource<TParams, TRequest, TResponse, TData, TError>, 'type'>,
): PlainQueryDataSource<TParams, TRequest, TResponse, TData, TError> => ({
export const makePlainQueryDataSource = <
TParams,
TRequest,
TResponse,
TData,
TError,
TErrorResponse,
>(
config: Omit<
PlainQueryDataSource<TParams, TRequest, TResponse, TData, TError, TErrorResponse>,
'type'
>,
): PlainQueryDataSource<TParams, TRequest, TResponse, TData, TError, TErrorResponse> => ({
...config,
type: 'plain',
});
Loading