eden-tanstack-query 0.0.1

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 ADDED
@@ -0,0 +1,218 @@
1
+ # @elysiajs/eden-tanstack-query
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
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ bun add @elysiajs/eden-tanstack-query @tanstack/query-core
19
+ ```
20
+
21
+ ## Basic Usage
22
+
23
+ ### Setup
24
+
25
+ ```typescript
26
+ import { treaty } from '@elysiajs/eden/treaty2'
27
+ import { createEdenQuery } from '@elysiajs/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
+ }
43
+
44
+ // Create Eden Query client
45
+ const eden = createEdenQuery<App>('http://localhost:8080')
46
+ ```
47
+
48
+ ### Queries
49
+
50
+ ```typescript
51
+ // Basic query (auto-generated query key)
52
+ const query = useQuery(eden.users.get.queryOptions())
53
+
54
+ // Query with parameters
55
+ const query = useQuery(
56
+ eden.users.get.queryOptions({ query: { page: 1 } })
57
+ )
58
+
59
+ // Access the data
60
+ query.data?.users // Fully typed from your Elysia response
61
+ ```
62
+
63
+ ### Mutations
64
+
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
+ ### Throw on Error (Default for most apps)
95
+
96
+ ```typescript
97
+ const eden = createEdenQuery<App>('http://localhost:8080', {
98
+ throwOnError: true
99
+ })
100
+
101
+ useQuery(eden.users.get.queryOptions(undefined, {
102
+ onError: (error: EdenFetchError) => {
103
+ // error.status and error.value are available
104
+ // extra context is attached: error.queryKey, error.method, error.path, error.input
105
+ if (error.status === 401) {
106
+ router.push('/login')
107
+ }
108
+ }
109
+ }))
110
+ ```
111
+
112
+ ### Conditional Throwing
113
+
114
+ ```typescript
115
+ const eden = createEdenQuery<App>('http://localhost:8080', {
116
+ throwOnError: (queryKey, status) => {
117
+ // Don't throw on 404 (not found)
118
+ if (status === 404) return false
119
+ // Throw on server errors
120
+ if (status >= 500) return true
121
+ return false
122
+ }
123
+ })
124
+ ```
125
+
126
+ ## Advanced Usage
127
+
128
+ ### Custom Eden Treaty Options
129
+
130
+ ```typescript
131
+ const eden = createEdenQuery<App>('http://localhost:8080', {
132
+ treaty: {
133
+ headers: { authorization: 'Bearer token' },
134
+ fetch: customFetch
135
+ }
136
+ })
137
+
138
+ useQuery(eden.users.get.queryOptions(
139
+ { query: { page: 1 } },
140
+ {
141
+ eden: {
142
+ headers: { 'X-Custom': 'value' }
143
+ },
144
+ staleTime: 5000
145
+ }
146
+ ))
147
+ ```
148
+
149
+ ### Query Key Prefix
150
+
151
+ ```typescript
152
+ const eden = createEdenQuery<App>('http://localhost:8080', {
153
+ queryKeyPrefix: 'my-api'
154
+ })
155
+
156
+ // Keys will be prefixed: ['my-api', 'users', 'get']
157
+ ```
158
+
159
+ ### Using with React Query
160
+
161
+ ```typescript
162
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
163
+
164
+ const query = useQuery(eden.users.get.queryOptions())
165
+ const mutation = useMutation(eden.users.post.mutationOptions())
166
+ const queryClient = useQueryClient()
167
+ ```
168
+
169
+ ### Using with Solid Query
170
+
171
+ ```typescript
172
+ import { createQuery, createMutation } from '@tanstack/solid-query'
173
+
174
+ const query = createQuery(() => eden.users.get.queryOptions())
175
+ const mutation = createMutation(() => eden.users.post.mutationOptions())
176
+ ```
177
+
178
+ ### Using with Vue Query
179
+
180
+ ```typescript
181
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query'
182
+
183
+ const query = useQuery(eden.users.get.queryOptions())
184
+ const mutation = useMutation(eden.users.post.mutationOptions())
185
+ const queryClient = useQueryClient()
186
+ ```
187
+
188
+ ## Query Key Structure
189
+
190
+ Query keys are auto-generated from your API paths:
191
+
192
+ ```typescript
193
+ // Simple path
194
+ eden.users.get.queryKey()
195
+ // → ['users', 'get']
196
+
197
+ // Path with parameters
198
+ eden.users({ id: '123' }).get.queryKey()
199
+ // → ['users', { id: '123' }, 'get']
200
+
201
+ // Nested paths
202
+ eden.users.posts({ userId: '123' }).get.queryKey()
203
+ // → ['users', 'posts', { userId: '123' }, 'get']
204
+ ```
205
+
206
+ ## Type Safety
207
+
208
+ All types are fully inferred from your Elysia server:
209
+
210
+ - ✅ Query data type (from success responses)
211
+ - ✅ Error type (from EdenFetchError or Treaty response)
212
+ - ✅ Input validation (query params, body)
213
+ - ✅ Query keys (type-safe, auto-generated)
214
+ - ✅ Framework-agnostic (works with all TanStack Query variants)
215
+
216
+ ## License
217
+
218
+ MIT
@@ -0,0 +1,59 @@
1
+ import { QueryFilters, QueryKey, QueryObserverOptions, QueryFunctionContext, MutationOptions, InfiniteQueryObserverOptions } from '@tanstack/query-core';
2
+ import { EdenFetchError } from '@elysiajs/eden';
3
+ import { Elysia } from 'elysia';
4
+ import { Treaty } from '@elysiajs/eden/treaty2';
5
+ export { Treaty, treaty } from '@elysiajs/eden/treaty2';
6
+
7
+ type HTTPMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head' | 'options';
8
+ interface EdenThrowOnErrorContext {
9
+ queryKey: readonly unknown[];
10
+ status: number;
11
+ method: HTTPMethod;
12
+ path: string[];
13
+ input: unknown;
14
+ }
15
+ type EdenThrowOnError = ((queryKey: readonly unknown[], status: number) => boolean) | ((context: EdenThrowOnErrorContext) => boolean);
16
+ interface EdenQueryConfig {
17
+ throwOnError?: boolean | EdenThrowOnError;
18
+ queryKeyPrefix?: string | string[];
19
+ }
20
+ interface EdenOptions<TEden = unknown> {
21
+ eden?: TEden;
22
+ }
23
+ type EdenQueryKey<TInput = unknown, TMethod extends HTTPMethod = HTTPMethod> = readonly [...string[], TInput, TMethod];
24
+ type EdenQueryOptions<TQueryFnData, TError = Error, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TEden = unknown> = Omit<QueryObserverOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey>, 'queryKey' | 'queryFn'> & EdenOptions<TEden>;
25
+ type EdenMutationOptions<TData, TError = Error, TVariables = void, TContext = unknown, TEden = unknown> = Omit<MutationOptions<TData, TError, TVariables, TContext>, 'mutationFn'> & EdenOptions<TEden>;
26
+ type EdenInfiniteQueryOptions<TQueryFnData, TError = Error, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = unknown, TEden = unknown> = Omit<InfiniteQueryObserverOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>, 'queryKey' | 'queryFn'> & EdenOptions<TEden>;
27
+ type EdenPartial<T> = T extends object ? Partial<T> : never;
28
+ type AwaitedReturn<T> = T extends (...args: any[]) => infer R ? Awaited<R> : never;
29
+ type FirstArg<T> = T extends (arg1: infer A, ...args: any[]) => any ? A : never;
30
+ type SecondArg<T> = T extends (arg1: any, arg2: infer B, ...args: any[]) => any ? B : never;
31
+ type EdenData<T> = Extract<AwaitedReturn<T>, {
32
+ error: null;
33
+ }> extends {
34
+ data: infer D;
35
+ } ? D : never;
36
+ type EdenQueryMethod<TMethod extends Extract<HTTPMethod, 'get' | 'head'>, TCall extends (...args: any[]) => Promise<any>> = TCall & {
37
+ queryKey: (input?: FirstArg<TCall>) => EdenQueryKey<FirstArg<TCall>, TMethod>;
38
+ queryFilter: (input?: FirstArg<TCall>) => QueryFilters<EdenQueryKey<FirstArg<TCall>, TMethod>>;
39
+ queryOptions: (input?: FirstArg<TCall>, options?: EdenQueryOptions<EdenData<TCall>, EdenFetchError | Error, EdenData<TCall>, EdenQueryKey<FirstArg<TCall>, TMethod>, EdenPartial<NonNullable<FirstArg<TCall>>>>) => EdenQueryOptions<EdenData<TCall>, EdenFetchError | Error, EdenData<TCall>, EdenQueryKey<FirstArg<TCall>, TMethod>, EdenPartial<NonNullable<FirstArg<TCall>>>> & {
40
+ queryKey: EdenQueryKey<FirstArg<TCall>, TMethod>;
41
+ queryFn: (context: QueryFunctionContext<EdenQueryKey<FirstArg<TCall>, TMethod>>) => Promise<EdenData<TCall>>;
42
+ };
43
+ };
44
+ type EdenMutationMethod<TMethod extends Extract<HTTPMethod, 'post' | 'put' | 'patch' | 'delete'>, TCall extends (...args: any[]) => Promise<any>> = TCall & {
45
+ mutationOptions: (options?: EdenMutationOptions<EdenData<TCall>, EdenFetchError | Error, FirstArg<TCall>, unknown, EdenPartial<NonNullable<SecondArg<TCall>>>>) => EdenMutationOptions<EdenData<TCall>, EdenFetchError | Error, FirstArg<TCall>, unknown, EdenPartial<NonNullable<SecondArg<TCall>>>> & {
46
+ mutationKey: EdenQueryKey<undefined, TMethod>;
47
+ mutationFn: (variables: FirstArg<TCall>) => Promise<EdenData<TCall>>;
48
+ };
49
+ };
50
+ type EdenQueryify<T> = (T extends (...args: infer A) => infer R ? (...args: A) => EdenQueryify<R> : unknown) & (T extends object ? {
51
+ [K in keyof T]: EdenQueryifyValue<K, T[K]>;
52
+ } : unknown);
53
+ type EdenQueryifyValue<K, V> = K extends 'get' | 'head' ? V extends (...args: any[]) => Promise<any> ? EdenQueryMethod<Extract<K, 'get' | 'head'>, V> : V : K extends 'post' | 'put' | 'patch' | 'delete' ? V extends (...args: any[]) => Promise<any> ? EdenMutationMethod<Extract<K, 'post' | 'put' | 'patch' | 'delete'>, V> : V : EdenQueryify<V>;
54
+
55
+ declare function createEdenQuery<App extends Elysia<any, any, any, any, any, any, any>>(domain: string, options?: EdenQueryConfig & {
56
+ treaty?: Treaty.Config;
57
+ }): EdenQueryify<Treaty.Create<App>>;
58
+
59
+ export { type EdenInfiniteQueryOptions, type EdenMutationMethod, type EdenMutationOptions, type EdenOptions, type EdenQueryConfig, type EdenQueryKey, type EdenQueryMethod, type EdenQueryOptions, type EdenQueryify, type EdenThrowOnError, type EdenThrowOnErrorContext, type HTTPMethod, createEdenQuery };
package/dist/index.js ADDED
@@ -0,0 +1,196 @@
1
+ // src/index.ts
2
+ import { treaty } from "@elysiajs/eden/treaty2";
3
+
4
+ // src/query-options.ts
5
+ import { EdenFetchError } from "@elysiajs/eden";
6
+
7
+ // src/query-key.ts
8
+ function buildQueryKey(paths, input, method, prefix) {
9
+ if (prefix) {
10
+ return typeof prefix === "string" ? [prefix, ...paths, input, method] : [...prefix, ...paths, input, method];
11
+ }
12
+ return [...paths, input, method];
13
+ }
14
+
15
+ // src/query-options.ts
16
+ function shouldThrowOnError(config, context) {
17
+ if (typeof config.throwOnError === "boolean") return config.throwOnError;
18
+ if (typeof config.throwOnError === "function") {
19
+ const fn = config.throwOnError;
20
+ return fn.length >= 2 ? fn(
21
+ context.queryKey,
22
+ context.status
23
+ ) : fn(context);
24
+ }
25
+ return true;
26
+ }
27
+ function toEdenFetchError(error, status) {
28
+ if (error instanceof EdenFetchError) return error;
29
+ if (error && typeof error === "object" && "value" in error) {
30
+ return new EdenFetchError(status, error.value);
31
+ }
32
+ return new EdenFetchError(status, error);
33
+ }
34
+ function createQueryOptions(treatyCall, paths, method, globalConfig) {
35
+ return (input, options) => {
36
+ const eden = options?.eden;
37
+ const finalInput = input === void 0 && eden === void 0 ? void 0 : { ...input, ...eden };
38
+ const queryKey = buildQueryKey(paths, finalInput, method, globalConfig.queryKeyPrefix);
39
+ const { eden: _eden, ...tanstackOptions } = options ?? {};
40
+ return {
41
+ ...tanstackOptions,
42
+ queryKey,
43
+ queryFn: async (_context) => {
44
+ const result = await treatyCall(finalInput);
45
+ if (result.error) {
46
+ const error = toEdenFetchError(result.error, result.status);
47
+ error.queryKey = queryKey;
48
+ error.method = method;
49
+ error.path = paths;
50
+ error.input = finalInput;
51
+ error.response = result.response;
52
+ error.headers = result.headers;
53
+ if (shouldThrowOnError(globalConfig, {
54
+ queryKey,
55
+ status: result.status,
56
+ method,
57
+ path: paths,
58
+ input: finalInput
59
+ })) {
60
+ throw error;
61
+ }
62
+ return null;
63
+ }
64
+ return result.data;
65
+ }
66
+ };
67
+ };
68
+ }
69
+
70
+ // src/mutation-options.ts
71
+ import { EdenFetchError as EdenFetchError2 } from "@elysiajs/eden";
72
+ function shouldThrowOnError2(config, context) {
73
+ if (typeof config.throwOnError === "boolean") return config.throwOnError;
74
+ if (typeof config.throwOnError === "function") {
75
+ const fn = config.throwOnError;
76
+ return fn.length >= 2 ? fn(
77
+ context.queryKey,
78
+ context.status
79
+ ) : fn(context);
80
+ }
81
+ return true;
82
+ }
83
+ function toEdenFetchError2(error, status) {
84
+ if (error instanceof EdenFetchError2) return error;
85
+ if (error && typeof error === "object" && "value" in error) {
86
+ return new EdenFetchError2(status, error.value);
87
+ }
88
+ return new EdenFetchError2(status, error);
89
+ }
90
+ function createMutationOptions(treatyCall, paths, method, globalConfig) {
91
+ return (options) => {
92
+ const eden = options?.eden;
93
+ const mutationKey = buildQueryKey(
94
+ paths,
95
+ void 0,
96
+ method,
97
+ globalConfig.queryKeyPrefix
98
+ );
99
+ const { eden: _eden, ...tanstackOptions } = options ?? {};
100
+ return {
101
+ ...tanstackOptions,
102
+ mutationKey,
103
+ mutationFn: async (variables) => {
104
+ const result = await treatyCall(variables, eden);
105
+ if (result.error) {
106
+ const error = toEdenFetchError2(result.error, result.status);
107
+ error.queryKey = mutationKey;
108
+ error.method = method;
109
+ error.path = paths;
110
+ error.input = variables;
111
+ error.response = result.response;
112
+ error.headers = result.headers;
113
+ if (shouldThrowOnError2(globalConfig, {
114
+ queryKey: mutationKey,
115
+ status: result.status,
116
+ method,
117
+ path: paths,
118
+ input: variables
119
+ })) {
120
+ throw error;
121
+ }
122
+ return null;
123
+ }
124
+ return result.data;
125
+ }
126
+ };
127
+ };
128
+ }
129
+
130
+ // src/query-key-helpers.ts
131
+ function createQueryKeyGetter(paths, method, prefix) {
132
+ return (input) => buildQueryKey(paths, input, method, prefix);
133
+ }
134
+ function createQueryFilter(paths, method, prefix) {
135
+ return (input) => ({
136
+ queryKey: buildQueryKey(paths, input, method, prefix),
137
+ exact: true
138
+ });
139
+ }
140
+
141
+ // src/proxy.ts
142
+ var HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"];
143
+ function extendProxy(proxy, paths, config) {
144
+ return new Proxy(proxy, {
145
+ get(target, prop) {
146
+ if (typeof prop === "symbol") return target[prop];
147
+ const value = target[prop];
148
+ if (typeof value === "function" && HTTP_METHODS.includes(prop)) {
149
+ const httpMethod = prop;
150
+ const callable = (...args) => value(...args);
151
+ return Object.assign(callable, {
152
+ ...httpMethod === "get" || httpMethod === "head" ? {
153
+ queryOptions: createQueryOptions(
154
+ callable,
155
+ paths,
156
+ httpMethod,
157
+ config
158
+ ),
159
+ queryKey: createQueryKeyGetter(paths, httpMethod, config.queryKeyPrefix),
160
+ queryFilter: createQueryFilter(paths, httpMethod, config.queryKeyPrefix)
161
+ } : {},
162
+ ...httpMethod === "post" || httpMethod === "put" || httpMethod === "patch" || httpMethod === "delete" ? {
163
+ mutationOptions: createMutationOptions(
164
+ callable,
165
+ paths,
166
+ httpMethod,
167
+ config
168
+ )
169
+ } : {}
170
+ });
171
+ }
172
+ if (value !== null && (typeof value === "object" || typeof value === "function")) {
173
+ return extendProxy(value, [...paths, prop], config);
174
+ }
175
+ return value;
176
+ }
177
+ });
178
+ }
179
+
180
+ // src/index.ts
181
+ import { treaty as treaty2 } from "@elysiajs/eden/treaty2";
182
+ function createEdenQuery(domain, options) {
183
+ const treatyInstance = treaty(domain, options?.treaty);
184
+ return extendProxy(
185
+ treatyInstance,
186
+ [],
187
+ {
188
+ throwOnError: options?.throwOnError ?? true,
189
+ queryKeyPrefix: options?.queryKeyPrefix
190
+ }
191
+ );
192
+ }
193
+ export {
194
+ createEdenQuery,
195
+ treaty2 as treaty
196
+ };
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "eden-tanstack-query",
3
+ "version": "0.0.1",
4
+ "description": "TanStack Query integration for Eden Treaty",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsup",
19
+ "test": "bun test",
20
+ "typecheck": "tsc --noEmit"
21
+ },
22
+ "peerDependencies": {
23
+ "@elysiajs/eden": ">=1.0.0",
24
+ "@tanstack/query-core": "^5.0.0"
25
+ },
26
+ "devDependencies": {
27
+ "@elysiajs/eden": "file:../..",
28
+ "@tanstack/react-query": "^5.0.0",
29
+ "@tanstack/svelte-query": "^5.0.0",
30
+ "bun-types": "latest",
31
+ "typescript": "^5.0.0"
32
+ },
33
+ "keywords": [
34
+ "elysia",
35
+ "eden",
36
+ "tanstack",
37
+ "query",
38
+ "react-query",
39
+ "svelte-query",
40
+ "typescript"
41
+ ]
42
+ }