Skip to content

Commit 38ad227

Browse files
committed
Merge remote-tracking branch 'origin/main' into earth/fix/extras-and-validator-problem
2 parents c14e996 + f1bc95b commit 38ad227

13 files changed

Lines changed: 399 additions & 52 deletions

File tree

.changeset/kind-rivers-obey.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@kivotos/react-query': minor
3+
---
4+
5+
[[DRIZZ-41] React Query](https://app.plane.so/softnetics/browse/DRIZZ-41/)

examples/erp/drizzlify/client.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { createKivotosQueryClient } from '@kivotos/react-query'
2+
import { createRestClient } from '@kivotos/rest'
3+
4+
import type { serverConfig } from './config'
5+
6+
export const restClient = createRestClient<typeof serverConfig>({
7+
baseUrl: '',
8+
})
9+
export const queryClient = createKivotosQueryClient(restClient)

examples/erp/drizzlify/collections/categories.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const categoriesCollection = builder.collection('categories', {
2424
}),
2525
})),
2626
options: builder.options(async ({ db }) => {
27-
const result = await db.query.users.findMany({ columns: { id: true, name: true } })
27+
const result = await db.query.user.findMany({ columns: { id: true, name: true } })
2828
return result.map((user) => ({ label: user.name ?? user.id, value: user.id }))
2929
}),
3030
})),

