diff --git a/packages/client/src/dynamic-link.test-d.ts b/packages/client/src/dynamic-link.test-d.ts new file mode 100644 index 00000000..889ce5ff --- /dev/null +++ b/packages/client/src/dynamic-link.test-d.ts @@ -0,0 +1,21 @@ +import type { ClientLink } from './types' +import { DynamicLink } from './dynamic-link' + +describe('dynamicLink', () => { + it('pass correct context', () => { + void new DynamicLink<{ batch?: boolean }>((path, input, options) => { + expectTypeOf(options.context).toEqualTypeOf<{ batch?: boolean } >() + + return {} as any + }) + }) + + it('required return a another link', () => { + void new DynamicLink(() => ({} as ClientLink)) + void new DynamicLink<{ batch?: boolean }>(() => ({} as ClientLink<{ batch?: boolean }>)) + // @ts-expect-error - context is mismatch + void new DynamicLink<{ batch?: boolean }>(() => ({} as ClientLink<{ batch?: string }>)) + // @ts-expect-error - must return a ClientLink + void new DynamicLink<{ batch?: boolean }>(() => ({})) + }) +}) diff --git a/packages/client/src/dynamic-link.test.ts b/packages/client/src/dynamic-link.test.ts new file mode 100644 index 00000000..665ad9b0 --- /dev/null +++ b/packages/client/src/dynamic-link.test.ts @@ -0,0 +1,26 @@ +import type { ProcedureClientOptions } from '@orpc/server' +import { describe, expect, it, vi } from 'vitest' +import { DynamicLink } from './dynamic-link' + +describe('dynamicLink', () => { + it('works', async () => { + const mockedLink = { call: vi.fn().mockResolvedValue('__mocked__') } + const mockLinkResolver = vi.fn().mockResolvedValue(mockedLink) + const link = new DynamicLink(mockLinkResolver) + + const path = ['users', 'getProfile'] + const input = { id: 123 } + const options: ProcedureClientOptions = { + context: { + batch: true, + }, + } + + expect(await link.call(path, input, options)).toEqual('__mocked__') + + expect(mockLinkResolver).toHaveBeenCalledTimes(1) + expect(mockLinkResolver).toHaveBeenCalledWith(path, input, options) + expect(mockedLink.call).toHaveBeenCalledTimes(1) + expect(mockedLink.call).toHaveBeenCalledWith(path, input, options) + }) +}) diff --git a/packages/client/src/dynamic-link.ts b/packages/client/src/dynamic-link.ts new file mode 100644 index 00000000..8c2c584c --- /dev/null +++ b/packages/client/src/dynamic-link.ts @@ -0,0 +1,27 @@ +import type { ProcedureClientOptions } from '@orpc/server' +import type { Promisable } from '@orpc/shared' +import type { ClientLink } from './types' + +/** + * DynamicLink provides a way to dynamically resolve and delegate calls to other ClientLinks + * based on the request path, input, and context. + */ +export class DynamicLink implements ClientLink { + constructor( + private readonly linkResolver: ( + path: readonly string[], + input: unknown, + options: ProcedureClientOptions & { context: TClientContext }, + ) => Promisable>, + ) { + } + + async call(path: readonly string[], input: unknown, options: ProcedureClientOptions): Promise { + // Since the context is only optional when the context is undefinable, we can safely cast it + const resolvedLink = await this.linkResolver(path, input, options as typeof options & { context: TClientContext }) + + const output = await resolvedLink.call(path, input, options) + + return output + } +} diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 0918baae..547a4506 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -1,5 +1,7 @@ /** unnoq */ export * from './client' +export * from './dynamic-link' export * from './types' + export * from '@orpc/shared/error'