@geekmidas/client 2.0.0 → 4.0.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/CHANGELOG.md +20 -0
- package/dist/endpoint-hooks.cjs.map +1 -1
- package/dist/endpoint-hooks.d.cts +3 -3
- package/dist/endpoint-hooks.d.cts.map +1 -1
- package/dist/endpoint-hooks.d.mts +3 -3
- package/dist/endpoint-hooks.d.mts.map +1 -1
- package/dist/endpoint-hooks.mjs.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/mutation-inference.spec.tsx +343 -0
- package/src/endpoint-hooks.ts +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @geekmidas/client
|
|
2
2
|
|
|
3
|
+
## 4.0.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ✨ [`be4f7a9`](https://github.com/geekmidas/toolbox/commit/be4f7a9bd5de7f08adbca582916d6902e0c24de2) Thanks [@geekmidas](https://github.com/geekmidas)! - Add partition support for manifest generation. Users can now group constructs (routes, functions, crons, subscribers) into named partitions by providing a `partition` callback per construct type in the config. Manifests output partitioned fields as `Record<string, T[]>` while remaining flat `T[]` arrays when no partitions are configured.
|
|
8
|
+
|
|
9
|
+
Fix mutation type inference in endpoint hooks by using `UseMutationResult` and `UseQueryResult` types directly instead of `ReturnType<typeof useMutation>`, which could resolve to `never` for complex path definitions.
|
|
10
|
+
|
|
11
|
+
Add `FileCache` implementation that persists cache entries to a JSON file on disk. Default location is `process.cwd()/.gkm/cache.json`. Uses an in-process mutex combined with `proper-lockfile` for safe concurrent and cross-process writes.
|
|
12
|
+
|
|
13
|
+
- Updated dependencies []:
|
|
14
|
+
- @geekmidas/constructs@3.0.0
|
|
15
|
+
|
|
16
|
+
## 3.0.0
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- Updated dependencies [[`83a24de`](https://github.com/geekmidas/toolbox/commit/83a24de902b3fadd98444cab552ecd84f32b6661)]:
|
|
21
|
+
- @geekmidas/constructs@2.0.0
|
|
22
|
+
|
|
3
23
|
## 2.0.0
|
|
4
24
|
|
|
5
25
|
### Patch Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"endpoint-hooks.cjs","names":["endpoint: T","config?: FilteredRequestConfig<Paths, T>","key: unknown[]","fetcher: TypedApiFunction<Paths>","_options: CreateEndpointHooksOptions","mutationOptions?: Omit<\n\t\t\t\tUseMutationOptions<\n\t\t\t\t\tExtractEndpointResponse<Paths, T>,\n\t\t\t\t\tError,\n\t\t\t\t\tFilteredRequestConfig<Paths, T>\n\t\t\t\t>,\n\t\t\t\t'mutationFn'\n\t\t\t>","config: FilteredRequestConfig<Paths, T>"],"sources":["../src/endpoint-hooks.ts"],"sourcesContent":["import type {\n\tQueryClient,\n\tUseMutationOptions,\n\tUseQueryOptions,\n} from '@tanstack/react-query';\nimport { useMutation, useQuery } from '@tanstack/react-query';\nimport { useMemo } from 'react';\nimport type {\n\tExtractEndpointResponse,\n\tFilteredRequestConfig,\n\tIsConfigRequired,\n\tMutationEndpoint,\n\tQueryEndpoint,\n\tTypedApiFunction,\n} from './types';\n\n/**\n * Build query key from endpoint and config\n */\nfunction buildQueryKey<Paths, T extends QueryEndpoint<Paths>>(\n\tendpoint: T,\n\tconfig?: FilteredRequestConfig<Paths, T>,\n): unknown[] {\n\tconst key: unknown[] = [endpoint];\n\n\tif (config && 'params' in config && config.params) {\n\t\tkey.push({ params: config.params });\n\t}\n\n\tif (config && 'query' in config && config.query) {\n\t\tkey.push({ query: config.query });\n\t}\n\n\treturn key;\n}\n\n/**\n * Options for creating endpoint-based hooks\n */\nexport interface CreateEndpointHooksOptions {\n\tqueryClient?: QueryClient;\n}\n\n/**\n * Hook options type that conditionally requires config\n */\ntype UseQueryArgs<Paths, T extends QueryEndpoint<Paths>> = IsConfigRequired<\n\tPaths,\n\tT\n> extends true\n\t? [\n\t\t\tconfig: FilteredRequestConfig<Paths, T>,\n\t\t\toptions?: Omit<\n\t\t\t\tUseQueryOptions<ExtractEndpointResponse<Paths, T>, Error>,\n\t\t\t\t'queryKey' | 'queryFn'\n\t\t\t>,\n\t\t]\n\t: [\n\t\t\tconfig?: FilteredRequestConfig<Paths, T>,\n\t\t\toptions?: Omit<\n\t\t\t\tUseQueryOptions<ExtractEndpointResponse<Paths, T>, Error>,\n\t\t\t\t'queryKey' | 'queryFn'\n\t\t\t>,\n\t\t];\n\n/**\n * Endpoint-based React Query hooks\n */\nexport interface EndpointHooks<Paths> {\n\t/**\n\t * Use query hook for GET endpoints.\n\t * Config is required when endpoint has path params.\n\t */\n\tuseQuery: <T extends QueryEndpoint<Paths>>(\n\t\tendpoint: T,\n\t\t...args: UseQueryArgs<Paths, T>\n\t) =>
|
|
1
|
+
{"version":3,"file":"endpoint-hooks.cjs","names":["endpoint: T","config?: FilteredRequestConfig<Paths, T>","key: unknown[]","fetcher: TypedApiFunction<Paths>","_options: CreateEndpointHooksOptions","mutationOptions?: Omit<\n\t\t\t\tUseMutationOptions<\n\t\t\t\t\tExtractEndpointResponse<Paths, T>,\n\t\t\t\t\tError,\n\t\t\t\t\tFilteredRequestConfig<Paths, T>\n\t\t\t\t>,\n\t\t\t\t'mutationFn'\n\t\t\t>","config: FilteredRequestConfig<Paths, T>"],"sources":["../src/endpoint-hooks.ts"],"sourcesContent":["import type {\n\tQueryClient,\n\tUseMutationOptions,\n\tUseMutationResult,\n\tUseQueryOptions,\n\tUseQueryResult,\n} from '@tanstack/react-query';\nimport { useMutation, useQuery } from '@tanstack/react-query';\nimport { useMemo } from 'react';\nimport type {\n\tExtractEndpointResponse,\n\tFilteredRequestConfig,\n\tIsConfigRequired,\n\tMutationEndpoint,\n\tQueryEndpoint,\n\tTypedApiFunction,\n} from './types';\n\n/**\n * Build query key from endpoint and config\n */\nfunction buildQueryKey<Paths, T extends QueryEndpoint<Paths>>(\n\tendpoint: T,\n\tconfig?: FilteredRequestConfig<Paths, T>,\n): unknown[] {\n\tconst key: unknown[] = [endpoint];\n\n\tif (config && 'params' in config && config.params) {\n\t\tkey.push({ params: config.params });\n\t}\n\n\tif (config && 'query' in config && config.query) {\n\t\tkey.push({ query: config.query });\n\t}\n\n\treturn key;\n}\n\n/**\n * Options for creating endpoint-based hooks\n */\nexport interface CreateEndpointHooksOptions {\n\tqueryClient?: QueryClient;\n}\n\n/**\n * Hook options type that conditionally requires config\n */\ntype UseQueryArgs<Paths, T extends QueryEndpoint<Paths>> = IsConfigRequired<\n\tPaths,\n\tT\n> extends true\n\t? [\n\t\t\tconfig: FilteredRequestConfig<Paths, T>,\n\t\t\toptions?: Omit<\n\t\t\t\tUseQueryOptions<ExtractEndpointResponse<Paths, T>, Error>,\n\t\t\t\t'queryKey' | 'queryFn'\n\t\t\t>,\n\t\t]\n\t: [\n\t\t\tconfig?: FilteredRequestConfig<Paths, T>,\n\t\t\toptions?: Omit<\n\t\t\t\tUseQueryOptions<ExtractEndpointResponse<Paths, T>, Error>,\n\t\t\t\t'queryKey' | 'queryFn'\n\t\t\t>,\n\t\t];\n\n/**\n * Endpoint-based React Query hooks\n */\nexport interface EndpointHooks<Paths> {\n\t/**\n\t * Use query hook for GET endpoints.\n\t * Config is required when endpoint has path params.\n\t */\n\tuseQuery: <T extends QueryEndpoint<Paths>>(\n\t\tendpoint: T,\n\t\t...args: UseQueryArgs<Paths, T>\n\t) => UseQueryResult<ExtractEndpointResponse<Paths, T>, Error>;\n\n\t/**\n\t * Use mutation hook for POST, PUT, PATCH, DELETE endpoints.\n\t * Config with params/body is passed to mutate().\n\t */\n\tuseMutation: <T extends MutationEndpoint<Paths>>(\n\t\tendpoint: T,\n\t\toptions?: Omit<\n\t\t\tUseMutationOptions<\n\t\t\t\tExtractEndpointResponse<Paths, T>,\n\t\t\t\tError,\n\t\t\t\tFilteredRequestConfig<Paths, T>\n\t\t\t>,\n\t\t\t'mutationFn'\n\t\t>,\n\t) => UseMutationResult<\n\t\tExtractEndpointResponse<Paths, T>,\n\t\tError,\n\t\tFilteredRequestConfig<Paths, T>\n\t>;\n\n\t/**\n\t * Build a query key for manual cache operations\n\t */\n\tbuildQueryKey: <T extends QueryEndpoint<Paths>>(\n\t\tendpoint: T,\n\t\tconfig?: FilteredRequestConfig<Paths, T>,\n\t) => unknown[];\n}\n\n/**\n * Create endpoint-based React Query hooks from a typed fetcher.\n *\n * @example\n * ```typescript\n * const fetcher = createAuthAwareFetcher<paths>({ ... });\n * const hooks = createEndpointHooks<paths>(fetcher);\n *\n * // In a component\n * const { data } = hooks.useQuery('GET /users/{id}', { params: { id: '123' } });\n *\n * const mutation = hooks.useMutation('POST /users');\n * await mutation.mutateAsync({ body: { name: 'John' } });\n * ```\n */\nexport function createEndpointHooks<Paths>(\n\tfetcher: TypedApiFunction<Paths>,\n\t_options: CreateEndpointHooksOptions = {},\n): EndpointHooks<Paths> {\n\treturn {\n\t\tuseQuery: <T extends QueryEndpoint<Paths>>(\n\t\t\tendpoint: T,\n\t\t\t...args: UseQueryArgs<Paths, T>\n\t\t) => {\n\t\t\t// Parse args - config is first, options is second\n\t\t\tconst [config, queryOptions] = args as [\n\t\t\t\tFilteredRequestConfig<Paths, T> | undefined,\n\t\t\t\t(\n\t\t\t\t\t| Omit<\n\t\t\t\t\t\t\tUseQueryOptions<ExtractEndpointResponse<Paths, T>, Error>,\n\t\t\t\t\t\t\t'queryKey' | 'queryFn'\n\t\t\t\t\t >\n\t\t\t\t\t| undefined\n\t\t\t\t),\n\t\t\t];\n\n\t\t\tconst queryKey = buildQueryKey(endpoint, config);\n\n\t\t\tconst memoizedOptions = useMemo(\n\t\t\t\t() => ({\n\t\t\t\t\tqueryKey,\n\t\t\t\t\tqueryFn: () =>\n\t\t\t\t\t\t// Type assertion needed due to complex conditional types\n\t\t\t\t\t\t(\n\t\t\t\t\t\t\tfetcher as (\n\t\t\t\t\t\t\t\tendpoint: T,\n\t\t\t\t\t\t\t\tconfig?: unknown,\n\t\t\t\t\t\t\t) => Promise<ExtractEndpointResponse<Paths, T>>\n\t\t\t\t\t\t)(endpoint, config),\n\t\t\t\t\t...queryOptions,\n\t\t\t\t}),\n\t\t\t\t[endpoint, config, fetcher, queryKey, queryOptions],\n\t\t\t);\n\n\t\t\treturn useQuery<ExtractEndpointResponse<Paths, T>, Error>(\n\t\t\t\tmemoizedOptions,\n\t\t\t);\n\t\t},\n\n\t\tuseMutation: <T extends MutationEndpoint<Paths>>(\n\t\t\tendpoint: T,\n\t\t\tmutationOptions?: Omit<\n\t\t\t\tUseMutationOptions<\n\t\t\t\t\tExtractEndpointResponse<Paths, T>,\n\t\t\t\t\tError,\n\t\t\t\t\tFilteredRequestConfig<Paths, T>\n\t\t\t\t>,\n\t\t\t\t'mutationFn'\n\t\t\t>,\n\t\t) => {\n\t\t\tconst memoizedOptions = useMemo(\n\t\t\t\t() => ({\n\t\t\t\t\tmutationFn: (config: FilteredRequestConfig<Paths, T>) =>\n\t\t\t\t\t\t// Type assertion needed due to complex conditional types\n\t\t\t\t\t\t(\n\t\t\t\t\t\t\tfetcher as (\n\t\t\t\t\t\t\t\tendpoint: T,\n\t\t\t\t\t\t\t\tconfig?: unknown,\n\t\t\t\t\t\t\t) => Promise<ExtractEndpointResponse<Paths, T>>\n\t\t\t\t\t\t)(endpoint, config),\n\t\t\t\t\t...mutationOptions,\n\t\t\t\t}),\n\t\t\t\t[endpoint, fetcher, mutationOptions],\n\t\t\t);\n\n\t\t\treturn useMutation<\n\t\t\t\tExtractEndpointResponse<Paths, T>,\n\t\t\t\tError,\n\t\t\t\tFilteredRequestConfig<Paths, T>\n\t\t\t>(memoizedOptions);\n\t\t},\n\n\t\tbuildQueryKey,\n\t};\n}\n"],"mappings":";;;;;;;;AAqBA,SAAS,cACRA,UACAC,QACY;CACZ,MAAMC,MAAiB,CAAC,QAAS;AAEjC,KAAI,UAAU,YAAY,UAAU,OAAO,OAC1C,KAAI,KAAK,EAAE,QAAQ,OAAO,OAAQ,EAAC;AAGpC,KAAI,UAAU,WAAW,UAAU,OAAO,MACzC,KAAI,KAAK,EAAE,OAAO,OAAO,MAAO,EAAC;AAGlC,QAAO;AACP;;;;;;;;;;;;;;;;AAwFD,SAAgB,oBACfC,SACAC,WAAuC,CAAE,GAClB;AACvB,QAAO;EACN,UAAU,CACTJ,UACA,GAAG,SACC;GAEJ,MAAM,CAAC,QAAQ,aAAa,GAAG;GAW/B,MAAM,WAAW,cAAc,UAAU,OAAO;GAEhD,MAAM,kBAAkB,mBACvB,OAAO;IACN;IACA,SAAS,MAER,AACC,QAIC,UAAU,OAAO;IACpB,GAAG;GACH,IACD;IAAC;IAAU;IAAQ;IAAS;IAAU;GAAa,EACnD;AAED,UAAO,qCACN,gBACA;EACD;EAED,aAAa,CACZA,UACAK,oBAQI;GACJ,MAAM,kBAAkB,mBACvB,OAAO;IACN,YAAY,CAACC,WAEZ,AACC,QAIC,UAAU,OAAO;IACpB,GAAG;GACH,IACD;IAAC;IAAU;IAAS;GAAgB,EACpC;AAED,UAAO,wCAIL,gBAAgB;EAClB;EAED;CACA;AACD"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ExtractEndpointResponse, FilteredRequestConfig, IsConfigRequired, MutationEndpoint, QueryEndpoint, TypedApiFunction } from "./types-4K4N-Fl1.cjs";
|
|
2
|
-
import { QueryClient, UseMutationOptions,
|
|
2
|
+
import { QueryClient, UseMutationOptions, UseMutationResult, UseQueryOptions, UseQueryResult } from "@tanstack/react-query";
|
|
3
3
|
|
|
4
4
|
//#region src/endpoint-hooks.d.ts
|
|
5
5
|
|
|
@@ -21,12 +21,12 @@ interface EndpointHooks<Paths> {
|
|
|
21
21
|
* Use query hook for GET endpoints.
|
|
22
22
|
* Config is required when endpoint has path params.
|
|
23
23
|
*/
|
|
24
|
-
useQuery: <T extends QueryEndpoint<Paths>>(endpoint: T, ...args: UseQueryArgs<Paths, T>) =>
|
|
24
|
+
useQuery: <T extends QueryEndpoint<Paths>>(endpoint: T, ...args: UseQueryArgs<Paths, T>) => UseQueryResult<ExtractEndpointResponse<Paths, T>, Error>;
|
|
25
25
|
/**
|
|
26
26
|
* Use mutation hook for POST, PUT, PATCH, DELETE endpoints.
|
|
27
27
|
* Config with params/body is passed to mutate().
|
|
28
28
|
*/
|
|
29
|
-
useMutation: <T extends MutationEndpoint<Paths>>(endpoint: T, options?: Omit<UseMutationOptions<ExtractEndpointResponse<Paths, T>, Error, FilteredRequestConfig<Paths, T>>, 'mutationFn'>) =>
|
|
29
|
+
useMutation: <T extends MutationEndpoint<Paths>>(endpoint: T, options?: Omit<UseMutationOptions<ExtractEndpointResponse<Paths, T>, Error, FilteredRequestConfig<Paths, T>>, 'mutationFn'>) => UseMutationResult<ExtractEndpointResponse<Paths, T>, Error, FilteredRequestConfig<Paths, T>>;
|
|
30
30
|
/**
|
|
31
31
|
* Build a query key for manual cache operations
|
|
32
32
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"endpoint-hooks.d.cts","names":[],"sources":["../src/endpoint-hooks.ts"],"sourcesContent":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"endpoint-hooks.d.cts","names":[],"sources":["../src/endpoint-hooks.ts"],"sourcesContent":[],"mappings":";;;;;;;AAyCA;AAOK,UAPY,0BAAA,CAOA;EAAA,WAAA,CAAA,EANF,WAME;;;;;KAAZ,YAAsD,CAAA,KAAA,EAAA,UAAxB,aAAwB,CAAV,KAAU,CAAA,CAAA,GAAA,gBAAA,CAC1D,KAD0D,EAE1D,CAF0D,CAAA,SAAA,IAAA,GAAA,CAAgB,MAK1C,EAAtB,qBAAsB,CAAA,KAAA,EAAO,CAAP,CAAA,EAAK,OAAE,GAC3B,IAD2B,CAEpC,eAFoC,CAEpB,uBAFoB,CAEI,KAFJ,EAEW,CAFX,CAAA,EAEe,KAFf,CAAA,EAAA,UAAA,GAAA,SAAA,CAAA,CAAC,GAAA,CAAT,MAEY,GAKhC,qBALgC,CAKV,KALU,EAKH,CALG,CAAA,EAAK,OAAE,GAMtC,IANsC,CAO/C,eAP+C,CAO/B,uBAP+B,CAOP,KAPO,EAOA,CAPA,CAAA,EAOI,KAPJ,CAAA,EAAA,UAAA,GAAA,SAAA,CAAA,CAAC;;;;AAKlB,UAUjB,aAViB,CAAA,KAAA,CAAA,CAAA;EAAK;;;;EAEa,QAAhC,EAAA,CAAA,UAaE,aAbF,CAagB,KAbhB,CAAA,CAAA,CAAA,QAAA,EAcR,CAdQ,EAAA,GAAA,IAAA,EAeT,YAfS,CAeI,KAfJ,EAeW,CAfX,CAAA,EAAA,GAgBd,cAhBc,CAgBC,uBAhBD,CAgByB,KAhBzB,EAgBgC,CAhBhC,CAAA,EAgBoC,KAhBpC,CAAA;EAAuB;;;AAD1B;EASA,WAAA,EAAA,CAAA,UAcQ,gBAdK,CAcY,KAdZ,CAAA,CAAA,CAAA,QAAA,EAelB,CAfkB,EAAA,OAAA,CAAA,EAgBlB,IAhBkB,CAiB3B,kBAjB2B,CAkB1B,uBAlB0B,CAkBF,KAlBE,EAkBK,CAlBL,CAAA,EAmB1B,KAnB0B,EAoB1B,qBApB0B,CAoBJ,KApBI,EAoBG,CApBH,CAAA,CAAA,EAAA,YAAA,CAAA,EAAA,GAwBxB,iBAxBwB,CAyB5B,uBAzB4B,CAyBJ,KAzBI,EAyBG,CAzBH,CAAA,EA0B5B,KA1B4B,EA2B5B,qBA3B4B,CA2BN,KA3BM,EA2BC,CA3BD,CAAA,CAAA;EAAA;;;EAKK,aACvB,EAAA,CAAA,UA2Be,aA3Bf,CA2B6B,KA3B7B,CAAA,CAAA,CAAA,QAAA,EA4BA,CA5BA,EAAA,MAAA,CAAA,EA6BD,qBA7BC,CA6BqB,KA7BrB,EA6B4B,CA7B5B,CAAA,EAAA,GAAA,OAAA,EAAA;;;;;;;;;;;;;;;;;AAcqB,iBAkCjB,mBAlCiB,CAAA,KAAA,CAAA,CAAA,OAAA,EAmCvB,gBAnCuB,CAmCN,KAnCM,CAAA,EAAA,QAAA,CAAA,EAoCtB,0BApCsB,CAAA,EAqC9B,aArC8B,CAqChB,KArCgB,CAAA"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ExtractEndpointResponse, FilteredRequestConfig, IsConfigRequired, MutationEndpoint, QueryEndpoint, TypedApiFunction } from "./types-hhkZWjQn.mjs";
|
|
2
|
-
import { QueryClient, UseMutationOptions,
|
|
2
|
+
import { QueryClient, UseMutationOptions, UseMutationResult, UseQueryOptions, UseQueryResult } from "@tanstack/react-query";
|
|
3
3
|
|
|
4
4
|
//#region src/endpoint-hooks.d.ts
|
|
5
5
|
|
|
@@ -21,12 +21,12 @@ interface EndpointHooks<Paths> {
|
|
|
21
21
|
* Use query hook for GET endpoints.
|
|
22
22
|
* Config is required when endpoint has path params.
|
|
23
23
|
*/
|
|
24
|
-
useQuery: <T extends QueryEndpoint<Paths>>(endpoint: T, ...args: UseQueryArgs<Paths, T>) =>
|
|
24
|
+
useQuery: <T extends QueryEndpoint<Paths>>(endpoint: T, ...args: UseQueryArgs<Paths, T>) => UseQueryResult<ExtractEndpointResponse<Paths, T>, Error>;
|
|
25
25
|
/**
|
|
26
26
|
* Use mutation hook for POST, PUT, PATCH, DELETE endpoints.
|
|
27
27
|
* Config with params/body is passed to mutate().
|
|
28
28
|
*/
|
|
29
|
-
useMutation: <T extends MutationEndpoint<Paths>>(endpoint: T, options?: Omit<UseMutationOptions<ExtractEndpointResponse<Paths, T>, Error, FilteredRequestConfig<Paths, T>>, 'mutationFn'>) =>
|
|
29
|
+
useMutation: <T extends MutationEndpoint<Paths>>(endpoint: T, options?: Omit<UseMutationOptions<ExtractEndpointResponse<Paths, T>, Error, FilteredRequestConfig<Paths, T>>, 'mutationFn'>) => UseMutationResult<ExtractEndpointResponse<Paths, T>, Error, FilteredRequestConfig<Paths, T>>;
|
|
30
30
|
/**
|
|
31
31
|
* Build a query key for manual cache operations
|
|
32
32
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"endpoint-hooks.d.mts","names":[],"sources":["../src/endpoint-hooks.ts"],"sourcesContent":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"endpoint-hooks.d.mts","names":[],"sources":["../src/endpoint-hooks.ts"],"sourcesContent":[],"mappings":";;;;;;;AAyCA;AAOK,UAPY,0BAAA,CAOA;EAAA,WAAA,CAAA,EANF,WAME;;;;;KAAZ,YAAsD,CAAA,KAAA,EAAA,UAAxB,aAAwB,CAAV,KAAU,CAAA,CAAA,GAAA,gBAAA,CAC1D,KAD0D,EAE1D,CAF0D,CAAA,SAAA,IAAA,GAAA,CAAgB,MAK1C,EAAtB,qBAAsB,CAAA,KAAA,EAAO,CAAP,CAAA,EAAK,OAAE,GAC3B,IAD2B,CAEpC,eAFoC,CAEpB,uBAFoB,CAEI,KAFJ,EAEW,CAFX,CAAA,EAEe,KAFf,CAAA,EAAA,UAAA,GAAA,SAAA,CAAA,CAAC,GAAA,CAAT,MAEY,GAKhC,qBALgC,CAKV,KALU,EAKH,CALG,CAAA,EAAK,OAAE,GAMtC,IANsC,CAO/C,eAP+C,CAO/B,uBAP+B,CAOP,KAPO,EAOA,CAPA,CAAA,EAOI,KAPJ,CAAA,EAAA,UAAA,GAAA,SAAA,CAAA,CAAC;;;;AAKlB,UAUjB,aAViB,CAAA,KAAA,CAAA,CAAA;EAAK;;;;EAEa,QAAhC,EAAA,CAAA,UAaE,aAbF,CAagB,KAbhB,CAAA,CAAA,CAAA,QAAA,EAcR,CAdQ,EAAA,GAAA,IAAA,EAeT,YAfS,CAeI,KAfJ,EAeW,CAfX,CAAA,EAAA,GAgBd,cAhBc,CAgBC,uBAhBD,CAgByB,KAhBzB,EAgBgC,CAhBhC,CAAA,EAgBoC,KAhBpC,CAAA;EAAuB;;;AAD1B;EASA,WAAA,EAAA,CAAA,UAcQ,gBAdK,CAcY,KAdZ,CAAA,CAAA,CAAA,QAAA,EAelB,CAfkB,EAAA,OAAA,CAAA,EAgBlB,IAhBkB,CAiB3B,kBAjB2B,CAkB1B,uBAlB0B,CAkBF,KAlBE,EAkBK,CAlBL,CAAA,EAmB1B,KAnB0B,EAoB1B,qBApB0B,CAoBJ,KApBI,EAoBG,CApBH,CAAA,CAAA,EAAA,YAAA,CAAA,EAAA,GAwBxB,iBAxBwB,CAyB5B,uBAzB4B,CAyBJ,KAzBI,EAyBG,CAzBH,CAAA,EA0B5B,KA1B4B,EA2B5B,qBA3B4B,CA2BN,KA3BM,EA2BC,CA3BD,CAAA,CAAA;EAAA;;;EAKK,aACvB,EAAA,CAAA,UA2Be,aA3Bf,CA2B6B,KA3B7B,CAAA,CAAA,CAAA,QAAA,EA4BA,CA5BA,EAAA,MAAA,CAAA,EA6BD,qBA7BC,CA6BqB,KA7BrB,EA6B4B,CA7B5B,CAAA,EAAA,GAAA,OAAA,EAAA;;;;;;;;;;;;;;;;;AAcqB,iBAkCjB,mBAlCiB,CAAA,KAAA,CAAA,CAAA,OAAA,EAmCvB,gBAnCuB,CAmCN,KAnCM,CAAA,EAAA,QAAA,CAAA,EAoCtB,0BApCsB,CAAA,EAqC9B,aArC8B,CAqChB,KArCgB,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"endpoint-hooks.mjs","names":["endpoint: T","config?: FilteredRequestConfig<Paths, T>","key: unknown[]","fetcher: TypedApiFunction<Paths>","_options: CreateEndpointHooksOptions","mutationOptions?: Omit<\n\t\t\t\tUseMutationOptions<\n\t\t\t\t\tExtractEndpointResponse<Paths, T>,\n\t\t\t\t\tError,\n\t\t\t\t\tFilteredRequestConfig<Paths, T>\n\t\t\t\t>,\n\t\t\t\t'mutationFn'\n\t\t\t>","config: FilteredRequestConfig<Paths, T>"],"sources":["../src/endpoint-hooks.ts"],"sourcesContent":["import type {\n\tQueryClient,\n\tUseMutationOptions,\n\tUseQueryOptions,\n} from '@tanstack/react-query';\nimport { useMutation, useQuery } from '@tanstack/react-query';\nimport { useMemo } from 'react';\nimport type {\n\tExtractEndpointResponse,\n\tFilteredRequestConfig,\n\tIsConfigRequired,\n\tMutationEndpoint,\n\tQueryEndpoint,\n\tTypedApiFunction,\n} from './types';\n\n/**\n * Build query key from endpoint and config\n */\nfunction buildQueryKey<Paths, T extends QueryEndpoint<Paths>>(\n\tendpoint: T,\n\tconfig?: FilteredRequestConfig<Paths, T>,\n): unknown[] {\n\tconst key: unknown[] = [endpoint];\n\n\tif (config && 'params' in config && config.params) {\n\t\tkey.push({ params: config.params });\n\t}\n\n\tif (config && 'query' in config && config.query) {\n\t\tkey.push({ query: config.query });\n\t}\n\n\treturn key;\n}\n\n/**\n * Options for creating endpoint-based hooks\n */\nexport interface CreateEndpointHooksOptions {\n\tqueryClient?: QueryClient;\n}\n\n/**\n * Hook options type that conditionally requires config\n */\ntype UseQueryArgs<Paths, T extends QueryEndpoint<Paths>> = IsConfigRequired<\n\tPaths,\n\tT\n> extends true\n\t? [\n\t\t\tconfig: FilteredRequestConfig<Paths, T>,\n\t\t\toptions?: Omit<\n\t\t\t\tUseQueryOptions<ExtractEndpointResponse<Paths, T>, Error>,\n\t\t\t\t'queryKey' | 'queryFn'\n\t\t\t>,\n\t\t]\n\t: [\n\t\t\tconfig?: FilteredRequestConfig<Paths, T>,\n\t\t\toptions?: Omit<\n\t\t\t\tUseQueryOptions<ExtractEndpointResponse<Paths, T>, Error>,\n\t\t\t\t'queryKey' | 'queryFn'\n\t\t\t>,\n\t\t];\n\n/**\n * Endpoint-based React Query hooks\n */\nexport interface EndpointHooks<Paths> {\n\t/**\n\t * Use query hook for GET endpoints.\n\t * Config is required when endpoint has path params.\n\t */\n\tuseQuery: <T extends QueryEndpoint<Paths>>(\n\t\tendpoint: T,\n\t\t...args: UseQueryArgs<Paths, T>\n\t) =>
|
|
1
|
+
{"version":3,"file":"endpoint-hooks.mjs","names":["endpoint: T","config?: FilteredRequestConfig<Paths, T>","key: unknown[]","fetcher: TypedApiFunction<Paths>","_options: CreateEndpointHooksOptions","mutationOptions?: Omit<\n\t\t\t\tUseMutationOptions<\n\t\t\t\t\tExtractEndpointResponse<Paths, T>,\n\t\t\t\t\tError,\n\t\t\t\t\tFilteredRequestConfig<Paths, T>\n\t\t\t\t>,\n\t\t\t\t'mutationFn'\n\t\t\t>","config: FilteredRequestConfig<Paths, T>"],"sources":["../src/endpoint-hooks.ts"],"sourcesContent":["import type {\n\tQueryClient,\n\tUseMutationOptions,\n\tUseMutationResult,\n\tUseQueryOptions,\n\tUseQueryResult,\n} from '@tanstack/react-query';\nimport { useMutation, useQuery } from '@tanstack/react-query';\nimport { useMemo } from 'react';\nimport type {\n\tExtractEndpointResponse,\n\tFilteredRequestConfig,\n\tIsConfigRequired,\n\tMutationEndpoint,\n\tQueryEndpoint,\n\tTypedApiFunction,\n} from './types';\n\n/**\n * Build query key from endpoint and config\n */\nfunction buildQueryKey<Paths, T extends QueryEndpoint<Paths>>(\n\tendpoint: T,\n\tconfig?: FilteredRequestConfig<Paths, T>,\n): unknown[] {\n\tconst key: unknown[] = [endpoint];\n\n\tif (config && 'params' in config && config.params) {\n\t\tkey.push({ params: config.params });\n\t}\n\n\tif (config && 'query' in config && config.query) {\n\t\tkey.push({ query: config.query });\n\t}\n\n\treturn key;\n}\n\n/**\n * Options for creating endpoint-based hooks\n */\nexport interface CreateEndpointHooksOptions {\n\tqueryClient?: QueryClient;\n}\n\n/**\n * Hook options type that conditionally requires config\n */\ntype UseQueryArgs<Paths, T extends QueryEndpoint<Paths>> = IsConfigRequired<\n\tPaths,\n\tT\n> extends true\n\t? [\n\t\t\tconfig: FilteredRequestConfig<Paths, T>,\n\t\t\toptions?: Omit<\n\t\t\t\tUseQueryOptions<ExtractEndpointResponse<Paths, T>, Error>,\n\t\t\t\t'queryKey' | 'queryFn'\n\t\t\t>,\n\t\t]\n\t: [\n\t\t\tconfig?: FilteredRequestConfig<Paths, T>,\n\t\t\toptions?: Omit<\n\t\t\t\tUseQueryOptions<ExtractEndpointResponse<Paths, T>, Error>,\n\t\t\t\t'queryKey' | 'queryFn'\n\t\t\t>,\n\t\t];\n\n/**\n * Endpoint-based React Query hooks\n */\nexport interface EndpointHooks<Paths> {\n\t/**\n\t * Use query hook for GET endpoints.\n\t * Config is required when endpoint has path params.\n\t */\n\tuseQuery: <T extends QueryEndpoint<Paths>>(\n\t\tendpoint: T,\n\t\t...args: UseQueryArgs<Paths, T>\n\t) => UseQueryResult<ExtractEndpointResponse<Paths, T>, Error>;\n\n\t/**\n\t * Use mutation hook for POST, PUT, PATCH, DELETE endpoints.\n\t * Config with params/body is passed to mutate().\n\t */\n\tuseMutation: <T extends MutationEndpoint<Paths>>(\n\t\tendpoint: T,\n\t\toptions?: Omit<\n\t\t\tUseMutationOptions<\n\t\t\t\tExtractEndpointResponse<Paths, T>,\n\t\t\t\tError,\n\t\t\t\tFilteredRequestConfig<Paths, T>\n\t\t\t>,\n\t\t\t'mutationFn'\n\t\t>,\n\t) => UseMutationResult<\n\t\tExtractEndpointResponse<Paths, T>,\n\t\tError,\n\t\tFilteredRequestConfig<Paths, T>\n\t>;\n\n\t/**\n\t * Build a query key for manual cache operations\n\t */\n\tbuildQueryKey: <T extends QueryEndpoint<Paths>>(\n\t\tendpoint: T,\n\t\tconfig?: FilteredRequestConfig<Paths, T>,\n\t) => unknown[];\n}\n\n/**\n * Create endpoint-based React Query hooks from a typed fetcher.\n *\n * @example\n * ```typescript\n * const fetcher = createAuthAwareFetcher<paths>({ ... });\n * const hooks = createEndpointHooks<paths>(fetcher);\n *\n * // In a component\n * const { data } = hooks.useQuery('GET /users/{id}', { params: { id: '123' } });\n *\n * const mutation = hooks.useMutation('POST /users');\n * await mutation.mutateAsync({ body: { name: 'John' } });\n * ```\n */\nexport function createEndpointHooks<Paths>(\n\tfetcher: TypedApiFunction<Paths>,\n\t_options: CreateEndpointHooksOptions = {},\n): EndpointHooks<Paths> {\n\treturn {\n\t\tuseQuery: <T extends QueryEndpoint<Paths>>(\n\t\t\tendpoint: T,\n\t\t\t...args: UseQueryArgs<Paths, T>\n\t\t) => {\n\t\t\t// Parse args - config is first, options is second\n\t\t\tconst [config, queryOptions] = args as [\n\t\t\t\tFilteredRequestConfig<Paths, T> | undefined,\n\t\t\t\t(\n\t\t\t\t\t| Omit<\n\t\t\t\t\t\t\tUseQueryOptions<ExtractEndpointResponse<Paths, T>, Error>,\n\t\t\t\t\t\t\t'queryKey' | 'queryFn'\n\t\t\t\t\t >\n\t\t\t\t\t| undefined\n\t\t\t\t),\n\t\t\t];\n\n\t\t\tconst queryKey = buildQueryKey(endpoint, config);\n\n\t\t\tconst memoizedOptions = useMemo(\n\t\t\t\t() => ({\n\t\t\t\t\tqueryKey,\n\t\t\t\t\tqueryFn: () =>\n\t\t\t\t\t\t// Type assertion needed due to complex conditional types\n\t\t\t\t\t\t(\n\t\t\t\t\t\t\tfetcher as (\n\t\t\t\t\t\t\t\tendpoint: T,\n\t\t\t\t\t\t\t\tconfig?: unknown,\n\t\t\t\t\t\t\t) => Promise<ExtractEndpointResponse<Paths, T>>\n\t\t\t\t\t\t)(endpoint, config),\n\t\t\t\t\t...queryOptions,\n\t\t\t\t}),\n\t\t\t\t[endpoint, config, fetcher, queryKey, queryOptions],\n\t\t\t);\n\n\t\t\treturn useQuery<ExtractEndpointResponse<Paths, T>, Error>(\n\t\t\t\tmemoizedOptions,\n\t\t\t);\n\t\t},\n\n\t\tuseMutation: <T extends MutationEndpoint<Paths>>(\n\t\t\tendpoint: T,\n\t\t\tmutationOptions?: Omit<\n\t\t\t\tUseMutationOptions<\n\t\t\t\t\tExtractEndpointResponse<Paths, T>,\n\t\t\t\t\tError,\n\t\t\t\t\tFilteredRequestConfig<Paths, T>\n\t\t\t\t>,\n\t\t\t\t'mutationFn'\n\t\t\t>,\n\t\t) => {\n\t\t\tconst memoizedOptions = useMemo(\n\t\t\t\t() => ({\n\t\t\t\t\tmutationFn: (config: FilteredRequestConfig<Paths, T>) =>\n\t\t\t\t\t\t// Type assertion needed due to complex conditional types\n\t\t\t\t\t\t(\n\t\t\t\t\t\t\tfetcher as (\n\t\t\t\t\t\t\t\tendpoint: T,\n\t\t\t\t\t\t\t\tconfig?: unknown,\n\t\t\t\t\t\t\t) => Promise<ExtractEndpointResponse<Paths, T>>\n\t\t\t\t\t\t)(endpoint, config),\n\t\t\t\t\t...mutationOptions,\n\t\t\t\t}),\n\t\t\t\t[endpoint, fetcher, mutationOptions],\n\t\t\t);\n\n\t\t\treturn useMutation<\n\t\t\t\tExtractEndpointResponse<Paths, T>,\n\t\t\t\tError,\n\t\t\t\tFilteredRequestConfig<Paths, T>\n\t\t\t>(memoizedOptions);\n\t\t},\n\n\t\tbuildQueryKey,\n\t};\n}\n"],"mappings":";;;;;;;AAqBA,SAAS,cACRA,UACAC,QACY;CACZ,MAAMC,MAAiB,CAAC,QAAS;AAEjC,KAAI,UAAU,YAAY,UAAU,OAAO,OAC1C,KAAI,KAAK,EAAE,QAAQ,OAAO,OAAQ,EAAC;AAGpC,KAAI,UAAU,WAAW,UAAU,OAAO,MACzC,KAAI,KAAK,EAAE,OAAO,OAAO,MAAO,EAAC;AAGlC,QAAO;AACP;;;;;;;;;;;;;;;;AAwFD,SAAgB,oBACfC,SACAC,WAAuC,CAAE,GAClB;AACvB,QAAO;EACN,UAAU,CACTJ,UACA,GAAG,SACC;GAEJ,MAAM,CAAC,QAAQ,aAAa,GAAG;GAW/B,MAAM,WAAW,cAAc,UAAU,OAAO;GAEhD,MAAM,kBAAkB,QACvB,OAAO;IACN;IACA,SAAS,MAER,AACC,QAIC,UAAU,OAAO;IACpB,GAAG;GACH,IACD;IAAC;IAAU;IAAQ;IAAS;IAAU;GAAa,EACnD;AAED,UAAO,SACN,gBACA;EACD;EAED,aAAa,CACZA,UACAK,oBAQI;GACJ,MAAM,kBAAkB,QACvB,OAAO;IACN,YAAY,CAACC,WAEZ,AACC,QAIC,UAAU,OAAO;IACpB,GAAG;GACH,IACD;IAAC;IAAU;IAAS;GAAgB,EACpC;AAED,UAAO,YAIL,gBAAgB;EAClB;EAED;CACA;AACD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geekmidas/client",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"@types/react-dom": "~19.1.6",
|
|
67
67
|
"jsdom": "~26.1.0",
|
|
68
68
|
"msw": "~2.10.3",
|
|
69
|
-
"@geekmidas/constructs": "^
|
|
69
|
+
"@geekmidas/constructs": "^3.0.0",
|
|
70
70
|
"@geekmidas/schema": "^1.0.0"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"react": ">=18.0.0",
|
|
75
75
|
"react-dom": ">=18.0.0",
|
|
76
76
|
"zod": "~4.1.13",
|
|
77
|
-
"@geekmidas/constructs": "^
|
|
77
|
+
"@geekmidas/constructs": "^3.0.0",
|
|
78
78
|
"@geekmidas/schema": "^1.0.0"
|
|
79
79
|
},
|
|
80
80
|
"peerDependenciesMeta": {
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*
|
|
4
|
+
* Type inference test for mutation hooks with complex paths.
|
|
5
|
+
* This mirrors the structure generated by `gkm openapi` to verify
|
|
6
|
+
* that FilteredRequestConfig resolves correctly (not `never`).
|
|
7
|
+
*/
|
|
8
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
9
|
+
import { renderHook, waitFor } from '@testing-library/react';
|
|
10
|
+
import { HttpResponse, http } from 'msw';
|
|
11
|
+
// biome-ignore lint/style/useImportType: needed for JSX
|
|
12
|
+
import React, { createElement } from 'react';
|
|
13
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
14
|
+
import { createEndpointHooks } from '../endpoint-hooks';
|
|
15
|
+
import type { TypedApiFunction } from '../types';
|
|
16
|
+
import { server } from './setup';
|
|
17
|
+
|
|
18
|
+
// ============================================================
|
|
19
|
+
// Paths that mirror a real generated openapi.ts structure
|
|
20
|
+
// ============================================================
|
|
21
|
+
interface GeneratedPaths {
|
|
22
|
+
'/account/profile': {
|
|
23
|
+
post: {
|
|
24
|
+
requestBody: {
|
|
25
|
+
content: {
|
|
26
|
+
'application/json': { email: string; name: string };
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
responses: {
|
|
30
|
+
200: {
|
|
31
|
+
content: {
|
|
32
|
+
'application/json': Record<string, unknown>;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
get: {
|
|
38
|
+
responses: {
|
|
39
|
+
200: {
|
|
40
|
+
content: {
|
|
41
|
+
'application/json': {
|
|
42
|
+
id: string;
|
|
43
|
+
email: string;
|
|
44
|
+
name: string;
|
|
45
|
+
} | null;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
'/chats/{chatId}/participants': {
|
|
52
|
+
parameters: {
|
|
53
|
+
path: { chatId: string };
|
|
54
|
+
};
|
|
55
|
+
post: {
|
|
56
|
+
requestBody: {
|
|
57
|
+
content: {
|
|
58
|
+
'application/json': { userId: string; role: 'Owner' | 'Member' };
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
responses: {
|
|
62
|
+
200: {
|
|
63
|
+
content: {
|
|
64
|
+
'application/json': {
|
|
65
|
+
id: string;
|
|
66
|
+
chatId: string;
|
|
67
|
+
userId: string;
|
|
68
|
+
role: string;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
'/chats': {
|
|
76
|
+
post: {
|
|
77
|
+
requestBody: {
|
|
78
|
+
content: {
|
|
79
|
+
'application/json': { type: string; name?: string };
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
responses: {
|
|
83
|
+
200: {
|
|
84
|
+
content: {
|
|
85
|
+
'application/json': { id: string; type: string; name: string };
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
get: {
|
|
91
|
+
parameters: {
|
|
92
|
+
query: { cursor?: string; limit: number };
|
|
93
|
+
};
|
|
94
|
+
responses: {
|
|
95
|
+
200: {
|
|
96
|
+
content: {
|
|
97
|
+
'application/json': {
|
|
98
|
+
data: Array<{ id: string; name: string }>;
|
|
99
|
+
hasMore: boolean;
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
'/chats/{id}': {
|
|
107
|
+
parameters: {
|
|
108
|
+
path: { id: string };
|
|
109
|
+
};
|
|
110
|
+
delete: {
|
|
111
|
+
responses: {
|
|
112
|
+
200: {
|
|
113
|
+
content: {
|
|
114
|
+
'application/json': { success: boolean };
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
get: {
|
|
120
|
+
responses: {
|
|
121
|
+
200: {
|
|
122
|
+
content: {
|
|
123
|
+
'application/json': { id: string; name: string };
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
patch: {
|
|
129
|
+
requestBody: {
|
|
130
|
+
content: {
|
|
131
|
+
'application/json': { name?: string; status?: string };
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
responses: {
|
|
135
|
+
200: {
|
|
136
|
+
content: {
|
|
137
|
+
'application/json': { id: string; name: string; status: string };
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
'/chats/{chatId}/leave': {
|
|
144
|
+
parameters: {
|
|
145
|
+
path: { chatId: string };
|
|
146
|
+
};
|
|
147
|
+
post: {
|
|
148
|
+
responses: {
|
|
149
|
+
200: {
|
|
150
|
+
content: {
|
|
151
|
+
'application/json': { success: boolean };
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ============================================================
|
|
160
|
+
// Mock fetcher
|
|
161
|
+
// ============================================================
|
|
162
|
+
function createMockFetcher(): TypedApiFunction<GeneratedPaths> {
|
|
163
|
+
return async (endpoint: string, config?: any) => {
|
|
164
|
+
const [method, path] = endpoint.split(' ');
|
|
165
|
+
let url = `https://api.example.com${path}`;
|
|
166
|
+
|
|
167
|
+
if (config?.params) {
|
|
168
|
+
for (const [key, value] of Object.entries(config.params)) {
|
|
169
|
+
url = url.replace(`{${key}}`, value as string);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const response = await fetch(url, {
|
|
174
|
+
method,
|
|
175
|
+
headers: { 'Content-Type': 'application/json', ...config?.headers },
|
|
176
|
+
body: config?.body ? JSON.stringify(config.body) : undefined,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return response.json();
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function createWrapper() {
|
|
184
|
+
const queryClient = new QueryClient({
|
|
185
|
+
defaultOptions: { queries: { retry: false } },
|
|
186
|
+
});
|
|
187
|
+
return ({ children }: { children: React.ReactNode }) =>
|
|
188
|
+
createElement(QueryClientProvider, { client: queryClient }, children);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ============================================================
|
|
192
|
+
// Tests
|
|
193
|
+
// ============================================================
|
|
194
|
+
describe('mutation type inference with generated paths', () => {
|
|
195
|
+
beforeEach(() => {
|
|
196
|
+
server.use(
|
|
197
|
+
http.post(
|
|
198
|
+
'https://api.example.com/account/profile',
|
|
199
|
+
async ({ request }) => {
|
|
200
|
+
const body = (await request.json()) as any;
|
|
201
|
+
return HttpResponse.json({ id: '1', ...body });
|
|
202
|
+
},
|
|
203
|
+
),
|
|
204
|
+
http.post('https://api.example.com/chats', async ({ request }) => {
|
|
205
|
+
const body = (await request.json()) as any;
|
|
206
|
+
return HttpResponse.json({
|
|
207
|
+
id: 'chat-1',
|
|
208
|
+
type: body.type,
|
|
209
|
+
name: body.name ?? 'New Chat',
|
|
210
|
+
});
|
|
211
|
+
}),
|
|
212
|
+
http.post(
|
|
213
|
+
'https://api.example.com/chats/:chatId/participants',
|
|
214
|
+
async ({ params, request }) => {
|
|
215
|
+
const body = (await request.json()) as any;
|
|
216
|
+
return HttpResponse.json({
|
|
217
|
+
id: 'p-1',
|
|
218
|
+
chatId: params.chatId,
|
|
219
|
+
userId: body.userId,
|
|
220
|
+
role: body.role,
|
|
221
|
+
});
|
|
222
|
+
},
|
|
223
|
+
),
|
|
224
|
+
http.delete('https://api.example.com/chats/:id', () => {
|
|
225
|
+
return HttpResponse.json({ success: true });
|
|
226
|
+
}),
|
|
227
|
+
http.patch(
|
|
228
|
+
'https://api.example.com/chats/:id',
|
|
229
|
+
async ({ params, request }) => {
|
|
230
|
+
const body = (await request.json()) as any;
|
|
231
|
+
return HttpResponse.json({
|
|
232
|
+
id: params.id,
|
|
233
|
+
name: body.name ?? 'Chat',
|
|
234
|
+
status: body.status ?? 'Active',
|
|
235
|
+
});
|
|
236
|
+
},
|
|
237
|
+
),
|
|
238
|
+
http.post('https://api.example.com/chats/:chatId/leave', () => {
|
|
239
|
+
return HttpResponse.json({ success: true });
|
|
240
|
+
}),
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('should resolve body-only mutation args (POST /chats)', async () => {
|
|
245
|
+
const hooks = createEndpointHooks<GeneratedPaths>(createMockFetcher());
|
|
246
|
+
const { result } = renderHook(() => hooks.useMutation('POST /chats'), {
|
|
247
|
+
wrapper: createWrapper(),
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// If args were `never`, this call would be a type error
|
|
251
|
+
result.current.mutate({ body: { type: 'GeneralChat', name: 'Test' } });
|
|
252
|
+
|
|
253
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
254
|
+
expect(result.current.data).toMatchObject({
|
|
255
|
+
id: 'chat-1',
|
|
256
|
+
type: 'GeneralChat',
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should resolve body-only mutation args (POST /account/profile)', async () => {
|
|
261
|
+
const hooks = createEndpointHooks<GeneratedPaths>(createMockFetcher());
|
|
262
|
+
const { result } = renderHook(
|
|
263
|
+
() => hooks.useMutation('POST /account/profile'),
|
|
264
|
+
{
|
|
265
|
+
wrapper: createWrapper(),
|
|
266
|
+
},
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
result.current.mutate({ body: { email: 'test@test.com', name: 'Test' } });
|
|
270
|
+
|
|
271
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
272
|
+
expect(result.current.data).toMatchObject({ email: 'test@test.com' });
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('should resolve params+body mutation args (POST /chats/{chatId}/participants)', async () => {
|
|
276
|
+
const hooks = createEndpointHooks<GeneratedPaths>(createMockFetcher());
|
|
277
|
+
const { result } = renderHook(
|
|
278
|
+
() => hooks.useMutation('POST /chats/{chatId}/participants'),
|
|
279
|
+
{ wrapper: createWrapper() },
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
result.current.mutate({
|
|
283
|
+
params: { chatId: 'chat-1' },
|
|
284
|
+
body: { userId: 'user-1', role: 'Member' },
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
288
|
+
expect(result.current.data).toMatchObject({
|
|
289
|
+
chatId: 'chat-1',
|
|
290
|
+
userId: 'user-1',
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('should resolve params-only mutation args (DELETE /chats/{id})', async () => {
|
|
295
|
+
const hooks = createEndpointHooks<GeneratedPaths>(createMockFetcher());
|
|
296
|
+
const { result } = renderHook(
|
|
297
|
+
() => hooks.useMutation('DELETE /chats/{id}'),
|
|
298
|
+
{
|
|
299
|
+
wrapper: createWrapper(),
|
|
300
|
+
},
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
result.current.mutate({ params: { id: 'chat-1' } });
|
|
304
|
+
|
|
305
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
306
|
+
expect(result.current.data).toEqual({ success: true });
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('should resolve params+body mutation args (PATCH /chats/{id})', async () => {
|
|
310
|
+
const hooks = createEndpointHooks<GeneratedPaths>(createMockFetcher());
|
|
311
|
+
const { result } = renderHook(
|
|
312
|
+
() => hooks.useMutation('PATCH /chats/{id}'),
|
|
313
|
+
{
|
|
314
|
+
wrapper: createWrapper(),
|
|
315
|
+
},
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
result.current.mutate({
|
|
319
|
+
params: { id: 'chat-1' },
|
|
320
|
+
body: { name: 'Renamed Chat' },
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
324
|
+
expect(result.current.data).toMatchObject({
|
|
325
|
+
id: 'chat-1',
|
|
326
|
+
name: 'Renamed Chat',
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('should resolve params-only POST mutation args (POST /chats/{chatId}/leave)', async () => {
|
|
331
|
+
const hooks = createEndpointHooks<GeneratedPaths>(createMockFetcher());
|
|
332
|
+
const { result } = renderHook(
|
|
333
|
+
() => hooks.useMutation('POST /chats/{chatId}/leave'),
|
|
334
|
+
{ wrapper: createWrapper() },
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
// POST with only path params, no body
|
|
338
|
+
result.current.mutate({ params: { chatId: 'chat-1' } });
|
|
339
|
+
|
|
340
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
341
|
+
expect(result.current.data).toEqual({ success: true });
|
|
342
|
+
});
|
|
343
|
+
});
|
package/src/endpoint-hooks.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
QueryClient,
|
|
3
3
|
UseMutationOptions,
|
|
4
|
+
UseMutationResult,
|
|
4
5
|
UseQueryOptions,
|
|
6
|
+
UseQueryResult,
|
|
5
7
|
} from '@tanstack/react-query';
|
|
6
8
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
|
7
9
|
import { useMemo } from 'react';
|
|
@@ -74,7 +76,7 @@ export interface EndpointHooks<Paths> {
|
|
|
74
76
|
useQuery: <T extends QueryEndpoint<Paths>>(
|
|
75
77
|
endpoint: T,
|
|
76
78
|
...args: UseQueryArgs<Paths, T>
|
|
77
|
-
) =>
|
|
79
|
+
) => UseQueryResult<ExtractEndpointResponse<Paths, T>, Error>;
|
|
78
80
|
|
|
79
81
|
/**
|
|
80
82
|
* Use mutation hook for POST, PUT, PATCH, DELETE endpoints.
|
|
@@ -90,12 +92,10 @@ export interface EndpointHooks<Paths> {
|
|
|
90
92
|
>,
|
|
91
93
|
'mutationFn'
|
|
92
94
|
>,
|
|
93
|
-
) =>
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
FilteredRequestConfig<Paths, T>
|
|
98
|
-
>
|
|
95
|
+
) => UseMutationResult<
|
|
96
|
+
ExtractEndpointResponse<Paths, T>,
|
|
97
|
+
Error,
|
|
98
|
+
FilteredRequestConfig<Paths, T>
|
|
99
99
|
>;
|
|
100
100
|
|
|
101
101
|
/**
|