Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/kind-rivers-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@kivotos/react-query': minor
---

[[DRIZZ-41] React Query](https://app.plane.so/softnetics/browse/DRIZZ-41/)
9 changes: 9 additions & 0 deletions examples/erp/drizzlify/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createKivotosQueryClient } from '@kivotos/react-query'
import { createRestClient } from '@kivotos/rest'

import type { serverConfig } from './config'

export const restClient = createRestClient<typeof serverConfig>({
baseUrl: '',
})
export const queryClient = createKivotosQueryClient(restClient)
2 changes: 2 additions & 0 deletions examples/erp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
"dependencies": {
"@kivotos/core": "workspace:^",
"@kivotos/next": "workspace:^",
"@kivotos/react-query": "workspace:^",
"@kivotos/rest": "workspace:^",
"@tailwindcss/postcss": "^4.0.14",
"@tanstack/react-query": "^5.71.5",
"drizzle-orm": "^0.41.0",
"next": "15.2.2",
"next-themes": "^0.4.6",
Expand Down
8 changes: 7 additions & 1 deletion examples/erp/src/app/(admin)/playground/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
'use client'

import { UIPlayground } from '@kivotos/next'

import { queryClient } from '../../../../drizzlify/client'

interface PlaygroundPageProps {
params: Promise<{ segments: string[] }>
searchParams: Promise<{ [key: string]: string | string[] }>
}

export default async function Playground(props: PlaygroundPageProps) {
export default function Playground(props: PlaygroundPageProps) {
const result = queryClient.useQuery('GET', '/api/hello2', {})
console.log(result.data)
return <UIPlayground />
}
50 changes: 41 additions & 9 deletions examples/erp/src/components/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,50 @@

import type { PropsWithChildren } from 'react'

import { isServer, QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ThemeProvider as NextThemesProvider } from 'next-themes'

function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
},
})
}

let browserQueryClient: QueryClient | undefined = undefined

function getQueryClient() {
if (isServer) {
// Server: always make a new query client
return makeQueryClient()
} else {
// Browser: make a new query client if we don't already have one
// This is very important, so we don't re-make a new client if React
// suspends during the initial render. This may not be needed if we
// have a suspense boundary BELOW the creation of the query client
if (!browserQueryClient) browserQueryClient = makeQueryClient()
return browserQueryClient
}
}

export function Providers(props: PropsWithChildren) {
const queryClient = getQueryClient()
return (
<NextThemesProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
enableColorScheme
>
{props.children}
</NextThemesProvider>
<QueryClientProvider client={queryClient}>
<NextThemesProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
enableColorScheme
>
{props.children}
</NextThemesProvider>
</QueryClientProvider>
)
}
33 changes: 33 additions & 0 deletions packages/react-query/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "@kivotos/react-query",
"private": true,
"sideEffects": false,
"description": "",
"type": "module",
"main": "./src/index.tsx",
"module": "./src/index.tsx",
"exports": {
".": "./src/index.tsx"
},
"scripts": {
"lint": "eslint .",
"format": "prettier --write .",
"format:check": "prettier --check .",
"typecheck": "tsc --noEmit"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@internals/project-config": "workspace:^",
"@kivotos/core": "workspace:^",
"@kivotos/rest": "workspace:^",
"@types/node": "^22.15.23",
"@types/react": "^19.1.6",
"type-fest": "^4.41.0",
"vitest": "^3.0.9"
},
"dependencies": {
"@tanstack/react-query": "^5.71.5"
}
}
122 changes: 122 additions & 0 deletions packages/react-query/src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import type {
DefaultError,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from '@tanstack/react-query'
import { useMutation, useQuery } from '@tanstack/react-query'
import type { ValueOf } from 'type-fest'

import type { ApiRouter } from '@kivotos/core'
import {
type FilterByMethod,
type RestClient,
type RestPayload,
type RestResponse,
} from '@kivotos/rest'

export interface KivotosQueryClient<TApiRouter extends ApiRouter> {
useQuery: <
const TMethod extends keyof RestClient<TApiRouter>,
const TPath extends ValueOf<FilterByMethod<TApiRouter, TMethod>>['schema']['path'],
const TPayload = RestPayload<TApiRouter, TPath>,
const TResponse = RestResponse<TApiRouter, TPath>,
const TError = DefaultError,
>(
method: TMethod,
path: TPath,
payload: TPayload,
options?: Omit<UseQueryOptions<TResponse, TError, TPayload>, 'queryKey'>
) => UseQueryResult<TResponse, TError>
useMutation: <
const TMethod extends keyof RestClient<TApiRouter>,
const TPath extends ValueOf<FilterByMethod<TApiRouter, TMethod>>['schema']['path'],
const TPayload = RestPayload<TApiRouter, TPath>,
const TResponse = RestResponse<TApiRouter, TPath>,
const TError = DefaultError,
const TContext = unknown,
>(
method: TMethod,
path: TPath,
payload: TPayload,
options?: UseMutationOptions<TResponse, TError, TPayload, TContext>
) => UseMutationResult<TResponse, TError, TPayload, TContext>
queryOptions: <
const TMethod extends keyof RestClient<TApiRouter>,
const TPath extends ValueOf<FilterByMethod<TApiRouter, TMethod>>['schema']['path'],
const TPayload = RestPayload<TApiRouter, TPath>,
const TResponse = RestResponse<TApiRouter, TPath>,
const TError = DefaultError,
>(
method: TMethod,
path: TPath,
payload: TPayload,
options?: Omit<UseQueryOptions<TResponse, TError, TPayload>, 'queryKey'>
) => UseQueryOptions<TResponse, TError, TPayload>
mutationOptions: <
const TMethod extends keyof RestClient<TApiRouter>,
const TPath extends ValueOf<FilterByMethod<TApiRouter, TMethod>>['schema']['path'],
const TPayload = RestPayload<TApiRouter, TPath>,
const TResponse = RestResponse<TApiRouter, TPath>,
const TError = DefaultError,
const TContext = unknown,
>(
method: TMethod,
path: TPath,
payload: TPayload,
options?: UseMutationOptions<TResponse, TError, TPayload, TContext>
) => UseMutationOptions<TResponse, TError, TPayload, TContext>
}

export function createKivotosQueryClient<TApiRouter extends ApiRouter<any>>(
restClient: RestClient<TApiRouter>
): KivotosQueryClient<TApiRouter> {
return {
useQuery: function (method: string, path: string, payload: any, options?: any) {
return useQuery({
queryKey: queryKey(method, path, payload),
queryFn: () => {
return (restClient as any)[method](path, payload)
},
...options,
}) as UseQueryResult<any, any>
},
useMutation: function (method: string, path: string, payload: any, options?: any) {
return useMutation({
mutationKey: queryKey(method, path, payload),
mutationFn: (data) => {
return (restClient as any)[method](path, data)
},
...options,
}) as UseMutationResult<any, any, any, any>
},
queryOptions: function (method: string, path: string, payload: any, options?: any) {
return {
queryKey: queryKey(method, path, payload),
queryFn: () => {
return (restClient as any)[method](path, payload)
},
...options,
} as UseQueryOptions<any, any, any>
},
mutationOptions: function (method: string, path: string, payload: any, options?: any) {
return {
mutationKey: queryKey(method, path, payload),
mutationFn: (data) => {
return (restClient as any)[method](path, data)
},
...options,
} as UseMutationOptions<any, any, any, any>
},
}
}

export function queryKey(method: string, path: string | number | symbol, payload: any) {
const payloadKey = {
pathParams: payload?.pathParams ?? {},
query: payload?.query ?? {},
headers: payload?.headers ?? {},
}
return [method, path, payloadKey] as const
}
6 changes: 6 additions & 0 deletions packages/react-query/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "@internals/project-config/tsconfig/base.json",
"compilerOptions": {
"baseUrl": "."
}
}
12 changes: 6 additions & 6 deletions packages/rest/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { ApiRouteHandlerPayload, ApiRouteSchema } from '@kivotos/core'

