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/src/index.ts ADDED
@@ -0,0 +1,217 @@
1
+ import type { Elysia } from 'elysia'
2
+ import type { QueryKey, QueryClient } from '@tanstack/query-core'
3
+ import { treaty, type Treaty } from '@elysiajs/eden/treaty2'
4
+ import type { EdenTQ, EdenQueryOptions, EdenMutationOptions } from './types'
5
+
6
+ const HTTP_METHODS = [
7
+ 'get',
8
+ 'post',
9
+ 'put',
10
+ 'delete',
11
+ 'patch',
12
+ 'options',
13
+ 'head'
14
+ ] as const
15
+
16
+ type HttpMethod = (typeof HTTP_METHODS)[number]
17
+
18
+ function isHttpMethod(value: string): value is HttpMethod {
19
+ return HTTP_METHODS.includes(value as HttpMethod)
20
+ }
21
+
22
+ function materializePath(
23
+ paths: string[],
24
+ params?: Record<string, string | number>
25
+ ): string[] {
26
+ const result: string[] = []
27
+
28
+ for (const segment of paths) {
29
+ const match = /^:(.+?)(\?)?$/.exec(segment)
30
+ if (!match) {
31
+ result.push(segment)
32
+ continue
33
+ }
34
+
35
+ const paramName = match[1]
36
+ const isOptional = !!match[2]
37
+ const value = params?.[paramName]
38
+
39
+ if (value == null) {
40
+ if (isOptional) continue
41
+ throw new Error(`Missing required route parameter: "${paramName}"`)
42
+ }
43
+
44
+ result.push(String(value))
45
+ }
46
+
47
+ return result
48
+ }
49
+
50
+ function buildQueryKey(
51
+ prefix: QueryKey,
52
+ method: string,
53
+ pathTemplate: string[],
54
+ input?: { params?: unknown; query?: unknown }
55
+ ): QueryKey {
56
+ return [
57
+ ...prefix,
58
+ method,
59
+ pathTemplate,
60
+ input?.params ?? null,
61
+ input?.query ?? null
62
+ ] as const
63
+ }
64
+
65
+ function callTreaty(
66
+ raw: any,
67
+ segments: string[],
68
+ method: string,
69
+ input?: { body?: unknown; query?: unknown; headers?: unknown; fetch?: RequestInit }
70
+ ): Promise<Treaty.TreatyResponse<any>> {
71
+ let current = raw
72
+
73
+ for (const segment of segments) {
74
+ current = current[segment]
75
+ }
76
+
77
+ const options: Record<string, unknown> = {}
78
+ if (input?.query !== undefined) options.query = input.query
79
+ if (input?.headers !== undefined) options.headers = input.headers
80
+ if (input?.fetch !== undefined) Object.assign(options, input.fetch)
81
+
82
+ if (method === 'get' || method === 'head') {
83
+ return current[method](Object.keys(options).length > 0 ? options : undefined)
84
+ }
85
+
86
+ return current[method](
87
+ input?.body,
88
+ Object.keys(options).length > 0 ? options : undefined
89
+ )
90
+ }
91
+
92
+ async function dataOrThrow<T>(
93
+ promise: Promise<Treaty.TreatyResponse<any>>
94
+ ): Promise<T> {
95
+ const result = await promise
96
+ if (result.error) throw result.error
97
+ return result.data as T
98
+ }
99
+
100
+ interface ProxyContext {
101
+ raw: any
102
+ prefix: QueryKey
103
+ }
104
+
105
+ function createMethodDecorator(
106
+ ctx: ProxyContext,
107
+ paths: string[],
108
+ method: string
109
+ ) {
110
+ const pathTemplate = [...paths]
111
+
112
+ const fn = (
113
+ input?: { params?: Record<string, string | number>; body?: unknown; query?: unknown; headers?: unknown; fetch?: RequestInit }
114
+ ) => {
115
+ const materializedPath = materializePath(pathTemplate, input?.params)
116
+ return callTreaty(ctx.raw, materializedPath, method, input)
117
+ }
118
+
119
+ fn.queryKey = (
120
+ input?: { params?: Record<string, string | number>; query?: unknown }
121
+ ): QueryKey => {
122
+ return buildQueryKey(ctx.prefix, method, pathTemplate, input)
123
+ }
124
+
125
+ fn.queryOptions = <TData = unknown>(
126
+ input: { params?: Record<string, string | number>; query?: unknown; headers?: unknown; fetch?: RequestInit },
127
+ overrides?: Partial<EdenQueryOptions<TData>>
128
+ ): EdenQueryOptions<TData> => {
129
+ return {
130
+ queryKey: fn.queryKey(input),
131
+ queryFn: () => {
132
+ const materializedPath = materializePath(pathTemplate, input?.params)
133
+ return dataOrThrow(callTreaty(ctx.raw, materializedPath, method, input))
134
+ },
135
+ ...overrides
136
+ }
137
+ }
138
+
139
+ fn.mutationKey = (
140
+ input?: { params?: Record<string, string | number>; query?: unknown }
141
+ ): QueryKey => {
142
+ return buildQueryKey(ctx.prefix, method, pathTemplate, input)
143
+ }
144
+
145
+ fn.mutationOptions = <TData = unknown, TVariables = unknown>(
146
+ overrides?: Partial<EdenMutationOptions<TData, unknown, TVariables>>
147
+ ): EdenMutationOptions<TData, unknown, TVariables> => {
148
+ return {
149
+ mutationKey: [...ctx.prefix, method, pathTemplate],
150
+ mutationFn: (variables: TVariables) => {
151
+ const vars = variables as { params?: Record<string, string | number>; body?: unknown; query?: unknown; headers?: unknown; fetch?: RequestInit }
152
+ const materializedPath = materializePath(pathTemplate, vars?.params)
153
+ return dataOrThrow(callTreaty(ctx.raw, materializedPath, method, vars))
154
+ },
155
+ ...overrides
156
+ }
157
+ }
158
+
159
+ fn.invalidate = async (
160
+ queryClient: QueryClient,
161
+ input?: { params?: Record<string, string | number>; query?: unknown },
162
+ exact = false
163
+ ): Promise<void> => {
164
+ const queryKey = input
165
+ ? fn.queryKey(input)
166
+ : [...ctx.prefix, method, pathTemplate]
167
+ await queryClient.invalidateQueries({ queryKey, exact })
168
+ }
169
+
170
+ return fn
171
+ }
172
+
173
+ function createEdenTQProxy(
174
+ ctx: ProxyContext,
175
+ paths: string[] = []
176
+ ): any {
177
+ return new Proxy(() => {}, {
178
+ get(_, prop: string): any {
179
+ if (isHttpMethod(prop)) {
180
+ return createMethodDecorator(ctx, paths, prop)
181
+ }
182
+
183
+ return createEdenTQProxy(
184
+ ctx,
185
+ prop === 'index' ? paths : [...paths, prop]
186
+ )
187
+ },
188
+ apply(_, __, [body]) {
189
+ if (typeof body === 'object' && body !== null) {
190
+ const paramValue = Object.values(body)[0] as string
191
+ return createEdenTQProxy(ctx, [...paths, paramValue])
192
+ }
193
+ return createEdenTQProxy(ctx, paths)
194
+ }
195
+ })
196
+ }
197
+
198
+ export function createEdenTQ<
199
+ const App extends Elysia<any, any, any, any, any, any, any>
200
+ >(
201
+ domain: string | App,
202
+ config: EdenTQ.Config = {}
203
+ ): EdenTQ.Create<App> {
204
+ const { queryKeyPrefix = ['eden'], ...treatyConfig } = config
205
+
206
+ const raw = treaty<App>(domain as any, treatyConfig)
207
+
208
+ const ctx: ProxyContext = {
209
+ raw,
210
+ prefix: queryKeyPrefix
211
+ }
212
+
213
+ return createEdenTQProxy(ctx) as EdenTQ.Create<App>
214
+ }
215
+
216
+ export type { EdenTQ, EdenQueryOptions, EdenMutationOptions }
217
+ export type { QueryKey, QueryClient } from '@tanstack/query-core'
package/src/types.ts ADDED
@@ -0,0 +1,178 @@
1
+ import type { Elysia, ELYSIA_FORM_DATA } from 'elysia'
2
+ import type { QueryKey, QueryClient } from '@tanstack/query-core'
3
+ import type { Treaty } from '@elysiajs/eden/treaty2'
4
+
5
+ type IsNever<T> = [T] extends [never] ? true : false
6
+
7
+ type Prettify<T> = {
8
+ [K in keyof T]: T[K]
9
+ } & {}
10
+
11
+ type Enumerate<
12
+ N extends number,
13
+ Acc extends number[] = []
14
+ > = Acc['length'] extends N
15
+ ? Acc[number]
16
+ : Enumerate<N, [...Acc, Acc['length']]>
17
+
18
+ type IntegerRange<F extends number, T extends number> = Exclude<
19
+ Enumerate<T>,
20
+ Enumerate<F>
21
+ >
22
+
23
+ type SuccessCodeRange = IntegerRange<200, 300>
24
+
25
+ type ExtractData<Res extends Record<number, unknown>> =
26
+ Res[Extract<keyof Res, SuccessCodeRange>] extends {
27
+ [ELYSIA_FORM_DATA]: infer Data
28
+ }
29
+ ? Data
30
+ : Res[Extract<keyof Res, SuccessCodeRange>]
31
+
32
+ type ExtractError<Res extends Record<number, unknown>> = Exclude<
33
+ keyof Res,
34
+ SuccessCodeRange
35
+ > extends never
36
+ ? { status: unknown; value: unknown }
37
+ : {
38
+ [Status in keyof Res]: {
39
+ status: Status
40
+ value: Res[Status] extends { [ELYSIA_FORM_DATA]: infer Data }
41
+ ? Data
42
+ : Res[Status]
43
+ }
44
+ }[Exclude<keyof Res, SuccessCodeRange>]
45
+
46
+ interface TQParamBase {
47
+ fetch?: RequestInit
48
+ }
49
+
50
+ type SerializeQueryParams<T> = T extends Record<string, any>
51
+ ? {
52
+ [K in keyof T]: T[K] extends Date
53
+ ? string
54
+ : T[K] extends Date | undefined
55
+ ? string | undefined
56
+ : T[K]
57
+ }
58
+ : T
59
+
60
+ type IsEmptyObject<T> = T extends Record<string, never>
61
+ ? [keyof T] extends [never]
62
+ ? true
63
+ : false
64
+ : false
65
+
66
+ type MaybeEmptyObject<T, K extends PropertyKey> = [T] extends [never]
67
+ ? {}
68
+ : [T] extends [undefined]
69
+ ? { [P in K]?: T }
70
+ : IsEmptyObject<T> extends true
71
+ ? { [P in K]?: T }
72
+ : undefined extends T
73
+ ? { [P in K]?: T }
74
+ : { [P in K]: T }
75
+
76
+ type TQMethodParam<
77
+ Body,
78
+ Headers,
79
+ Query,
80
+ Params
81
+ > = MaybeEmptyObject<Headers, 'headers'> &
82
+ MaybeEmptyObject<SerializeQueryParams<Query>, 'query'> &
83
+ MaybeEmptyObject<Params, 'params'> &
84
+ MaybeEmptyObject<Body, 'body'> &
85
+ TQParamBase
86
+
87
+ export interface EdenQueryOptions<TData = unknown, TError = unknown> {
88
+ queryKey: QueryKey
89
+ queryFn: () => Promise<TData>
90
+ }
91
+
92
+ export interface EdenMutationOptions<
93
+ TData = unknown,
94
+ TError = unknown,
95
+ TVariables = unknown
96
+ > {
97
+ mutationKey: QueryKey
98
+ mutationFn: (variables: TVariables) => Promise<TData>
99
+ }
100
+
101
+ export interface EdenTQMethod<
102
+ Body,
103
+ Headers,
104
+ Query,
105
+ Params,
106
+ Res extends Record<number, unknown>
107
+ > {
108
+ <TQueryFnData = ExtractData<Res>>(
109
+ input: TQMethodParam<Body, Headers, Query, Params>,
110
+ options?: RequestInit
111
+ ): Promise<Treaty.TreatyResponse<Res>>
112
+
113
+ queryKey(input?: TQMethodParam<Body, Headers, Query, Params>): QueryKey
114
+
115
+ queryOptions<TData = ExtractData<Res>>(
116
+ input: TQMethodParam<Body, Headers, Query, Params>,
117
+ overrides?: Partial<EdenQueryOptions<TData, ExtractError<Res>>>
118
+ ): EdenQueryOptions<TData, ExtractError<Res>>
119
+
120
+ mutationKey(input?: TQMethodParam<Body, Headers, Query, Params>): QueryKey
121
+
122
+ mutationOptions<TData = ExtractData<Res>>(
123
+ overrides?: Partial<EdenMutationOptions<TData, ExtractError<Res>, TQMethodParam<Body, Headers, Query, Params>>>
124
+ ): EdenMutationOptions<TData, ExtractError<Res>, TQMethodParam<Body, Headers, Query, Params>>
125
+
126
+ invalidate(
127
+ queryClient: QueryClient,
128
+ input?: TQMethodParam<Body, Headers, Query, Params>,
129
+ exact?: boolean
130
+ ): Promise<void>
131
+ }
132
+
133
+ export namespace EdenTQ {
134
+ export type Config = Treaty.Config & {
135
+ queryKeyPrefix?: QueryKey
136
+ }
137
+
138
+ export type Create<App extends Elysia<any, any, any, any, any, any, any>> =
139
+ App extends {
140
+ '~Routes': infer Schema extends Record<any, any>
141
+ }
142
+ ? Prettify<Sign<Schema>> & CreateParams<Schema>
143
+ : 'Please install Elysia before using Eden'
144
+
145
+ export type Sign<in out Route extends Record<any, any>> = {
146
+ [K in keyof Route as K extends `:${string}`
147
+ ? never
148
+ : K]: Route[K] extends {
149
+ body: infer Body
150
+ headers: infer Headers
151
+ params: infer Params
152
+ query: infer Query
153
+ response: infer Res extends Record<number, unknown>
154
+ }
155
+ ? EdenTQMethod<Body, Headers, Query, Params, Res>
156
+ : CreateParams<Route[K]>
157
+ }
158
+
159
+ type CreateParams<Route extends Record<string, any>> =
160
+ Extract<keyof Route, `:${string}`> extends infer Path extends string
161
+ ? IsNever<Path> extends true
162
+ ? Prettify<Sign<Route>>
163
+ : (((params: {
164
+ [param in Path extends `:${infer Param}`
165
+ ? Param extends `${infer P}?`
166
+ ? P
167
+ : Param
168
+ : never]: string | number
169
+ }) => Prettify<Sign<Route[Path]>> &
170
+ CreateParams<Route[Path]>) &
171
+ Prettify<Sign<Route>>) &
172
+ (Path extends `:${string}?`
173
+ ? CreateParams<Route[Path]>
174
+ : {})
175
+ : never
176
+ }
177
+
178
+ export type { QueryKey, QueryClient }