Skip to content

Commit

Permalink
feat: pathBasedClient
Browse files Browse the repository at this point in the history
  • Loading branch information
openint-bot committed Dec 23, 2024
1 parent 89479ae commit edb39a6
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 20 deletions.
86 changes: 68 additions & 18 deletions packages/runtime/createClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type {
InitParam,
MaybeOptionalInit,
} from 'openapi-fetch'
import _createClient from 'openapi-fetch'
import _createClient, {wrapAsPathBasedClient} from 'openapi-fetch'
import type {
HttpMethod,
MediaType,
Expand Down Expand Up @@ -55,17 +55,20 @@ export function createClient<

const customFetch: typeof fetch = (url, init) =>
applyLinks(new Request(url, init), links)
const client = _createClient<Paths, Media>({

const fetchClient = _createClient<Paths, Media>({
...clientOptions,
fetch: customFetch,
})

const pathBasedClient = wrapAsPathBasedClient(fetchClient)

const clientThatThrows = Object.fromEntries(
HTTP_METHODS.map((method) => [
method,
/* eslint-disable */
(...args: unknown[]) =>
(client as any)[method](...args).then(throwIfNotOk(method)),
(fetchClient as any)[method](...args).then(throwIfNotOk(method)),
/* eslint-enable */
]),
) as {
Expand All @@ -76,36 +79,46 @@ export function createClient<
>
}

return {
const pathBasecClientThatThrows = new Proxy(pathBasedClient, {
/* eslint-disable */
get(target, prop) {
if (prop in target) {
return (target as any)[prop]
}
return Object.fromEntries(
HTTP_METHODS.map((method) => [
method,
(...args: unknown[]) =>
(target as any)[prop][method](...args).then(throwIfNotOk(method)),
]),
)
},
/* eslint-enable */
}) as PathBasedClientThatThrows<Paths, Media>

const _ret = {
fetchClient,
clientOptions,
links,
client,
/** Untyped request */
request: <T>(
method: Uppercase<HttpMethod>,
url: string,
options?: Omit<FetchOptions<unknown>, 'body'> & {body?: unknown},
) =>
client[method as 'GET'](url as never, options as never).then(
fetchClient[method as 'GET'](url as never, options as never).then(
throwIfNotOk(method),
) as Promise<{
data: T
response: FetchResponse<{}, {}, MediaType>['response']
}>,
...clientThatThrows,
}
}
Object.assign(pathBasecClientThatThrows, clientThatThrows)
Object.assign(pathBasecClientThatThrows, _ret)

export function throwIfNotOk<T extends Record<string | number, any>>(
method: Uppercase<HttpMethod>,
) {
return (res: FetchResponse<T, {}, MediaType>) => {
if (res.error) {
throw new HTTPError<T>({method, error: res.error, response: res.response})
}
// error is not set, so safely casting..
return res as Extract<typeof res, {data: unknown}>
}
return pathBasecClientThatThrows as typeof pathBasecClientThatThrows &
typeof clientThatThrows &
typeof _ret
}

/**
Expand Down Expand Up @@ -134,6 +147,21 @@ export const createFormUrlEncodedBodySerializer =
// return data.toString()
}

// MARK: - Type helpers to handle throwing

/* eslint-disable @typescript-eslint/no-explicit-any */
export function throwIfNotOk<T extends Record<string | number, any>>(
method: Uppercase<HttpMethod>,
) {
return (res: FetchResponse<T, {}, MediaType>) => {
if (res.error) {
throw new HTTPError<T>({method, error: res.error, response: res.response})
}
// error is not set, so safely casting..
return res as Extract<typeof res, {data: unknown}>
}
}

export type ClientMethodThatThrows<
Paths extends Record<string, Record<HttpMethod, {}>>,
Method extends HttpMethod,
Expand All @@ -147,3 +175,25 @@ export type ClientMethodThatThrows<
) => Promise<
Extract<FetchResponse<Paths[Path][Method], Init, Media>, {data: unknown}>
>

export type PathBasedClientThatThrows<
Paths extends Record<string | number, any>,
Media extends MediaType = MediaType,
> = {
[Path in keyof Paths]: ClientThatThrowsForPath<Paths[Path], Media>
}

export type ClientThatThrowsForPath<
PathInfo extends Record<string | number, any>,
Media extends MediaType,
> = {
[Method in keyof PathInfo as Uppercase<string & Method>]: <
Init extends MaybeOptionalInit<PathInfo, Method>,
>(
...init: InitParam<Init>
) => Promise<
Extract<FetchResponse<PathInfo[Method], Init, Media>, {data: unknown}>
>
}

/* eslint-enable @typescript-eslint/no-explicit-any */
5 changes: 4 additions & 1 deletion packages/runtime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,11 @@ export function initSDK<
sdkDef.createClient?.({createClient}, clientOptions) ??
createClient(clientOptions)

// make sure this works with proxy based client
Object.assign(client, {def: sdkDef})

// eslint-disable-next-line @typescript-eslint/no-explicit-any
return {...client, def: sdkDef} as any
return client as any
}

// MARK: - Type utils
Expand Down
56 changes: 56 additions & 0 deletions packages/runtime/proxy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/* eslint-disable */

test('should get property value through proxy', () => {
const target: {[key: string]: any} = {message: 'Hello, world!'}
const proxy = new Proxy(target, {
get: (obj, prop) => {
return prop in obj ? (obj as any)[prop] : {GET: (arg: unknown) => arg}
},
})
Object.assign(proxy, {override: 'This is an override'})

expect(proxy['override']).toBe('This is an override')
expect(proxy['message']).toBe('Hello, world!')
expect(proxy['/nonExistent'].GET('missing')).toBe('missing')

const proxy2 = new Proxy(target, {
get: () => {
return 'from proxy'
},
})
Object.assign(proxy2, {override: 'This is an override'})
expect(proxy2['override']).toBe('from proxy')
expect(proxy2['message']).toBe('from proxy')
expect(proxy2['/nonExistent']).toBe('from proxy')
})

test('should set property value through proxy', () => {
const target = {message: 'Hello, world!'}

const proxy = new Proxy(target, {
set: (obj, prop, value) => {
;(obj as any)[prop] = value
return true
},
})

proxy.message = 'Hello, Jest!'
expect(target.message).toBe('Hello, Jest!')
})

test('should delete property through proxy', () => {
const target = {message: 'Hello, world!'}

const proxy = new Proxy(target, {
deleteProperty: (obj, prop) => {
if (prop in obj) {
delete (obj as any)[prop]
return true
}
return false
},
})

delete (proxy as any).message
expect(target.message).toBeUndefined()
})
4 changes: 3 additions & 1 deletion sdks/sdk-openint/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import initOpenIntSDK from './index.js'

jest.setTimeout(70 * 15 * 1000) // In case of cold start

const openint = initOpenIntSDK({headers: {}})

test('healthcheck with default init', async () => {
const openint = initOpenIntSDK({headers: {}})
expect(await openint['/health'].GET().then((r) => r.data)).toBeTruthy()
expect(await openint.GET('/health').then((r) => r.data)).toBeTruthy()
})

0 comments on commit edb39a6

Please sign in to comment.