From 2cdc35bf84ccbd47765592970303a933d2702b23 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Tue, 2 Dec 2025 12:13:11 +0900 Subject: [PATCH 1/2] Implement react query --- bun.lock | 39 +- packages/fetch/src/__tests__/index.test.ts | 1 + packages/fetch/src/api.ts | 2 +- packages/fetch/src/index.ts | 1 + packages/react-query/README.md | 251 ++++++ packages/react-query/package.json | 35 + .../react-query/src/create-query-client.ts | 9 + packages/react-query/src/index.ts | 2 + packages/react-query/src/query-client.ts | 773 ++++++++++++++++++ packages/react-query/tsconfig.json | 34 + 10 files changed, 1138 insertions(+), 9 deletions(-) create mode 100644 packages/react-query/README.md create mode 100644 packages/react-query/package.json create mode 100644 packages/react-query/src/create-query-client.ts create mode 100644 packages/react-query/src/index.ts create mode 100644 packages/react-query/src/query-client.ts create mode 100644 packages/react-query/tsconfig.json diff --git a/bun.lock b/bun.lock index 592fae2..0f5259d 100644 --- a/bun.lock +++ b/bun.lock @@ -84,7 +84,7 @@ }, "packages/core": { "name": "@devup-api/core", - "version": "0.1.3", + "version": "0.1.5", "devDependencies": { "@types/node": "^24.10", "typescript": "^5.9", @@ -92,7 +92,7 @@ }, "packages/fetch": { "name": "@devup-api/fetch", - "version": "0.1.3", + "version": "0.1.5", "dependencies": { "@devup-api/core": "workspace:*", }, @@ -103,7 +103,7 @@ }, "packages/generator": { "name": "@devup-api/generator", - "version": "0.1.3", + "version": "0.1.5", "dependencies": { "@devup-api/core": "workspace:*", "@devup-api/utils": "workspace:*", @@ -116,7 +116,7 @@ }, "packages/next-plugin": { "name": "@devup-api/next-plugin", - "version": "0.1.3", + "version": "0.1.5", "dependencies": { "@devup-api/core": "workspace:*", "@devup-api/generator": "workspace:*", @@ -133,9 +133,26 @@ "next": "*", }, }, + "packages/react-query": { + "name": "@devup-api/react-query", + "version": "0.1.0", + "dependencies": { + "@devup-api/fetch": "workspace:*", + "@tanstack/react-query": ">=5.90", + }, + "devDependencies": { + "@types/node": "^24.10", + "@types/react": "^19.2", + "typescript": "^5.9", + }, + "peerDependencies": { + "@tanstack/react-query": "*", + "react": "*", + }, + }, "packages/rsbuild-plugin": { "name": "@devup-api/rsbuild-plugin", - "version": "0.1.3", + "version": "0.1.5", "dependencies": { "@devup-api/core": "workspace:*", "@devup-api/generator": "workspace:*", @@ -152,7 +169,7 @@ }, "packages/utils": { "name": "@devup-api/utils", - "version": "0.1.3", + "version": "0.1.5", "devDependencies": { "@types/node": "^24.10", "openapi-types": "^12.1", @@ -161,7 +178,7 @@ }, "packages/vite-plugin": { "name": "@devup-api/vite-plugin", - "version": "0.1.3", + "version": "0.1.5", "dependencies": { "@devup-api/core": "workspace:*", "@devup-api/generator": "workspace:*", @@ -178,7 +195,7 @@ }, "packages/webpack-plugin": { "name": "@devup-api/webpack-plugin", - "version": "0.1.3", + "version": "0.1.5", "dependencies": { "@devup-api/core": "workspace:*", "@devup-api/generator": "workspace:*", @@ -259,6 +276,8 @@ "@devup-api/next-plugin": ["@devup-api/next-plugin@workspace:packages/next-plugin"], + "@devup-api/react-query": ["@devup-api/react-query@workspace:packages/react-query"], + "@devup-api/rsbuild-plugin": ["@devup-api/rsbuild-plugin@workspace:packages/rsbuild-plugin"], "@devup-api/utils": ["@devup-api/utils@workspace:packages/utils"], @@ -513,6 +532,10 @@ "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], + "@tanstack/query-core": ["@tanstack/query-core@5.90.11", "", {}, "sha512-f9z/nXhCgWDF4lHqgIE30jxLe4sYv15QodfdPDKYAk7nAEjNcndy4dHz3ezhdUaR23BpWa4I2EH4/DZ0//Uf8A=="], + + "@tanstack/react-query": ["@tanstack/react-query@5.90.11", "", { "dependencies": { "@tanstack/query-core": "5.90.11" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-3uyzz01D1fkTLXuxF3JfoJoHQMU2fxsfJwE+6N5hHy0dVNoZOvwKP8Z2k7k1KDeD54N20apcJnG75TBAStIrBA=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], diff --git a/packages/fetch/src/__tests__/index.test.ts b/packages/fetch/src/__tests__/index.test.ts index 04ee6ed..3113f99 100644 --- a/packages/fetch/src/__tests__/index.test.ts +++ b/packages/fetch/src/__tests__/index.test.ts @@ -5,6 +5,7 @@ import * as indexModule from '../index' test('index.ts exports', () => { expect({ ...indexModule }).toEqual({ + DevupApi: expect.any(Function), createApi: expect.any(Function), }) }) diff --git a/packages/fetch/src/api.ts b/packages/fetch/src/api.ts index 300e8dc..eba556b 100644 --- a/packages/fetch/src/api.ts +++ b/packages/fetch/src/api.ts @@ -25,7 +25,7 @@ import { getApiEndpointInfo } from './url-map' import { getApiEndpoint, isPlainObject } from './utils' // biome-ignore lint/suspicious/noExplicitAny: any is used to allow for flexibility in the type -type DevupApiResponse = +export type DevupApiResponse = | { data: T error?: undefined diff --git a/packages/fetch/src/index.ts b/packages/fetch/src/index.ts index 6f197e6..1802b29 100644 --- a/packages/fetch/src/index.ts +++ b/packages/fetch/src/index.ts @@ -1,2 +1,3 @@ export * from '@devup-api/core' +export * from './api' export { createApi } from './create-api' diff --git a/packages/react-query/README.md b/packages/react-query/README.md new file mode 100644 index 0000000..c889c0c --- /dev/null +++ b/packages/react-query/README.md @@ -0,0 +1,251 @@ +# @devup-api/react-query + +Type-safe React Query hooks built on top of `@devup-api/fetch` and `@tanstack/react-query`. + +## Installation + +```bash +npm install @devup-api/react-query @tanstack/react-query +``` + +## Prerequisites + +Make sure you have `@tanstack/react-query` set up in your React application: + +```tsx +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' + +const queryClient = new QueryClient() + +function App() { + return ( + + {/* Your app */} + + ) +} +``` + +## Usage + +### Create API Hooks Instance + +```tsx +import { createApi } from '@devup-api/react-query' + +const api = createApi('https://api.example.com', { + headers: { + 'Content-Type': 'application/json' + } +}) +``` + +### Using Query Hooks (GET requests) + +```tsx +import { createApi } from '@devup-api/react-query' + +const api = createApi('https://api.example.com') + +function UsersList() { + // Using operationId + const { data, isLoading, error } = api.useGet('getUsers', { + query: { page: 1, limit: 20 } + }) + + // Using path + const { data: user } = api.useGet('/users/{id}', { + params: { id: '123' }, + query: { include: 'posts' } + }) + + if (isLoading) return
Loading...
+ if (error) return
Error: {error.message}
+ if (data?.error) return
API Error: {data.error}
+ if (data?.data) { + return
{/* Render your data */}
+ } + return null +} +``` + +### Using Mutation Hooks (POST, PUT, PATCH, DELETE) + +#### POST Request + +```tsx +function CreateUser() { + const createUser = api.usePost('createUser') + + const handleSubmit = () => { + createUser.mutate({ + body: { + name: 'John Doe', + email: 'john@example.com' + } + }) + } + + return ( +
+ + {createUser.isError &&
Error: {createUser.error?.message}
} + {createUser.data?.data &&
Success!
} +
+ ) +} +``` + +#### PUT Request + +```tsx +function UpdateUser() { + const updateUser = api.usePut('updateUser') + + const handleUpdate = () => { + updateUser.mutate({ + params: { id: '123' }, + body: { + name: 'Jane Doe' + } + }) + } + + return +} +``` + +#### PATCH Request + +```tsx +function PatchUser() { + const patchUser = api.usePatch('patchUser') + + const handlePatch = () => { + patchUser.mutate({ + params: { id: '123' }, + body: { + name: 'Jane Doe' + } + }) + } + + return +} +``` + +#### DELETE Request + +```tsx +function DeleteUser() { + const deleteUser = api.useDelete('deleteUser') + + const handleDelete = () => { + deleteUser.mutate({ + params: { id: '123' } + }) + } + + return +} +``` + +### Advanced Query Options + +You can pass additional React Query options to customize behavior: + +```tsx +const { data, isLoading } = api.useGet( + 'getUsers', + { query: { page: 1 } }, + { + staleTime: 5 * 60 * 1000, // 5 minutes + refetchOnWindowFocus: false, + retry: 3, + } +) +``` + +### Advanced Mutation Options + +You can pass additional React Query mutation options: + +```tsx +const createUser = api.usePost('createUser', { + onSuccess: (data) => { + console.log('User created:', data.data) + // Invalidate and refetch users list + queryClient.invalidateQueries({ queryKey: ['getUsers'] }) + }, + onError: (error) => { + console.error('Failed to create user:', error) + }, +}) +``` + +### Creating Hooks from Existing API Instance + +If you already have a `DevupApi` instance from `@devup-api/fetch`, you can create hooks from it: + +```tsx +import { createApi as createFetchApi } from '@devup-api/fetch' +import { createApiHooks } from '@devup-api/react-query' + +const fetchApi = createFetchApi('https://api.example.com') +const api = createApiHooks(fetchApi) + +// Now you can use api.useGet, api.usePost, etc. +``` + +## Response Handling + +All hooks return React Query's standard return values, with the response data following the same structure as `@devup-api/fetch`: + +```tsx +type DevupApiResponse = + | { data: T; error?: undefined; response: Response } + | { data?: undefined; error: E; response: Response } +``` + +Example: + +```tsx +const { data } = api.useGet('getUser', { params: { id: '123' } }) + +if (data?.data) { + // Success - data.data is fully typed based on your OpenAPI schema + console.log(data.data.name) + console.log(data.data.email) +} else if (data?.error) { + // Error - data.error is typed based on your OpenAPI error schemas + console.error(data.error.message) +} + +// Access raw Response object +console.log(data?.response.status) +``` + +## API Methods + +- `api.useGet(path, options, queryOptions)` - GET request hook +- `api.usePost(path, mutationOptions)` - POST request hook +- `api.usePut(path, mutationOptions)` - PUT request hook +- `api.usePatch(path, mutationOptions)` - PATCH request hook +- `api.useDelete(path, mutationOptions)` - DELETE request hook + +## Type Safety + +All API hooks are fully typed based on your OpenAPI schema: + +- Path parameters are type-checked +- Request bodies are type-checked +- Query parameters are type-checked +- Response types are inferred automatically +- Error types are inferred automatically + +## License + +Apache 2.0 + diff --git a/packages/react-query/package.json b/packages/react-query/package.json new file mode 100644 index 0000000..6d78538 --- /dev/null +++ b/packages/react-query/package.json @@ -0,0 +1,35 @@ +{ + "name": "@devup-api/react-query", + "version": "0.0.0", + "license": "Apache-2.0", + "type": "module", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.cjs", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc && bun build --target node --outfile=dist/index.js src/index.ts --production --packages=external && bun build --target node --outfile=dist/index.cjs --format=cjs src/index.ts --production --packages=external" + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@devup-api/fetch": "workspace:*", + "@tanstack/react-query": ">=5.90" + }, + "peerDependencies": { + "react": "*", + "@tanstack/react-query": "*" + }, + "devDependencies": { + "@types/node": "^24.10", + "@types/react": "^19.2", + "typescript": "^5.9" + } +} diff --git a/packages/react-query/src/create-query-client.ts b/packages/react-query/src/create-query-client.ts new file mode 100644 index 0000000..6519bba --- /dev/null +++ b/packages/react-query/src/create-query-client.ts @@ -0,0 +1,9 @@ +import type { ConditionalKeys } from '@devup-api/core' +import type { DevupApi, DevupApiServers } from '@devup-api/fetch' +import { DevupQueryClient } from './query-client' + +export function createQueryClient>( + api: DevupApi, +): DevupQueryClient { + return new DevupQueryClient(api) +} diff --git a/packages/react-query/src/index.ts b/packages/react-query/src/index.ts new file mode 100644 index 0000000..d7a4a85 --- /dev/null +++ b/packages/react-query/src/index.ts @@ -0,0 +1,2 @@ +export { createQueryClient } from './create-query-client' +export { DevupQueryClient } from './query-client' diff --git a/packages/react-query/src/query-client.ts b/packages/react-query/src/query-client.ts new file mode 100644 index 0000000..b689b4b --- /dev/null +++ b/packages/react-query/src/query-client.ts @@ -0,0 +1,773 @@ +import type { + Additional, + ConditionalKeys, + ConditionalScope, + DevupApi, + DevupApiRequestInit, + DevupApiResponse, + DevupApiServers, + DevupApiStruct, + DevupApiStructKey, + DevupDeleteApiStruct, + DevupDeleteApiStructKey, + DevupGetApiStruct, + DevupGetApiStructKey, + DevupPatchApiStruct, + DevupPatchApiStructKey, + DevupPostApiStruct, + DevupPostApiStructKey, + DevupPutApiStruct, + DevupPutApiStructKey, + ExtractValue, + RequiredOptions, +} from '@devup-api/fetch' +import { + useInfiniteQuery, + useMutation, + useQuery, + useSuspenseQuery, +} from '@tanstack/react-query' + +function getQueryKey( + method: M, + path: P, + options: OP, +): [M, P, NonNullable] | [M, P] { + return options === undefined + ? ([method, path] as [M, P]) + : ([method, path, options] as [M, P, NonNullable]) +} + +export class DevupQueryClient> { + private api: DevupApi + + constructor(api: DevupApi) { + this.api = api + } + + useQuery< + T extends DevupGetApiStructKey, + O extends Additional>, + D extends ExtractValue, + E extends ExtractValue, + >( + method: 'get' | 'GET', + path: T, + ...options: [RequiredOptions] extends [never] + ? [ + options?: DevupApiRequestInit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + : [ + options: DevupApiRequestInit & Omit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + ): ReturnType> + + useQuery< + T extends DevupPostApiStructKey, + O extends Additional>, + D extends ExtractValue, + E extends ExtractValue, + >( + method: 'post' | 'POST', + path: T, + ...options: [RequiredOptions] extends [never] + ? [ + options?: DevupApiRequestInit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + : [ + options: DevupApiRequestInit & Omit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + ): ReturnType> + + useQuery< + T extends DevupPutApiStructKey, + O extends Additional>, + D extends ExtractValue, + E extends ExtractValue, + >( + method: 'put' | 'PUT', + path: T, + ...options: [RequiredOptions] extends [never] + ? [ + options?: DevupApiRequestInit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + : [ + options: DevupApiRequestInit & Omit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + ): ReturnType> + + useQuery< + T extends DevupPatchApiStructKey, + O extends Additional>, + D extends ExtractValue, + E extends ExtractValue, + >( + method: 'patch' | 'PATCH', + path: T, + ...options: [RequiredOptions] extends [never] + ? [ + options?: DevupApiRequestInit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + : [ + options: DevupApiRequestInit & Omit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + ): ReturnType> + + useQuery< + T extends DevupDeleteApiStructKey, + O extends Additional>, + D extends ExtractValue, + E extends ExtractValue, + >( + method: 'delete' | 'DELETE', + path: T, + ...options: [RequiredOptions] extends [never] + ? [ + options?: DevupApiRequestInit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + : [ + options: DevupApiRequestInit & Omit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + ): ReturnType> + + useQuery< + T extends DevupApiStructKey, + O extends Additional>, + D extends ExtractValue, + E extends ExtractValue, + >( + method: + | 'get' + | 'post' + | 'put' + | 'delete' + | 'patch' + | 'GET' + | 'POST' + | 'PUT' + | 'DELETE' + | 'PATCH', + path: T, + ...options: [RequiredOptions] extends [never] + ? [ + options?: DevupApiRequestInit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + : [ + options: DevupApiRequestInit & Omit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + ): ReturnType> { + return useQuery( + { + queryKey: getQueryKey(method, path, options[0]), + queryFn: ({ + queryKey: [method, path, ...options], + signal, + }): Promise => + // biome-ignore lint/suspicious/noExplicitAny: can't use method as a function + (this.api as any) + [method as string](path, { + signal, + ...(options[0] as DevupApiRequestInit), + }) + .then(({ data, error }: DevupApiResponse) => { + if (error) throw error + return data + }), + ...options[1], + }, + options[2], + ) + } + + useMutation< + T extends DevupGetApiStructKey, + O extends Additional>, + D extends ExtractValue, + E extends ExtractValue, + V extends [RequiredOptions] extends [never] + ? DevupApiRequestInit + : DevupApiRequestInit & Omit, + >( + method: 'get' | 'GET', + path: T, + queryOptions?: Omit< + Parameters>[0], + 'mutationFn' | 'mutationKey' + >, + queryClient?: Parameters>[1], + ): ReturnType> + + useMutation< + T extends DevupPostApiStructKey, + O extends Additional>, + D extends ExtractValue, + E extends ExtractValue, + V extends [RequiredOptions] extends [never] + ? DevupApiRequestInit + : DevupApiRequestInit & Omit, + >( + method: 'post' | 'POST', + path: T, + queryOptions?: Omit< + Parameters>[0], + 'mutationFn' | 'mutationKey' + >, + queryClient?: Parameters>[1], + ): ReturnType> + + useMutation< + T extends DevupPutApiStructKey, + O extends Additional>, + D extends ExtractValue, + E extends ExtractValue, + V extends [RequiredOptions] extends [never] + ? DevupApiRequestInit + : DevupApiRequestInit & Omit, + >( + method: 'put' | 'PUT', + path: T, + queryOptions?: Omit< + Parameters>[0], + 'mutationFn' | 'mutationKey' + >, + queryClient?: Parameters>[1], + ): ReturnType> + + useMutation< + T extends DevupPatchApiStructKey, + O extends Additional>, + D extends ExtractValue, + E extends ExtractValue, + V extends [RequiredOptions] extends [never] + ? DevupApiRequestInit + : DevupApiRequestInit & Omit, + >( + method: 'patch' | 'PATCH', + path: T, + queryOptions?: Omit< + Parameters>[0], + 'mutationFn' | 'mutationKey' + >, + queryClient?: Parameters>[1], + ): ReturnType> + + useMutation< + T extends DevupDeleteApiStructKey, + O extends Additional>, + D extends ExtractValue, + E extends ExtractValue, + V extends [RequiredOptions] extends [never] + ? DevupApiRequestInit + : DevupApiRequestInit & Omit, + >( + method: 'delete' | 'DELETE', + path: T, + queryOptions?: Omit< + Parameters>[0], + 'mutationFn' | 'mutationKey' + >, + queryClient?: Parameters>[1], + ): ReturnType> + + useMutation< + T extends DevupApiStructKey, + O extends Additional>, + D extends ExtractValue, + E extends ExtractValue, + V extends [RequiredOptions] extends [never] + ? DevupApiRequestInit + : DevupApiRequestInit & Omit, + >( + method: + | 'get' + | 'post' + | 'put' + | 'delete' + | 'patch' + | 'GET' + | 'POST' + | 'PUT' + | 'DELETE' + | 'PATCH', + path: T, + queryOptions?: Omit< + Parameters>[0], + 'mutationFn' | 'mutationKey' + >, + queryClient?: Parameters>[1], + ): ReturnType> { + return useMutation( + { + mutationKey: [method, path], + mutationFn: (variables: V, { mutationKey }): Promise => + // biome-ignore lint/suspicious/noExplicitAny: can't use method as a function + (this.api as any) + [mutationKey?.[0] as string](mutationKey?.[1] as T, variables) + .then(({ data, error }: DevupApiResponse) => { + if (error) throw error + return data + }), + ...queryOptions, + }, + queryClient, + ) + } + + useSuspenseQuery< + T extends DevupGetApiStructKey, + O extends Additional>, + D extends ExtractValue, + E extends ExtractValue, + >( + method: 'get' | 'GET', + path: T, + ...options: [RequiredOptions] extends [never] + ? [ + options?: DevupApiRequestInit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + : [ + options: DevupApiRequestInit & Omit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + ): ReturnType> + + useSuspenseQuery< + T extends DevupPostApiStructKey, + O extends Additional>, + D extends ExtractValue, + E extends ExtractValue, + >( + method: 'post' | 'POST', + path: T, + ...options: [RequiredOptions] extends [never] + ? [ + options?: DevupApiRequestInit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + : [ + options: DevupApiRequestInit & Omit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + ): ReturnType> + + useSuspenseQuery< + T extends DevupPutApiStructKey, + O extends Additional>, + D extends ExtractValue, + E extends ExtractValue, + >( + method: 'put' | 'PUT', + path: T, + ...options: [RequiredOptions] extends [never] + ? [ + options?: DevupApiRequestInit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + : [ + options: DevupApiRequestInit & Omit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + ): ReturnType> + + useSuspenseQuery< + T extends DevupPatchApiStructKey, + O extends Additional>, + D extends ExtractValue, + E extends ExtractValue, + >( + method: 'patch' | 'PATCH', + path: T, + ...options: [RequiredOptions] extends [never] + ? [ + options?: DevupApiRequestInit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + : [ + options: DevupApiRequestInit & Omit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + ): ReturnType> + + useSuspenseQuery< + T extends DevupDeleteApiStructKey, + O extends Additional>, + D extends ExtractValue, + E extends ExtractValue, + >( + method: 'delete' | 'DELETE', + path: T, + ...options: [RequiredOptions] extends [never] + ? [ + options?: DevupApiRequestInit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + : [ + options: DevupApiRequestInit & Omit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + ): ReturnType> + + useSuspenseQuery< + T extends DevupApiStructKey, + O extends Additional>, + D extends ExtractValue, + E extends ExtractValue, + OP extends [RequiredOptions] extends [never] + ? DevupApiRequestInit | undefined + : DevupApiRequestInit & Omit, + >( + method: + | 'get' + | 'post' + | 'put' + | 'delete' + | 'patch' + | 'GET' + | 'POST' + | 'PUT' + | 'DELETE' + | 'PATCH', + path: T, + ...options: [RequiredOptions] extends [never] + ? [ + options?: OP, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + : [ + options: OP, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + ): ReturnType> { + return useSuspenseQuery( + { + queryKey: getQueryKey(method, path, options[0]), + queryFn: ({ + queryKey: [method, path, ...options], + signal, + }): Promise => + // biome-ignore lint/suspicious/noExplicitAny: can't use method as a function + (this.api as any) + [method as string](path, { + signal, + ...(options[0] as DevupApiRequestInit), + }) + .then(({ data, error }: DevupApiResponse) => { + if (error) throw error + return data + }), + ...options[1], + }, + options[2], + ) + } + + useInfiniteQuery< + T extends DevupGetApiStructKey, + O extends Additional>, + D extends ExtractValue, + E extends ExtractValue, + >( + method: 'get' | 'GET', + path: T, + ...options: [RequiredOptions] extends [never] + ? [ + options?: DevupApiRequestInit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + : [ + options: DevupApiRequestInit & Omit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + ): ReturnType> + + useInfiniteQuery< + T extends DevupPostApiStructKey, + O extends Additional>, + D extends ExtractValue, + E extends ExtractValue, + >( + method: 'post' | 'POST', + path: T, + ...options: [RequiredOptions] extends [never] + ? [ + options?: DevupApiRequestInit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + : [ + options: DevupApiRequestInit & Omit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + ): ReturnType> + + useInfiniteQuery< + T extends DevupPutApiStructKey, + O extends Additional>, + D extends ExtractValue, + E extends ExtractValue, + >( + method: 'put' | 'PUT', + path: T, + ...options: [RequiredOptions] extends [never] + ? [ + options?: DevupApiRequestInit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + : [ + options: DevupApiRequestInit & Omit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + ): ReturnType> + + useInfiniteQuery< + T extends DevupPatchApiStructKey, + O extends Additional>, + D extends ExtractValue, + E extends ExtractValue, + >( + method: 'patch' | 'PATCH', + path: T, + ...options: [RequiredOptions] extends [never] + ? [ + options?: DevupApiRequestInit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + : [ + options: DevupApiRequestInit & Omit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + ): ReturnType> + + useInfiniteQuery< + T extends DevupDeleteApiStructKey, + O extends Additional>, + D extends ExtractValue, + E extends ExtractValue, + >( + method: 'delete' | 'DELETE', + path: T, + ...options: [RequiredOptions] extends [never] + ? [ + options?: DevupApiRequestInit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + : [ + options: DevupApiRequestInit & Omit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + ): ReturnType> + + useInfiniteQuery< + T extends DevupApiStructKey, + O extends Additional>, + D extends ExtractValue, + E extends ExtractValue, + >( + method: + | 'get' + | 'post' + | 'put' + | 'delete' + | 'patch' + | 'GET' + | 'POST' + | 'PUT' + | 'DELETE' + | 'PATCH', + path: T, + ...options: [RequiredOptions] extends [never] + ? [ + options?: DevupApiRequestInit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + : [ + options: DevupApiRequestInit & Omit, + queryOptions?: Omit< + Parameters>[0], + 'queryFn' | 'queryKey' + >, + queryClient?: Parameters>[1], + ] + ): ReturnType> { + return useInfiniteQuery( + { + queryKey: getQueryKey(method, path, options[0]), + queryFn: ({ queryKey, pageParam, signal }): Promise => { + const [methodKey, pathKey, ...restOptions] = queryKey + const apiOptions = restOptions[0] as DevupApiRequestInit | undefined + // biome-ignore lint/suspicious/noExplicitAny: can't use method as a function + return (this.api as any) + [methodKey as string]( + pathKey as T, + { + signal, + ...apiOptions, + query: { + ...(apiOptions as { query?: Record })?.query, + page: pageParam, + }, + } as DevupApiRequestInit, + ) + .then(({ data, error }: DevupApiResponse) => { + if (error) throw error + return data as D + }) + }, + ...options[1], + } as Parameters>[0], + options[2], + ) + } +} diff --git a/packages/react-query/tsconfig.json b/packages/react-query/tsconfig.json new file mode 100644 index 0000000..c6497e6 --- /dev/null +++ b/packages/react-query/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "verbatimModuleSyntax": true, + "emitDeclarationOnly": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false, + "declaration": true, + "declarationMap": true, + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["dist", "node_modules", "src/**/__tests__/**"] +} From 420696f097163678df2ec2d12b25dd14603ebf5a Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Tue, 2 Dec 2025 12:26:46 +0900 Subject: [PATCH 2/2] Implement react query --- .../changepack_log_MZUjkFzEPm0xEZstNHS2P.json | 1 + bun.lock | 54 ++- bunfig.toml | 1 + package.json | 8 +- packages/react-query/package.json | 2 + packages/react-query/setup.ts | 27 ++ .../src/__tests__/create-query-client.test.ts | 50 +++ .../react-query/src/__tests__/index.test.ts | 11 + .../src/__tests__/query-client.test.ts | 75 ++++ .../src/__tests__/query-client.test.tsx | 363 ++++++++++++++++++ packages/react-query/src/query-client.ts | 2 +- 11 files changed, 583 insertions(+), 11 deletions(-) create mode 100644 .changepacks/changepack_log_MZUjkFzEPm0xEZstNHS2P.json create mode 100644 packages/react-query/setup.ts create mode 100644 packages/react-query/src/__tests__/create-query-client.test.ts create mode 100644 packages/react-query/src/__tests__/index.test.ts create mode 100644 packages/react-query/src/__tests__/query-client.test.ts create mode 100644 packages/react-query/src/__tests__/query-client.test.tsx diff --git a/.changepacks/changepack_log_MZUjkFzEPm0xEZstNHS2P.json b/.changepacks/changepack_log_MZUjkFzEPm0xEZstNHS2P.json new file mode 100644 index 0000000..9354ccc --- /dev/null +++ b/.changepacks/changepack_log_MZUjkFzEPm0xEZstNHS2P.json @@ -0,0 +1 @@ +{"changes":{"packages/fetch/package.json":"Patch","packages/react-query/package.json":"Minor"},"note":"Implement react-query","date":"2025-12-02T03:13:38.013730100Z"} \ No newline at end of file diff --git a/bun.lock b/bun.lock index 0f5259d..cfcc839 100644 --- a/bun.lock +++ b/bun.lock @@ -6,8 +6,12 @@ "name": "devup-api", "devDependencies": { "@biomejs/biome": "^2.3", + "@testing-library/react": "^16.3.0", + "@testing-library/react-hooks": "^8.0.1", "@types/bun": "latest", "husky": "^9", + "react": "^19.2.0", + "react-dom": "^19.2.0", }, }, "examples/next": { @@ -135,14 +139,16 @@ }, "packages/react-query": { "name": "@devup-api/react-query", - "version": "0.1.0", + "version": "0.0.0", "dependencies": { "@devup-api/fetch": "workspace:*", "@tanstack/react-query": ">=5.90", }, "devDependencies": { + "@testing-library/react-hooks": "^8.0.1", "@types/node": "^24.10", "@types/react": "^19.2", + "happy-dom": "^20.0.11", "typescript": "^5.9", }, "peerDependencies": { @@ -244,6 +250,8 @@ "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], + "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], + "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], "@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], @@ -536,8 +544,16 @@ "@tanstack/react-query": ["@tanstack/react-query@5.90.11", "", { "dependencies": { "@tanstack/query-core": "5.90.11" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-3uyzz01D1fkTLXuxF3JfoJoHQMU2fxsfJwE+6N5hHy0dVNoZOvwKP8Z2k7k1KDeD54N20apcJnG75TBAStIrBA=="], + "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], + + "@testing-library/react": ["@testing-library/react@16.3.0", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw=="], + + "@testing-library/react-hooks": ["@testing-library/react-hooks@8.0.1", "", { "dependencies": { "@babel/runtime": "^7.12.5", "react-error-boundary": "^3.1.0" }, "peerDependencies": { "@types/react": "^16.9.0 || ^17.0.0", "react": "^16.9.0 || ^17.0.0", "react-dom": "^16.9.0 || ^17.0.0", "react-test-renderer": "^16.9.0 || ^17.0.0" }, "optionalPeers": ["@types/react", "react-dom", "react-test-renderer"] }, "sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], @@ -564,6 +580,8 @@ "@types/webpack": ["@types/webpack@5.28.5", "", { "dependencies": { "@types/node": "*", "tapable": "^2.2.0", "webpack": "^5" } }, "sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw=="], + "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.1", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.47", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA=="], "@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="], @@ -610,9 +628,11 @@ "ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="], - "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.8.31", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw=="], @@ -646,8 +666,12 @@ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], "electron-to-chromium": ["electron-to-chromium@1.5.260", "", {}, "sha512-ov8rBoOBhVawpzdre+Cmz4FB+y66Eqrk6Gwqd8NGxuhv99GQ8XqMAr351KEkOt7gukXWDg6gJWEMKgL2RLMPtA=="], @@ -690,6 +714,8 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "happy-dom": ["happy-dom@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "@types/whatwg-mimetype": "^3.0.2", "whatwg-mimetype": "^3.0.0" } }, "sha512-QsCdAUHAmiDeKeaNojb1OHOPF7NjcWPBR7obdu3NwH2a/oyQaLg5d0aaCy/9My6CdPChYF07dvz5chaXBGaD4g=="], + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], "html-entities": ["html-entities@2.6.0", "", {}, "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ=="], @@ -720,6 +746,8 @@ "lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="], + "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], @@ -758,12 +786,18 @@ "postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], + "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], + "randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="], "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="], "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="], + "react-error-boundary": ["react-error-boundary@3.1.4", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "react": ">=16.13.1" } }, "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA=="], + + "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], + "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], @@ -836,6 +870,8 @@ "webpack-sources": ["webpack-sources@3.3.3", "", {}, "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg=="], + "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], @@ -860,16 +896,20 @@ "esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + "happy-dom/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], + "jest-worker/@types/node": ["@types/node@20.19.25", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ=="], "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "vite/postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -880,12 +920,10 @@ "bun-types/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "jest-worker/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "happy-dom/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "jest-worker/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], } } diff --git a/bunfig.toml b/bunfig.toml index 3a82f81..8fe65c6 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -2,3 +2,4 @@ coverage = true coveragePathIgnorePatterns = ["node_modules", "**/dist/**"] coverageSkipTestFiles = true +preload = ["./packages/react-query/setup.ts"] \ No newline at end of file diff --git a/package.json b/package.json index 2e83555..54d8514 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,13 @@ "type": "module", "private": true, "devDependencies": { - "@types/bun": "latest", "@biomejs/biome": "^2.3", - "husky": "^9" + "@testing-library/react": "^16.3.0", + "@testing-library/react-hooks": "^8.0.1", + "@types/bun": "latest", + "husky": "^9", + "react": "^19.2.0", + "react-dom": "^19.2.0" }, "author": "JeongMin Oh", "license": "Apache-2.0", diff --git a/packages/react-query/package.json b/packages/react-query/package.json index 6d78538..035d023 100644 --- a/packages/react-query/package.json +++ b/packages/react-query/package.json @@ -28,8 +28,10 @@ "@tanstack/react-query": "*" }, "devDependencies": { + "@testing-library/react-hooks": "^8.0.1", "@types/node": "^24.10", "@types/react": "^19.2", + "happy-dom": "^20.0.11", "typescript": "^5.9" } } diff --git a/packages/react-query/setup.ts b/packages/react-query/setup.ts new file mode 100644 index 0000000..08f52f4 --- /dev/null +++ b/packages/react-query/setup.ts @@ -0,0 +1,27 @@ +import { beforeAll } from 'bun:test' + +// Setup DOM environment for React testing +if (typeof globalThis.document === 'undefined') { + // @ts-expect-error - happy-dom types + const { Window } = await import('happy-dom') + const window = new Window() + const document = window.document + + // @ts-expect-error - setting global document + globalThis.window = window + // @ts-expect-error - setting global document + globalThis.document = document + // @ts-expect-error - setting global navigator + globalThis.navigator = window.navigator + // @ts-expect-error - setting global HTMLElement + globalThis.HTMLElement = window.HTMLElement +} + +beforeAll(() => { + // Ensure DOM is ready + if (globalThis.document) { + const root = globalThis.document.createElement('div') + root.id = 'root' + globalThis.document.body.appendChild(root) + } +}) diff --git a/packages/react-query/src/__tests__/create-query-client.test.ts b/packages/react-query/src/__tests__/create-query-client.test.ts new file mode 100644 index 0000000..ec051dd --- /dev/null +++ b/packages/react-query/src/__tests__/create-query-client.test.ts @@ -0,0 +1,50 @@ +/** biome-ignore-all lint/suspicious/noExplicitAny: any is used to allow for flexibility in the type */ +import { expect, test } from 'bun:test' +import { createApi } from '@devup-api/fetch' +import { createQueryClient } from '../create-query-client' +import { DevupQueryClient } from '../query-client' + +test.each([ + ['https://api.example.com'], + ['https://api.example.com/'], + ['http://localhost:3000'], + ['http://localhost:3000/'], +] as const)('createQueryClient returns DevupQueryClient instance: %s', (baseUrl) => { + const api = createApi({ baseUrl }) + const queryClient = createQueryClient(api) + expect(queryClient).toBeInstanceOf(DevupQueryClient) +}) + +test.each([ + ['https://api.example.com', undefined], + ['https://api.example.com', {}], + ['https://api.example.com', { headers: { Authorization: 'Bearer token' } }], +] as const)('createQueryClient accepts api with defaultOptions: %s', (baseUrl, defaultOptions) => { + const api = createApi({ baseUrl, ...defaultOptions }) + const queryClient = createQueryClient(api) + expect(queryClient).toBeInstanceOf(DevupQueryClient) +}) + +test.each([ + ['openapi.json'], + ['openapi2.json'], +] as const)('createQueryClient accepts api with serverName: %s', (serverName) => { + const api = createApi({ + baseUrl: 'https://api.example.com', + serverName: serverName as any, + }) + const queryClient = createQueryClient(api) + expect(queryClient).toBeInstanceOf(DevupQueryClient) +}) + +test('createQueryClient uses default serverName when not provided', () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = createQueryClient(api) + expect(queryClient).toBeInstanceOf(DevupQueryClient) +}) + +test('createQueryClient uses empty baseUrl when not provided', () => { + const api = createApi({}) + const queryClient = createQueryClient(api) + expect(queryClient).toBeInstanceOf(DevupQueryClient) +}) diff --git a/packages/react-query/src/__tests__/index.test.ts b/packages/react-query/src/__tests__/index.test.ts new file mode 100644 index 0000000..9a6b342 --- /dev/null +++ b/packages/react-query/src/__tests__/index.test.ts @@ -0,0 +1,11 @@ +import { expect, test } from 'bun:test' +import * as indexModule from '../index' + +// Type imports to verify types are exported (compile-time check) + +test('index.ts exports', () => { + expect({ ...indexModule }).toEqual({ + createQueryClient: expect.any(Function), + DevupQueryClient: expect.any(Function), + }) +}) diff --git a/packages/react-query/src/__tests__/query-client.test.ts b/packages/react-query/src/__tests__/query-client.test.ts new file mode 100644 index 0000000..7d4dc54 --- /dev/null +++ b/packages/react-query/src/__tests__/query-client.test.ts @@ -0,0 +1,75 @@ +/** biome-ignore-all lint/suspicious/noExplicitAny: any is used to allow for flexibility in the type */ +import { expect, test } from 'bun:test' +import { createApi } from '@devup-api/fetch' +import { DevupQueryClient, getQueryKey } from '../query-client' + +test('DevupQueryClient constructor', () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = new DevupQueryClient(api) + expect(queryClient).toBeInstanceOf(DevupQueryClient) +}) + +test('DevupQueryClient useQuery method exists', () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = new DevupQueryClient(api) + expect(typeof queryClient.useQuery).toBe('function') +}) + +test('DevupQueryClient useMutation method exists', () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = new DevupQueryClient(api) + expect(typeof queryClient.useMutation).toBe('function') +}) + +test('DevupQueryClient useSuspenseQuery method exists', () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = new DevupQueryClient(api) + expect(typeof queryClient.useSuspenseQuery).toBe('function') +}) + +test('DevupQueryClient useInfiniteQuery method exists', () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = new DevupQueryClient(api) + expect(typeof queryClient.useInfiniteQuery).toBe('function') +}) + +test('getQueryKey returns correct key without options', () => { + const result = getQueryKey('get', '/test', undefined) + expect(result).toEqual(['get', '/test']) +}) + +test('getQueryKey returns correct key with options', () => { + const options = { params: { id: '123' } } + const result = getQueryKey('get', '/test', options) + expect(result).toEqual(['get', '/test', options]) +}) + +test('getQueryKey handles different methods', () => { + const methods = ['get', 'post', 'put', 'delete', 'patch'] as const + for (const method of methods) { + const result = getQueryKey(method, '/test', undefined) + expect(result).toEqual([method, '/test']) + } +}) + +test('getQueryKey handles different paths', () => { + const paths = ['/test', '/users', '/users/{id}'] as const + for (const path of paths) { + const result = getQueryKey('get', path, undefined) + expect(result).toEqual(['get', path]) + } +}) + +test('getQueryKey handles different option types', () => { + const options1 = { params: { id: '123' } } + const result1 = getQueryKey('get', '/test', options1) + expect(result1).toEqual(['get', '/test', options1]) + + const options2 = { query: { page: 1 } } + const result2 = getQueryKey('get', '/test', options2) + expect(result2).toEqual(['get', '/test', options2]) + + const options3 = { params: { id: '123' }, query: { page: 1 } } + const result3 = getQueryKey('get', '/test', options3) + expect(result3).toEqual(['get', '/test', options3]) +}) diff --git a/packages/react-query/src/__tests__/query-client.test.tsx b/packages/react-query/src/__tests__/query-client.test.tsx new file mode 100644 index 0000000..f074d35 --- /dev/null +++ b/packages/react-query/src/__tests__/query-client.test.tsx @@ -0,0 +1,363 @@ +/** biome-ignore-all lint/suspicious/noExplicitAny: any is used to allow for flexibility in the type */ +import { afterEach, beforeEach, expect, mock, test } from 'bun:test' +import { createApi } from '@devup-api/fetch' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { renderHook, waitFor } from '@testing-library/react' +import { createElement, type ReactNode } from 'react' +import { DevupQueryClient } from '../query-client' + +const originalFetch = globalThis.fetch + +beforeEach(() => { + globalThis.fetch = mock(() => + Promise.resolve( + new Response(JSON.stringify({ id: 1, name: 'test' }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ), + ) as unknown as typeof fetch +}) + +afterEach(() => { + globalThis.fetch = originalFetch +}) + +function createWrapper() { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + gcTime: 0, + }, + }, + }) + return ({ children }: { children: ReactNode }) => + createElement(QueryClientProvider, { client: queryClient }, children) +} + +test('DevupQueryClient useQuery with GET method', async () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = new DevupQueryClient(api) + + const { result } = renderHook( + () => queryClient.useQuery('get', '/test' as any), + { wrapper: createWrapper() }, + ) + + await waitFor( + () => { + expect(result.current.isSuccess).toBe(true) + }, + { timeout: 5000 }, + ) + + expect(result.current.data).toEqual({ id: 1, name: 'test' }) +}) + +test('DevupQueryClient useQuery with options', async () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = new DevupQueryClient(api) + + const { result } = renderHook( + () => + queryClient.useQuery('get', '/test' as any, { + params: { id: '123' }, + }), + { wrapper: createWrapper() }, + ) + + await waitFor( + () => { + expect(result.current.isSuccess).toBe(true) + }, + { timeout: 5000 }, + ) + + expect(result.current.data).toEqual({ id: 1, name: 'test' }) +}) + +test('DevupQueryClient useQuery with queryOptions', async () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = new DevupQueryClient(api) + + const { result } = renderHook( + () => + queryClient.useQuery('get', '/test' as any, undefined, { + staleTime: 1000, + }), + { wrapper: createWrapper() }, + ) + + await waitFor( + () => { + expect(result.current.isSuccess).toBe(true) + }, + { timeout: 5000 }, + ) + + expect(result.current.data).toEqual({ id: 1, name: 'test' }) +}) + +test('DevupQueryClient useMutation with POST method', async () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = new DevupQueryClient(api) + + const { result } = renderHook( + () => queryClient.useMutation('post', '/test' as any), + { wrapper: createWrapper() }, + ) + + expect(result.current.mutate).toBeDefined() + expect(typeof result.current.mutate).toBe('function') + + // Execute mutation to cover mutationFn + result.current.mutate({ + body: { name: 'test' }, + }) + + await waitFor( + () => { + expect(result.current.isSuccess).toBe(true) + }, + { timeout: 5000 }, + ) + + expect(result.current.data).toEqual({ id: 1, name: 'test' }) +}) + +test('DevupQueryClient useMutation with mutationOptions', async () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = new DevupQueryClient(api) + let successCalled = false + + const { result } = renderHook( + () => + queryClient.useMutation('post', '/test' as any, { + onSuccess: () => { + successCalled = true + }, + }), + { wrapper: createWrapper() }, + ) + + expect(result.current.mutate).toBeDefined() + + // Execute mutation to cover mutationFn + result.current.mutate({ + body: { name: 'test' }, + }) + + await waitFor( + () => { + expect(result.current.isSuccess).toBe(true) + }, + { timeout: 5000 }, + ) + + expect(result.current.data).toEqual({ id: 1, name: 'test' }) + expect(successCalled).toBe(true) +}) + +test('DevupQueryClient useSuspenseQuery with GET method', async () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = new DevupQueryClient(api) + + const { result } = renderHook( + () => queryClient.useSuspenseQuery('get', '/test' as any), + { wrapper: createWrapper() }, + ) + + await waitFor( + () => { + expect(result.current.isSuccess).toBe(true) + }, + { timeout: 5000 }, + ) + + expect(result.current.data).toEqual({ id: 1, name: 'test' }) +}) + +test('DevupQueryClient useSuspenseQuery with options', async () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = new DevupQueryClient(api) + + const { result } = renderHook( + () => + queryClient.useSuspenseQuery('get', '/test' as any, { + params: { id: '123' }, + }), + { wrapper: createWrapper() }, + ) + + await waitFor( + () => { + expect(result.current.isSuccess).toBe(true) + }, + { timeout: 5000 }, + ) + + expect(result.current.data).toEqual({ id: 1, name: 'test' }) +}) + +test('DevupQueryClient useInfiniteQuery with GET method', async () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = new DevupQueryClient(api) + + const { result } = renderHook( + () => + queryClient.useInfiniteQuery('get', '/test' as any, undefined, { + initialPageParam: 1, + getNextPageParam: () => undefined, + }), + { wrapper: createWrapper() }, + ) + + await waitFor( + () => { + expect(result.current.isSuccess).toBe(true) + }, + { timeout: 5000 }, + ) + + expect(result.current.data).toEqual({ + pages: [{ id: 1, name: 'test' }], + pageParams: [1], + }) +}) + +test('DevupQueryClient useInfiniteQuery with options', async () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = new DevupQueryClient(api) + + const { result } = renderHook( + () => + queryClient.useInfiniteQuery( + 'get', + '/test' as any, + { + query: { page: 1 }, + }, + { + initialPageParam: 1, + getNextPageParam: () => undefined, + }, + ), + { wrapper: createWrapper() }, + ) + + await waitFor( + () => { + expect(result.current.isSuccess).toBe(true) + }, + { timeout: 5000 }, + ) + + expect(result.current.data).toEqual({ + pages: [{ id: 1, name: 'test' }], + pageParams: [1], + }) +}) + +test('DevupQueryClient useQuery with different HTTP methods', async () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = new DevupQueryClient(api) + + const methods = ['get', 'GET', 'post', 'POST'] as const + + for (const method of methods) { + const { result } = renderHook( + () => queryClient.useQuery(method as any, '/test' as any), + { wrapper: createWrapper() }, + ) + + await waitFor( + () => { + expect(result.current.isSuccess).toBe(true) + }, + { timeout: 5000 }, + ) + + expect(result.current.data).toEqual({ id: 1, name: 'test' }) + } +}) + +test('DevupQueryClient useMutation with different HTTP methods', () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = new DevupQueryClient(api) + + const methods = [ + 'get', + 'GET', + 'post', + 'POST', + 'put', + 'PUT', + 'patch', + 'PATCH', + 'delete', + 'DELETE', + ] as const + + for (const method of methods) { + const { result } = renderHook( + () => queryClient.useMutation(method as any, '/test' as any), + { wrapper: createWrapper() }, + ) + + expect(result.current.mutate).toBeDefined() + } +}) + +test('DevupQueryClient useSuspenseQuery with different HTTP methods', async () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = new DevupQueryClient(api) + + const methods = ['get', 'GET', 'post', 'POST'] as const + + for (const method of methods) { + const { result } = renderHook( + () => queryClient.useSuspenseQuery(method as any, '/test' as any), + { wrapper: createWrapper() }, + ) + + await waitFor( + () => { + expect(result.current.isSuccess).toBe(true) + }, + { timeout: 5000 }, + ) + + expect(result.current.data).toEqual({ id: 1, name: 'test' }) + } +}) + +test('DevupQueryClient useInfiniteQuery with different HTTP methods', async () => { + const api = createApi({ baseUrl: 'https://api.example.com' }) + const queryClient = new DevupQueryClient(api) + + const methods = ['get', 'GET', 'post', 'POST'] as const + + for (const method of methods) { + const { result } = renderHook( + () => + queryClient.useInfiniteQuery(method as any, '/test' as any, undefined, { + initialPageParam: 1, + getNextPageParam: () => undefined, + }), + { wrapper: createWrapper() }, + ) + + await waitFor( + () => { + expect(result.current.isSuccess).toBe(true) + }, + { timeout: 5000 }, + ) + + expect(result.current.data).toEqual({ + pages: [{ id: 1, name: 'test' }], + pageParams: [1], + }) + } +}) diff --git a/packages/react-query/src/query-client.ts b/packages/react-query/src/query-client.ts index b689b4b..44d5a24 100644 --- a/packages/react-query/src/query-client.ts +++ b/packages/react-query/src/query-client.ts @@ -28,7 +28,7 @@ import { useSuspenseQuery, } from '@tanstack/react-query' -function getQueryKey( +export function getQueryKey( method: M, path: P, options: OP,