examples/erp/drizzlify/collections/posts.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const postsCollection = builder.collection('posts', {
1515
description: 'The content of the post',
1616
}),
1717
author: fb.relations('author', (fb) => ({
18-
type: 'connect',
18+
type: 'create',
1919
label: 'Author',
2020
description: 'The author of the post',
2121
fields: fb.fields('user', (fb) => ({

examples/erp/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
"dependencies": {
1717
"@kivotos/core": "workspace:^",
1818
"@kivotos/next": "workspace:^",
19+
"@kivotos/react-query": "workspace:^",
1920
"@kivotos/rest": "workspace:^",
2021
"@tailwindcss/postcss": "^4.0.14",
22+
"@tanstack/react-query": "^5.71.5",
2123
"drizzle-orm": "^0.41.0",
2224
"next": "15.2.2",
2325
"next-themes": "^0.4.6",
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1+
'use client'
2+
13
import { UIPlayground } from '@kivotos/next'
24

5+
import { queryClient } from '../../../../drizzlify/client'
6+
37
interface PlaygroundPageProps {
48
params: Promise<{ segments: string[] }>
59
searchParams: Promise<{ [key: string]: string | string[] }>
610
}
711

8-
export default async function Playground(_props: PlaygroundPageProps) {
12+
export default function Playground(_props: PlaygroundPageProps) {
13+
const result = queryClient.useQuery('GET', '/api/hello2', {})
14+
console.log(result.data)
915
return <UIPlayground />
1016
}

examples/erp/src/components/providers.tsx

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,50 @@
22

33
import type { PropsWithChildren } from 'react'
44

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

8+
function makeQueryClient() {
9+
return new QueryClient({
10+
defaultOptions: {
11+
queries: {
12+
// With SSR, we usually want to set some default staleTime
13+
// above 0 to avoid refetching immediately on the client
14+
staleTime: 60 * 1000,
15+
},
16+
},
17+
})
18+
}
19+
20+
let browserQueryClient: QueryClient | undefined = undefined
21+
22+
function getQueryClient() {
23+
if (isServer) {
24+
// Server: always make a new query client
25+
return makeQueryClient()
26+
} else {
27+
// Browser: make a new query client if we don't already have one
28+
// This is very important, so we don't re-make a new client if React
29+
// suspends during the initial render. This may not be needed if we
30+
// have a suspense boundary BELOW the creation of the query client
31+
if (!browserQueryClient) browserQueryClient = makeQueryClient()
32+
return browserQueryClient
33+
}
34+
}
35+
736
export function Providers(props: PropsWithChildren) {
37+
const queryClient = getQueryClient()
838
return (
9-
<NextThemesProvider
10-
attribute="class"
11-
defaultTheme="system"
12-
enableSystem
13-
disableTransitionOnChange
14-
enableColorScheme
15-
>
16-
{props.children}
17-
</NextThemesProvider>
39+
<QueryClientProvider client={queryClient}>
40+
<NextThemesProvider
41+
attribute="class"
42+
defaultTheme="system"
43+
enableSystem
44+
disableTransitionOnChange
45+
enableColorScheme
46+
>
47+
{props.children}
48+
</NextThemesProvider>
49+
</QueryClientProvider>
1850
)
1951
}

packages/core/src/utils.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@ import { is, Table } from 'drizzle-orm'
33
import type { IsNever, Simplify, ValueOf } from 'type-fest'
44
import type { ZodError, ZodObject, ZodOptional, ZodType } from 'zod'
55

6-
import type { MaybePromise } from './collection'
7-
import type { ApiHttpStatus, ApiRouteHandlerPayloadWithContext, ApiRouteSchema } from './endpoint'
6+
import type {
7+
ApiHttpStatus,
8+
ApiRouteHandler,
9+
ApiRouteHandlerPayloadWithContext,
10+
ApiRouteSchema,
11+
} from './endpoint'
812
import type { Field, FieldRelation, Fields, FieldsInitial, FieldsWithFieldName } from './field'
913

1014
export function isRelationField(field: Field): field is FieldRelation {
@@ -184,11 +188,11 @@ export function withValidator<
184188
TContext extends Record<string, unknown>,
185189
>(
186190
schema: TApiRouteSchema,
187-
handler: (
191+
handler: ApiRouteHandler<TContext, TApiRouteSchema>
192+
): ApiRouteHandler<TContext, TApiRouteSchema> {
193+
const wrappedHandler = async (
188194
payload: ApiRouteHandlerPayloadWithContext<TApiRouteSchema, TContext>
189-
) => MaybePromise<any>
190-
): (payload: ApiRouteHandlerPayloadWithContext<TApiRouteSchema, TContext>) => MaybePromise<any> {
191-
return async (payload: ApiRouteHandlerPayloadWithContext<TApiRouteSchema, TContext>) => {
195+
) => {
192196
const zodErrors = await validateRequestBody(schema, payload)
193197
if (zodErrors) {
194198
return {
@@ -202,7 +206,11 @@ export function withValidator<
202206

203207
const response = await handler(payload)
204208

205-
const validationError = validateResponseBody(schema, response.status, response.body)
209+
const validationError = validateResponseBody(
210+
schema,
211+
response.status as ApiHttpStatus,
212+
response.body
213+
)
206214

207215
if (validationError) {
208216
return {
@@ -216,6 +224,8 @@ export function withValidator<
216224

217225
return response
218226
}
227+
228+
return wrappedHandler as ApiRouteHandler<TContext, TApiRouteSchema>
219229
}
220230

221231
export type JoinArrays<T extends any[]> = Simplify<

packages/react-query/package.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "@kivotos/react-query",
3+
"private": true,
4+
"sideEffects": false,
5+
"description": "",
6+
"type": "module",
7+
"main": "./src/index.tsx",
8+
"module": "./src/index.tsx",
9+
"exports": {
10+
".": "./src/index.tsx"
11+
},
12+
"scripts": {
13+
"lint": "eslint .",
14+
"format": "prettier --write .",
15+
"format:check": "prettier --check .",
16+
"typecheck": "tsc --noEmit"
17+
},
18+
"keywords": [],
19+
"author": "",
20+
"license": "ISC",
21+
"devDependencies": {
22+
"@internals/project-config": "workspace:^",
23+
"@kivotos/core": "workspace:^",
24+
"@kivotos/rest": "workspace:^",
25+
"@types/node": "^22.15.23",
26+
"@types/react": "^19.1.6",
27+
"type-fest": "^4.41.0",
28+
"vitest": "^3.0.9"
29+
},
30+
"dependencies": {
31+
"@tanstack/react-query": "^5.71.5"
32+
}
33+
}

packages/react-query/src/index.tsx

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import type {
2+
DefaultError,
3+
UseMutationOptions,
4+
UseMutationResult,
5+
UseQueryOptions,
6+
UseQueryResult,
7+
} from '@tanstack/react-query'
8+
import { useMutation, useQuery } from '@tanstack/react-query'
9+
import type { ValueOf } from 'type-fest'
10+
11+
import type { ApiRouter } from '@kivotos/core'
12+
import {
13+
type FilterByMethod,
14+
type RestClient,
15+
type RestPayload,
16+
type RestResponse,
17+
} from '@kivotos/rest'
18+
19+
export interface KivotosQueryClient<TApiRouter extends ApiRouter> {
20+
useQuery: <
21+
const TMethod extends keyof RestClient<TApiRouter>,
22+
const TPath extends ValueOf<FilterByMethod<TApiRouter, TMethod>>['schema']['path'],
23+
const TPayload = RestPayload<TApiRouter, TPath>,
24+
const TResponse = RestResponse<TApiRouter, TPath>,
25+
const TError = DefaultError,
26+
>(
27+
method: TMethod,
28+
path: TPath,
29+
payload: TPayload,
30+
options?: Omit<UseQueryOptions<TResponse, TError, TPayload>, 'queryKey'>
31+
) => UseQueryResult<TResponse, TError>
32+
useMutation: <
33+
const TMethod extends keyof RestClient<TApiRouter>,
34+
const TPath extends ValueOf<FilterByMethod<TApiRouter, TMethod>>['schema']['path'],
35+
const TPayload = RestPayload<TApiRouter, TPath>,
36+
const TResponse = RestResponse<TApiRouter, TPath>,
37+
const TError = DefaultError,
38+
const TContext = unknown,
39+
>(
40+
method: TMethod,
41+
path: TPath,
42+
payload: TPayload,
43+
options?: UseMutationOptions<TResponse, TError, TPayload, TContext>
44+
) => UseMutationResult<TResponse, TError, TPayload, TContext>
45+
queryOptions: <
46+
const TMethod extends keyof RestClient<TApiRouter>,
47+
const TPath extends ValueOf<FilterByMethod<TApiRouter, TMethod>>['schema']['path'],
48+
const TPayload = RestPayload<TApiRouter, TPath>,
49+
const TResponse = RestResponse<TApiRouter, TPath>,
50+
const TError = DefaultError,
51+
>(
52+
method: TMethod,
53+
path: TPath,
54+
payload: TPayload,
55+
options?: Omit<UseQueryOptions<TResponse, TError, TPayload>, 'queryKey'>
56+
) => UseQueryOptions<TResponse, TError, TPayload>
57+
mutationOptions: <
58+
const TMethod extends keyof RestClient<TApiRouter>,
59+
const TPath extends ValueOf<FilterByMethod<TApiRouter, TMethod>>['schema']['path'],
60+
const TPayload = RestPayload<TApiRouter, TPath>,
61+
const TResponse = RestResponse<TApiRouter, TPath>,
62+
const TError = DefaultError,
63+
const TContext = unknown,
64+
>(
65+
method: TMethod,
66+
path: TPath,
67+
payload: TPayload,
68+
options?: UseMutationOptions<TResponse, TError, TPayload, TContext>
69+
) => UseMutationOptions<TResponse, TError, TPayload, TContext>
70+
}
71+
72+
export function createKivotosQueryClient<TApiRouter extends ApiRouter<any>>(
73+
restClient: RestClient<TApiRouter>
74+
): KivotosQueryClient<TApiRouter> {
75+
return {
76+
useQuery: function (method: string, path: string, payload: any, options?: any) {
77+
return useQuery({
78+
queryKey: queryKey(method, path, payload),
79+
queryFn: () => {
80+
return (restClient as any)[method](path, payload)
81+
},
82+
...options,
83+
}) as UseQueryResult<any, any>
84+
},
85+
useMutation: function (method: string, path: string, payload: any, options?: any) {
86+
return useMutation({
87+
mutationKey: queryKey(method, path, payload),
88+
mutationFn: (data) => {
89+
return (restClient as any)[method](path, data)
90+
},
91+
...options,
92+
}) as UseMutationResult<any, any, any, any>
93+
},
94+
queryOptions: function (method: string, path: string, payload: any, options?: any) {
95+
return {
96+
queryKey: queryKey(method, path, payload),
97+
queryFn: () => {
98+
return (restClient as any)[method](path, payload)
99+
},
100+
...options,
101+
} as UseQueryOptions<any, any, any>
102+
},
103+
mutationOptions: function (method: string, path: string, payload: any, options?: any) {
104+
return {
105+
mutationKey: queryKey(method, path, payload),
106+
mutationFn: (data) => {
107+
return (restClient as any)[method](path, data)
108+
},
109+
...options,
110+
} as UseMutationOptions<any, any, any, any>
111+
},
112+
}
113+
}
114+
115+
export function queryKey(method: string, path: string | number | symbol, payload: any) {
116+
const payloadKey = {
117+
pathParams: payload?.pathParams ?? {},
118+
query: payload?.query ?? {},
119+
headers: payload?.headers ?? {},
120+
}
121+
return [method, path, payloadKey] as const
122+
}

0 commit comments

Comments
 (0)