eden-tanstack-query 0.0.9 → 0.1.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,360 +1,119 @@
1
1
  # eden-tanstack-query
2
2
 
3
- A TanStack Query integration for [Eden Treaty](https://eden.ts), the type-safe client for [Elysia](https://elysiajs.com).
4
-
5
- ## Features
6
-
7
- - 🔗 **Type-safe** - Full TypeScript inference from Elysia server types
8
- - ⚡ **Framework-agnostic** - Works with React Query, Svelte Query, Solid Query, Vue Query
9
- - 🎯 **queryOptions** - Reusable, type-safe query configurations
10
- - 🔄 **mutationOptions** - Type-safe mutation configurations
11
- - 🔑 **Query keys** - Auto-generated, type-safe query keys for cache operations
12
- - ⚠️ **Error handling** - Configurable error throwing
13
- - 🛠️ **Eden integration** - Seamlessly integrates with existing Treaty clients
3
+ TanStack Query integration for [Elysia Eden](https://github.com/elysiajs/eden) - type-safe queries and mutations with zero boilerplate.
14
4
 
15
5
  ## Installation
16
6
 
17
7
  ```bash
18
- bun add eden-tanstack-query @tanstack/query-core
8
+ bun add eden-tanstack-query @elysiajs/eden @tanstack/query-core
9
+ # or
10
+ npm install eden-tanstack-query @elysiajs/eden @tanstack/query-core
19
11
  ```
20
12
 
21
- ## Basic Usage
22
-
23
- ### Setup
24
-
25
- ```typescript
26
- import { treaty } from '@elysiajs/eden/treaty2'
27
- import { createEdenQuery } from 'eden-tanstack-query'
28
- import { useQuery } from '@tanstack/svelte-query'
29
-
30
- // Your Elysia app type
31
- type App = {
32
- users: {
33
- get: {
34
- query: { page?: number }
35
- response: { users: User[] }
36
- }
37
- post: {
38
- body: { name: string, email: string }
39
- response: { user: User }
40
- }
41
- }
42
- }
13
+ ## Usage
43
14
 
44
- // Create Eden Query client
45
- const eden = createEdenQuery<App>('http://localhost:8080')
15
+ ```ts
16
+ import { createEdenTQ } from 'eden-tanstack-query'
17
+ import type { App } from './server' // Your Elysia app type
18
+
19
+ const eden = createEdenTQ<App>('http://localhost:3000')
46
20
  ```
47
21
 
48
22
  ### Queries
49
23
 
50
- ```typescript
51
- // Basic query (auto-generated query key)
52
- const query = useQuery(eden.users.get.queryOptions())
24
+ ```ts
25
+ import { createQuery } from '@tanstack/svelte-query' // or react-query, vue-query, etc.
53
26
 
54
- // Query with parameters
55
- const query = useQuery(
56
- eden.users.get.queryOptions({ query: { page: 1 } })
27
+ // Fully type-safe, auto-generated query key
28
+ const query = createQuery(() =>
29
+ eden.users({ id: '123' }).get.queryOptions({
30
+ params: { id: '123' }
31
+ })
57
32
  )
58
33
 
59
- // Access the data
60
- query.data?.users // Fully typed from your Elysia response
34
+ // query.data is typed as your Elysia response type!
61
35
  ```
62
36
 
63
37
  ### Mutations
64
38
 
65
- ```typescript
66
- import { useMutation } from '@tanstack/svelte-query'
67
-
68
- // Basic mutation
69
- const mutation = useMutation(eden.users.post.mutationOptions())
70
-
71
- // Using the mutation
72
- mutation.mutate({ name: 'John', email: 'john@example.com' })
73
-
74
- // Access the response
75
- mutation.data?.user // Fully typed
76
- ```
77
-
78
- ### Query Keys
79
-
80
- ```typescript
81
- // Get type-safe query keys for cache operations
82
- const usersKey = eden.users.get.queryKey()
83
-
84
- // Invalidate queries
85
- const queryClient = useQueryClient()
86
- queryClient.invalidateQueries({ queryKey: usersKey })
87
-
88
- // Get query data type-safely
89
- const data = queryClient.getQueryData(eden.users.get.queryKey())
90
- ```
91
-
92
- ## Error Handling
93
-
94
- ### Global Error Handler
95
-
96
- Define error handling logic once when creating the client:
39
+ ```ts
40
+ import { createMutation } from '@tanstack/svelte-query'
97
41
 
98
- ```typescript
99
- import type { EdenErrorContext } from 'eden-tanstack-query'
100
-
101
- const eden = createEdenQuery<App>('http://localhost:8080', {
102
- throwOnError: true,
103
- onError: ({ error, path, method, type }: EdenErrorContext) => {
104
- // Runs for ALL queries and mutations before throwing
105
-
106
- if (error.status === 401) {
107
- authStore.logout()
108
- router.push('/login')
109
- }
110
-
111
- if (error.status === 403) {
112
- toast.error('Not authorized')
113
- }
114
-
115
- if (error.status >= 500) {
116
- toast.error('Server error, please try again')
117
- logger.error('API Error', { path, method, error })
118
- }
119
- }
120
- })
121
- ```
122
-
123
- The `EdenErrorContext` provides:
124
- - `error` - The `EdenFetchError` with status and value
125
- - `queryKey` - The generated query key
126
- - `method` - HTTP method ('get', 'post', etc.)
127
- - `path` - API path segments (['users', 'posts'])
128
- - `input` - The request input
129
- - `type` - Either 'query' or 'mutation'
130
-
131
- ### Throw on Error (Default)
132
-
133
- ```typescript
134
- const eden = createEdenQuery<App>('http://localhost:8080', {
135
- throwOnError: true
136
- })
137
-
138
- // Per-query error handling (in addition to global handler)
139
- useQuery(eden.users.get.queryOptions(undefined, {
140
- onError: (error: EdenFetchError) => {
141
- // Runs after global handler, only for this query
142
- if (error.status === 404) {
143
- // Handle not found specifically for this query
144
- }
145
- }
146
- }))
147
- ```
42
+ const mutation = createMutation(() =>
43
+ eden.users.post.mutationOptions()
44
+ )
148
45
 
149
- ### Conditional Throwing
150
-
151
- ```typescript
152
- const eden = createEdenQuery<App>('http://localhost:8080', {
153
- throwOnError: (queryKey, status) => {
154
- // Don't throw on 404 (not found)
155
- if (status === 404) return false
156
- // Throw on server errors
157
- if (status >= 500) return true
158
- return false
159
- }
46
+ // Type-safe variables
47
+ mutation.mutate({
48
+ body: { name: 'Alice', email: 'alice@example.com' }
160
49
  })
161
50
  ```
162
51
 
163
- ### Type-Safe Data Narrowing
164
-
165
- When `throwOnError` is `true` (the default), errors are thrown before reaching callbacks like `select` and `onSuccess`. The library automatically narrows the data type to exclude error shapes:
166
-
167
- ```typescript
168
- // Given an endpoint that returns:
169
- // - Success: { users: User[] }
170
- // - Error: { error: string }
171
-
172
- // Default: throwOnError = true (or omitted)
173
- const eden = createEdenQuery<App>('http://localhost:8080')
174
- // OR explicitly:
175
- const eden = createEdenQuery<App>('http://localhost:8080', { throwOnError: true })
176
-
177
- useQuery(eden.users.get.queryOptions(undefined, {
178
- select: (data) => {
179
- // data: { users: User[] }
180
- // Error shape is excluded - errors throw before reaching here
181
- return data.users // No type guard needed
182
- },
183
- onSuccess: (data) => {
184
- // data: { users: User[] }
185
- console.log(data.users)
186
- }
187
- }))
188
-
189
- // query.data type: { users: User[] } | undefined
190
- ```
191
-
192
- When `throwOnError` is `false`, the full union type is preserved since errors don't throw:
52
+ ### Invalidation
193
53
 
194
- ```typescript
195
- // Explicit: throwOnError = false
196
- const eden = createEdenQuery<App>('http://localhost:8080', {
197
- throwOnError: false
198
- })
54
+ ```ts
55
+ import { useQueryClient } from '@tanstack/svelte-query'
199
56
 
200
- useQuery(eden.users.get.queryOptions(undefined, {
201
- select: (data) => {
202
- // data: { users: User[] } | { error: string }
203
- // Must handle both cases since errors don't throw
204
- if ('error' in data) return null
205
- return data.users
206
- },
207
- onSuccess: (data) => {
208
- // data: { users: User[] } | { error: string }
209
- if ('error' in data) {
210
- console.log('Error:', data.error)
211
- } else {
212
- console.log(data.users)
213
- }
214
- }
215
- }))
216
-
217
- // query.data type: { users: User[] } | { error: string } | undefined
218
- ```
219
-
220
- When `throwOnError` is a function, the full union type is also preserved (since throwing is conditional):
57
+ const queryClient = useQueryClient()
221
58
 
222
- ```typescript
223
- // Conditional throwing
224
- const eden = createEdenQuery<App>('http://localhost:8080', {
225
- throwOnError: (queryKey, status) => status >= 500
59
+ // Invalidate specific query
60
+ await eden.users({ id: '123' }).get.invalidate(queryClient, {
61
+ params: { id: '123' }
226
62
  })
227
63
 
228
- useQuery(eden.users.get.queryOptions(undefined, {
229
- select: (data) => {
230
- // data: { users: User[] } | { error: string }
231
- // Full union - some errors may not throw (e.g., 404)
232
- if ('error' in data) return null
233
- return data.users
234
- }
235
- }))
64
+ // Invalidate all queries for a route
65
+ await eden.users({ id: '123' }).get.invalidate(queryClient)
236
66
  ```
237
67
 
238
- ### Known Limitation: createQuery Inference
68
+ ## API
239
69
 
240
- The type narrowing works correctly at the library level (queryFn return type), but TanStack Query's `createQuery`/`useQuery` may not always infer the narrowed type due to complex generic inference.
70
+ ### `createEdenTQ<App>(domain, config?)`
241
71
 
242
- **What works:**
243
- - `NarrowedData` type correctly excludes error shapes
244
- - `queryFn` return type is narrowed at the library level
245
- - Query keys are correctly typed
246
- - Direct access to options properties
72
+ Creates a type-safe Eden client with TanStack Query helpers.
247
73
 
248
- **What may vary:**
249
- - `createQuery(options)` inference depends on framework version and TS config
250
- - `query.data` may show as `unknown` or full union in some cases
74
+ - `domain`: Your API URL or Elysia app instance
75
+ - `config.queryKeyPrefix`: Custom prefix for query keys (default: `['eden']`)
251
76
 
252
- **Workarounds:**
77
+ ### Method Helpers
253
78
 
254
- ```typescript
255
- // 1. Use select to transform with explicit types
256
- useQuery(eden.users.get.queryOptions(undefined, {
257
- select: (data) => data.users // data is narrowed here
258
- }))
79
+ Each HTTP method (`get`, `post`, `put`, `delete`, `patch`) has:
259
80
 
260
- // 2. Add explicit type annotation
261
- const query = useQuery(eden.users.get.queryOptions()) as CreateQueryResult<{ users: User[] }>
81
+ - `.queryOptions(input, overrides?)` - Returns `{ queryKey, queryFn }`
82
+ - `.mutationOptions(overrides?)` - Returns `{ mutationKey, mutationFn }`
83
+ - `.queryKey(input?)` - Returns the query key
84
+ - `.mutationKey(input?)` - Returns the mutation key
85
+ - `.invalidate(queryClient, input?, exact?)` - Invalidates matching queries
262
86
 
263
- // 3. Access queryFn directly for fully typed results
264
- const options = eden.users.get.queryOptions()
265
- const data = await options.queryFn() // Correctly typed
266
- ```
87
+ ## Before / After
267
88
 
268
- ## Advanced Usage
89
+ ### Before (manual boilerplate)
269
90
 
270
- ### Custom Eden Treaty Options
271
-
272
- ```typescript
273
- const eden = createEdenQuery<App>('http://localhost:8080', {
274
- treaty: {
275
- headers: { authorization: 'Bearer token' },
276
- fetch: customFetch
277
- }
278
- })
279
-
280
- useQuery(eden.users.get.queryOptions(
281
- { query: { page: 1 } },
282
- {
283
- eden: {
284
- headers: { 'X-Custom': 'value' }
91
+ ```ts
92
+ export function createUserQuery(userId: string) {
93
+ return createQuery<User>(() => ({
94
+ queryKey: ['users', userId],
95
+ queryFn: async () => {
96
+ const { data, error } = await api.users({ id: userId }).get()
97
+ if (error) throw error
98
+ return data as User // Manual cast!
285
99
  },
286
- staleTime: 5000
287
- }
288
- ))
289
- ```
290
-
291
- ### Query Key Prefix
292
-
293
- ```typescript
294
- const eden = createEdenQuery<App>('http://localhost:8080', {
295
- queryKeyPrefix: 'my-api'
296
- })
297
-
298
- // Keys will be prefixed: ['my-api', 'users', 'get']
299
- ```
300
-
301
- ### Using with React Query
302
-
303
- ```typescript
304
- import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
305
-
306
- const query = useQuery(eden.users.get.queryOptions())
307
- const mutation = useMutation(eden.users.post.mutationOptions())
308
- const queryClient = useQueryClient()
309
- ```
310
-
311
- ### Using with Solid Query
312
-
313
- ```typescript
314
- import { createQuery, createMutation } from '@tanstack/solid-query'
315
-
316
- const query = createQuery(() => eden.users.get.queryOptions())
317
- const mutation = createMutation(() => eden.users.post.mutationOptions())
318
- ```
319
-
320
- ### Using with Vue Query
321
-
322
- ```typescript
323
- import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query'
324
-
325
- const query = useQuery(eden.users.get.queryOptions())
326
- const mutation = useMutation(eden.users.post.mutationOptions())
327
- const queryClient = useQueryClient()
100
+ }))
101
+ }
328
102
  ```
329
103
 
330
- ## Query Key Structure
331
-
332
- Query keys are auto-generated from your API paths:
333
-
334
- ```typescript
335
- // Simple path
336
- eden.users.get.queryKey()
337
- // → ['users', 'get']
338
-
339
- // Path with parameters
340
- eden.users({ id: '123' }).get.queryKey()
341
- // → ['users', { id: '123' }, 'get']
104
+ ### After (with eden-tanstack-query)
342
105
 
343
- // Nested paths
344
- eden.users.posts({ userId: '123' }).get.queryKey()
345
- // ['users', 'posts', { userId: '123' }, 'get']
106
+ ```ts
107
+ export function createUserQuery(userId: string) {
108
+ return createQuery(() =>
109
+ eden.users({ id: userId }).get.queryOptions({
110
+ params: { id: userId }
111
+ })
112
+ )
113
+ }
114
+ // Types are inferred from your Elysia server!
346
115
  ```
347
116
 
348
- ## Type Safety
349
-
350
- All types are fully inferred from your Elysia server:
351
-
352
- - ✅ Query data type (from success responses)
353
- - ✅ Error type (from EdenFetchError or Treaty response)
354
- - ✅ Input validation (query params, body)
355
- - ✅ Query keys (type-safe, auto-generated)
356
- - ✅ Framework-agnostic (works with all TanStack Query variants)
357
-
358
117
  ## License
359
118
 
360
119
  MIT
@@ -0,0 +1,84 @@
1
+ import { Elysia, ELYSIA_FORM_DATA } from 'elysia';
2
+ import { QueryKey, QueryClient } from '@tanstack/query-core';
3
+ export { QueryClient, QueryKey } from '@tanstack/query-core';
4
+ import { Treaty } from '@elysiajs/eden/treaty2';
5
+
6
+ type IsNever<T> = [T] extends [never] ? true : false;
7
+ type Prettify<T> = {
8
+ [K in keyof T]: T[K];
9
+ } & {};
10
+ type Enumerate<N extends number, Acc extends number[] = []> = Acc['length'] extends N ? Acc[number] : Enumerate<N, [...Acc, Acc['length']]>;
11
+ type IntegerRange<F extends number, T extends number> = Exclude<Enumerate<T>, Enumerate<F>>;
12
+ type SuccessCodeRange = IntegerRange<200, 300>;
13
+ type ExtractData<Res extends Record<number, unknown>> = Res[Extract<keyof Res, SuccessCodeRange>] extends {
14
+ [ELYSIA_FORM_DATA]: infer Data;
15
+ } ? Data : Res[Extract<keyof Res, SuccessCodeRange>];
16
+ type ExtractError<Res extends Record<number, unknown>> = Exclude<keyof Res, SuccessCodeRange> extends never ? {
17
+ status: unknown;
18
+ value: unknown;
19
+ } : {
20
+ [Status in keyof Res]: {
21
+ status: Status;
22
+ value: Res[Status] extends {
23
+ [ELYSIA_FORM_DATA]: infer Data;
24
+ } ? Data : Res[Status];
25
+ };
26
+ }[Exclude<keyof Res, SuccessCodeRange>];
27
+ interface TQParamBase {
28
+ fetch?: RequestInit;
29
+ }
30
+ type SerializeQueryParams<T> = T extends Record<string, any> ? {
31
+ [K in keyof T]: T[K] extends Date ? string : T[K] extends Date | undefined ? string | undefined : T[K];
32
+ } : T;
33
+ type IsEmptyObject<T> = T extends Record<string, never> ? [keyof T] extends [never] ? true : false : false;
34
+ type MaybeEmptyObject<T, K extends PropertyKey> = [T] extends [never] ? {} : [T] extends [undefined] ? {
35
+ [P in K]?: T;
36
+ } : IsEmptyObject<T> extends true ? {
37
+ [P in K]?: T;
38
+ } : undefined extends T ? {
39
+ [P in K]?: T;
40
+ } : {
41
+ [P in K]: T;
42
+ };
43
+ type TQMethodParam<Body, Headers, Query, Params> = MaybeEmptyObject<Headers, 'headers'> & MaybeEmptyObject<SerializeQueryParams<Query>, 'query'> & MaybeEmptyObject<Params, 'params'> & MaybeEmptyObject<Body, 'body'> & TQParamBase;
44
+ interface EdenQueryOptions<TData = unknown, TError = unknown> {
45
+ queryKey: QueryKey;
46
+ queryFn: () => Promise<TData>;
47
+ }
48
+ interface EdenMutationOptions<TData = unknown, TError = unknown, TVariables = unknown> {
49
+ mutationKey: QueryKey;
50
+ mutationFn: (variables: TVariables) => Promise<TData>;
51
+ }
52
+ interface EdenTQMethod<Body, Headers, Query, Params, Res extends Record<number, unknown>> {
53
+ <TQueryFnData = ExtractData<Res>>(input: TQMethodParam<Body, Headers, Query, Params>, options?: RequestInit): Promise<Treaty.TreatyResponse<Res>>;
54
+ queryKey(input?: TQMethodParam<Body, Headers, Query, Params>): QueryKey;
55
+ queryOptions<TData = ExtractData<Res>>(input: TQMethodParam<Body, Headers, Query, Params>, overrides?: Partial<EdenQueryOptions<TData, ExtractError<Res>>>): EdenQueryOptions<TData, ExtractError<Res>>;
56
+ mutationKey(input?: TQMethodParam<Body, Headers, Query, Params>): QueryKey;
57
+ mutationOptions<TData = ExtractData<Res>>(overrides?: Partial<EdenMutationOptions<TData, ExtractError<Res>, TQMethodParam<Body, Headers, Query, Params>>>): EdenMutationOptions<TData, ExtractError<Res>, TQMethodParam<Body, Headers, Query, Params>>;
58
+ invalidate(queryClient: QueryClient, input?: TQMethodParam<Body, Headers, Query, Params>, exact?: boolean): Promise<void>;
59
+ }
60
+ declare namespace EdenTQ {
61
+ export type Config = Treaty.Config & {
62
+ queryKeyPrefix?: QueryKey;
63
+ };
64
+ export type Create<App extends Elysia<any, any, any, any, any, any, any>> = App extends {
65
+ '~Routes': infer Schema extends Record<any, any>;
66
+ } ? Prettify<Sign<Schema>> & CreateParams<Schema> : 'Please install Elysia before using Eden';
67
+ export type Sign<in out Route extends Record<any, any>> = {
68
+ [K in keyof Route as K extends `:${string}` ? never : K]: Route[K] extends {
69
+ body: infer Body;
70
+ headers: infer Headers;
71
+ params: infer Params;
72
+ query: infer Query;
73
+ response: infer Res extends Record<number, unknown>;
74
+ } ? EdenTQMethod<Body, Headers, Query, Params, Res> : CreateParams<Route[K]>;
75
+ };
76
+ type CreateParams<Route extends Record<string, any>> = Extract<keyof Route, `:${string}`> extends infer Path extends string ? IsNever<Path> extends true ? Prettify<Sign<Route>> : (((params: {
77
+ [param in Path extends `:${infer Param}` ? Param extends `${infer P}?` ? P : Param : never]: string | number;
78
+ }) => Prettify<Sign<Route[Path]>> & CreateParams<Route[Path]>) & Prettify<Sign<Route>>) & (Path extends `:${string}?` ? CreateParams<Route[Path]> : {}) : never;
79
+ export { };
80
+ }
81
+
82
+ declare function createEdenTQ<const App extends Elysia<any, any, any, any, any, any, any>>(domain: string | App, config?: EdenTQ.Config): EdenTQ.Create<App>;
83
+
84
+ export { type EdenMutationOptions, type EdenQueryOptions, EdenTQ, createEdenTQ };