From ce19612f10362907420208aa4c9cd1c3891fd5c0 Mon Sep 17 00:00:00 2001 From: m1212e <14091540+m1212e@users.noreply.github.com> Date: Mon, 24 Feb 2025 16:52:51 +0100 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20treaty=20error=20?= =?UTF-8?q?shorthand=20option?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/treaty2/index.ts | 23 ++++++++++++++++------- src/treaty2/types.ts | 41 ++++++++++++++++++++++------------------- test/treaty2.test.ts | 19 +++++++++++++++++++ test/types/treaty2.ts | 10 ++++++++++ 4 files changed, 67 insertions(+), 26 deletions(-) diff --git a/src/treaty2/index.ts b/src/treaty2/index.ts index fb4a599..36e951f 100644 --- a/src/treaty2/index.ts +++ b/src/treaty2/index.ts @@ -61,8 +61,8 @@ const createNewFile = (v: File) => reader.readAsArrayBuffer(v) }) -const processHeaders = ( - h: Treaty.Config['headers'], +const processHeaders = ( + h: Treaty.Config['headers'], path: string, options: RequestInit = {}, headers: Record = {} @@ -134,9 +134,9 @@ export async function* streamResponse(response: Response) { } } -const createProxy = ( +const createProxy = ( domain: string, - config: Treaty.Config, + config: Treaty.Config, paths: string[] = [], elysia?: Elysia ): any => @@ -400,6 +400,7 @@ const createProxy = ( } if (data !== null) { + if(config.shouldThrow) return data return { data, error, @@ -445,6 +446,13 @@ const createProxy = ( data = null } + if (config.shouldThrow) { + if (error) { + throw error + } + return data + } + return { data, error, @@ -468,11 +476,12 @@ const createProxy = ( }) as any export const treaty = < - const App extends Elysia + const App extends Elysia, + ShouldThrow extends boolean = false >( domain: string | App, - config: Treaty.Config = {} -): Treaty.Create => { + config: Treaty.Config = {} +): Treaty.Create => { if (typeof domain === 'string') { if (!config.keepDomain) { if (!domain.includes('://')) diff --git a/src/treaty2/types.ts b/src/treaty2/types.ts index 856d3b7..8f21a0a 100644 --- a/src/treaty2/types.ts +++ b/src/treaty2/types.ts @@ -58,14 +58,15 @@ export namespace Treaty { } export type Create< - App extends Elysia + App extends Elysia, + ShouldThrow extends boolean > = App extends { _routes: infer Schema extends Record } - ? Prettify> + ? Prettify> : 'Please install Elysia before using Eden' - export type Sign> = { + export type Sign, ShouldThrow extends boolean> = { [K in keyof Route as K extends `:${string}` ? never : K]: K extends 'subscribe' // ? Websocket route @@ -105,7 +106,7 @@ export namespace Treaty { options?: Prettify ) => Promise< TreatyResponse< - ReplaceGeneratorWithAsyncGenerator + ReplaceGeneratorWithAsyncGenerator, ShouldThrow > > : ( @@ -113,7 +114,7 @@ export namespace Treaty { options?: Prettify ) => Promise< TreatyResponse< - ReplaceGeneratorWithAsyncGenerator + ReplaceGeneratorWithAsyncGenerator, ShouldThrow > > : ( @@ -123,7 +124,7 @@ export namespace Treaty { options?: Prettify ) => Promise< TreatyResponse< - ReplaceGeneratorWithAsyncGenerator + ReplaceGeneratorWithAsyncGenerator, ShouldThrow > > : K extends 'get' | 'head' @@ -131,7 +132,7 @@ export namespace Treaty { options: Prettify ) => Promise< TreatyResponse< - ReplaceGeneratorWithAsyncGenerator + ReplaceGeneratorWithAsyncGenerator, ShouldThrow > > : ( @@ -141,17 +142,17 @@ export namespace Treaty { options: Prettify ) => Promise< TreatyResponse< - ReplaceGeneratorWithAsyncGenerator + ReplaceGeneratorWithAsyncGenerator, ShouldThrow > > : never - : CreateParams + : CreateParams } - type CreateParams> = + type CreateParams, ShouldThrow extends boolean> = Extract extends infer Path extends string ? IsNever extends true - ? Prettify> + ? Prettify> : // ! DO NOT USE PRETTIFY ON THIS LINE, OTHERWISE FUNCTION CALLING WILL BE OMITTED (((params: { [param in Path extends `:${infer Param}` @@ -159,15 +160,15 @@ export namespace Treaty { ? Param : Param : never]: string | number - }) => Prettify> & - CreateParams) & - Prettify>) & + }) => Prettify> & + CreateParams) & + Prettify>) & (Path extends `:${string}?` - ? CreateParams + ? CreateParams : {}) : never - export interface Config { + export interface Config { fetch?: Omit fetcher?: typeof fetch headers?: MaybeArray< @@ -184,14 +185,16 @@ export namespace Treaty { ) => MaybePromise > onResponse?: MaybeArray<(response: Response) => MaybePromise> - keepDomain?: boolean + keepDomain?: boolean, + shouldThrow?: ShouldThrow } // type UnwrapAwaited> = { // [K in keyof T]: Awaited // } - export type TreatyResponse> = + export type TreatyResponse, ShouldThrow extends boolean> = ShouldThrow extends false + ? | { data: Res[200] error: null @@ -215,7 +218,7 @@ export namespace Treaty { response: Response status: number headers: FetchRequestInit['headers'] - } + } : Res[200] export interface OnMessage extends MessageEvent { data: Data diff --git a/test/treaty2.test.ts b/test/treaty2.test.ts index d3f74db..7b58224 100644 --- a/test/treaty2.test.ts +++ b/test/treaty2.test.ts @@ -173,6 +173,7 @@ const app = new Elysia() .get('/id/:id?', ({ params: { id = 'unknown' } }) => id) const client = treaty(app) +const throwingClient = treaty(app, { shouldThrow: true }) describe('Treaty2', () => { it('get index', async () => { @@ -644,4 +645,22 @@ describe('Treaty2 - Using endpoint URL', () => { done() }) }) + + it('return concise response', async () => { + const r = await throwingClient.index.get() + + expect(r).toBe('a') + }) + + it('throw error', async () => { + let errorThrown = false + + try { + await throwingClient.error.get() + } catch (error) { + errorThrown = true + } + + expect(errorThrown).toBe(true) + }) }) diff --git a/test/types/treaty2.ts b/test/types/treaty2.ts index 63c66ee..75172a1 100644 --- a/test/types/treaty2.ts +++ b/test/types/treaty2.ts @@ -132,7 +132,9 @@ const app = new Elysia() .use(plugin) const api = treaty(app) +const throwingApi = treaty(app, { shouldThrow: true }) type api = typeof api +type throwingApi = typeof throwingApi type Result = T extends (...args: any[]) => infer R ? Awaited @@ -1035,3 +1037,11 @@ type ValidationError = { | undefined >() } + +// ? Throwing api config flag should result in slimmed down result structure +{ + type Route = throwingApi['index']['get'] + type Res = Result + + expectTypeOf().toEqualTypeOf<'a'>() +} \ No newline at end of file From 8e1060f4e29f3b31632e1f92fc2e90d87ff7d3a3 Mon Sep 17 00:00:00 2001 From: m1212e <14091540+m1212e@users.noreply.github.com> Date: Mon, 24 Feb 2025 18:54:45 +0100 Subject: [PATCH 2/2] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20improvement:=20rename?= =?UTF-8?q?=20option=20&=20add=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/treaty2/index.ts | 899 ++++++++++---------- src/treaty2/types.ts | 20 +- test/treaty2.test.ts | 1178 +++++++++++++------------- test/types/treaty2.ts | 1832 ++++++++++++++++++++--------------------- 4 files changed, 1973 insertions(+), 1956 deletions(-) diff --git a/src/treaty2/index.ts b/src/treaty2/index.ts index 36e951f..07a0044 100644 --- a/src/treaty2/index.ts +++ b/src/treaty2/index.ts @@ -9,15 +9,15 @@ import { EdenWS } from './ws' import { parseStringifiedValue } from '../utils/parsingUtils' const method = [ - 'get', - 'post', - 'put', - 'delete', - 'patch', - 'options', - 'head', - 'connect', - 'subscribe' + 'get', + 'post', + 'put', + 'delete', + 'patch', + 'options', + 'head', + 'connect', + 'subscribe' ] as const const locals = ['localhost', '127.0.0.1', '0.0.0.0'] @@ -25,483 +25,482 @@ const locals = ['localhost', '127.0.0.1', '0.0.0.0'] const isServer = typeof FileList === 'undefined' const isFile = (v: any) => { - if (isServer) return v instanceof Blob + if (isServer) return v instanceof Blob - return v instanceof FileList || v instanceof File + return v instanceof FileList || v instanceof File } // FormData is 1 level deep const hasFile = (obj: Record) => { - if (!obj) return false + if (!obj) return false - for (const key in obj) { - if (isFile(obj[key])) return true + for (const key in obj) { + if (isFile(obj[key])) return true - if (Array.isArray(obj[key]) && (obj[key] as unknown[]).find(isFile)) - return true - } + if (Array.isArray(obj[key]) && (obj[key] as unknown[]).find(isFile)) + return true + } - return false + return false } const createNewFile = (v: File) => - isServer - ? v - : new Promise((resolve) => { - const reader = new FileReader() - - reader.onload = () => { - const file = new File([reader.result!], v.name, { - lastModified: v.lastModified, - type: v.type - }) - resolve(file) - } - - reader.readAsArrayBuffer(v) - }) + isServer + ? v + : new Promise((resolve) => { + const reader = new FileReader() + + reader.onload = () => { + const file = new File([reader.result!], v.name, { + lastModified: v.lastModified, + type: v.type + }) + resolve(file) + } + + reader.readAsArrayBuffer(v) + }) const processHeaders = ( - h: Treaty.Config['headers'], - path: string, - options: RequestInit = {}, - headers: Record = {} + h: Treaty.Config['headers'], + path: string, + options: RequestInit = {}, + headers: Record = {} ): Record => { - if (Array.isArray(h)) { - for (const value of h) - if (!Array.isArray(value)) - headers = processHeaders(value, path, options, headers) - else { - const key = value[0] - if (typeof key === 'string') - headers[key.toLowerCase()] = value[1] as string - else - for (const [k, value] of key) - headers[k.toLowerCase()] = value as string - } - - return headers - } - - if (!h) return headers - - switch (typeof h) { - case 'function': - if (h instanceof Headers) - return processHeaders(h, path, options, headers) - - const v = h(path, options) - if (v) return processHeaders(v, path, options, headers) - return headers - - case 'object': - if (h instanceof Headers) { - h.forEach((value, key) => { - headers[key.toLowerCase()] = value - }) - return headers - } - - for (const [key, value] of Object.entries(h)) - headers[key.toLowerCase()] = value as string - - return headers - - default: - return headers - } + if (Array.isArray(h)) { + for (const value of h) + if (!Array.isArray(value)) + headers = processHeaders(value, path, options, headers) + else { + const key = value[0] + if (typeof key === 'string') + headers[key.toLowerCase()] = value[1] as string + else + for (const [k, value] of key) + headers[k.toLowerCase()] = value as string + } + + return headers + } + + if (!h) return headers + + switch (typeof h) { + case 'function': + if (h instanceof Headers) + return processHeaders(h, path, options, headers) + + const v = h(path, options) + if (v) return processHeaders(v, path, options, headers) + return headers + + case 'object': + if (h instanceof Headers) { + h.forEach((value, key) => { + headers[key.toLowerCase()] = value + }) + return headers + } + + for (const [key, value] of Object.entries(h)) + headers[key.toLowerCase()] = value as string + + return headers + + default: + return headers + } } export async function* streamResponse(response: Response) { - const body = response.body + const body = response.body - if (!body) return + if (!body) return - const reader = body.getReader() - const decoder = new TextDecoder() + const reader = body.getReader() + const decoder = new TextDecoder() - try { - while (true) { - const { done, value } = await reader.read() - if (done) break + try { + while (true) { + const { done, value } = await reader.read() + if (done) break - const data = decoder.decode(value) + const data = decoder.decode(value) - yield parseStringifiedValue(data) - } - } finally { - reader.releaseLock() - } + yield parseStringifiedValue(data) + } + } finally { + reader.releaseLock() + } } const createProxy = ( - domain: string, - config: Treaty.Config, - paths: string[] = [], - elysia?: Elysia + domain: string, + config: Treaty.Config, + paths: string[] = [], + elysia?: Elysia ): any => - new Proxy(() => {}, { - get(_, param: string): any { - return createProxy( - domain, - config, - param === 'index' ? paths : [...paths, param], - elysia - ) - }, - apply(_, __, [body, options]) { - if ( - !body || - options || - (typeof body === 'object' && Object.keys(body).length !== 1) || - method.includes(paths.at(-1) as any) - ) { - const methodPaths = [...paths] - const method = methodPaths.pop() - const path = '/' + methodPaths.join('/') - - let { - fetcher = fetch, - headers, - onRequest, - onResponse, - fetch: conf - } = config - - const isGetOrHead = - method === 'get' || - method === 'head' || - method === 'subscribe' - - headers = processHeaders(headers, path, options) - - const query = isGetOrHead - ? (body as Record) - ?.query - : options?.query - - let q = '' - if (query) { - const append = (key: string, value: string) => { - q += - (q ? '&' : '?') + - `${encodeURIComponent(key)}=${encodeURIComponent( - value - )}` - } - - for (const [key, value] of Object.entries(query)) { - if (Array.isArray(value)) { - for (const v of value) append(key, v) - continue - } - - if (typeof value === 'object') { - append(key, JSON.stringify(value)) - continue - } - - - append(key, `${value}`) - } - } - - if (method === 'subscribe') { - const url = - domain.replace( - /^([^]+):\/\//, - domain.startsWith('https://') - ? 'wss://' - : domain.startsWith('http://') - ? 'ws://' - : locals.find((v) => - (domain as string).includes(v) - ) - ? 'ws://' - : 'wss://' - ) + - path + - q - - return new EdenWS(url) - } - - return (async () => { - let fetchInit = { - method: method?.toUpperCase(), - body, - ...conf, - headers - } satisfies FetchRequestInit - - fetchInit.headers = { - ...headers, - ...processHeaders( - // For GET and HEAD, options is moved to body (1st param) - isGetOrHead ? body?.headers : options?.headers, - path, - fetchInit - ) - } - - const fetchOpts = - isGetOrHead && typeof body === 'object' - ? body.fetch - : options?.fetch - - fetchInit = { - ...fetchInit, - ...fetchOpts - } - - if (isGetOrHead) delete fetchInit.body - - if (onRequest) { - if (!Array.isArray(onRequest)) onRequest = [onRequest] - - for (const value of onRequest) { - const temp = await value(path, fetchInit) - - if (typeof temp === 'object') - fetchInit = { - ...fetchInit, - ...temp, - headers: { - ...fetchInit.headers, - ...processHeaders( - temp.headers, - path, - fetchInit - ) - } - } - } - } - - // ? Duplicate because end-user might add a body in onRequest - if (isGetOrHead) delete fetchInit.body - - if (hasFile(body)) { - const formData = new FormData() - - // FormData is 1 level deep - for (const [key, field] of Object.entries( - fetchInit.body - )) { - if (isServer) { - formData.append(key, field as any) - - continue - } - - if (field instanceof File) { - formData.append( - key, - await createNewFile(field as any) - ) - - continue - } - - if (field instanceof FileList) { - for (let i = 0; i < field.length; i++) - formData.append( - key as any, - await createNewFile((field as any)[i]) - ) - - continue - } - - if (Array.isArray(field)) { - for (let i = 0; i < field.length; i++) { - const value = (field as any)[i] - - formData.append( - key as any, - value instanceof File - ? await createNewFile(value) - : value - ) - } - - continue - } - - formData.append(key, field as string) - } - - // We don't do this because we need to let the browser set the content type with the correct boundary - // fetchInit.headers['content-type'] = 'multipart/form-data' - fetchInit.body = formData - } else if (typeof body === 'object') { - ;(fetchInit.headers as Record)[ - 'content-type' - ] = 'application/json' - - fetchInit.body = JSON.stringify(body) - } else if (body !== undefined && body !== null) { - ;(fetchInit.headers as Record)[ - 'content-type' - ] = 'text/plain' - } - - if (isGetOrHead) delete fetchInit.body - - if (onRequest) { - if (!Array.isArray(onRequest)) onRequest = [onRequest] - - for (const value of onRequest) { - const temp = await value(path, fetchInit) - - if (typeof temp === 'object') - fetchInit = { - ...fetchInit, - ...temp, - headers: { - ...fetchInit.headers, - ...processHeaders( - temp.headers, - path, - fetchInit - ) - } as Record - } - } - } - - const url = domain + path + q - const response = await (elysia?.handle( - new Request(url, fetchInit) - ) ?? fetcher!(url, fetchInit)) - - // @ts-ignore - let data = null - let error = null - - if (onResponse) { - if (!Array.isArray(onResponse)) - onResponse = [onResponse] - - for (const value of onResponse) - try { - const temp = await value(response.clone()) - - if (temp !== undefined && temp !== null) { - data = temp - break - } - } catch (err) { - if (err instanceof EdenFetchError) error = err - else error = new EdenFetchError(422, err) - - break - } - } - - if (data !== null) { - if(config.shouldThrow) return data - return { - data, - error, - response, - status: response.status, - headers: response.headers - } - } - - switch ( - response.headers.get('Content-Type')?.split(';')[0] - ) { - case 'text/event-stream': - data = streamResponse(response) - break - - case 'application/json': - data = await response.json() - break - case 'application/octet-stream': - data = await response.arrayBuffer() - break - - case 'multipart/form-data': - const temp = await response.formData() - - data = {} - temp.forEach((value, key) => { - // @ts-ignore - data[key] = value - }) - - break - - default: - data = await response - .text() - .then(parseStringifiedValue) - } - - if (response.status >= 300 || response.status < 200) { - error = new EdenFetchError(response.status, data) - data = null - } - - if (config.shouldThrow) { + new Proxy(() => {}, { + get(_, param: string): any { + return createProxy( + domain, + config, + param === 'index' ? paths : [...paths, param], + elysia + ) + }, + apply(_, __, [body, options]) { + if ( + !body || + options || + (typeof body === 'object' && Object.keys(body).length !== 1) || + method.includes(paths.at(-1) as any) + ) { + const methodPaths = [...paths] + const method = methodPaths.pop() + const path = '/' + methodPaths.join('/') + + let { + fetcher = fetch, + headers, + onRequest, + onResponse, + fetch: conf + } = config + + const isGetOrHead = + method === 'get' || + method === 'head' || + method === 'subscribe' + + headers = processHeaders(headers, path, options) + + const query = isGetOrHead + ? (body as Record) + ?.query + : options?.query + + let q = '' + if (query) { + const append = (key: string, value: string) => { + q += + (q ? '&' : '?') + + `${encodeURIComponent(key)}=${encodeURIComponent( + value + )}` + } + + for (const [key, value] of Object.entries(query)) { + if (Array.isArray(value)) { + for (const v of value) append(key, v) + continue + } + + if (typeof value === 'object') { + append(key, JSON.stringify(value)) + continue + } + + append(key, `${value}`) + } + } + + if (method === 'subscribe') { + const url = + domain.replace( + /^([^]+):\/\//, + domain.startsWith('https://') + ? 'wss://' + : domain.startsWith('http://') + ? 'ws://' + : locals.find((v) => + (domain as string).includes(v) + ) + ? 'ws://' + : 'wss://' + ) + + path + + q + + return new EdenWS(url) + } + + return (async () => { + let fetchInit = { + method: method?.toUpperCase(), + body, + ...conf, + headers + } satisfies FetchRequestInit + + fetchInit.headers = { + ...headers, + ...processHeaders( + // For GET and HEAD, options is moved to body (1st param) + isGetOrHead ? body?.headers : options?.headers, + path, + fetchInit + ) + } + + const fetchOpts = + isGetOrHead && typeof body === 'object' + ? body.fetch + : options?.fetch + + fetchInit = { + ...fetchInit, + ...fetchOpts + } + + if (isGetOrHead) delete fetchInit.body + + if (onRequest) { + if (!Array.isArray(onRequest)) onRequest = [onRequest] + + for (const value of onRequest) { + const temp = await value(path, fetchInit) + + if (typeof temp === 'object') + fetchInit = { + ...fetchInit, + ...temp, + headers: { + ...fetchInit.headers, + ...processHeaders( + temp.headers, + path, + fetchInit + ) + } + } + } + } + + // ? Duplicate because end-user might add a body in onRequest + if (isGetOrHead) delete fetchInit.body + + if (hasFile(body)) { + const formData = new FormData() + + // FormData is 1 level deep + for (const [key, field] of Object.entries( + fetchInit.body + )) { + if (isServer) { + formData.append(key, field as any) + + continue + } + + if (field instanceof File) { + formData.append( + key, + await createNewFile(field as any) + ) + + continue + } + + if (field instanceof FileList) { + for (let i = 0; i < field.length; i++) + formData.append( + key as any, + await createNewFile((field as any)[i]) + ) + + continue + } + + if (Array.isArray(field)) { + for (let i = 0; i < field.length; i++) { + const value = (field as any)[i] + + formData.append( + key as any, + value instanceof File + ? await createNewFile(value) + : value + ) + } + + continue + } + + formData.append(key, field as string) + } + + // We don't do this because we need to let the browser set the content type with the correct boundary + // fetchInit.headers['content-type'] = 'multipart/form-data' + fetchInit.body = formData + } else if (typeof body === 'object') { + ;(fetchInit.headers as Record)[ + 'content-type' + ] = 'application/json' + + fetchInit.body = JSON.stringify(body) + } else if (body !== undefined && body !== null) { + ;(fetchInit.headers as Record)[ + 'content-type' + ] = 'text/plain' + } + + if (isGetOrHead) delete fetchInit.body + + if (onRequest) { + if (!Array.isArray(onRequest)) onRequest = [onRequest] + + for (const value of onRequest) { + const temp = await value(path, fetchInit) + + if (typeof temp === 'object') + fetchInit = { + ...fetchInit, + ...temp, + headers: { + ...fetchInit.headers, + ...processHeaders( + temp.headers, + path, + fetchInit + ) + } as Record + } + } + } + + const url = domain + path + q + const response = await (elysia?.handle( + new Request(url, fetchInit) + ) ?? fetcher!(url, fetchInit)) + + // @ts-ignore + let data = null + let error = null + + if (onResponse) { + if (!Array.isArray(onResponse)) + onResponse = [onResponse] + + for (const value of onResponse) + try { + const temp = await value(response.clone()) + + if (temp !== undefined && temp !== null) { + data = temp + break + } + } catch (err) { + if (err instanceof EdenFetchError) error = err + else error = new EdenFetchError(422, err) + + break + } + } + + if (data !== null) { + if (config.throw) return data + return { + data, + error, + response, + status: response.status, + headers: response.headers + } + } + + switch ( + response.headers.get('Content-Type')?.split(';')[0] + ) { + case 'text/event-stream': + data = streamResponse(response) + break + + case 'application/json': + data = await response.json() + break + case 'application/octet-stream': + data = await response.arrayBuffer() + break + + case 'multipart/form-data': + const temp = await response.formData() + + data = {} + temp.forEach((value, key) => { + // @ts-ignore + data[key] = value + }) + + break + + default: + data = await response + .text() + .then(parseStringifiedValue) + } + + if (response.status >= 300 || response.status < 200) { + error = new EdenFetchError(response.status, data) + data = null + } + + if (config.throw) { if (error) { throw error } return data } - return { - data, - error, - response, - status: response.status, - headers: response.headers - } - })() - } - - if (typeof body === 'object') - return createProxy( - domain, - config, - [...paths, Object.values(body)[0] as string], - elysia - ) - - return createProxy(domain, config, paths) - } - }) as any + return { + data, + error, + response, + status: response.status, + headers: response.headers + } + })() + } + + if (typeof body === 'object') + return createProxy( + domain, + config, + [...paths, Object.values(body)[0] as string], + elysia + ) + + return createProxy(domain, config, paths) + } + }) as any export const treaty = < - const App extends Elysia, - ShouldThrow extends boolean = false + const App extends Elysia, + ShouldThrow extends boolean = false >( - domain: string | App, - config: Treaty.Config = {} + domain: string | App, + config: Treaty.Config = {} ): Treaty.Create => { - if (typeof domain === 'string') { - if (!config.keepDomain) { - if (!domain.includes('://')) - domain = - (locals.find((v) => (domain as string).includes(v)) - ? 'http://' - : 'https://') + domain - - if (domain.endsWith('/')) domain = domain.slice(0, -1) - } - - return createProxy(domain, config) - } - - if (typeof window !== 'undefined') - console.warn( - 'Elysia instance server found on client side, this is not recommended for security reason. Use generic type instead.' - ) - - return createProxy('http://e.ly', config, [], domain) + if (typeof domain === 'string') { + if (!config.keepDomain) { + if (!domain.includes('://')) + domain = + (locals.find((v) => (domain as string).includes(v)) + ? 'http://' + : 'https://') + domain + + if (domain.endsWith('/')) domain = domain.slice(0, -1) + } + + return createProxy(domain, config) + } + + if (typeof window !== 'undefined') + console.warn( + 'Elysia instance server found on client side, this is not recommended for security reason. Use generic type instead.' + ) + + return createProxy('http://e.ly', config, [], domain) } export type { Treaty } diff --git a/src/treaty2/types.ts b/src/treaty2/types.ts index 8f21a0a..1092613 100644 --- a/src/treaty2/types.ts +++ b/src/treaty2/types.ts @@ -186,7 +186,25 @@ export namespace Treaty { > onResponse?: MaybeArray<(response: Response) => MaybePromise> keepDomain?: boolean, - shouldThrow?: ShouldThrow + /** + * If set to true the calls made by this client will throw an actual error and will not return a response object + * in case of an unsuccessful request. + * Setting this to true reduces the complexity in usage but increases hides away the details of the request. + * @default false + * + * @example + * ```ts + * const userResult = await backend.auth["upsert-self"].post(); + * if (userResult.error) { + * throw userResult.error; + * } + * const user = userResult.data; + * + * // becomes + * const user = await backend.auth["upsert-self"].post(); + * ``` + */ + throw?: ShouldThrow } // type UnwrapAwaited> = { diff --git a/test/treaty2.test.ts b/test/treaty2.test.ts index 7b58224..403d766 100644 --- a/test/treaty2.test.ts +++ b/test/treaty2.test.ts @@ -4,649 +4,649 @@ import { treaty } from '../src' import { describe, expect, it, beforeAll, afterAll, mock } from 'bun:test' const randomObject = { - a: 'a', - b: 2, - c: true, - d: false, - e: null, - f: new Date(0) + a: 'a', + b: 2, + c: true, + d: false, + e: null, + f: new Date(0) } const randomArray = [ - 'a', - 2, - true, - false, - null, - new Date(0), - { a: 'a', b: 2, c: true, d: false, e: null, f: new Date(0) } + 'a', + 2, + true, + false, + null, + new Date(0), + { a: 'a', b: 2, c: true, d: false, e: null, f: new Date(0) } ] const websocketPayloads = [ - // strings - 'str', - // numbers - 1, - 1.2, - // booleans - true, - false, - // null values - null, - // A date - new Date(0), - // A random object - randomObject, - // A random array - randomArray + // strings + 'str', + // numbers + 1, + 1.2, + // booleans + true, + false, + // null values + null, + // A date + new Date(0), + // A random object + randomObject, + // A random array + randomArray ] as const const app = new Elysia() - .get('/', 'a') - .post('/', 'a') - .get('/number', () => 1) - .get('/true', () => true) - .get('/false', () => false) - .post('/array', ({ body }) => body, { - body: t.Array(t.String()) - }) - .post('/mirror', ({ body }) => body) - .post('/body', ({ body }) => body, { - body: t.String() - }) - .delete('/empty', ({ body }) => ({ body: body ?? null })) - .post('/deep/nested/mirror', ({ body }) => body, { - body: t.Object({ - username: t.String(), - password: t.String() - }) - }) - .get('/query', ({ query }) => query, { - query: t.Object({ - username: t.String() - }) - }) - .get('/queries', ({ query }) => query, { - query: t.Object({ - username: t.String(), - alias: t.Literal('Kristen') - }) - }) - .post('/queries', ({ query }) => query, { - query: t.Object({ - username: t.String(), - alias: t.Literal('Kristen') - }) - }) - .head('/queries', ({ query }) => query, { - query: t.Object({ - username: t.String(), - alias: t.Literal('Kristen') - }) - }) - .group('/nested', (app) => app.guard((app) => app.get('/data', () => 'hi'))) - .get('/error', ({ error }) => error("I'm a teapot", 'Kirifuji Nagisa'), { - response: { - 200: t.Void(), - 418: t.Literal('Kirifuji Nagisa'), - 420: t.Literal('Snoop Dogg') - } - }) - .get( - '/headers', - ({ headers: { username, alias } }) => ({ username, alias }), - { - headers: t.Object({ - username: t.String(), - alias: t.Literal('Kristen') - }) - } - ) - .post( - '/headers', - ({ headers: { username, alias } }) => ({ username, alias }), - { - headers: t.Object({ - username: t.String(), - alias: t.Literal('Kristen') - }) - } - ) - .get( - '/headers-custom', - ({ headers, headers: { username, alias } }) => ({ - username, - alias, - 'x-custom': headers['x-custom'] - }), - { - headers: t.Object({ - username: t.String(), - alias: t.Literal('Kristen'), - 'x-custom': t.Optional(t.Literal('custom')) - }) - } - ) - .post('/date', ({ body: { date } }) => date, { - body: t.Object({ - date: t.Date() - }) - }) - .get('/dateObject', () => ({ date: new Date() })) - .get('/redirect', ({ redirect }) => redirect('http://localhost:8083/true')) - .post( - '/redirect', - ({ redirect }) => redirect('http://localhost:8083/true'), - { - body: t.Object({ - username: t.String() - }) - } - ) - .get('/formdata', () => - form({ - image: Bun.file('./test/kyuukurarin.mp4') - }) - ) - .ws('/json-serialization-deserialization', { - open: async (ws) => { - for (const item of websocketPayloads) { - ws.send(item) - } - ws.close() - } - }) - .get('/stream', function* stream() { - yield 'a' - yield 'b' - yield 'c' - }) - .get('/stream-async', async function* stream() { - yield 'a' - yield 'b' - yield 'c' - }) - .get('/stream-return', function* stream() { - return 'a' - }) - .get('/stream-return-async', function* stream() { - return 'a' - }) - .get('/id/:id?', ({ params: { id = 'unknown' } }) => id) + .get('/', 'a') + .post('/', 'a') + .get('/number', () => 1) + .get('/true', () => true) + .get('/false', () => false) + .post('/array', ({ body }) => body, { + body: t.Array(t.String()) + }) + .post('/mirror', ({ body }) => body) + .post('/body', ({ body }) => body, { + body: t.String() + }) + .delete('/empty', ({ body }) => ({ body: body ?? null })) + .post('/deep/nested/mirror', ({ body }) => body, { + body: t.Object({ + username: t.String(), + password: t.String() + }) + }) + .get('/query', ({ query }) => query, { + query: t.Object({ + username: t.String() + }) + }) + .get('/queries', ({ query }) => query, { + query: t.Object({ + username: t.String(), + alias: t.Literal('Kristen') + }) + }) + .post('/queries', ({ query }) => query, { + query: t.Object({ + username: t.String(), + alias: t.Literal('Kristen') + }) + }) + .head('/queries', ({ query }) => query, { + query: t.Object({ + username: t.String(), + alias: t.Literal('Kristen') + }) + }) + .group('/nested', (app) => app.guard((app) => app.get('/data', () => 'hi'))) + .get('/error', ({ error }) => error("I'm a teapot", 'Kirifuji Nagisa'), { + response: { + 200: t.Void(), + 418: t.Literal('Kirifuji Nagisa'), + 420: t.Literal('Snoop Dogg') + } + }) + .get( + '/headers', + ({ headers: { username, alias } }) => ({ username, alias }), + { + headers: t.Object({ + username: t.String(), + alias: t.Literal('Kristen') + }) + } + ) + .post( + '/headers', + ({ headers: { username, alias } }) => ({ username, alias }), + { + headers: t.Object({ + username: t.String(), + alias: t.Literal('Kristen') + }) + } + ) + .get( + '/headers-custom', + ({ headers, headers: { username, alias } }) => ({ + username, + alias, + 'x-custom': headers['x-custom'] + }), + { + headers: t.Object({ + username: t.String(), + alias: t.Literal('Kristen'), + 'x-custom': t.Optional(t.Literal('custom')) + }) + } + ) + .post('/date', ({ body: { date } }) => date, { + body: t.Object({ + date: t.Date() + }) + }) + .get('/dateObject', () => ({ date: new Date() })) + .get('/redirect', ({ redirect }) => redirect('http://localhost:8083/true')) + .post( + '/redirect', + ({ redirect }) => redirect('http://localhost:8083/true'), + { + body: t.Object({ + username: t.String() + }) + } + ) + .get('/formdata', () => + form({ + image: Bun.file('./test/kyuukurarin.mp4') + }) + ) + .ws('/json-serialization-deserialization', { + open: async (ws) => { + for (const item of websocketPayloads) { + ws.send(item) + } + ws.close() + } + }) + .get('/stream', function* stream() { + yield 'a' + yield 'b' + yield 'c' + }) + .get('/stream-async', async function* stream() { + yield 'a' + yield 'b' + yield 'c' + }) + .get('/stream-return', function* stream() { + return 'a' + }) + .get('/stream-return-async', function* stream() { + return 'a' + }) + .get('/id/:id?', ({ params: { id = 'unknown' } }) => id) const client = treaty(app) -const throwingClient = treaty(app, { shouldThrow: true }) +const throwingClient = treaty(app, { throw: true }) describe('Treaty2', () => { - it('get index', async () => { - const { data, error } = await client.index.get() + it('get index', async () => { + const { data, error } = await client.index.get() + + expect(data).toBe('a') + expect(error).toBeNull() + }) + + it('post index', async () => { + const { data, error } = await client.index.post() + + expect(data).toBe('a') + expect(error).toBeNull() + }) + + it('parse number', async () => { + const { data } = await client.number.get() + + expect(data).toEqual(1) + }) + + it('parse true', async () => { + const { data } = await client.true.get() + + expect(data).toEqual(true) + }) + + it('parse false', async () => { + const { data } = await client.false.get() + + expect(data).toEqual(false) + }) + + it.todo('parse object with date', async () => { + const { data } = await client.dateObject.get() + + expect(data?.date).toBeInstanceOf(Date) + }) + + it('post array', async () => { + const { data } = await client.array.post(['a', 'b']) + + expect(data).toEqual(['a', 'b']) + }) + + it('post body', async () => { + const { data } = await client.body.post('a') + + expect(data).toEqual('a') + }) + + it('post mirror', async () => { + const body = { username: 'A', password: 'B' } + + const { data } = await client.mirror.post(body) + + expect(data).toEqual(body) + }) + + it('delete empty', async () => { + const { data } = await client.empty.delete() + + expect(data).toEqual({ body: null }) + }) + + it('post deep nested mirror', async () => { + const body = { username: 'A', password: 'B' } + + const { data } = await client.deep.nested.mirror.post(body) - expect(data).toBe('a') - expect(error).toBeNull() - }) + expect(data).toEqual(body) + }) - it('post index', async () => { - const { data, error } = await client.index.post() + it('get query', async () => { + const query = { username: 'A' } - expect(data).toBe('a') - expect(error).toBeNull() - }) + const { data } = await client.query.get({ + query + }) - it('parse number', async () => { - const { data } = await client.number.get() + expect(data).toEqual(query) + }) - expect(data).toEqual(1) - }) + it('get queries', async () => { + const query = { username: 'A', alias: 'Kristen' } as const - it('parse true', async () => { - const { data } = await client.true.get() + const { data } = await client.queries.get({ + query + }) - expect(data).toEqual(true) - }) + expect(data).toEqual(query) + }) - it('parse false', async () => { - const { data } = await client.false.get() + it('post queries', async () => { + const query = { username: 'A', alias: 'Kristen' } as const - expect(data).toEqual(false) - }) + const { data } = await client.queries.post(null, { + query + }) - it.todo('parse object with date', async () => { - const { data } = await client.dateObject.get() + expect(data).toEqual(query) + }) - expect(data?.date).toBeInstanceOf(Date) - }) + it('head queries', async () => { + const query = { username: 'A', alias: 'Kristen' } as const - it('post array', async () => { - const { data } = await client.array.post(['a', 'b']) + const { data } = await client.queries.post(null, { + query + }) - expect(data).toEqual(['a', 'b']) - }) + expect(data).toEqual(query) + }) - it('post body', async () => { - const { data } = await client.body.post('a') + it('get nested data', async () => { + const { data } = await client.nested.data.get() - expect(data).toEqual('a') - }) + expect(data).toEqual('hi') + }) - it('post mirror', async () => { - const body = { username: 'A', password: 'B' } + it('handle error', async () => { + const { data, error } = await client.error.get() - const { data } = await client.mirror.post(body) + let value - expect(data).toEqual(body) - }) + if (error) + switch (error.status) { + case 418: + value = error.value + break - it('delete empty', async () => { - const { data } = await client.empty.delete() + case 420: + value = error.value + break + } - expect(data).toEqual({ body: null }) - }) + expect(data).toBeNull() + expect(value).toEqual('Kirifuji Nagisa') + }) - it('post deep nested mirror', async () => { - const body = { username: 'A', password: 'B' } + it('get headers', async () => { + const headers = { username: 'A', alias: 'Kristen' } as const - const { data } = await client.deep.nested.mirror.post(body) + const { data } = await client.headers.get({ + headers + }) + + expect(data).toEqual(headers) + }) + + it('post headers', async () => { + const headers = { username: 'A', alias: 'Kristen' } as const + + const { data } = await client.headers.post(null, { + headers + }) + + expect(data).toEqual(headers) + }) - expect(data).toEqual(body) - }) + it('handle interception', async () => { + const client = treaty(app, { + onRequest(path) { + if (path === '/headers-custom') + return { + headers: { + 'x-custom': 'custom' + } + } + }, + async onResponse(response) { + return { intercepted: true, data: await response.json() } + } + }) + + const headers = { username: 'a', alias: 'Kristen' } as const + + const { data } = await client['headers-custom'].get({ + headers + }) + + expect(data).toEqual({ + // @ts-expect-error + intercepted: true, + data: { + ...headers, + 'x-custom': 'custom' + } + }) + }) - it('get query', async () => { - const query = { username: 'A' } + it('handle interception array', async () => { + const client = treaty(app, { + onRequest: [ + () => ({ + headers: { + 'x-custom': 'a' + } + }), + () => ({ + headers: { + 'x-custom': 'custom' + } + }) + ], + onResponse: [ + () => {}, + async (response) => { + return { intercepted: true, data: await response.json() } + } + ] + }) + + const headers = { username: 'a', alias: 'Kristen' } as const + + const { data } = await client['headers-custom'].get({ + headers + }) + + expect(data).toEqual({ + // @ts-expect-error + intercepted: true, + data: { + ...headers, + 'x-custom': 'custom' + } + }) + }) - const { data } = await client.query.get({ - query - }) + it('accept headers configuration', async () => { + const client = treaty(app, { + headers(path) { + if (path === '/headers-custom') + return { + 'x-custom': 'custom' + } + }, + async onResponse(response) { + return { intercepted: true, data: await response.json() } + } + }) + + const headers = { username: 'a', alias: 'Kristen' } as const + + const { data } = await client['headers-custom'].get({ + headers + }) + + expect(data).toEqual({ + // @ts-expect-error + intercepted: true, + data: { + ...headers, + 'x-custom': 'custom' + } + }) + }) - expect(data).toEqual(query) - }) + it('accept headers configuration array', async () => { + const client = treaty(app, { + headers: [ + (path) => { + if (path === '/headers-custom') + return { + 'x-custom': 'custom' + } + } + ], + async onResponse(response) { + return { intercepted: true, data: await response.json() } + } + }) + + const headers = { username: 'a', alias: 'Kristen' } as const + + const { data } = await client['headers-custom'].get({ + headers + }) + + expect(data).toEqual({ + // @ts-expect-error + intercepted: true, + data: { + ...headers, + 'x-custom': 'custom' + } + }) + }) - it('get queries', async () => { - const query = { username: 'A', alias: 'Kristen' } as const + it('send date', async () => { + const { data } = await client.date.post({ date: new Date() }) - const { data } = await client.queries.get({ - query - }) + expect(data).toBeInstanceOf(Date) + }) - expect(data).toEqual(query) - }) + it('redirect should set location header', async () => { + const { headers, status } = await client['redirect'].get({ + fetch: { + redirect: 'manual' + } + }) + expect(status).toEqual(302) + expect(new Headers(headers).get('location')).toEqual( + 'http://localhost:8083/true' + ) + }) - it('post queries', async () => { - const query = { username: 'A', alias: 'Kristen' } as const + it('generator return stream', async () => { + const a = await client.stream.get() + const result = [] - const { data } = await client.queries.post(null, { - query - }) + for await (const chunk of a.data!) result.push(chunk) - expect(data).toEqual(query) - }) + expect(result).toEqual(['a', 'b', 'c']) + }) - it('head queries', async () => { - const query = { username: 'A', alias: 'Kristen' } as const + it('generator return async stream', async () => { + const a = await client['stream-async'].get() + const result = [] - const { data } = await client.queries.post(null, { - query - }) + for await (const chunk of a.data!) result.push(chunk) - expect(data).toEqual(query) - }) + expect(result).toEqual(['a', 'b', 'c']) + }) - it('get nested data', async () => { - const { data } = await client.nested.data.get() + it('generator return value', async () => { + const a = await client['stream-return'].get() - expect(data).toEqual('hi') - }) + expect(a.data).toBe('a') + }) - it('handle error', async () => { - const { data, error } = await client.error.get() + it('generator return async value', async () => { + const a = await client['stream-return-async'].get() - let value + expect(a.data).toBe('a') + }) - if (error) - switch (error.status) { - case 418: - value = error.value - break - - case 420: - value = error.value - break - } - - expect(data).toBeNull() - expect(value).toEqual('Kirifuji Nagisa') - }) - - it('get headers', async () => { - const headers = { username: 'A', alias: 'Kristen' } as const - - const { data } = await client.headers.get({ - headers - }) - - expect(data).toEqual(headers) - }) - - it('post headers', async () => { - const headers = { username: 'A', alias: 'Kristen' } as const - - const { data } = await client.headers.post(null, { - headers - }) - - expect(data).toEqual(headers) - }) - - it('handle interception', async () => { - const client = treaty(app, { - onRequest(path) { - if (path === '/headers-custom') - return { - headers: { - 'x-custom': 'custom' - } - } - }, - async onResponse(response) { - return { intercepted: true, data: await response.json() } - } - }) - - const headers = { username: 'a', alias: 'Kristen' } as const - - const { data } = await client['headers-custom'].get({ - headers - }) - - expect(data).toEqual({ - // @ts-expect-error - intercepted: true, - data: { - ...headers, - 'x-custom': 'custom' - } - }) - }) - - it('handle interception array', async () => { - const client = treaty(app, { - onRequest: [ - () => ({ - headers: { - 'x-custom': 'a' - } - }), - () => ({ - headers: { - 'x-custom': 'custom' - } - }) - ], - onResponse: [ - () => {}, - async (response) => { - return { intercepted: true, data: await response.json() } - } - ] - }) - - const headers = { username: 'a', alias: 'Kristen' } as const - - const { data } = await client['headers-custom'].get({ - headers - }) - - expect(data).toEqual({ - // @ts-expect-error - intercepted: true, - data: { - ...headers, - 'x-custom': 'custom' - } - }) - }) - - it('accept headers configuration', async () => { - const client = treaty(app, { - headers(path) { - if (path === '/headers-custom') - return { - 'x-custom': 'custom' - } - }, - async onResponse(response) { - return { intercepted: true, data: await response.json() } - } - }) - - const headers = { username: 'a', alias: 'Kristen' } as const - - const { data } = await client['headers-custom'].get({ - headers - }) - - expect(data).toEqual({ - // @ts-expect-error - intercepted: true, - data: { - ...headers, - 'x-custom': 'custom' - } - }) - }) - - it('accept headers configuration array', async () => { - const client = treaty(app, { - headers: [ - (path) => { - if (path === '/headers-custom') - return { - 'x-custom': 'custom' - } - } - ], - async onResponse(response) { - return { intercepted: true, data: await response.json() } - } - }) - - const headers = { username: 'a', alias: 'Kristen' } as const - - const { data } = await client['headers-custom'].get({ - headers - }) - - expect(data).toEqual({ - // @ts-expect-error - intercepted: true, - data: { - ...headers, - 'x-custom': 'custom' - } - }) - }) - - it('send date', async () => { - const { data } = await client.date.post({ date: new Date() }) - - expect(data).toBeInstanceOf(Date) - }) - - it('redirect should set location header', async () => { - const { headers, status } = await client['redirect'].get({ - fetch: { - redirect: 'manual' - } - }) - expect(status).toEqual(302) - expect(new Headers(headers).get('location')).toEqual( - 'http://localhost:8083/true' - ) - }) - - it('generator return stream', async () => { - const a = await client.stream.get() - const result = [] - - for await (const chunk of a.data!) result.push(chunk) - - expect(result).toEqual(['a', 'b', 'c']) - }) - - it('generator return async stream', async () => { - const a = await client['stream-async'].get() - const result = [] - - for await (const chunk of a.data!) result.push(chunk) - - expect(result).toEqual(['a', 'b', 'c']) - }) - - it('generator return value', async () => { - const a = await client['stream-return'].get() - - expect(a.data).toBe('a') - }) - - it('generator return async value', async () => { - const a = await client['stream-return-async'].get() - - expect(a.data).toBe('a') - }) - - it('handle optional params', async () => { - const data = await Promise.all([ - client.id.get(), - client.id({ id: 'salty' }).get() - ]) - expect(data.map((x) => x.data)).toEqual(['unknown', 'salty']) - }) + it('handle optional params', async () => { + const data = await Promise.all([ + client.id.get(), + client.id({ id: 'salty' }).get() + ]) + expect(data.map((x) => x.data)).toEqual(['unknown', 'salty']) + }) }) describe('Treaty2 - Using endpoint URL', () => { - const treatyApp = treaty('http://localhost:8083') - - beforeAll(async () => { - await new Promise((resolve) => { - app.listen(8083, () => { - resolve(null) - }) - }) - }) - - afterAll(() => { - app.stop() - }) - - it('redirect should set location header', async () => { - const { headers, status } = await treatyApp.redirect.get({ - fetch: { - redirect: 'manual' - } - }) - expect(status).toEqual(302) - expect(new Headers(headers).get('location')).toEqual( - 'http://localhost:8083/true' - ) - }) - - it('redirect should set location header with post', async () => { - const { headers, status } = await treatyApp.redirect.post( - { - username: 'a' - }, - { - fetch: { - redirect: 'manual' - } - } - ) - expect(status).toEqual(302) - expect(new Headers(headers).get('location')).toEqual( - 'http://localhost:8083/true' - ) - }) - - it('get formdata', async () => { - const { data } = await treatyApp.formdata.get() - - expect(data!.image.size).toBeGreaterThan(0) - }) - - it("doesn't encode if it doesn't need to", async () => { - const mockedFetch: any = mock((url: string) => { - return new Response(url) - }) - - const client = treaty('localhost', { fetcher: mockedFetch }) - - const { data } = await client.index.get({ - query: { - hello: 'world' - } - }) - - expect(data).toEqual('http://localhost/?hello=world' as any) - }) - - it('encodes query parameters if it needs to', async () => { - const mockedFetch: any = mock((url: string) => { - return new Response(url) - }) - - const client = treaty('localhost', { fetcher: mockedFetch }) - - const { data } = await client.index.get({ - query: { - ['1/2']: '1/2' - } - }) - - expect(data).toEqual('http://localhost/?1%2F2=1%2F2' as any) - }) - - it('accepts and serializes several values for the same query parameter', async () => { - const mockedFetch: any = mock((url: string) => { - return new Response(url) - }) - - const client = treaty('localhost', { fetcher: mockedFetch }) - - const { data } = await client.index.get({ - query: { - ['1/2']: ['1/2', '1 2'] - } - }) - - expect(data).toEqual('http://localhost/?1%2F2=1%2F2&1%2F2=1%202' as any) - }) - - it('Receives the proper objects back from the other end of the websocket', async (done) => { - app.listen(8080, async () => { - const client = treaty('http://localhost:8080') - - const dataOutOfSocket = await new Promise((res) => { - const data: any = [] - // Wait until we've gotten all the data - const socket = - client['json-serialization-deserialization'].subscribe() - socket.subscribe(({ data: dataItem }) => { - data.push(dataItem) - // Only continue when we got all the messages - if (data.length === websocketPayloads.length) { - res(data) - } - }) - }) - - // expect that everything that came out of the socket - // got deserialized into the same thing that we inteded to send - for (let i = 0; i < websocketPayloads.length; i++) { - expect(dataOutOfSocket[i]).toEqual(websocketPayloads[i]) - } - - done() - }) - }) - - it('return concise response', async () => { + const treatyApp = treaty('http://localhost:8083') + + beforeAll(async () => { + await new Promise((resolve) => { + app.listen(8083, () => { + resolve(null) + }) + }) + }) + + afterAll(() => { + app.stop() + }) + + it('redirect should set location header', async () => { + const { headers, status } = await treatyApp.redirect.get({ + fetch: { + redirect: 'manual' + } + }) + expect(status).toEqual(302) + expect(new Headers(headers).get('location')).toEqual( + 'http://localhost:8083/true' + ) + }) + + it('redirect should set location header with post', async () => { + const { headers, status } = await treatyApp.redirect.post( + { + username: 'a' + }, + { + fetch: { + redirect: 'manual' + } + } + ) + expect(status).toEqual(302) + expect(new Headers(headers).get('location')).toEqual( + 'http://localhost:8083/true' + ) + }) + + it('get formdata', async () => { + const { data } = await treatyApp.formdata.get() + + expect(data!.image.size).toBeGreaterThan(0) + }) + + it("doesn't encode if it doesn't need to", async () => { + const mockedFetch: any = mock((url: string) => { + return new Response(url) + }) + + const client = treaty('localhost', { fetcher: mockedFetch }) + + const { data } = await client.index.get({ + query: { + hello: 'world' + } + }) + + expect(data).toEqual('http://localhost/?hello=world' as any) + }) + + it('encodes query parameters if it needs to', async () => { + const mockedFetch: any = mock((url: string) => { + return new Response(url) + }) + + const client = treaty('localhost', { fetcher: mockedFetch }) + + const { data } = await client.index.get({ + query: { + ['1/2']: '1/2' + } + }) + + expect(data).toEqual('http://localhost/?1%2F2=1%2F2' as any) + }) + + it('accepts and serializes several values for the same query parameter', async () => { + const mockedFetch: any = mock((url: string) => { + return new Response(url) + }) + + const client = treaty('localhost', { fetcher: mockedFetch }) + + const { data } = await client.index.get({ + query: { + ['1/2']: ['1/2', '1 2'] + } + }) + + expect(data).toEqual('http://localhost/?1%2F2=1%2F2&1%2F2=1%202' as any) + }) + + it('Receives the proper objects back from the other end of the websocket', async (done) => { + app.listen(8080, async () => { + const client = treaty('http://localhost:8080') + + const dataOutOfSocket = await new Promise((res) => { + const data: any = [] + // Wait until we've gotten all the data + const socket = + client['json-serialization-deserialization'].subscribe() + socket.subscribe(({ data: dataItem }) => { + data.push(dataItem) + // Only continue when we got all the messages + if (data.length === websocketPayloads.length) { + res(data) + } + }) + }) + + // expect that everything that came out of the socket + // got deserialized into the same thing that we inteded to send + for (let i = 0; i < websocketPayloads.length; i++) { + expect(dataOutOfSocket[i]).toEqual(websocketPayloads[i]) + } + + done() + }) + }) + + it('return concise response', async () => { const r = await throwingClient.index.get() expect(r).toBe('a') diff --git a/test/types/treaty2.ts b/test/types/treaty2.ts index 75172a1..b58c7db 100644 --- a/test/types/treaty2.ts +++ b/test/types/treaty2.ts @@ -3,1039 +3,1039 @@ import { treaty } from '../../src' import { expectTypeOf } from 'expect-type' const plugin = new Elysia({ prefix: '/level' }) - .get('/', '2') - .get('/level', '2') - .get('/:id', ({ params: { id } }) => id) - .get('/:id/ok', ({ params: { id } }) => id) + .get('/', '2') + .get('/level', '2') + .get('/:id', ({ params: { id } }) => id) + .get('/:id/ok', ({ params: { id } }) => id) const app = new Elysia() - .get('/', 'a') - .post('/', 'a') - .get('/number', () => 1 as const) - .get('/true', () => true) - .post('/array', ({ body }) => body, { - body: t.Array(t.String()) - }) - .post('/mirror', ({ body }) => body) - .post('/body', ({ body }) => body, { - body: t.String() - }) - .post('/deep/nested/mirror', ({ body }) => body, { - body: t.Object({ - username: t.String(), - password: t.String() - }) - }) - .get('/query', ({ query }) => query, { - query: t.Object({ - username: t.String() - }) - }) - .get('/queries', ({ query }) => query, { - query: t.Object({ - username: t.String(), - alias: t.Literal('Kristen') - }) - }) - .post('/queries', ({ query }) => query, { - query: t.Object({ - username: t.String(), - alias: t.Literal('Kristen') - }) - }) - .head('/queries', ({ query }) => query, { - query: t.Object({ - username: t.String(), - alias: t.Literal('Kristen') - }) - }) - .group('/nested', (app) => app.guard((app) => app.get('/data', () => 'hi'))) - .get('/error', ({ error }) => error("I'm a teapot", 'Kirifuji Nagisa'), { - response: { - 200: t.Void(), - 418: t.Literal('Kirifuji Nagisa'), - 420: t.Literal('Snoop Dogg') - } - }) - .get( - '/headers', - ({ headers: { username, alias } }) => ({ username, alias }), - { - headers: t.Object({ - username: t.String(), - alias: t.Literal('Kristen') - }) - } - ) - .post( - '/headers', - ({ headers: { username, alias } }) => ({ username, alias }), - { - headers: t.Object({ - username: t.String(), - alias: t.Literal('Kristen') - }) - } - ) - .get( - '/queries-headers', - ({ headers: { username, alias } }) => ({ username, alias }), - { - query: t.Object({ - username: t.String(), - alias: t.Literal('Kristen') - }), - headers: t.Object({ - username: t.String(), - alias: t.Literal('Kristen') - }) - } - ) - .post( - '/queries-headers', - ({ headers: { username, alias } }) => ({ username, alias }), - { - query: t.Object({ - username: t.String(), - alias: t.Literal('Kristen') - }), - headers: t.Object({ - username: t.String(), - alias: t.Literal('Kristen') - }) - } - ) - .post( - '/body-queries-headers', - ({ headers: { username, alias } }) => ({ username, alias }), - { - body: t.Object({ - username: t.String(), - alias: t.Literal('Kristen') - }), - query: t.Object({ - username: t.String(), - alias: t.Literal('Kristen') - }), - headers: t.Object({ - username: t.String(), - alias: t.Literal('Kristen') - }) - } - ) - .get('/async', async ({ error }) => { - if (Math.random() > 0.5) return error(418, 'Nagisa') - if (Math.random() > 0.5) return error(401, 'Himari') - - return 'Hifumi' - }) - .use(plugin) + .get('/', 'a') + .post('/', 'a') + .get('/number', () => 1 as const) + .get('/true', () => true) + .post('/array', ({ body }) => body, { + body: t.Array(t.String()) + }) + .post('/mirror', ({ body }) => body) + .post('/body', ({ body }) => body, { + body: t.String() + }) + .post('/deep/nested/mirror', ({ body }) => body, { + body: t.Object({ + username: t.String(), + password: t.String() + }) + }) + .get('/query', ({ query }) => query, { + query: t.Object({ + username: t.String() + }) + }) + .get('/queries', ({ query }) => query, { + query: t.Object({ + username: t.String(), + alias: t.Literal('Kristen') + }) + }) + .post('/queries', ({ query }) => query, { + query: t.Object({ + username: t.String(), + alias: t.Literal('Kristen') + }) + }) + .head('/queries', ({ query }) => query, { + query: t.Object({ + username: t.String(), + alias: t.Literal('Kristen') + }) + }) + .group('/nested', (app) => app.guard((app) => app.get('/data', () => 'hi'))) + .get('/error', ({ error }) => error("I'm a teapot", 'Kirifuji Nagisa'), { + response: { + 200: t.Void(), + 418: t.Literal('Kirifuji Nagisa'), + 420: t.Literal('Snoop Dogg') + } + }) + .get( + '/headers', + ({ headers: { username, alias } }) => ({ username, alias }), + { + headers: t.Object({ + username: t.String(), + alias: t.Literal('Kristen') + }) + } + ) + .post( + '/headers', + ({ headers: { username, alias } }) => ({ username, alias }), + { + headers: t.Object({ + username: t.String(), + alias: t.Literal('Kristen') + }) + } + ) + .get( + '/queries-headers', + ({ headers: { username, alias } }) => ({ username, alias }), + { + query: t.Object({ + username: t.String(), + alias: t.Literal('Kristen') + }), + headers: t.Object({ + username: t.String(), + alias: t.Literal('Kristen') + }) + } + ) + .post( + '/queries-headers', + ({ headers: { username, alias } }) => ({ username, alias }), + { + query: t.Object({ + username: t.String(), + alias: t.Literal('Kristen') + }), + headers: t.Object({ + username: t.String(), + alias: t.Literal('Kristen') + }) + } + ) + .post( + '/body-queries-headers', + ({ headers: { username, alias } }) => ({ username, alias }), + { + body: t.Object({ + username: t.String(), + alias: t.Literal('Kristen') + }), + query: t.Object({ + username: t.String(), + alias: t.Literal('Kristen') + }), + headers: t.Object({ + username: t.String(), + alias: t.Literal('Kristen') + }) + } + ) + .get('/async', async ({ error }) => { + if (Math.random() > 0.5) return error(418, 'Nagisa') + if (Math.random() > 0.5) return error(401, 'Himari') + + return 'Hifumi' + }) + .use(plugin) const api = treaty(app) -const throwingApi = treaty(app, { shouldThrow: true }) +const throwingApi = treaty(app, { throw: true }) type api = typeof api type throwingApi = typeof throwingApi type Result = T extends (...args: any[]) => infer R - ? Awaited - : never + ? Awaited + : never type ValidationError = { - data: null - error: { - status: 422 - value: { - type: 'validation' - on: string - summary?: string - message?: string - found?: unknown - property?: string - expected?: string - } - } - response: Response - status: number - headers: FetchRequestInit['headers'] + data: null + error: { + status: 422 + value: { + type: 'validation' + on: string + summary?: string + message?: string + found?: unknown + property?: string + expected?: string + } + } + response: Response + status: number + headers: FetchRequestInit['headers'] } // ? Get should have 1 parameter and is optional when no parameter is defined { - type Route = api['index']['get'] - - expectTypeOf().parameter(0).toEqualTypeOf< - | { - headers?: Record | undefined - query?: Record | undefined - fetch?: RequestInit | undefined - } - | undefined - >() - - expectTypeOf().parameter(1).toBeUndefined() - - type Res = Result - - expectTypeOf().toEqualTypeOf< - | { - data: 'a' - error: null - response: Response - status: number - headers: HeadersInit | undefined - } - | { - data: null - error: { - status: unknown - value: unknown - } - response: Response - status: number - headers: HeadersInit | undefined - } - >() + type Route = api['index']['get'] + + expectTypeOf().parameter(0).toEqualTypeOf< + | { + headers?: Record | undefined + query?: Record | undefined + fetch?: RequestInit | undefined + } + | undefined + >() + + expectTypeOf().parameter(1).toBeUndefined() + + type Res = Result + + expectTypeOf().toEqualTypeOf< + | { + data: 'a' + error: null + response: Response + status: number + headers: HeadersInit | undefined + } + | { + data: null + error: { + status: unknown + value: unknown + } + response: Response + status: number + headers: HeadersInit | undefined + } + >() } // ? Non-get should have 2 parameter and is optional when no parameter is defined { - type Route = api['index']['post'] - - expectTypeOf().parameter(0).toBeUnknown() - - expectTypeOf().parameter(1).toEqualTypeOf< - | { - headers?: Record | undefined - query?: Record | undefined - fetch?: RequestInit | undefined - } - | undefined - >() - - type Res = Result - - expectTypeOf().toEqualTypeOf< - | { - data: 'a' - error: null - response: Response - status: number - headers: HeadersInit | undefined - } - | { - data: null - error: { - status: unknown - value: unknown - } - response: Response - status: number - headers: HeadersInit | undefined - } - >() + type Route = api['index']['post'] + + expectTypeOf().parameter(0).toBeUnknown() + + expectTypeOf().parameter(1).toEqualTypeOf< + | { + headers?: Record | undefined + query?: Record | undefined + fetch?: RequestInit | undefined + } + | undefined + >() + + type Res = Result + + expectTypeOf().toEqualTypeOf< + | { + data: 'a' + error: null + response: Response + status: number + headers: HeadersInit | undefined + } + | { + data: null + error: { + status: unknown + value: unknown + } + response: Response + status: number + headers: HeadersInit | undefined + } + >() } // ? Should return literal { - type Route = api['number']['get'] - - expectTypeOf().parameter(0).toEqualTypeOf< - | { - headers?: Record | undefined - query?: Record | undefined - fetch?: RequestInit | undefined - } - | undefined - >() - - expectTypeOf().parameter(1).toBeUndefined() - - type Res = Result - - expectTypeOf().toEqualTypeOf< - | { - data: 1 - error: null - response: Response - status: number - headers: HeadersInit | undefined - } - | { - data: null - error: { - status: unknown - value: unknown - } - response: Response - status: number - headers: HeadersInit | undefined - } - >() + type Route = api['number']['get'] + + expectTypeOf().parameter(0).toEqualTypeOf< + | { + headers?: Record | undefined + query?: Record | undefined + fetch?: RequestInit | undefined + } + | undefined + >() + + expectTypeOf().parameter(1).toBeUndefined() + + type Res = Result + + expectTypeOf().toEqualTypeOf< + | { + data: 1 + error: null + response: Response + status: number + headers: HeadersInit | undefined + } + | { + data: null + error: { + status: unknown + value: unknown + } + response: Response + status: number + headers: HeadersInit | undefined + } + >() } // ? Should return boolean { - type Route = api['true']['get'] - - expectTypeOf().parameter(0).toEqualTypeOf< - | { - headers?: Record | undefined - query?: Record | undefined - fetch?: RequestInit | undefined - } - | undefined - >() - - expectTypeOf().parameter(1).toBeUndefined() - - type Res = Result - - expectTypeOf().toEqualTypeOf< - | { - data: boolean - error: null - response: Response - status: number - headers: HeadersInit | undefined - } - | { - data: null - error: { - status: unknown - value: unknown - } - response: Response - status: number - headers: HeadersInit | undefined - } - >() + type Route = api['true']['get'] + + expectTypeOf().parameter(0).toEqualTypeOf< + | { + headers?: Record | undefined + query?: Record | undefined + fetch?: RequestInit | undefined + } + | undefined + >() + + expectTypeOf().parameter(1).toBeUndefined() + + type Res = Result + + expectTypeOf().toEqualTypeOf< + | { + data: boolean + error: null + response: Response + status: number + headers: HeadersInit | undefined + } + | { + data: null + error: { + status: unknown + value: unknown + } + response: Response + status: number + headers: HeadersInit | undefined + } + >() } // ? Should return array of string { - type Route = api['array']['post'] - - expectTypeOf().parameter(0).toEqualTypeOf() - - expectTypeOf().parameter(1).toEqualTypeOf< - | { - headers?: Record | undefined - query?: Record | undefined - fetch?: RequestInit | undefined - } - | undefined - >() - - type Res = Result - - expectTypeOf().toEqualTypeOf< - | { - data: string[] - error: null - response: Response - status: number - headers: FetchRequestInit['headers'] - } - | ValidationError - >() + type Route = api['array']['post'] + + expectTypeOf().parameter(0).toEqualTypeOf() + + expectTypeOf().parameter(1).toEqualTypeOf< + | { + headers?: Record | undefined + query?: Record | undefined + fetch?: RequestInit | undefined + } + | undefined + >() + + type Res = Result + + expectTypeOf().toEqualTypeOf< + | { + data: string[] + error: null + response: Response + status: number + headers: FetchRequestInit['headers'] + } + | ValidationError + >() } // ? Should return body { - type Route = api['mirror']['post'] - - expectTypeOf().parameter(0).toBeUnknown() - - expectTypeOf().parameter(1).toEqualTypeOf< - | { - headers?: Record | undefined - query?: Record | undefined - fetch?: RequestInit | undefined - } - | undefined - >() - type Res = Result - - expectTypeOf().toEqualTypeOf< - | { - data: unknown - error: null - response: Response - status: number - headers: HeadersInit | undefined - } - | { - data: null - error: { - status: unknown - value: unknown - } - response: Response - status: number - headers: HeadersInit | undefined - } - >() + type Route = api['mirror']['post'] + + expectTypeOf().parameter(0).toBeUnknown() + + expectTypeOf().parameter(1).toEqualTypeOf< + | { + headers?: Record | undefined + query?: Record | undefined + fetch?: RequestInit | undefined + } + | undefined + >() + type Res = Result + + expectTypeOf().toEqualTypeOf< + | { + data: unknown + error: null + response: Response + status: number + headers: HeadersInit | undefined + } + | { + data: null + error: { + status: unknown + value: unknown + } + response: Response + status: number + headers: HeadersInit | undefined + } + >() } // ? Should return body { - type Route = api['body']['post'] - - expectTypeOf().parameter(0).toEqualTypeOf() - - expectTypeOf().parameter(1).toEqualTypeOf< - | { - headers?: Record | undefined - query?: Record | undefined - fetch?: RequestInit | undefined - } - | undefined - >() - type Res = Result - - expectTypeOf().toEqualTypeOf< - | { - data: string - error: null - response: Response - status: number - headers: FetchRequestInit['headers'] - } - | { - data: null - error: { - status: 422 - value: { - type: 'validation' - on: string - summary?: string - message?: string - found?: unknown - property?: string - expected?: string - } - } - response: Response - status: number - headers: FetchRequestInit['headers'] - } - >() + type Route = api['body']['post'] + + expectTypeOf().parameter(0).toEqualTypeOf() + + expectTypeOf().parameter(1).toEqualTypeOf< + | { + headers?: Record | undefined + query?: Record | undefined + fetch?: RequestInit | undefined + } + | undefined + >() + type Res = Result + + expectTypeOf().toEqualTypeOf< + | { + data: string + error: null + response: Response + status: number + headers: FetchRequestInit['headers'] + } + | { + data: null + error: { + status: 422 + value: { + type: 'validation' + on: string + summary?: string + message?: string + found?: unknown + property?: string + expected?: string + } + } + response: Response + status: number + headers: FetchRequestInit['headers'] + } + >() } // ? Should return body { - type Route = api['deep']['nested']['mirror']['post'] - - expectTypeOf().parameter(0).toEqualTypeOf<{ - username: string - password: string - }>() - - expectTypeOf().parameter(1).toEqualTypeOf< - | { - headers?: Record | undefined - query?: Record | undefined - fetch?: RequestInit | undefined - } - | undefined - >() - - type Res = Result - - expectTypeOf().toEqualTypeOf< - | { - data: { - username: string - password: string - } - error: null - response: Response - status: number - headers: FetchRequestInit['headers'] - } - | ValidationError - >() + type Route = api['deep']['nested']['mirror']['post'] + + expectTypeOf().parameter(0).toEqualTypeOf<{ + username: string + password: string + }>() + + expectTypeOf().parameter(1).toEqualTypeOf< + | { + headers?: Record | undefined + query?: Record | undefined + fetch?: RequestInit | undefined + } + | undefined + >() + + type Res = Result + + expectTypeOf().toEqualTypeOf< + | { + data: { + username: string + password: string + } + error: null + response: Response + status: number + headers: FetchRequestInit['headers'] + } + | ValidationError + >() } // ? Get should have 1 parameter and is required when query is defined { - type Route = api['query']['get'] - - expectTypeOf().parameter(0).toEqualTypeOf<{ - headers?: Record | undefined - query: { - username: string - } - fetch?: RequestInit | undefined - }>() - - expectTypeOf().parameter(1).toBeUndefined() - - type Res = Result - - expectTypeOf().toEqualTypeOf< - | { - data: { - username: string - } - error: null - response: Response - status: number - headers: HeadersInit | undefined - } - | ValidationError - >() + type Route = api['query']['get'] + + expectTypeOf().parameter(0).toEqualTypeOf<{ + headers?: Record | undefined + query: { + username: string + } + fetch?: RequestInit | undefined + }>() + + expectTypeOf().parameter(1).toBeUndefined() + + type Res = Result + + expectTypeOf().toEqualTypeOf< + | { + data: { + username: string + } + error: null + response: Response + status: number + headers: HeadersInit | undefined + } + | ValidationError + >() } // ? Get should have 1 parameter and is required when query is defined { - type Route = api['queries']['get'] - - expectTypeOf().parameter(0).toEqualTypeOf<{ - headers?: Record | undefined - query: { - username: string - alias: 'Kristen' - } - fetch?: RequestInit | undefined - }>() - - expectTypeOf().parameter(1).toBeUndefined() - - type Res = Result - - expectTypeOf().toEqualTypeOf< - | { - data: { - username: string - alias: 'Kristen' - } - error: null - response: Response - status: number - headers: HeadersInit | undefined - } - | ValidationError - >() + type Route = api['queries']['get'] + + expectTypeOf().parameter(0).toEqualTypeOf<{ + headers?: Record | undefined + query: { + username: string + alias: 'Kristen' + } + fetch?: RequestInit | undefined + }>() + + expectTypeOf().parameter(1).toBeUndefined() + + type Res = Result + + expectTypeOf().toEqualTypeOf< + | { + data: { + username: string + alias: 'Kristen' + } + error: null + response: Response + status: number + headers: HeadersInit | undefined + } + | ValidationError + >() } // ? Post should have 2 parameter and is required when query is defined { - type Route = api['queries']['post'] - - expectTypeOf().parameter(0).toBeUnknown() - - expectTypeOf().parameter(1).toEqualTypeOf<{ - headers?: Record | undefined - query: { - username: string - alias: 'Kristen' - } - fetch?: RequestInit | undefined - }>() - - type Res = Result - - expectTypeOf().toEqualTypeOf< - | { - data: { - username: string - alias: 'Kristen' - } - error: null - response: Response - status: number - headers: HeadersInit | undefined - } - | ValidationError - >() + type Route = api['queries']['post'] + + expectTypeOf().parameter(0).toBeUnknown() + + expectTypeOf().parameter(1).toEqualTypeOf<{ + headers?: Record | undefined + query: { + username: string + alias: 'Kristen' + } + fetch?: RequestInit | undefined + }>() + + type Res = Result + + expectTypeOf().toEqualTypeOf< + | { + data: { + username: string + alias: 'Kristen' + } + error: null + response: Response + status: number + headers: HeadersInit | undefined + } + | ValidationError + >() } // ? Head should have 1 parameter and is required when query is defined { - type Route = api['queries']['head'] - - expectTypeOf().parameter(0).toEqualTypeOf<{ - headers?: Record | undefined - query: { - username: string - alias: 'Kristen' - } - fetch?: RequestInit | undefined - }>() - - expectTypeOf().parameter(1).toBeUndefined() - - type Res = Result - - expectTypeOf().toEqualTypeOf< - | { - data: { - username: string - alias: 'Kristen' - } - error: null - response: Response - status: number - headers: HeadersInit | undefined - } - | ValidationError - >() + type Route = api['queries']['head'] + + expectTypeOf().parameter(0).toEqualTypeOf<{ + headers?: Record | undefined + query: { + username: string + alias: 'Kristen' + } + fetch?: RequestInit | undefined + }>() + + expectTypeOf().parameter(1).toBeUndefined() + + type Res = Result + + expectTypeOf().toEqualTypeOf< + | { + data: { + username: string + alias: 'Kristen' + } + error: null + response: Response + status: number + headers: HeadersInit | undefined + } + | ValidationError + >() } // ? Should return error { - type Route = api['error']['get'] - - expectTypeOf().parameter(0).toEqualTypeOf< - | { - headers?: Record | undefined - query?: Record | undefined - fetch?: RequestInit | undefined - } - | undefined - >() - - expectTypeOf().parameter(1).toBeUndefined() - - type Res = Result - - expectTypeOf().toEqualTypeOf< - | { - data: never - error: null - response: Response - status: number - headers: HeadersInit | undefined - } - | { - data: null - error: - | { - status: 418 - value: 'Kirifuji Nagisa' - } - | { - status: 420 - value: 'Snoop Dogg' - } - | { - status: 422 - value: { - type: 'validation' - on: string - summary?: string - message?: string - found?: unknown - property?: string - expected?: string - } - } - response: Response - status: number - headers: HeadersInit | undefined - } - >() + type Route = api['error']['get'] + + expectTypeOf().parameter(0).toEqualTypeOf< + | { + headers?: Record | undefined + query?: Record | undefined + fetch?: RequestInit | undefined + } + | undefined + >() + + expectTypeOf().parameter(1).toBeUndefined() + + type Res = Result + + expectTypeOf().toEqualTypeOf< + | { + data: never + error: null + response: Response + status: number + headers: HeadersInit | undefined + } + | { + data: null + error: + | { + status: 418 + value: 'Kirifuji Nagisa' + } + | { + status: 420 + value: 'Snoop Dogg' + } + | { + status: 422 + value: { + type: 'validation' + on: string + summary?: string + message?: string + found?: unknown + property?: string + expected?: string + } + } + response: Response + status: number + headers: HeadersInit | undefined + } + >() } // ? Get should have 1 parameter and is required when headers is defined { - type Route = api['headers']['get'] - - expectTypeOf().parameter(0).toEqualTypeOf<{ - headers: { - username: string - alias: 'Kristen' - } - query?: Record | undefined - fetch?: RequestInit | undefined - }>() - - expectTypeOf().parameter(1).toBeUndefined() - - type Res = Result - - expectTypeOf().toEqualTypeOf< - | { - data: { - username: string - alias: 'Kristen' - } - error: null - response: Response - status: number - headers: HeadersInit | undefined - } - | ValidationError - >() + type Route = api['headers']['get'] + + expectTypeOf().parameter(0).toEqualTypeOf<{ + headers: { + username: string + alias: 'Kristen' + } + query?: Record | undefined + fetch?: RequestInit | undefined + }>() + + expectTypeOf().parameter(1).toBeUndefined() + + type Res = Result + + expectTypeOf().toEqualTypeOf< + | { + data: { + username: string + alias: 'Kristen' + } + error: null + response: Response + status: number + headers: HeadersInit | undefined + } + | ValidationError + >() } // ? Post should have 2 parameter and is required when headers is defined { - type Route = api['headers']['post'] - - expectTypeOf().parameter(0).toBeUnknown() - - expectTypeOf().parameter(1).toEqualTypeOf<{ - headers: { - username: string - alias: 'Kristen' - } - query?: Record | undefined - fetch?: RequestInit | undefined - }>() - - type Res = Result - - expectTypeOf().toEqualTypeOf< - | { - data: { - username: string - alias: 'Kristen' - } - error: null - response: Response - status: number - headers: HeadersInit | undefined - } - | ValidationError - >() + type Route = api['headers']['post'] + + expectTypeOf().parameter(0).toBeUnknown() + + expectTypeOf().parameter(1).toEqualTypeOf<{ + headers: { + username: string + alias: 'Kristen' + } + query?: Record | undefined + fetch?: RequestInit | undefined + }>() + + type Res = Result + + expectTypeOf().toEqualTypeOf< + | { + data: { + username: string + alias: 'Kristen' + } + error: null + response: Response + status: number + headers: HeadersInit | undefined + } + | ValidationError + >() } // ? Get should have 1 parameter and is required when queries and headers is defined { - type Route = api['queries-headers']['get'] - - expectTypeOf().parameter(0).toEqualTypeOf<{ - headers: { - username: string - alias: 'Kristen' - } - query: { - username: string - alias: 'Kristen' - } - fetch?: RequestInit | undefined - }>() - - expectTypeOf().parameter(1).toBeUndefined() - - type Res = Result - - expectTypeOf().toEqualTypeOf< - | { - data: { - username: string - alias: 'Kristen' - } - error: null - response: Response - status: number - headers: HeadersInit | undefined - } - | ValidationError - >() + type Route = api['queries-headers']['get'] + + expectTypeOf().parameter(0).toEqualTypeOf<{ + headers: { + username: string + alias: 'Kristen' + } + query: { + username: string + alias: 'Kristen' + } + fetch?: RequestInit | undefined + }>() + + expectTypeOf().parameter(1).toBeUndefined() + + type Res = Result + + expectTypeOf().toEqualTypeOf< + | { + data: { + username: string + alias: 'Kristen' + } + error: null + response: Response + status: number + headers: HeadersInit | undefined + } + | ValidationError + >() } // ? Post should have 2 parameter and is required when queries and headers is defined { - type Route = api['queries-headers']['post'] - - expectTypeOf().parameter(0).toBeUnknown() - - expectTypeOf().parameter(1).toEqualTypeOf<{ - headers: { - username: string - alias: 'Kristen' - } - query: { - username: string - alias: 'Kristen' - } - fetch?: RequestInit | undefined - }>() - - type Res = Result - - expectTypeOf().toEqualTypeOf< - | { - data: { - username: string - alias: 'Kristen' - } - error: null - response: Response - status: number - headers: HeadersInit | undefined - } - | ValidationError - >() + type Route = api['queries-headers']['post'] + + expectTypeOf().parameter(0).toBeUnknown() + + expectTypeOf().parameter(1).toEqualTypeOf<{ + headers: { + username: string + alias: 'Kristen' + } + query: { + username: string + alias: 'Kristen' + } + fetch?: RequestInit | undefined + }>() + + type Res = Result + + expectTypeOf().toEqualTypeOf< + | { + data: { + username: string + alias: 'Kristen' + } + error: null + response: Response + status: number + headers: HeadersInit | undefined + } + | ValidationError + >() } // ? Post should have 2 parameter and is required when queries, headers and body is defined { - type Route = api['body-queries-headers']['post'] - - expectTypeOf().parameter(0).toEqualTypeOf<{ - username: string - alias: 'Kristen' - }>() - - expectTypeOf().parameter(1).toEqualTypeOf<{ - headers: { - username: string - alias: 'Kristen' - } - query: { - username: string - alias: 'Kristen' - } - fetch?: RequestInit | undefined - }>() - - type Res = Result - - expectTypeOf().toEqualTypeOf< - | { - data: { - username: string - alias: 'Kristen' - } - error: null - response: Response - status: number - headers: HeadersInit | undefined - } - | ValidationError - >() + type Route = api['body-queries-headers']['post'] + + expectTypeOf().parameter(0).toEqualTypeOf<{ + username: string + alias: 'Kristen' + }>() + + expectTypeOf().parameter(1).toEqualTypeOf<{ + headers: { + username: string + alias: 'Kristen' + } + query: { + username: string + alias: 'Kristen' + } + fetch?: RequestInit | undefined + }>() + + type Res = Result + + expectTypeOf().toEqualTypeOf< + | { + data: { + username: string + alias: 'Kristen' + } + error: null + response: Response + status: number + headers: HeadersInit | undefined + } + | ValidationError + >() } // ? Should handle async { - type Route = api['async']['get'] - - expectTypeOf().parameter(0).toEqualTypeOf< - | { - headers?: Record | undefined - query?: Record | undefined - fetch?: RequestInit | undefined - } - | undefined - >() - - expectTypeOf().parameter(1).toBeUndefined() - - type Res = Result - - expectTypeOf().toEqualTypeOf< - | { - data: 'Hifumi' - error: null - response: Response - status: number - headers: HeadersInit | undefined - } - | { - data: null - error: - | { - status: 401 - value: 'Himari' - } - | { - status: 418 - value: 'Nagisa' - } - response: Response - status: number - headers: HeadersInit | undefined - } - >() + type Route = api['async']['get'] + + expectTypeOf().parameter(0).toEqualTypeOf< + | { + headers?: Record | undefined + query?: Record | undefined + fetch?: RequestInit | undefined + } + | undefined + >() + + expectTypeOf().parameter(1).toBeUndefined() + + type Res = Result + + expectTypeOf().toEqualTypeOf< + | { + data: 'Hifumi' + error: null + response: Response + status: number + headers: HeadersInit | undefined + } + | { + data: null + error: + | { + status: 401 + value: 'Himari' + } + | { + status: 418 + value: 'Nagisa' + } + response: Response + status: number + headers: HeadersInit | undefined + } + >() } // ? Handle param with nested path { - type SubModule = api['level'] - - expectTypeOf().toEqualTypeOf< - ((params: { id: string | number }) => { - get: ( - options?: - | { - headers?: Record | undefined - query?: Record | undefined - fetch?: RequestInit | undefined - } - | undefined - ) => Promise< - | { - data: string - error: null - response: Response - status: number - headers: HeadersInit | undefined - } - | ValidationError - > - ok: { - get: ( - options?: - | { - headers?: Record | undefined - query?: Record | undefined - fetch?: RequestInit | undefined - } - | undefined - ) => Promise< - | { - data: string - error: null - response: Response - status: number - headers: HeadersInit | undefined - } - | ValidationError - > - } - }) & { - index: { - get: ( - options?: - | { - headers?: Record | undefined - query?: Record | undefined - fetch?: RequestInit | undefined - } - | undefined - ) => Promise< - | { - data: '2' - error: null - response: Response - status: number - headers: HeadersInit | undefined - } - | ValidationError - > - } - level: { - get: ( - options?: - | { - headers?: Record | undefined - query?: Record | undefined - fetch?: RequestInit | undefined - } - | undefined - ) => Promise< - | { - data: '2' - error: null - response: Response - status: number - headers: HeadersInit | undefined - } - | ValidationError - > - } - } - > + type SubModule = api['level'] + + expectTypeOf().toEqualTypeOf< + ((params: { id: string | number }) => { + get: ( + options?: + | { + headers?: Record | undefined + query?: Record | undefined + fetch?: RequestInit | undefined + } + | undefined + ) => Promise< + | { + data: string + error: null + response: Response + status: number + headers: HeadersInit | undefined + } + | ValidationError + > + ok: { + get: ( + options?: + | { + headers?: Record | undefined + query?: Record | undefined + fetch?: RequestInit | undefined + } + | undefined + ) => Promise< + | { + data: string + error: null + response: Response + status: number + headers: HeadersInit | undefined + } + | ValidationError + > + } + }) & { + index: { + get: ( + options?: + | { + headers?: Record | undefined + query?: Record | undefined + fetch?: RequestInit | undefined + } + | undefined + ) => Promise< + | { + data: '2' + error: null + response: Response + status: number + headers: HeadersInit | undefined + } + | ValidationError + > + } + level: { + get: ( + options?: + | { + headers?: Record | undefined + query?: Record | undefined + fetch?: RequestInit | undefined + } + | undefined + ) => Promise< + | { + data: '2' + error: null + response: Response + status: number + headers: HeadersInit | undefined + } + | ValidationError + > + } + } + > } // ? Return AsyncGenerator on yield { - const app = new Elysia().get('', function* () { - yield 1 - yield 2 - yield 3 - }) - - const { data } = await treaty(app).index.get() - - expectTypeOf().toEqualTypeOf | null>() + const app = new Elysia().get('', function* () { + yield 1 + yield 2 + yield 3 + }) + + const { data } = await treaty(app).index.get() + + expectTypeOf().toEqualTypeOf | null>() } // ? Return actual value on generator if not yield { - const app = new Elysia().get('', function* () { - return 'a' - }) + const app = new Elysia().get('', function* () { + return 'a' + }) - const { data } = await treaty(app).index.get() + const { data } = await treaty(app).index.get() - expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf() } // ? Return both actual value and generator if yield and return { - const app = new Elysia().get('', function* () { - if (Math.random() > 0.5) return 'a' - - yield 1 - yield 2 - yield 3 - }) - - const { data } = await treaty(app).index.get() - - expectTypeOf().toEqualTypeOf< - | 'a' - | AsyncGenerator<1 | 2 | 3, 'a' | undefined, unknown> - | null - | undefined - >() + const app = new Elysia().get('', function* () { + if (Math.random() > 0.5) return 'a' + + yield 1 + yield 2 + yield 3 + }) + + const { data } = await treaty(app).index.get() + + expectTypeOf().toEqualTypeOf< + | 'a' + | AsyncGenerator<1 | 2 | 3, 'a' | undefined, unknown> + | null + | undefined + >() } // ? Return AsyncGenerator on yield { - const app = new Elysia().get('', async function* () { - yield 1 - yield 2 - yield 3 - }) - - const { data } = await treaty(app).index.get() - - expectTypeOf().toEqualTypeOf | null>() + const app = new Elysia().get('', async function* () { + yield 1 + yield 2 + yield 3 + }) + + const { data } = await treaty(app).index.get() + + expectTypeOf().toEqualTypeOf | null>() } // ? Return actual value on generator if not yield { - const app = new Elysia().get('', async function* () { - return 'a' - }) + const app = new Elysia().get('', async function* () { + return 'a' + }) - const { data } = await treaty(app).index.get() + const { data } = await treaty(app).index.get() - expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf() } // ? Return both actual value and generator if yield and return { - const app = new Elysia().get('', async function* () { - if (Math.random() > 0.5) return 'a' - - yield 1 - yield 2 - yield 3 - }) - - const { data } = await treaty(app).index.get() - - expectTypeOf().toEqualTypeOf< - | 'a' - | AsyncGenerator<1 | 2 | 3, 'a' | undefined, unknown> - | null - | undefined - >() + const app = new Elysia().get('', async function* () { + if (Math.random() > 0.5) return 'a' + + yield 1 + yield 2 + yield 3 + }) + + const { data } = await treaty(app).index.get() + + expectTypeOf().toEqualTypeOf< + | 'a' + | AsyncGenerator<1 | 2 | 3, 'a' | undefined, unknown> + | null + | undefined + >() } // ? Throwing api config flag should result in slimmed down result structure @@ -1044,4 +1044,4 @@ type ValidationError = { type Res = Result expectTypeOf().toEqualTypeOf<'a'>() -} \ No newline at end of file +}