@geekmidas/client 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +153 -2
- package/dist/auth-fetcher.cjs +78 -0
- package/dist/auth-fetcher.cjs.map +1 -0
- package/dist/auth-fetcher.d.cts +157 -0
- package/dist/auth-fetcher.d.mts +157 -0
- package/dist/auth-fetcher.mjs +78 -0
- package/dist/auth-fetcher.mjs.map +1 -0
- package/dist/endpoint-hooks.cjs +60 -0
- package/dist/endpoint-hooks.cjs.map +1 -0
- package/dist/endpoint-hooks.d.cts +53 -0
- package/dist/endpoint-hooks.d.mts +53 -0
- package/dist/endpoint-hooks.mjs +59 -0
- package/dist/endpoint-hooks.mjs.map +1 -0
- package/dist/fetcher.d.cts +1 -1
- package/dist/fetcher.d.mts +1 -1
- package/dist/infer.cjs +0 -0
- package/dist/infer.d.cts +133 -0
- package/dist/infer.d.mts +133 -0
- package/dist/infer.mjs +0 -0
- package/dist/openapi-hooks.d.cts +4 -4
- package/dist/openapi-hooks.d.mts +1 -1
- package/dist/react-query.d.cts +8 -8
- package/dist/react-query.d.mts +1 -1
- package/dist/{types-csACmD6U.d.cts → types-CyMkwV3g.d.mts} +34 -3
- package/dist/{types-tMp85Lt_.d.mts → types-DVVUpwI4.d.cts} +34 -3
- package/dist/types.d.cts +2 -2
- package/dist/types.d.mts +2 -2
- package/package.json +38 -3
- package/src/__tests__/endpoint-hooks.spec.tsx +374 -0
- package/src/__tests__/infer.integration.spec.ts +188 -0
- package/src/__tests__/infer.spec.ts +189 -0
- package/src/auth-fetcher.ts +288 -0
- package/src/endpoint-hooks.ts +203 -0
- package/src/infer.ts +161 -0
- package/src/types.ts +69 -8
package/dist/react-query.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ExtractEndpointResponse, FetcherOptions, FilteredRequestConfig, MutationEndpoint, QueryEndpoint, TypedEndpoint } from "./types-
|
|
2
|
-
import * as
|
|
1
|
+
import { ExtractEndpointResponse, FetcherOptions, FilteredRequestConfig, MutationEndpoint, QueryEndpoint, TypedEndpoint } from "./types-DVVUpwI4.cjs";
|
|
2
|
+
import * as _tanstack_react_query3 from "@tanstack/react-query";
|
|
3
3
|
import { QueryClient, UseInfiniteQueryOptions, UseMutationOptions, UseQueryOptions } from "@tanstack/react-query";
|
|
4
4
|
|
|
5
5
|
//#region src/react-query.d.ts
|
|
@@ -10,15 +10,15 @@ declare class TypedQueryClient<Paths> {
|
|
|
10
10
|
private fetcher;
|
|
11
11
|
private queryClient?;
|
|
12
12
|
constructor(options?: TypedQueryClientOptions);
|
|
13
|
-
useQuery<T extends QueryEndpoint<Paths>>(endpoint: T, config?: FilteredRequestConfig<Paths, T>, options?: Omit<UseQueryOptions<ExtractEndpointResponse<Paths, T>, Response>, 'queryKey' | 'queryFn'>):
|
|
14
|
-
useMutation<T extends MutationEndpoint<Paths>>(endpoint: T, options?: Omit<UseMutationOptions<ExtractEndpointResponse<Paths, T>, Response, FilteredRequestConfig<Paths, T>>, 'mutationFn'>):
|
|
13
|
+
useQuery<T extends QueryEndpoint<Paths>>(endpoint: T, config?: FilteredRequestConfig<Paths, T>, options?: Omit<UseQueryOptions<ExtractEndpointResponse<Paths, T>, Response>, 'queryKey' | 'queryFn'>): _tanstack_react_query3.UseQueryResult<_tanstack_react_query3.NoInfer<ExtractEndpointResponse<Paths, T>>, Response>;
|
|
14
|
+
useMutation<T extends MutationEndpoint<Paths>>(endpoint: T, options?: Omit<UseMutationOptions<ExtractEndpointResponse<Paths, T>, Response, FilteredRequestConfig<Paths, T>>, 'mutationFn'>): _tanstack_react_query3.UseMutationResult<ExtractEndpointResponse<Paths, T>, Response, FilteredRequestConfig<Paths, T>, unknown>;
|
|
15
15
|
useInfiniteQuery<T extends QueryEndpoint<Paths>, TPageData = ExtractEndpointResponse<Paths, T>, TPageParam = unknown>(endpoint: T, options: Omit<UseInfiniteQueryOptions<TPageData, Response, {
|
|
16
16
|
pages: TPageData[];
|
|
17
17
|
pageParams: TPageParam[];
|
|
18
18
|
}, unknown[], TPageParam>, 'queryKey' | 'queryFn' | 'getNextPageParam' | 'initialPageParam'> & {
|
|
19
19
|
getNextPageParam: (lastPage: TPageData, allPages: TPageData[], lastPageParam: TPageParam, allPageParams: TPageParam[]) => TPageParam | undefined;
|
|
20
20
|
initialPageParam: TPageParam;
|
|
21
|
-
}, config?: FilteredRequestConfig<Paths, T>):
|
|
21
|
+
}, config?: FilteredRequestConfig<Paths, T>): _tanstack_react_query3.UseInfiniteQueryResult<{
|
|
22
22
|
pages: TPageData[];
|
|
23
23
|
pageParams: TPageParam[];
|
|
24
24
|
}, Response>;
|
|
@@ -47,15 +47,15 @@ declare class TypedQueryClient<Paths> {
|
|
|
47
47
|
setQueryClient(queryClient: QueryClient): void;
|
|
48
48
|
}
|
|
49
49
|
declare function createTypedQueryClient<Paths>(options?: TypedQueryClientOptions): TypedQueryClient<Paths>;
|
|
50
|
-
declare function useTypedQuery<Paths, T extends QueryEndpoint<Paths>>(client: TypedQueryClient<Paths>, endpoint: T, config?: FilteredRequestConfig<Paths, T>, options?: Omit<UseQueryOptions<ExtractEndpointResponse<Paths, T>, Response>, 'queryKey' | 'queryFn'>):
|
|
51
|
-
declare function useTypedMutation<Paths, T extends MutationEndpoint<Paths>>(client: TypedQueryClient<Paths>, endpoint: T, options?: Omit<UseMutationOptions<ExtractEndpointResponse<Paths, T>, Response, FilteredRequestConfig<Paths, T>>, 'mutationFn'>):
|
|
50
|
+
declare function useTypedQuery<Paths, T extends QueryEndpoint<Paths>>(client: TypedQueryClient<Paths>, endpoint: T, config?: FilteredRequestConfig<Paths, T>, options?: Omit<UseQueryOptions<ExtractEndpointResponse<Paths, T>, Response>, 'queryKey' | 'queryFn'>): _tanstack_react_query3.UseQueryResult<_tanstack_react_query3.NoInfer<ExtractEndpointResponse<Paths, T>>, Response>;
|
|
51
|
+
declare function useTypedMutation<Paths, T extends MutationEndpoint<Paths>>(client: TypedQueryClient<Paths>, endpoint: T, options?: Omit<UseMutationOptions<ExtractEndpointResponse<Paths, T>, Response, FilteredRequestConfig<Paths, T>>, 'mutationFn'>): _tanstack_react_query3.UseMutationResult<ExtractEndpointResponse<Paths, T>, Response, FilteredRequestConfig<Paths, T>, unknown>;
|
|
52
52
|
declare function useTypedInfiniteQuery<Paths, T extends QueryEndpoint<Paths>, TPageData = ExtractEndpointResponse<Paths, T>, TPageParam = unknown>(client: TypedQueryClient<Paths>, endpoint: T, options: Omit<UseInfiniteQueryOptions<TPageData, Response, {
|
|
53
53
|
pages: TPageData[];
|
|
54
54
|
pageParams: TPageParam[];
|
|
55
55
|
}, unknown[], TPageParam>, 'queryKey' | 'queryFn' | 'getNextPageParam' | 'initialPageParam'> & {
|
|
56
56
|
getNextPageParam: (lastPage: TPageData, allPages: TPageData[], lastPageParam: TPageParam, allPageParams: TPageParam[]) => TPageParam | undefined;
|
|
57
57
|
initialPageParam: TPageParam;
|
|
58
|
-
}, config?: FilteredRequestConfig<Paths, T>):
|
|
58
|
+
}, config?: FilteredRequestConfig<Paths, T>): _tanstack_react_query3.UseInfiniteQueryResult<{
|
|
59
59
|
pages: TPageData[];
|
|
60
60
|
pageParams: TPageParam[];
|
|
61
61
|
}, Response>;
|
package/dist/react-query.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ExtractEndpointResponse, FetcherOptions, FilteredRequestConfig, MutationEndpoint, QueryEndpoint, TypedEndpoint } from "./types-
|
|
1
|
+
import { ExtractEndpointResponse, FetcherOptions, FilteredRequestConfig, MutationEndpoint, QueryEndpoint, TypedEndpoint } from "./types-CyMkwV3g.mjs";
|
|
2
2
|
import * as _tanstack_react_query3 from "@tanstack/react-query";
|
|
3
3
|
import { QueryClient, UseInfiniteQueryOptions, UseMutationOptions, UseQueryOptions } from "@tanstack/react-query";
|
|
4
4
|
|
|
@@ -54,7 +54,38 @@ type ParseEndpoint<T extends EndpointString> = T extends `${infer Method} ${infe
|
|
|
54
54
|
} : never;
|
|
55
55
|
type ExtractEndpointResponse<Paths, T extends EndpointString> = T extends `${infer Method} ${infer Route}` ? Route extends OpenAPIRoutes<Paths> ? Lowercase<Method> extends ExtractMethod<Paths, Route> ? ExtractResponse<Paths, Route, Lowercase<Method>> : never : never : never;
|
|
56
56
|
type ExtractEndpointConfig<Paths, T extends EndpointString> = T extends `${infer Method} ${infer Route}` ? Route extends OpenAPIRoutes<Paths> ? Lowercase<Method> extends ExtractMethod<Paths, Route> ? RequestConfig<Paths, Route, Lowercase<Method>> : never : never : never;
|
|
57
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Build a request config type where:
|
|
59
|
+
* - `params` is required if the endpoint has path parameters
|
|
60
|
+
* - `body` is required if the endpoint has a request body
|
|
61
|
+
* - `query` and `headers` are always optional
|
|
62
|
+
*/
|
|
63
|
+
type FilteredRequestConfig<Paths, T extends EndpointString> = T extends `${infer Method} ${infer Route}` ? Route extends OpenAPIRoutes<Paths> ? Lowercase<Method> extends ExtractMethod<Paths, Route> ? BuildRequestConfig<ExtractPathParams<Paths, Route>, ExtractQueryParams<Paths, Route, Lowercase<Method>>, ExtractRequestBody<Paths, Route, Lowercase<Method>>> : never : never : never;
|
|
64
|
+
/**
|
|
65
|
+
* Helper to build request config with correct required/optional fields
|
|
66
|
+
*/
|
|
67
|
+
type BuildRequestConfig<TParams, TQuery, TBody> = SimplifyIntersection<(TParams extends never ? {} : {
|
|
68
|
+
params: TParams;
|
|
69
|
+
}) & (TBody extends never ? {} : {
|
|
70
|
+
body: TBody;
|
|
71
|
+
}) & (TQuery extends never ? {} : {
|
|
72
|
+
query?: TQuery;
|
|
73
|
+
}) & {
|
|
74
|
+
headers?: Record<string, string>;
|
|
75
|
+
}>;
|
|
76
|
+
/**
|
|
77
|
+
* Simplify intersection types for better IDE display
|
|
78
|
+
*/
|
|
79
|
+
type SimplifyIntersection<T> = { [K in keyof T]: T[K] };
|
|
80
|
+
/**
|
|
81
|
+
* Check if the config object is required (has any required fields)
|
|
82
|
+
*/
|
|
83
|
+
type IsConfigRequired<Paths, T extends EndpointString> = T extends `${infer Method} ${infer Route}` ? Route extends OpenAPIRoutes<Paths> ? Lowercase<Method> extends ExtractMethod<Paths, Route> ? ExtractPathParams<Paths, Route> extends never ? ExtractRequestBody<Paths, Route, Lowercase<Method>> extends never ? false : true : true : false : false : false;
|
|
84
|
+
/**
|
|
85
|
+
* Typed function signature for the API client.
|
|
86
|
+
* Config is required when endpoint has path params or body.
|
|
87
|
+
*/
|
|
88
|
+
type TypedApiFunction<Paths> = <T extends TypedEndpoint<Paths>>(endpoint: T, ...args: IsConfigRequired<Paths, T> extends true ? [config: FilteredRequestConfig<Paths, T>] : [config?: FilteredRequestConfig<Paths, T>]) => Promise<ExtractEndpointResponse<Paths, T>>;
|
|
58
89
|
interface FetcherOptions {
|
|
59
90
|
baseURL?: string;
|
|
60
91
|
headers?: Record<string, string>;
|
|
@@ -64,5 +95,5 @@ interface FetcherOptions {
|
|
|
64
95
|
fetch?: typeof fetch;
|
|
65
96
|
}
|
|
66
97
|
//#endregion
|
|
67
|
-
export { EndpointString, ExtractEndpointConfig, ExtractEndpointResponse, ExtractMethod, ExtractPathParams, ExtractQueryParams, ExtractRequestBody, ExtractResponse, FetcherOptions, FilterEndpointByMethod, FilteredRequestConfig, MutationEndpoint, OpenAPIRoutes, ParseEndpoint, QueryEndpoint, RequestConfig, TypedEndpoint, ValidEndpoint };
|
|
68
|
-
//# sourceMappingURL=types-
|
|
98
|
+
export { EndpointString, ExtractEndpointConfig, ExtractEndpointResponse, ExtractMethod, ExtractPathParams, ExtractQueryParams, ExtractRequestBody, ExtractResponse, FetcherOptions, FilterEndpointByMethod, FilteredRequestConfig, IsConfigRequired, MutationEndpoint, OpenAPIRoutes, ParseEndpoint, QueryEndpoint, RequestConfig, TypedApiFunction, TypedEndpoint, ValidEndpoint };
|
|
99
|
+
//# sourceMappingURL=types-CyMkwV3g.d.mts.map
|
|
@@ -54,7 +54,38 @@ type ParseEndpoint<T extends EndpointString> = T extends `${infer Method} ${infe
|
|
|
54
54
|
} : never;
|
|
55
55
|
type ExtractEndpointResponse<Paths, T extends EndpointString> = T extends `${infer Method} ${infer Route}` ? Route extends OpenAPIRoutes<Paths> ? Lowercase<Method> extends ExtractMethod<Paths, Route> ? ExtractResponse<Paths, Route, Lowercase<Method>> : never : never : never;
|
|
56
56
|
type ExtractEndpointConfig<Paths, T extends EndpointString> = T extends `${infer Method} ${infer Route}` ? Route extends OpenAPIRoutes<Paths> ? Lowercase<Method> extends ExtractMethod<Paths, Route> ? RequestConfig<Paths, Route, Lowercase<Method>> : never : never : never;
|
|
57
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Build a request config type where:
|
|
59
|
+
* - `params` is required if the endpoint has path parameters
|
|
60
|
+
* - `body` is required if the endpoint has a request body
|
|
61
|
+
* - `query` and `headers` are always optional
|
|
62
|
+
*/
|
|
63
|
+
type FilteredRequestConfig<Paths, T extends EndpointString> = T extends `${infer Method} ${infer Route}` ? Route extends OpenAPIRoutes<Paths> ? Lowercase<Method> extends ExtractMethod<Paths, Route> ? BuildRequestConfig<ExtractPathParams<Paths, Route>, ExtractQueryParams<Paths, Route, Lowercase<Method>>, ExtractRequestBody<Paths, Route, Lowercase<Method>>> : never : never : never;
|
|
64
|
+
/**
|
|
65
|
+
* Helper to build request config with correct required/optional fields
|
|
66
|
+
*/
|
|
67
|
+
type BuildRequestConfig<TParams, TQuery, TBody> = SimplifyIntersection<(TParams extends never ? {} : {
|
|
68
|
+
params: TParams;
|
|
69
|
+
}) & (TBody extends never ? {} : {
|
|
70
|
+
body: TBody;
|
|
71
|
+
}) & (TQuery extends never ? {} : {
|
|
72
|
+
query?: TQuery;
|
|
73
|
+
}) & {
|
|
74
|
+
headers?: Record<string, string>;
|
|
75
|
+
}>;
|
|
76
|
+
/**
|
|
77
|
+
* Simplify intersection types for better IDE display
|
|
78
|
+
*/
|
|
79
|
+
type SimplifyIntersection<T> = { [K in keyof T]: T[K] };
|
|
80
|
+
/**
|
|
81
|
+
* Check if the config object is required (has any required fields)
|
|
82
|
+
*/
|
|
83
|
+
type IsConfigRequired<Paths, T extends EndpointString> = T extends `${infer Method} ${infer Route}` ? Route extends OpenAPIRoutes<Paths> ? Lowercase<Method> extends ExtractMethod<Paths, Route> ? ExtractPathParams<Paths, Route> extends never ? ExtractRequestBody<Paths, Route, Lowercase<Method>> extends never ? false : true : true : false : false : false;
|
|
84
|
+
/**
|
|
85
|
+
* Typed function signature for the API client.
|
|
86
|
+
* Config is required when endpoint has path params or body.
|
|
87
|
+
*/
|
|
88
|
+
type TypedApiFunction<Paths> = <T extends TypedEndpoint<Paths>>(endpoint: T, ...args: IsConfigRequired<Paths, T> extends true ? [config: FilteredRequestConfig<Paths, T>] : [config?: FilteredRequestConfig<Paths, T>]) => Promise<ExtractEndpointResponse<Paths, T>>;
|
|
58
89
|
interface FetcherOptions {
|
|
59
90
|
baseURL?: string;
|
|
60
91
|
headers?: Record<string, string>;
|
|
@@ -64,5 +95,5 @@ interface FetcherOptions {
|
|
|
64
95
|
fetch?: typeof fetch;
|
|
65
96
|
}
|
|
66
97
|
//#endregion
|
|
67
|
-
export { EndpointString, ExtractEndpointConfig, ExtractEndpointResponse, ExtractMethod, ExtractPathParams, ExtractQueryParams, ExtractRequestBody, ExtractResponse, FetcherOptions, FilterEndpointByMethod, FilteredRequestConfig, MutationEndpoint, OpenAPIRoutes, ParseEndpoint, QueryEndpoint, RequestConfig, TypedEndpoint, ValidEndpoint };
|
|
68
|
-
//# sourceMappingURL=types-
|
|
98
|
+
export { EndpointString, ExtractEndpointConfig, ExtractEndpointResponse, ExtractMethod, ExtractPathParams, ExtractQueryParams, ExtractRequestBody, ExtractResponse, FetcherOptions, FilterEndpointByMethod, FilteredRequestConfig, IsConfigRequired, MutationEndpoint, OpenAPIRoutes, ParseEndpoint, QueryEndpoint, RequestConfig, TypedApiFunction, TypedEndpoint, ValidEndpoint };
|
|
99
|
+
//# sourceMappingURL=types-DVVUpwI4.d.cts.map
|
package/dist/types.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { EndpointString, ExtractEndpointConfig, ExtractEndpointResponse, ExtractMethod, ExtractPathParams, ExtractQueryParams, ExtractRequestBody, ExtractResponse, FetcherOptions, FilterEndpointByMethod, FilteredRequestConfig, MutationEndpoint, OpenAPIRoutes, ParseEndpoint, QueryEndpoint, RequestConfig, TypedEndpoint, ValidEndpoint } from "./types-
|
|
2
|
-
export { EndpointString, ExtractEndpointConfig, ExtractEndpointResponse, ExtractMethod, ExtractPathParams, ExtractQueryParams, ExtractRequestBody, ExtractResponse, FetcherOptions, FilterEndpointByMethod, FilteredRequestConfig, MutationEndpoint, OpenAPIRoutes, ParseEndpoint, QueryEndpoint, RequestConfig, TypedEndpoint, ValidEndpoint };
|
|
1
|
+
import { EndpointString, ExtractEndpointConfig, ExtractEndpointResponse, ExtractMethod, ExtractPathParams, ExtractQueryParams, ExtractRequestBody, ExtractResponse, FetcherOptions, FilterEndpointByMethod, FilteredRequestConfig, IsConfigRequired, MutationEndpoint, OpenAPIRoutes, ParseEndpoint, QueryEndpoint, RequestConfig, TypedApiFunction, TypedEndpoint, ValidEndpoint } from "./types-DVVUpwI4.cjs";
|
|
2
|
+
export { EndpointString, ExtractEndpointConfig, ExtractEndpointResponse, ExtractMethod, ExtractPathParams, ExtractQueryParams, ExtractRequestBody, ExtractResponse, FetcherOptions, FilterEndpointByMethod, FilteredRequestConfig, IsConfigRequired, MutationEndpoint, OpenAPIRoutes, ParseEndpoint, QueryEndpoint, RequestConfig, TypedApiFunction, TypedEndpoint, ValidEndpoint };
|
package/dist/types.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { EndpointString, ExtractEndpointConfig, ExtractEndpointResponse, ExtractMethod, ExtractPathParams, ExtractQueryParams, ExtractRequestBody, ExtractResponse, FetcherOptions, FilterEndpointByMethod, FilteredRequestConfig, MutationEndpoint, OpenAPIRoutes, ParseEndpoint, QueryEndpoint, RequestConfig, TypedEndpoint, ValidEndpoint } from "./types-
|
|
2
|
-
export { EndpointString, ExtractEndpointConfig, ExtractEndpointResponse, ExtractMethod, ExtractPathParams, ExtractQueryParams, ExtractRequestBody, ExtractResponse, FetcherOptions, FilterEndpointByMethod, FilteredRequestConfig, MutationEndpoint, OpenAPIRoutes, ParseEndpoint, QueryEndpoint, RequestConfig, TypedEndpoint, ValidEndpoint };
|
|
1
|
+
import { EndpointString, ExtractEndpointConfig, ExtractEndpointResponse, ExtractMethod, ExtractPathParams, ExtractQueryParams, ExtractRequestBody, ExtractResponse, FetcherOptions, FilterEndpointByMethod, FilteredRequestConfig, IsConfigRequired, MutationEndpoint, OpenAPIRoutes, ParseEndpoint, QueryEndpoint, RequestConfig, TypedApiFunction, TypedEndpoint, ValidEndpoint } from "./types-CyMkwV3g.mjs";
|
|
2
|
+
export { EndpointString, ExtractEndpointConfig, ExtractEndpointResponse, ExtractMethod, ExtractPathParams, ExtractQueryParams, ExtractRequestBody, ExtractResponse, FetcherOptions, FilterEndpointByMethod, FilteredRequestConfig, IsConfigRequired, MutationEndpoint, OpenAPIRoutes, ParseEndpoint, QueryEndpoint, RequestConfig, TypedApiFunction, TypedEndpoint, ValidEndpoint };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geekmidas/client",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
"import": "./dist/fetcher.mjs",
|
|
15
15
|
"require": "./dist/fetcher.cjs"
|
|
16
16
|
},
|
|
17
|
+
"./infer": {
|
|
18
|
+
"types": "./dist/infer.d.ts",
|
|
19
|
+
"import": "./dist/infer.mjs",
|
|
20
|
+
"require": "./dist/infer.cjs"
|
|
21
|
+
},
|
|
17
22
|
"./react-query": {
|
|
18
23
|
"types": "./dist/react-query.d.ts",
|
|
19
24
|
"import": "./dist/react-query.mjs",
|
|
@@ -28,16 +33,30 @@
|
|
|
28
33
|
"types": "./dist/types.d.ts",
|
|
29
34
|
"import": "./dist/types.mjs",
|
|
30
35
|
"require": "./dist/types.cjs"
|
|
36
|
+
},
|
|
37
|
+
"./auth-fetcher": {
|
|
38
|
+
"types": "./dist/auth-fetcher.d.ts",
|
|
39
|
+
"import": "./dist/auth-fetcher.mjs",
|
|
40
|
+
"require": "./dist/auth-fetcher.cjs"
|
|
41
|
+
},
|
|
42
|
+
"./endpoint-hooks": {
|
|
43
|
+
"types": "./dist/endpoint-hooks.d.ts",
|
|
44
|
+
"import": "./dist/endpoint-hooks.mjs",
|
|
45
|
+
"require": "./dist/endpoint-hooks.cjs"
|
|
31
46
|
}
|
|
32
47
|
},
|
|
33
48
|
"repository": {
|
|
34
49
|
"type": "git",
|
|
35
|
-
"url": "https://github.com/geekmidas/toolbox
|
|
50
|
+
"url": "https://github.com/geekmidas/toolbox"
|
|
36
51
|
},
|
|
37
52
|
"publishConfig": {
|
|
38
53
|
"registry": "https://registry.npmjs.org/",
|
|
39
54
|
"access": "public"
|
|
40
55
|
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@standard-schema/spec": "^1.0.0",
|
|
58
|
+
"@geekmidas/schema": "0.0.2"
|
|
59
|
+
},
|
|
41
60
|
"devDependencies": {
|
|
42
61
|
"@testing-library/jest-dom": "~6.6.3",
|
|
43
62
|
"@testing-library/dom": "~10.4.0",
|
|
@@ -51,7 +70,23 @@
|
|
|
51
70
|
"peerDependencies": {
|
|
52
71
|
"@tanstack/react-query": ">=5.0.0",
|
|
53
72
|
"react": ">=18.0.0",
|
|
54
|
-
"react-dom": ">=18.0.0"
|
|
73
|
+
"react-dom": ">=18.0.0",
|
|
74
|
+
"zod": "~4.1.13",
|
|
75
|
+
"@geekmidas/constructs": "0.2.0"
|
|
76
|
+
},
|
|
77
|
+
"peerDependenciesMeta": {
|
|
78
|
+
"@geekmidas/constructs": {
|
|
79
|
+
"optional": false
|
|
80
|
+
},
|
|
81
|
+
"@tanstack/react-query": {
|
|
82
|
+
"optional": true
|
|
83
|
+
},
|
|
84
|
+
"react": {
|
|
85
|
+
"optional": true
|
|
86
|
+
},
|
|
87
|
+
"react-dom": {
|
|
88
|
+
"optional": true
|
|
89
|
+
}
|
|
55
90
|
},
|
|
56
91
|
"scripts": {
|
|
57
92
|
"ts": "tsc --noEmit --skipLibCheck src/**/*.ts"
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
5
|
+
import { renderHook, waitFor } from '@testing-library/react';
|
|
6
|
+
import { http, HttpResponse } from 'msw';
|
|
7
|
+
// biome-ignore lint/style/useImportType: needed for JSX
|
|
8
|
+
import React, { createElement } from 'react';
|
|
9
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
10
|
+
import { createEndpointHooks } from '../endpoint-hooks';
|
|
11
|
+
import type { TypedApiFunction } from '../types';
|
|
12
|
+
import { server } from './setup';
|
|
13
|
+
|
|
14
|
+
// Test API paths type
|
|
15
|
+
interface TestPaths {
|
|
16
|
+
'/health': {
|
|
17
|
+
get: {
|
|
18
|
+
responses: {
|
|
19
|
+
200: {
|
|
20
|
+
content: {
|
|
21
|
+
'application/json': { status: string };
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
'/users/{id}': {
|
|
28
|
+
parameters: {
|
|
29
|
+
path: { id: string };
|
|
30
|
+
};
|
|
31
|
+
get: {
|
|
32
|
+
responses: {
|
|
33
|
+
200: {
|
|
34
|
+
content: {
|
|
35
|
+
'application/json': { id: string; name: string };
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
'/users': {
|
|
42
|
+
get: {
|
|
43
|
+
parameters: {
|
|
44
|
+
query: { limit?: number; offset?: number };
|
|
45
|
+
};
|
|
46
|
+
responses: {
|
|
47
|
+
200: {
|
|
48
|
+
content: {
|
|
49
|
+
'application/json': Array<{ id: string; name: string }>;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
post: {
|
|
55
|
+
requestBody: {
|
|
56
|
+
content: {
|
|
57
|
+
'application/json': { name: string; email: string };
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
responses: {
|
|
61
|
+
201: {
|
|
62
|
+
content: {
|
|
63
|
+
'application/json': { id: string; name: string; email: string };
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
'/posts/{postId}/comments': {
|
|
70
|
+
parameters: {
|
|
71
|
+
path: { postId: string };
|
|
72
|
+
};
|
|
73
|
+
post: {
|
|
74
|
+
requestBody: {
|
|
75
|
+
content: {
|
|
76
|
+
'application/json': { content: string };
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
responses: {
|
|
80
|
+
201: {
|
|
81
|
+
content: {
|
|
82
|
+
'application/json': { id: string; postId: string; content: string };
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Create a mock fetcher
|
|
91
|
+
function createMockFetcher(): TypedApiFunction<TestPaths> {
|
|
92
|
+
return async (endpoint: string, config?: any) => {
|
|
93
|
+
const [method, path] = endpoint.split(' ');
|
|
94
|
+
let url = `https://api.example.com${path}`;
|
|
95
|
+
|
|
96
|
+
// Replace path params
|
|
97
|
+
if (config?.params) {
|
|
98
|
+
for (const [key, value] of Object.entries(config.params)) {
|
|
99
|
+
url = url.replace(`{${key}}`, value as string);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Add query params
|
|
104
|
+
if (config?.query) {
|
|
105
|
+
const searchParams = new URLSearchParams();
|
|
106
|
+
for (const [key, value] of Object.entries(config.query)) {
|
|
107
|
+
if (value !== undefined) {
|
|
108
|
+
searchParams.set(key, String(value));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (searchParams.toString()) {
|
|
112
|
+
url += `?${searchParams.toString()}`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const response = await fetch(url, {
|
|
117
|
+
method,
|
|
118
|
+
headers: {
|
|
119
|
+
'Content-Type': 'application/json',
|
|
120
|
+
...config?.headers,
|
|
121
|
+
},
|
|
122
|
+
body: config?.body ? JSON.stringify(config.body) : undefined,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return response.json();
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Test wrapper with QueryClient
|
|
130
|
+
function createWrapper() {
|
|
131
|
+
const queryClient = new QueryClient({
|
|
132
|
+
defaultOptions: {
|
|
133
|
+
queries: {
|
|
134
|
+
retry: false,
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return ({ children }: { children: React.ReactNode }) =>
|
|
140
|
+
createElement(QueryClientProvider, { client: queryClient }, children);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
describe('createEndpointHooks', () => {
|
|
144
|
+
beforeEach(() => {
|
|
145
|
+
// Add test-specific handlers
|
|
146
|
+
server.use(
|
|
147
|
+
http.get('https://api.example.com/health', () => {
|
|
148
|
+
return HttpResponse.json({ status: 'ok' });
|
|
149
|
+
}),
|
|
150
|
+
http.get('https://api.example.com/posts/:postId/comments', () => {
|
|
151
|
+
return HttpResponse.json([{ id: 'c1', content: 'Test comment' }]);
|
|
152
|
+
}),
|
|
153
|
+
http.post(
|
|
154
|
+
'https://api.example.com/posts/:postId/comments',
|
|
155
|
+
async ({ params, request }) => {
|
|
156
|
+
const body = (await request.json()) as { content: string };
|
|
157
|
+
return HttpResponse.json(
|
|
158
|
+
{
|
|
159
|
+
id: 'comment-1',
|
|
160
|
+
postId: params.postId as string,
|
|
161
|
+
content: body.content,
|
|
162
|
+
},
|
|
163
|
+
{ status: 201 },
|
|
164
|
+
);
|
|
165
|
+
},
|
|
166
|
+
),
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('useQuery', () => {
|
|
171
|
+
it('should fetch data for endpoint without params', async () => {
|
|
172
|
+
const fetcher = createMockFetcher();
|
|
173
|
+
const hooks = createEndpointHooks<TestPaths>(fetcher);
|
|
174
|
+
|
|
175
|
+
const { result } = renderHook(() => hooks.useQuery('GET /health'), {
|
|
176
|
+
wrapper: createWrapper(),
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
180
|
+
|
|
181
|
+
expect(result.current.data).toEqual({ status: 'ok' });
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should fetch data for endpoint with path params', async () => {
|
|
185
|
+
const fetcher = createMockFetcher();
|
|
186
|
+
const hooks = createEndpointHooks<TestPaths>(fetcher);
|
|
187
|
+
|
|
188
|
+
const { result } = renderHook(
|
|
189
|
+
() => hooks.useQuery('GET /users/{id}', { params: { id: '123' } }),
|
|
190
|
+
{ wrapper: createWrapper() },
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
194
|
+
|
|
195
|
+
// Uses mock from setup.ts which returns { id, name: 'John Doe', email: ... }
|
|
196
|
+
expect(result.current.data).toMatchObject({
|
|
197
|
+
id: '123',
|
|
198
|
+
name: 'John Doe',
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should pass query options to useQuery', async () => {
|
|
203
|
+
const fetcher = createMockFetcher();
|
|
204
|
+
const hooks = createEndpointHooks<TestPaths>(fetcher);
|
|
205
|
+
|
|
206
|
+
const { result } = renderHook(
|
|
207
|
+
() => hooks.useQuery('GET /health', undefined, { enabled: false }),
|
|
208
|
+
{ wrapper: createWrapper() },
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// Should not fetch because enabled: false
|
|
212
|
+
expect(result.current.isLoading).toBe(false);
|
|
213
|
+
expect(result.current.isFetching).toBe(false);
|
|
214
|
+
expect(result.current.data).toBeUndefined();
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe('useMutation', () => {
|
|
219
|
+
it('should mutate data for POST endpoint', async () => {
|
|
220
|
+
const fetcher = createMockFetcher();
|
|
221
|
+
const hooks = createEndpointHooks<TestPaths>(fetcher);
|
|
222
|
+
|
|
223
|
+
const { result } = renderHook(() => hooks.useMutation('POST /users'), {
|
|
224
|
+
wrapper: createWrapper(),
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
result.current.mutate({ body: { name: 'John', email: 'john@test.com' } });
|
|
228
|
+
|
|
229
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
230
|
+
|
|
231
|
+
// Uses mock from setup.ts
|
|
232
|
+
expect(result.current.data).toMatchObject({
|
|
233
|
+
id: '123',
|
|
234
|
+
name: 'John',
|
|
235
|
+
email: 'john@test.com',
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should mutate data for endpoint with params and body', async () => {
|
|
240
|
+
const fetcher = createMockFetcher();
|
|
241
|
+
const hooks = createEndpointHooks<TestPaths>(fetcher);
|
|
242
|
+
|
|
243
|
+
const { result } = renderHook(
|
|
244
|
+
() => hooks.useMutation('POST /posts/{postId}/comments'),
|
|
245
|
+
{ wrapper: createWrapper() },
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
result.current.mutate({
|
|
249
|
+
params: { postId: 'post-123' },
|
|
250
|
+
body: { content: 'Great post!' },
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
254
|
+
|
|
255
|
+
expect(result.current.data).toEqual({
|
|
256
|
+
id: 'comment-1',
|
|
257
|
+
postId: 'post-123',
|
|
258
|
+
content: 'Great post!',
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('should pass mutation options', async () => {
|
|
263
|
+
const fetcher = createMockFetcher();
|
|
264
|
+
const hooks = createEndpointHooks<TestPaths>(fetcher);
|
|
265
|
+
|
|
266
|
+
let onSuccessCalled = false;
|
|
267
|
+
|
|
268
|
+
const { result } = renderHook(
|
|
269
|
+
() =>
|
|
270
|
+
hooks.useMutation('POST /users', {
|
|
271
|
+
onSuccess: () => {
|
|
272
|
+
onSuccessCalled = true;
|
|
273
|
+
},
|
|
274
|
+
}),
|
|
275
|
+
{ wrapper: createWrapper() },
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
result.current.mutate({ body: { name: 'Jane', email: 'jane@test.com' } });
|
|
279
|
+
|
|
280
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
281
|
+
|
|
282
|
+
expect(onSuccessCalled).toBe(true);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
describe('buildQueryKey', () => {
|
|
287
|
+
it('should build key with just endpoint', () => {
|
|
288
|
+
const fetcher = createMockFetcher();
|
|
289
|
+
const hooks = createEndpointHooks<TestPaths>(fetcher);
|
|
290
|
+
|
|
291
|
+
const key = hooks.buildQueryKey('GET /health');
|
|
292
|
+
|
|
293
|
+
expect(key).toEqual(['GET /health']);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('should build key with params', () => {
|
|
297
|
+
const fetcher = createMockFetcher();
|
|
298
|
+
const hooks = createEndpointHooks<TestPaths>(fetcher);
|
|
299
|
+
|
|
300
|
+
const key = hooks.buildQueryKey('GET /users/{id}', {
|
|
301
|
+
params: { id: '123' },
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
expect(key).toEqual(['GET /users/{id}', { params: { id: '123' } }]);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('should build key with both params and query', () => {
|
|
308
|
+
const fetcher = createMockFetcher();
|
|
309
|
+
const hooks = createEndpointHooks<TestPaths>(fetcher);
|
|
310
|
+
|
|
311
|
+
// Using a hypothetical endpoint that has both
|
|
312
|
+
const key = hooks.buildQueryKey(
|
|
313
|
+
'GET /users/{id}' as any,
|
|
314
|
+
{
|
|
315
|
+
params: { id: '123' },
|
|
316
|
+
query: { include: 'posts' },
|
|
317
|
+
} as any,
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
expect(key).toEqual([
|
|
321
|
+
'GET /users/{id}',
|
|
322
|
+
{ params: { id: '123' } },
|
|
323
|
+
{ query: { include: 'posts' } },
|
|
324
|
+
]);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
describe('type enforcement', () => {
|
|
329
|
+
it('should allow optional config for endpoints without required fields', () => {
|
|
330
|
+
const fetcher = createMockFetcher();
|
|
331
|
+
const hooks = createEndpointHooks<TestPaths>(fetcher);
|
|
332
|
+
|
|
333
|
+
// This should compile - config is optional for GET /health
|
|
334
|
+
renderHook(() => hooks.useQuery('GET /health'), {
|
|
335
|
+
wrapper: createWrapper(),
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// This should also compile - providing optional config
|
|
339
|
+
renderHook(() => hooks.useQuery('GET /health', {}), {
|
|
340
|
+
wrapper: createWrapper(),
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('should require config for endpoints with path params', async () => {
|
|
345
|
+
const fetcher = createMockFetcher();
|
|
346
|
+
const hooks = createEndpointHooks<TestPaths>(fetcher);
|
|
347
|
+
|
|
348
|
+
// Config with params is required for GET /users/{id}
|
|
349
|
+
const { result } = renderHook(
|
|
350
|
+
() => hooks.useQuery('GET /users/{id}', { params: { id: '456' } }),
|
|
351
|
+
{ wrapper: createWrapper() },
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
355
|
+
expect(result.current.data?.id).toBe('456');
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('should require body in mutation config for POST endpoints', async () => {
|
|
359
|
+
const fetcher = createMockFetcher();
|
|
360
|
+
const hooks = createEndpointHooks<TestPaths>(fetcher);
|
|
361
|
+
|
|
362
|
+
const { result } = renderHook(() => hooks.useMutation('POST /users'), {
|
|
363
|
+
wrapper: createWrapper(),
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Body is required in mutate() for POST /users
|
|
367
|
+
result.current.mutate({
|
|
368
|
+
body: { name: 'Test', email: 'test@test.com' },
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
});
|