import { withPathParams, withQueryParams } from './utils'

interface CreateRestClientConfig {
export interface CreateRestClientConfig {
baseUrl: string
}

Expand Down Expand Up @@ -54,38 +54,38 @@ export function createRestClient<TServerConfig extends ServerConfig<any, any, an
}
}

type ExtractClientApiRouterPath<TApiRouter extends ApiRouter<any>> = ValueOf<{
export type ExtractClientApiRouterPath<TApiRouter extends ApiRouter<any>> = ValueOf<{
[TKey in keyof TApiRouter]: TApiRouter[TKey]['schema'] extends {
path: infer TPath extends string
}
? TPath
: never
}>

type RestResponse<TApiRouter extends ApiRouter<any>, TPath extends string> = ValueOf<{
export type RestResponse<TApiRouter extends ApiRouter<any>, TPath extends string> = ValueOf<{
[TKey in keyof TApiRouter]: TApiRouter[TKey]['schema'] extends { path: TPath }
? TApiRouter[TKey]['schema'] extends infer TApiRouteSchema extends ApiRouteSchema
? ApiRouteResponse<TApiRouteSchema['responses']>
: never
: never
}>

type RestPayload<TApiRouter extends ApiRouter<any>, TPath extends string> = ValueOf<{
export type RestPayload<TApiRouter extends ApiRouter<any>, TPath extends string> = ValueOf<{
[TKey in keyof TApiRouter]: TApiRouter[TKey]['schema'] extends { path: TPath }
? TApiRouter[TKey]['schema'] extends infer TApiRouteSchema extends ApiRouteSchema
? ApiRouteHandlerPayload<TApiRouteSchema>
: never
: never
}>

type RestMethod<TApiRouter extends ApiRouter<any>> = <
export type RestMethod<TApiRouter extends ApiRouter<any>> = <
TPath extends ExtractClientApiRouterPath<TApiRouter>,
>(
path: TPath,
payload: RestPayload<TApiRouter, TPath>
) => Promise<RestResponse<TApiRouter, TPath>>

type FilterByMethod<TApiRouter extends ApiRouter<any>, TMethod extends string> = {
export type FilterByMethod<TApiRouter extends ApiRouter<any>, TMethod extends string> = {
[TKey in keyof TApiRouter as TApiRouter[TKey]['schema'] extends { method: TMethod }
? TKey
: never]: TApiRouter[TKey]
Expand Down
Loading