From d17d646d821bdd28a9e0269a6c4b131a191c13ce Mon Sep 17 00:00:00 2001 From: unnoq Date: Sat, 14 Dec 2024 10:15:15 +0700 Subject: [PATCH 01/51] wip --- packages/contract/src/builder.ts | 4 +- .../src/procedure-decorated.test-d.ts | 10 +- .../contract/src/procedure-decorated.test.ts | 6 +- packages/contract/src/procedure-decorated.ts | 4 +- packages/contract/src/procedure.test.ts | 6 + packages/contract/src/procedure.ts | 16 +- packages/contract/src/router-builder.ts | 2 +- packages/server/src/index.ts | 4 +- packages/server/src/middleware.test-d.ts | 173 ++++++++++ packages/server/src/middleware.test.ts | 288 ++++------------ packages/server/src/middleware.ts | 79 ++--- packages/server/src/procedure-builder.ts | 101 +++--- packages/server/src/procedure-caller.ts | 4 +- .../server/src/procedure-decorated.test-d.ts | 69 ++++ packages/server/src/procedure-decorated.ts | 139 ++++++++ packages/server/src/procedure-implementer.ts | 72 ++-- packages/server/src/procedure.test-d.ts | 12 + packages/server/src/procedure.test.ts | 310 +----------------- packages/server/src/procedure.ts | 234 ++----------- packages/server/src/router-builder.ts | 2 +- packages/server/src/types.ts | 7 +- 21 files changed, 650 insertions(+), 892 deletions(-) create mode 100644 packages/server/src/middleware.test-d.ts create mode 100644 packages/server/src/procedure-decorated.test-d.ts create mode 100644 packages/server/src/procedure-decorated.ts create mode 100644 packages/server/src/procedure.test-d.ts diff --git a/packages/contract/src/builder.ts b/packages/contract/src/builder.ts index 52d317bbb..fa40a5dea 100644 --- a/packages/contract/src/builder.ts +++ b/packages/contract/src/builder.ts @@ -25,7 +25,7 @@ export class ContractBuilder { }) } - input(schema: U, example?: SchemaInput): DecoratedContractProcedure { + input(schema: U, example?: SchemaInput): DecoratedContractProcedure { return new DecoratedContractProcedure({ InputSchema: schema, inputExample: example, @@ -33,7 +33,7 @@ export class ContractBuilder { }) } - output(schema: U, example?: SchemaOutput): DecoratedContractProcedure { + output(schema: U, example?: SchemaOutput): DecoratedContractProcedure { return new DecoratedContractProcedure({ OutputSchema: schema, outputExample: example, diff --git a/packages/contract/src/procedure-decorated.test-d.ts b/packages/contract/src/procedure-decorated.test-d.ts index 253e72c3d..8ba2982a2 100644 --- a/packages/contract/src/procedure-decorated.test-d.ts +++ b/packages/contract/src/procedure-decorated.test-d.ts @@ -56,17 +56,17 @@ describe('prefix', () => { describe('pushTag', () => { it('return ContractProcedure', () => { - const tagged = decorated.pushTag('tag', 'tag2') + const tagged = decorated.unshiftTag('tag', 'tag2') expectTypeOf(tagged).toEqualTypeOf>() }) it('throw error on invalid tag', () => { - decorated.pushTag('tag') - decorated.pushTag('tag', 'tag2') + decorated.unshiftTag('tag') + decorated.unshiftTag('tag', 'tag2') // @ts-expect-error - invalid tag - decorated.pushTag(1) + decorated.unshiftTag(1) // @ts-expect-error - invalid tag - decorated.pushTag({}) + decorated.unshiftTag({}) }) }) diff --git a/packages/contract/src/procedure-decorated.test.ts b/packages/contract/src/procedure-decorated.test.ts index fa48554ab..b5efcc1c5 100644 --- a/packages/contract/src/procedure-decorated.test.ts +++ b/packages/contract/src/procedure-decorated.test.ts @@ -56,17 +56,17 @@ describe('pushTag', () => { const decorated = new DecoratedContractProcedure({ InputSchema: undefined, OutputSchema: undefined }) it('works', () => { - const tagged = decorated.pushTag('tag1', 'tag2') + const tagged = decorated.unshiftTag('tag1', 'tag2') expect(tagged).toBeInstanceOf(DecoratedContractProcedure) expect(tagged['~orpc']).toEqual({ route: { tags: ['tag1', 'tag2'] } }) - const tagged2 = tagged.pushTag('tag3') + const tagged2 = tagged.unshiftTag('tag3') expect(tagged2).toBeInstanceOf(DecoratedContractProcedure) expect(tagged2['~orpc']).toEqual({ route: { tags: ['tag1', 'tag2', 'tag3'] } }) }) it('not reference', () => { - const tagged = decorated.pushTag('tag1', 'tag2') + const tagged = decorated.unshiftTag('tag1', 'tag2') expect(tagged['~orpc']).not.toBe(decorated['~orpc']) expect(tagged).not.toBe(decorated) }) diff --git a/packages/contract/src/procedure-decorated.ts b/packages/contract/src/procedure-decorated.ts index 853d2372e..0f3eb9952 100644 --- a/packages/contract/src/procedure-decorated.ts +++ b/packages/contract/src/procedure-decorated.ts @@ -40,12 +40,12 @@ export class DecoratedContractProcedure< }) } - pushTag(...tags: string[]): DecoratedContractProcedure { + unshiftTag(...tags: string[]): DecoratedContractProcedure { return new DecoratedContractProcedure({ ...this['~orpc'], route: { ...this['~orpc'].route, - tags: [...(this['~orpc'].route?.tags ?? []), ...tags], + tags: [...tags, ...(this['~orpc'].route?.tags ?? [])], }, }) } diff --git a/packages/contract/src/procedure.test.ts b/packages/contract/src/procedure.test.ts index 18f820f7e..ff639b859 100644 --- a/packages/contract/src/procedure.test.ts +++ b/packages/contract/src/procedure.test.ts @@ -11,4 +11,10 @@ describe('isContractProcedure', () => { expect(1).not.toSatisfy(isContractProcedure) expect({ '~orpc': {} }).not.toSatisfy(isContractProcedure) }) + + it('works with raw object', () => { + expect(Object.assign({}, new ContractProcedure({ InputSchema: undefined, OutputSchema: undefined }))).toSatisfy(isContractProcedure) + expect(Object.assign({}, new ContractProcedure({ InputSchema: z.object({}), OutputSchema: undefined }))).toSatisfy(isContractProcedure) + expect(Object.assign({}, new ContractProcedure({ InputSchema: z.object({}), OutputSchema: undefined, route: {} }))).toSatisfy(isContractProcedure) + }) }) diff --git a/packages/contract/src/procedure.ts b/packages/contract/src/procedure.ts index ff358a787..cfc30e7ce 100644 --- a/packages/contract/src/procedure.ts +++ b/packages/contract/src/procedure.ts @@ -35,5 +35,19 @@ export type ANY_CONTRACT_PROCEDURE = ContractProcedure export type WELL_CONTRACT_PROCEDURE = ContractProcedure export function isContractProcedure(item: unknown): item is ANY_CONTRACT_PROCEDURE { - return item instanceof ContractProcedure + if (item instanceof ContractProcedure) { + return true + } + + return ( + (typeof item === 'object' || typeof item === 'function') + && item !== null + && '~type' in item + && item['~type'] === 'ContractProcedure' + && '~orpc' in item + && typeof item['~orpc'] === 'object' + && item['~orpc'] !== null + && 'InputSchema' in item['~orpc'] + && 'OutputSchema' in item['~orpc'] + ) } diff --git a/packages/contract/src/router-builder.ts b/packages/contract/src/router-builder.ts index db18f64bc..dd96097f6 100644 --- a/packages/contract/src/router-builder.ts +++ b/packages/contract/src/router-builder.ts @@ -49,7 +49,7 @@ export class ContractRouterBuilder { let decorated = DecoratedContractProcedure.decorate(item) if (this['~orpc'].tags) { - decorated = decorated.pushTag(...this['~orpc'].tags) + decorated = decorated.unshiftTag(...this['~orpc'].tags) } if (this['~orpc'].prefix) { diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 1af06f06e..692471a52 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1,3 +1,4 @@ +import type { WELL_CONTEXT } from './types' import { Builder } from './builder' export * from './builder' @@ -6,6 +7,7 @@ export * from './middleware' export * from './procedure' export * from './procedure-builder' export * from './procedure-caller' +export * from './procedure-decorated' export * from './procedure-implementer' export * from './router' export * from './router-builder' @@ -15,4 +17,4 @@ export * from './types' export * from './utils' export * from '@orpc/shared/error' -export const os = new Builder, undefined>() +export const os = new Builder() diff --git a/packages/server/src/middleware.test-d.ts b/packages/server/src/middleware.test-d.ts new file mode 100644 index 000000000..8cdb9fc29 --- /dev/null +++ b/packages/server/src/middleware.test-d.ts @@ -0,0 +1,173 @@ +import type { DecoratedMiddleware, Middleware, MiddlewareMeta } from './middleware' +import type { WELL_CONTEXT } from './types' +import { decorateMiddleware } from './middleware' + +describe('middleware', () => { + it('just a function', () => { + const mid: Middleware<{ auth: boolean }, undefined, unknown, unknown> = (input, context, meta) => { + expectTypeOf(input).toEqualTypeOf() + expectTypeOf(context).toEqualTypeOf<{ auth: boolean }>() + expectTypeOf(meta).toEqualTypeOf>() + + return meta.next({}) + } + + const mid2: Middleware<{ auth: boolean }, undefined, unknown, unknown> = async (input, context, meta) => { + expectTypeOf(input).toEqualTypeOf() + expectTypeOf(context).toEqualTypeOf<{ auth: boolean }>() + expectTypeOf(meta).toEqualTypeOf>() + + return await meta.next({}) + } + + // @ts-expect-error - missing return type + const mid3: Middleware<{ auth: boolean }, undefined, unknown, unknown> = (input, context, meta) => { + } + + // @ts-expect-error - missing return type + const mid4: Middleware<{ auth: boolean }, undefined, unknown, unknown> = async (input, context, meta) => { + } + }) + + it('require return valid extra context', () => { + const mid0: Middleware = (_, __, meta) => { + return meta.next({ }) + } + + const mid: Middleware = (_, __, meta) => { + return meta.next({ context: { userId: '1' } }) + } + + // @ts-expect-error invalid extra context + const mid2: Middleware = (_, __, meta) => { + return meta.next({ context: { userId: 1 } }) + } + + const mid3: Middleware = (_, __, meta) => { + // @ts-expect-error missing extra context + return meta.next({}) + } + }) + + it('can type input', () => { + const mid: Middleware = (input, context, meta) => { + expectTypeOf(input).toEqualTypeOf<{ id: string }>() + + return meta.next({}) + } + }) + + it('can type output', () => { + const mid: Middleware = async (_, context, meta) => { + const result = await meta.next({}) + + expectTypeOf(result.output).toEqualTypeOf<{ id: string }>() + + return meta.output({ id: '1' }) + } + + // @ts-expect-error invalid output + const mid2: Middleware = async (_, context, meta) => { + return meta.output({ id: 123 }) + } + }) + + it('can infer types from function', () => { + const func = (input: 'input', context: { context: 'context' }, meta: MiddlewareMeta<'output'>) => { + return meta.next({ context: { extra: 'extra' as const } }) + } + + type Inferred = typeof func extends Middleware + ? [TContext, TExtraContext, TInput, TOutput] + : never + + expectTypeOf().toEqualTypeOf< + [{ context: 'context' }, { extra: 'extra' }, 'input', 'output'] + >() + }) +}) + +describe('decorateMiddleware', () => { + const decorated = decorateMiddleware( + (input: { name: string }, context: { user?: string }, meta) => meta.next({ context: { auth: true as const, user: 'string' } }), + ) + + it('assignable to middleware', () => { + const decorated = decorateMiddleware((input: { input: 'input' }, context, meta) => meta.next({})) + const mid: Middleware = decorated + + const decorated2 = decorateMiddleware((input, context, meta: MiddlewareMeta<'output'>) => meta.next({ context: { extra: true } })) + const mid2: Middleware = decorated2 + }) + + it('can map input', () => { + const mapped = decorated.mapInput((input: 'something') => ({ name: input })) + + expectTypeOf(mapped).toEqualTypeOf< + DecoratedMiddleware<{ user?: string }, { auth: true, user: string }, 'something', unknown> + >() + }) + + it('can concat', () => { + const mapped = decorated.concat( + (input: { age: number }, context, meta) => meta.next({ context: { db: true } }), + ) + + expectTypeOf(mapped).toEqualTypeOf< + DecoratedMiddleware< + { user?: string }, + { auth: true, user: string } & { db: boolean }, + { name: string } & { age: number }, + unknown + > + >() + }) + + it('can concat with map input', () => { + const mapped = decorated.concat( + (input: { age: number }, context, meta) => meta.next({ context: { db: true } }), + (input: { year: number }) => ({ age: 123 }), + ) + + expectTypeOf(mapped).toEqualTypeOf< + DecoratedMiddleware< + { user?: string }, + { auth: true, user: string } & { db: boolean }, + { name: string } & { year: number }, + unknown + > + >() + + decorated.concat( + (input: { age: number }, context, meta) => meta.next({ context: { db: true } }), + // @ts-expect-error - invalid return input + (input: { year: number }) => ({ age: '123' }), + ) + }) + + it('can concat and prevent conflict on context', () => { + const mapped = decorated.concat( + (input, context, meta) => meta.next({ context: { db: true } }), + ) + + expectTypeOf(mapped).toEqualTypeOf< + DecoratedMiddleware< + { user?: string }, + { auth: true, user: string } & { db: boolean }, + { name: string }, + unknown + > + >() + + decorated.concat( + // @ts-expect-error - user is not assignable to existing user context + (input, context, meta) => meta.next({ context: { user: true } }), + ) + + decorated.concat( + // @ts-expect-error - user is not assignable to existing user context + (input, context, meta) => meta.next({ context: { user: true } }), + () => 'anything', + ) + }) +}) diff --git a/packages/server/src/middleware.test.ts b/packages/server/src/middleware.test.ts index b76e3ce97..9e9a9317a 100644 --- a/packages/server/src/middleware.test.ts +++ b/packages/server/src/middleware.test.ts @@ -1,260 +1,84 @@ -import type { DecoratedMiddleware, Middleware, MiddlewareMeta } from './middleware' -import { os } from '.' import { decorateMiddleware } from './middleware' -describe('middleware', () => { - it('just a function', () => { - const mid: Middleware< - { auth: boolean }, - { userId: string }, - unknown, - unknown - > = (input, context, meta) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf<{ auth: boolean }>() - expectTypeOf(meta).toEqualTypeOf>() - - return meta.next({ context: { userId: '1' } }) - } - }) - - it('expect required return if has extra context', () => { - const mid: Middleware< - { auth: boolean }, - { userId: string }, - unknown, - unknown - > = (input, context, meta) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf<{ auth: boolean }>() - expectTypeOf(meta).toEqualTypeOf>() - - return meta.next({ context: { userId: '1' } }) - } - - // @ts-expect-error mid must call next - const mid2: Middleware< - { auth: boolean }, - { userId: string }, - unknown, - unknown - > = (input, context, meta) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf<{ auth: boolean }>() - expectTypeOf(meta).toEqualTypeOf>() - } - - // @ts-expect-error mid return invalid context - const mid3: Middleware< - { auth: boolean }, - { userId: string }, - unknown, - unknown - > = (input, context, meta) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf<{ auth: boolean }>() - expectTypeOf(meta).toEqualTypeOf>() - - return meta.next({ - context: { - valid: false, - }, - }) - } - }) -}) - describe('decorateMiddleware', () => { - it('infer types', () => { - const mid = decorateMiddleware< - { auth: boolean }, - { userId: string }, - { id: string }, - { name: string } - >((input, context, meta) => { - expectTypeOf(input).toEqualTypeOf<{ id: string }>() - expectTypeOf(context).toEqualTypeOf<{ auth: boolean }>() - expectTypeOf(meta).toEqualTypeOf>() - - return meta.next({ - context: { - userId: '1', - }, - }) - }) + it('just a function', () => { + const fn = vi.fn() + const decorated = decorateMiddleware(fn) as any - expectTypeOf(mid).toEqualTypeOf< - DecoratedMiddleware< - { auth: boolean }, - { userId: string }, - { id: string }, - { name: string } - > - >() + fn.mockReturnValueOnce('__mocked__') - expectTypeOf(mid).toMatchTypeOf< - Middleware< - { auth: boolean }, - { userId: string }, - { id: string }, - { name: string } - > - >() + expect(decorated('input')).toBe('__mocked__') + expect(fn).toHaveBeenCalledTimes(1) + expect(fn).toHaveBeenCalledWith('input') }) - it('concat: infer types', () => { - const mid = decorateMiddleware< - { auth: boolean }, - undefined, - { id: string }, - { name: string } - >((_, __, meta) => meta.next({})).concat(async (input, context, meta) => { - expectTypeOf(input).toEqualTypeOf<{ id: string }>() - expectTypeOf(context).toEqualTypeOf<{ auth: boolean }>() - expectTypeOf(meta).toEqualTypeOf>() - - return meta.next({ - context: { - userId: '1', - }, - }) - }) - - expectTypeOf(mid).toEqualTypeOf< - DecoratedMiddleware< - { auth: boolean }, - { userId: string }, - { id: string }, - { name: string } - > - >() - }) + it('can map input', () => { + const fn = vi.fn() + const map = vi.fn() + const decorated = decorateMiddleware(fn).mapInput(map) as any - it('concat: can expect input', () => { - const mid = decorateMiddleware< - { auth: boolean }, - undefined, - unknown, - unknown - >((_, __, meta) => meta.next({})) - .concat((input: { id: string }, _, meta) => meta.next({})) - .concat((input: { status: string }, _, meta) => meta.next({})) + fn.mockReturnValueOnce('__mocked__') + map.mockReturnValueOnce('__input__') - expectTypeOf(mid).toEqualTypeOf< - DecoratedMiddleware< - { auth: boolean }, - undefined, - { id: string } & { status: string }, - unknown - > - >() + expect(decorated('something')).toBe('__mocked__') - // MID2 isn't usable because input type is wrong - const mid2 = mid.concat((input: { id: number }, _, meta) => meta.next({})) - expectTypeOf(mid2).toMatchTypeOf< - DecoratedMiddleware< - { auth: boolean }, - undefined, - { id: never, status: string }, - unknown - > - >() - }) + expect(map).toHaveBeenCalledTimes(1) + expect(map).toHaveBeenCalledWith('something') - it('concat: deep copy', () => { - const middleware = decorateMiddleware((_, __, meta) => meta.next({})) - const mid2 = middleware.concat((_, __, meta) => meta.next({})) - expect(mid2).not.toBe(middleware) + expect(fn).toHaveBeenCalledTimes(1) + expect(fn).toHaveBeenCalledWith('__input__') }) - it('concat: can map input', async () => { - const middleware = decorateMiddleware< - { auth: boolean }, - undefined, - unknown, - unknown - >((_, __, meta) => meta.next({})) - - const mid2 = middleware.concat( - (input: { postId: number }, _, meta) => meta.next({ context: { a: 'a' } }), - input => ({ postId: 12455 }), - ) + it('can concat', async () => { + const fn = vi.fn() + const fn2 = vi.fn() + const next = vi.fn() - // mid2 input is unknown, because it's map input does not expect anything - expectTypeOf(mid2).toEqualTypeOf< - DecoratedMiddleware<{ auth: boolean }, { a: string }, unknown, unknown> - >() + const decorated = decorateMiddleware((input, context, meta) => { + fn(input, context, meta) + return meta.next({ context: { auth: true } }) + }).concat((input, context, meta) => { + fn2(input, context, meta) + return meta.next({}) + }) as any - const fn = vi.fn() - const mid3 = middleware.concat( - (input: { postId: string }, _, meta) => { - fn() - expect(input).toEqual({ postId: '123' }) + next.mockReturnValueOnce('__mocked__') - return meta.next({}) - }, - (input: { postId: number }) => { - fn() - expect(input).toEqual({ postId: 123 }) - return { - postId: `${input.postId}`, - } - }, - ) + expect((await decorated('input', undefined, { next }))).toBe('__mocked__') - await mid3({ postId: 123 }, {} as any, { next: () => {} } as any) - expect(fn).toHaveBeenCalledTimes(2) + expect(fn).toHaveBeenCalledTimes(1) + expect(fn).toHaveBeenCalledWith('input', undefined, { next: expect.any(Function) }) - // INPUT now follow expect types from map not from middleware - expectTypeOf(mid3).toMatchTypeOf< - DecoratedMiddleware< - { auth: boolean }, - undefined, - { postId: number }, - unknown - > - >() + expect(fn2).toHaveBeenCalledTimes(1) + expect(fn2).toHaveBeenCalledWith('input', { auth: true }, { next }) }) - it('mapInput', async () => { + it('can concat with map input', async () => { const fn = vi.fn() + const fn2 = vi.fn() + const map = vi.fn() + const next = vi.fn() - const mid = decorateMiddleware< - undefined, - undefined, - { id: string }, - unknown - >(fn).mapInput((input: { postId: string }) => { - return { id: input.postId } - }) + const decorated = decorateMiddleware((input, context, meta) => { + fn(input, context, meta) + return meta.next({ context: { auth: true } }) + }).concat((input, context, meta) => { + fn2(input, context, meta) + return meta.next({}) + }, map) as any - expectTypeOf(mid).toEqualTypeOf< - DecoratedMiddleware - >() + map.mockReturnValueOnce({ name: 'input' }) + next.mockReturnValueOnce('__mocked__') - await mid({ postId: '1' }, undefined, {} as any) + expect((await decorated('input', undefined, { next }))).toBe('__mocked__') - expect(fn).toHaveBeenCalledWith({ id: '1' }, undefined, {}) - }) -}) + expect(fn).toHaveBeenCalledTimes(1) + expect(fn).toHaveBeenCalledWith('input', undefined, { next: expect.any(Function) }) -it('middleware can output', async () => { - let mid2Called = false - let handlerCalled = false - const ping = os - .use((input, ctx, meta) => { - return meta.output('from middleware') - }) - .use((input, ctx, meta) => { - mid2Called = true - return meta.output('from middleware 2') - }) - .func(() => { - handlerCalled = true - return 'from handler' - }) + expect(map).toHaveBeenCalledTimes(1) + expect(map).toHaveBeenCalledWith('input') - expect(await ping(undefined)).toBe('from middleware') - expect(mid2Called).toBeFalsy() - expect(handlerCalled).toBeFalsy() + expect(fn2).toHaveBeenCalledTimes(1) + expect(fn2).toHaveBeenCalledWith({ name: 'input' }, { auth: true }, { next }) + }) }) diff --git a/packages/server/src/middleware.ts b/packages/server/src/middleware.ts index b1ce754ba..c394d13bd 100644 --- a/packages/server/src/middleware.ts +++ b/packages/server/src/middleware.ts @@ -1,15 +1,12 @@ import type { Promisable } from '@orpc/shared' -import type { Context, MergeContext, Meta } from './types' -import { mergeContext } from './utils' +import type { Context, MergeContext, Meta, WELL_CONTEXT } from './types' export type MiddlewareResult = Promisable<{ output: TOutput context: TExtraContext }> -export interface MiddlewareMeta< - TOutput, -> extends Meta { +export interface MiddlewareMeta extends Meta { next: ( options: UExtraContext extends undefined ? { context?: UExtraContext } : { context: UExtraContext } ) => MiddlewareResult @@ -31,10 +28,14 @@ export interface Middleware< > } +export type ANY_MIDDLEWARE = Middleware + export interface MapInputMiddleware { (input: TInput): TMappedInput } +export type ANY_MAP_INPUT_MIDDLEWARE = MapInputMiddleware + export interface DecoratedMiddleware< TContext extends Context, TExtraContext extends Context, @@ -42,8 +43,8 @@ export interface DecoratedMiddleware< TOutput, > extends Middleware { concat: (< - UExtraContext extends Partial>> | undefined = undefined, - UInput = TInput, + UExtraContext extends Context & (Partial> | undefined) = undefined, + UInput = unknown, >( middleware: Middleware< MergeContext, @@ -54,10 +55,10 @@ export interface DecoratedMiddleware< ) => DecoratedMiddleware< TContext, MergeContext, - TInput & UInput, + UInput & TInput, TOutput >) & (< - UExtraContext extends Partial>> | undefined = undefined, + UExtraContext extends Context & (Partial> | undefined) = undefined, UInput = TInput, UMappedInput = unknown, >( @@ -67,11 +68,11 @@ export interface DecoratedMiddleware< UMappedInput, TOutput >, - mapInput: MapInputMiddleware, + mapInput: MapInputMiddleware, ) => DecoratedMiddleware< TContext, MergeContext, - TInput & UInput, + UInput & TInput, TOutput >) @@ -80,57 +81,41 @@ export interface DecoratedMiddleware< ) => DecoratedMiddleware } -const decoratedMiddlewareSymbol = Symbol('🔒decoratedMiddleware') - export function decorateMiddleware< - TContext extends Context, - TExtraContext extends Context, - TInput, - TOutput, + TContext extends Context = WELL_CONTEXT, + TExtraContext extends Context = undefined, + TInput = unknown, + TOutput = unknown, >( middleware: Middleware, ): DecoratedMiddleware { - if (Reflect.get(middleware, decoratedMiddlewareSymbol)) { - return middleware as any + const decorated = middleware as DecoratedMiddleware + + decorated.mapInput = (mapInput) => { + const mapped = decorateMiddleware( + (input, ...rest) => middleware(mapInput(input as any), ...rest as [any, any]), + ) + + return mapped as any } - const concat = ( - concatMiddleware: Middleware, - mapInput?: MapInputMiddleware, - ): Middleware => { - const concatMiddleware_ = mapInput + decorated.concat = (concatMiddleware: ANY_MIDDLEWARE, mapInput?: ANY_MAP_INPUT_MIDDLEWARE) => { + const mapped = mapInput ? decorateMiddleware(concatMiddleware).mapInput(mapInput) : concatMiddleware - return decorateMiddleware(async (input, context, meta, ...rest) => { - const input_ = input as any - const context_ = context as any - const meta_ = meta as any - + const concatted = decorateMiddleware((input, context, meta, ...rest) => { const next: MiddlewareMeta['next'] = async (options) => { - return concatMiddleware_(input_, mergeContext(context_, options.context), meta_, ...rest) + return mapped(input, { ...context, ...options.context }, meta, ...rest) } - const m1 = await middleware(input_, context_, { - ...meta_, - next, - }, ...rest) + const merged = middleware(input as any, context as any, { ...meta, next }, ...rest) - return m1 + return merged }) - } - const mapInput = ( - map: MapInputMiddleware, - ): DecoratedMiddleware => { - return decorateMiddleware((input, ...rest) => - middleware(map(input), ...rest), - ) + return concatted as any } - return Object.assign(middleware, { - [decoratedMiddlewareSymbol]: true, - concat: concat as any, - mapInput, - }) + return decorated } diff --git a/packages/server/src/procedure-builder.ts b/packages/server/src/procedure-builder.ts index 52a257adc..37abf13a9 100644 --- a/packages/server/src/procedure-builder.ts +++ b/packages/server/src/procedure-builder.ts @@ -1,4 +1,5 @@ import type { MapInputMiddleware, Middleware } from './middleware' +import type { DecoratedProcedure } from './procedure-decorated' import type { Context, MergeContext } from './types' import { type ContractProcedure, @@ -9,37 +10,43 @@ import { type SchemaOutput, } from '@orpc/contract' import { - type DecoratedProcedure, - decorateProcedure, + Procedure, type ProcedureFunc, } from './procedure' +import { decorateProcedure } from './procedure-decorated' import { ProcedureImplementer } from './procedure-implementer' +export interface ProcedureBuilderDef< + _TContext extends Context, + _TExtraContext extends Context, + TInputSchema extends Schema, + TOutputSchema extends Schema, +> { + contract: ContractProcedure + middlewares?: Middleware[] +} + export class ProcedureBuilder< TContext extends Context, TExtraContext extends Context, TInputSchema extends Schema, TOutputSchema extends Schema, > { - constructor( - public zz$pb: { - contract: ContractProcedure - middlewares?: Middleware[] - }, - ) {} + '~type' = 'ProcedureBuilder' as const + '~orpc': ProcedureBuilderDef - /** - * Self chainable - */ + constructor(def: ProcedureBuilderDef) { + this['~orpc'] = def + } route( - opts: RouteOptions, + route: RouteOptions, ): ProcedureBuilder { return new ProcedureBuilder({ - ...this.zz$pb, - contract: DecoratedContractProcedure.decorate(this.zz$pb.contract).route( - opts, - ), + ...this['~orpc'], + contract: DecoratedContractProcedure + .decorate(this['~orpc'].contract) + .route(route), }) } @@ -48,11 +55,10 @@ export class ProcedureBuilder< example?: SchemaInput, ): ProcedureBuilder { return new ProcedureBuilder({ - ...this.zz$pb, - contract: DecoratedContractProcedure.decorate(this.zz$pb.contract).input( - schema, - example, - ), + ...this['~orpc'], + contract: DecoratedContractProcedure + .decorate(this['~orpc'].contract) + .input(schema, example), }) } @@ -61,18 +67,13 @@ export class ProcedureBuilder< example?: SchemaOutput, ): ProcedureBuilder { return new ProcedureBuilder({ - ...this.zz$pb, - contract: DecoratedContractProcedure.decorate(this.zz$pb.contract).output( - schema, - example, - ), + ...this['~orpc'], + contract: DecoratedContractProcedure + .decorate(this['~orpc'].contract) + .output(schema, example), }) } - /** - * Convert to ProcedureBuilder - */ - use< UExtraContext extends | Partial>> @@ -117,42 +118,24 @@ export class ProcedureBuilder< ): ProcedureImplementer { if (!mapInput) { return new ProcedureImplementer({ - contract: this.zz$pb.contract, - middlewares: this.zz$pb.middlewares, + contract: this['~orpc'].contract, + middlewares: this['~orpc'].middlewares, }).use(middleware) } return new ProcedureImplementer({ - contract: this.zz$pb.contract, - middlewares: this.zz$pb.middlewares, + contract: this['~orpc'].contract, + middlewares: this['~orpc'].middlewares, }).use(middleware, mapInput) } - /** - * Convert to Procedure - */ - func>( - func: ProcedureFunc< - TContext, - TExtraContext, - TInputSchema, - TOutputSchema, - UFuncOutput - >, - ): DecoratedProcedure< - TContext, - TExtraContext, - TInputSchema, - TOutputSchema, - UFuncOutput - > { - return decorateProcedure({ - zz$p: { - middlewares: this.zz$pb.middlewares, - contract: this.zz$pb.contract, - func, - }, - }) + func: ProcedureFunc, + ): DecoratedProcedure { + return decorateProcedure(new Procedure({ + middlewares: this['~orpc'].middlewares, + contract: this['~orpc'].contract, + func, + })) } } diff --git a/packages/server/src/procedure-caller.ts b/packages/server/src/procedure-caller.ts index 140ae6936..c9437cf75 100644 --- a/packages/server/src/procedure-caller.ts +++ b/packages/server/src/procedure-caller.ts @@ -2,7 +2,7 @@ import type { SchemaInput, SchemaOutput } from '@orpc/contract' import type { Hooks, PartialOnUndefinedDeep, Value } from '@orpc/shared' import type { Lazy } from './lazy' import type { MiddlewareMeta } from './middleware' -import type { ANY_LAZY_PROCEDURE, ANY_PROCEDURE, Procedure, WELL_DEFINED_PROCEDURE } from './procedure' +import type { ANY_LAZY_PROCEDURE, ANY_PROCEDURE, Procedure, WELL_PROCEDURE } from './procedure' import type { Caller, Context, Meta } from './types' import { executeWithHooks, trim, value } from '@orpc/shared' import { ORPCError } from '@orpc/shared/error' @@ -48,7 +48,7 @@ export function createProcedureCaller< const [input, callerOptions] = args const path = options.path ?? [] - const procedure = await loadProcedure(options.procedure) as WELL_DEFINED_PROCEDURE + const procedure = await loadProcedure(options.procedure) as WELL_PROCEDURE const context = await value(options.context) const execute = async () => { diff --git a/packages/server/src/procedure-decorated.test-d.ts b/packages/server/src/procedure-decorated.test-d.ts new file mode 100644 index 000000000..04d3044fd --- /dev/null +++ b/packages/server/src/procedure-decorated.test-d.ts @@ -0,0 +1,69 @@ +import { ContractProcedure } from '@orpc/contract' +import { Procedure } from './procedure' +import { decorateProcedure } from './procedure-decorated' + +const procedure = new Procedure({ + contract: new ContractProcedure({ + InputSchema: undefined, + OutputSchema: undefined, + }), + func: () => {}, +}) + +const decorated = decorateProcedure(procedure) + +describe('prefix', () => { + it('works', () => { + expectTypeOf(decorated.prefix('/test')).toEqualTypeOf() + + // @ts-expect-error - invalid prefix + decorated.prefix('') + // @ts-expect-error - invalid prefix + decorated.prefix(1) + }) +}) + +describe('route', () => { + it('works', () => { + expectTypeOf(decorated.route({ path: '/test', method: 'GET' })).toEqualTypeOf() + expectTypeOf(decorated.route({ + path: '/test', + method: 'GET', + description: 'description', + summary: 'summary', + deprecated: true, + tags: ['tag1', 'tag2'], + })).toEqualTypeOf() + + // @ts-expect-error - invalid method + decorated.route({ method: 'PUTT' }) + // @ts-expect-error - invalid path + decorated.route({ path: 1 }) + // @ts-expect-error - invalid tags + decorated.route({ tags: [1] }) + }) +}) + +describe('unshiftTag', () => { + it('works', () => { + expectTypeOf(decorated.unshiftTag('test')).toEqualTypeOf() + expectTypeOf(decorated.unshiftTag('test', 'test2', 'test3')).toEqualTypeOf() + + // @ts-expect-error - invalid tag + decorated.unshiftTag(1) + // @ts-expect-error - invalid tag + decorated.unshiftTag('123', 2) + }) +}) + +describe('unshiftMiddleware', () => { + it('works', () => { + expectTypeOf(decorated.unshiftMiddleware(() => {})).toEqualTypeOf() + expectTypeOf(decorated.unshiftMiddleware(() => {}, () => {})).toEqualTypeOf() + + // @ts-expect-error - invalid middleware + decorated.unshiftMiddleware(1) + // @ts-expect-error - invalid middleware + decorated.unshiftMiddleware(() => {}, 1) + }) +}) diff --git a/packages/server/src/procedure-decorated.ts b/packages/server/src/procedure-decorated.ts new file mode 100644 index 000000000..35be7ab2f --- /dev/null +++ b/packages/server/src/procedure-decorated.ts @@ -0,0 +1,139 @@ +import type { HTTPPath, RouteOptions, Schema, SchemaInput, SchemaOutput } from '@orpc/contract' +import type { MapInputMiddleware, Middleware } from './middleware' +import type { ProcedureCaller } from './procedure-caller' +import type { Context, MergeContext } from './types' +import { DecoratedContractProcedure } from '@orpc/contract' +import { decorateMiddleware } from './middleware' +import { Procedure } from './procedure' +import { createProcedureCaller } from './procedure-caller' + +export type DecoratedProcedure< + TContext extends Context, + TExtraContext extends Context, + TInputSchema extends Schema, + TOutputSchema extends Schema, + TFuncOutput extends SchemaOutput, +> = + & Procedure + & { + prefix: ( + prefix: HTTPPath, + ) => DecoratedProcedure + + route: ( + route: RouteOptions, + ) => DecoratedProcedure + + unshiftTag: (...tags: string[]) => DecoratedProcedure + + unshiftMiddleware: (...middlewares: Middleware[]) => DecoratedProcedure + + use: (< + UExtraContext extends + | Partial>> + | undefined = undefined, + >( + middleware: Middleware< + MergeContext, + UExtraContext, + SchemaOutput, + SchemaInput + >, + ) => DecoratedProcedure< + TContext, + MergeContext, + TInputSchema, + TOutputSchema, + TFuncOutput + >) & (< + UExtraContext extends + | Partial>> + | undefined = undefined, + UMappedInput = unknown, + >( + middleware: Middleware< + MergeContext, + UExtraContext, + UMappedInput, + SchemaInput + >, + mapInput: MapInputMiddleware< + SchemaOutput, + UMappedInput + >, + ) => DecoratedProcedure< + TContext, + MergeContext, + TInputSchema, + TOutputSchema, + TFuncOutput + >) + } + & (undefined extends TContext ? ProcedureCaller> : unknown) + +export function decorateProcedure< + TContext extends Context, + TExtraContext extends Context, + TInputSchema extends Schema, + TOutputSchema extends Schema, + TFuncOutput extends SchemaOutput, +>( + procedure: Procedure< + TContext, + TExtraContext, + TInputSchema, + TOutputSchema, + TFuncOutput + >, +): DecoratedProcedure { + const caller = createProcedureCaller({ + procedure: procedure as any, + context: undefined as any, + }) + + const decorated = caller as DecoratedProcedure + + decorated['~type'] = procedure['~type'] + decorated['~orpc'] = procedure['~orpc'] + + decorated.prefix = (prefix) => { + return decorateProcedure(new Procedure({ + ...procedure['~orpc'], + contract: DecoratedContractProcedure.decorate(procedure['~orpc'].contract).prefix(prefix), + })) + } + + decorated.route = (route) => { + return decorateProcedure(new Procedure({ + ...procedure['~orpc'], + contract: DecoratedContractProcedure.decorate(procedure['~orpc'].contract).route(route), + })) + } + + decorated.unshiftTag = (...tags) => { + return decorateProcedure(new Procedure({ + ...procedure['~orpc'], + contract: DecoratedContractProcedure.decorate(procedure['~orpc'].contract).unshiftTag(...tags), + })) + } + + decorated.unshiftMiddleware = (...middlewares) => { + return decorateProcedure(new Procedure({ + ...procedure['~orpc'], + middlewares: [...middlewares, ...(procedure['~orpc'].middlewares ?? [])], + })) + } + + decorated.use = (middleware: Middleware, mapInput?: MapInputMiddleware) => { + const middleware_ = mapInput + ? decorateMiddleware(middleware).mapInput(mapInput) + : middleware + + return decorateProcedure(new Procedure({ + ...procedure['~orpc'], + middlewares: [middleware_, ...(procedure['~orpc'].middlewares ?? [])], + })) as any + } + + return decorated +} diff --git a/packages/server/src/procedure-implementer.ts b/packages/server/src/procedure-implementer.ts index 672c8eb88..3ef6ace50 100644 --- a/packages/server/src/procedure-implementer.ts +++ b/packages/server/src/procedure-implementer.ts @@ -1,27 +1,35 @@ import type { ContractProcedure, Schema, SchemaInput, SchemaOutput } from '@orpc/contract' import type { DecoratedLazy } from './lazy' -import type { DecoratedProcedure, Procedure, ProcedureFunc } from './procedure' +import type { ProcedureFunc } from './procedure' +import type { DecoratedProcedure } from './procedure-decorated' import type { Context, MergeContext } from './types' -import { - decorateMiddleware, - type MapInputMiddleware, - type Middleware, -} from './middleware' -import { decorateProcedure } from './procedure' +import { decorateMiddleware, type MapInputMiddleware, type Middleware } from './middleware' +import { Procedure } from './procedure' +import { decorateProcedure } from './procedure-decorated' import { RouterBuilder } from './router-builder' +export type ProcedureImplementerDef< + _TContext extends Context, + _TExtraContext extends Context, + TInputSchema extends Schema, + TOutputSchema extends Schema, +> = { + contract: ContractProcedure + middlewares?: Middleware[] +} + export class ProcedureImplementer< TContext extends Context, TExtraContext extends Context, TInputSchema extends Schema, TOutputSchema extends Schema, > { - constructor( - public zz$pi: { - contract: ContractProcedure - middlewares?: Middleware[] - }, - ) {} + '~type' = 'ProcedureImplementer' as const + '~orpc': ProcedureImplementerDef + + constructor(def: ProcedureImplementerDef) { + this['~orpc'] = def + } use< UExtraContext extends @@ -65,44 +73,30 @@ export class ProcedureImplementer< middleware: Middleware, mapInput?: MapInputMiddleware, ): ProcedureImplementer { - const middleware_ = mapInput + const mappedMiddleware = mapInput ? decorateMiddleware(middleware).mapInput(mapInput) - : middleware + : decorateMiddleware(middleware) return new ProcedureImplementer({ - ...this.zz$pi, - middlewares: [...(this.zz$pi.middlewares ?? []), middleware_], + ...this['~orpc'], + middlewares: [...(this['~orpc'].middlewares ?? []), mappedMiddleware], }) } func>( - func: ProcedureFunc< - TContext, - TExtraContext, - TInputSchema, - TOutputSchema, - UFuncOutput - >, - ): DecoratedProcedure< - TContext, - TExtraContext, - TInputSchema, - TOutputSchema, - UFuncOutput - > { - return decorateProcedure({ - zz$p: { - middlewares: this.zz$pi.middlewares, - contract: this.zz$pi.contract, - func, - }, - }) + func: ProcedureFunc, + ): DecoratedProcedure { + return decorateProcedure(new Procedure({ + middlewares: this['~orpc'].middlewares, + contract: this['~orpc'].contract, + func, + })) } lazy>>( loader: () => Promise<{ default: U }>, ): DecoratedLazy { // TODO: replace with a more solid solution - return new RouterBuilder(this.zz$pi).lazy(loader as any) as any + return new RouterBuilder(this['~orpc']).lazy(loader as any) as any } } diff --git a/packages/server/src/procedure.test-d.ts b/packages/server/src/procedure.test-d.ts new file mode 100644 index 000000000..f1c77303c --- /dev/null +++ b/packages/server/src/procedure.test-d.ts @@ -0,0 +1,12 @@ +import type { ANY_PROCEDURE } from './procedure' +import { isProcedure } from './procedure' + +describe('isProcedure', () => { + it('works', () => { + const item = {} as unknown + + if (isProcedure(item)) { + expectTypeOf(item).toEqualTypeOf() + } + }) +}) diff --git a/packages/server/src/procedure.test.ts b/packages/server/src/procedure.test.ts index bdbd8e491..948e73349 100644 --- a/packages/server/src/procedure.test.ts +++ b/packages/server/src/procedure.test.ts @@ -1,302 +1,22 @@ -import type { MiddlewareMeta } from '.' -import { ContractProcedure, DecoratedContractProcedure } from '@orpc/contract' -import { z } from 'zod' -import { os } from '.' -import { - type DecoratedProcedure, - decorateProcedure, - isProcedure, - Procedure, -} from './procedure' +import { ContractProcedure } from '@orpc/contract' +import { isProcedure, Procedure } from './procedure' -it('isProcedure', () => { - expect( - isProcedure( - decorateProcedure( - new Procedure({ - contract: new ContractProcedure({ - InputSchema: undefined, - OutputSchema: undefined, - }), - func: () => {}, - }), - ), - ), - ).toBe(true) - expect({ - zz$p: { - contract: new DecoratedContractProcedure({ - InputSchema: undefined, - OutputSchema: undefined, - }), - func: () => {}, - }, - }).toSatisfy(isProcedure) - - expect({ - zz$p: { - contract: new DecoratedContractProcedure({ - InputSchema: undefined, - OutputSchema: undefined, - }), - }, - }).not.toSatisfy(isProcedure) - - expect({ - zz$p: { - handler: () => {}, - }, - }).not.toSatisfy(isProcedure) - - expect({}).not.toSatisfy(isProcedure) - expect(12233).not.toSatisfy(isProcedure) - expect('12233').not.toSatisfy(isProcedure) - expect(undefined).not.toSatisfy(isProcedure) - expect(null).not.toSatisfy(isProcedure) -}) - -describe('route method', () => { - it('sets route options correctly', () => { - const p = os.context<{ auth: boolean }>().func(() => { - return 'test' - }) - - const p2 = p.route({ path: '/test', method: 'GET' }) - - expect(p2.zz$p.contract['~orpc'].route?.path).toBe('/test') - expect(p2.zz$p.contract['~orpc'].route?.method).toBe('GET') - }) - - it('preserves existing context and handler', () => { - const handler = () => 'test' - const p = os.context<{ auth: boolean }>().func(handler) - - const p2 = p.route({ path: '/test' }) - - expect(p2.zz$p.func).toBe(handler) - // Context type is preserved through the route method - expectTypeOf(p2).toEqualTypeOf< - DecoratedProcedure< - { auth: boolean }, - undefined, - undefined, - undefined, - string - > - >() - }) - - it('works with prefix method', () => { - const p = os - .context<{ auth: boolean }>() - .route({ path: '/api', method: 'POST' }) - .func(() => 'test') - - const p2 = p.prefix('/v1') - - expect(p2.zz$p.contract['~orpc'].route?.path).toBe('/v1/api') - expect(p2.zz$p.contract['~orpc'].route?.method).toBe('POST') - }) - - it('works with middleware', () => { - const mid = os.middleware((_, __, meta) => meta.next({ context: { userId: '1' } })) - - const p = os - .context<{ auth: boolean }>() - .route({ path: '/test' }) - .use(mid) - .func((input, context) => { - expectTypeOf(context).toEqualTypeOf< - { auth: boolean } & { userId: string } - >() - return 'test' - }) - - expect(p.zz$p.contract['~orpc'].route?.path).toBe('/test') - expect(p.zz$p.middlewares).toEqual([mid]) - }) - - it('overrides existing route options', () => { - const p = os - .context<{ auth: boolean }>() - .route({ path: '/test1', method: 'GET' }) - .func(() => 'test') - - const p2 = p.route({ path: '/test2', method: 'POST' }) - - expect(p2.zz$p.contract['~orpc'].route?.path).toBe('/test2') - expect(p2.zz$p.contract['~orpc'].route?.method).toBe('POST') - }) - - it('preserves input/output schemas', () => { - const inputSchema = z.object({ id: z.number() }) - const outputSchema = z.string() - const p = os - .context<{ auth: boolean }>() - .input(inputSchema) - .output(outputSchema) - .route({ path: '/test' }) - .func((input) => { - expectTypeOf(input).toEqualTypeOf<{ id: number }>() - return 'test' - }) - - const p2 = p.route({ path: '/test2' }) - - // Type checking that schemas are preserved - expectTypeOf(p2).toEqualTypeOf< - DecoratedProcedure< - { auth: boolean }, - undefined, - typeof inputSchema, - typeof outputSchema, - string - > - >() - }) -}) - -it('prefix method', () => { - const p = os.context<{ auth: boolean }>().func(() => { - return 'unnoq' +describe('isProcedure', () => { + const procedure = new Procedure({ + contract: new ContractProcedure({ + InputSchema: undefined, + OutputSchema: undefined, + }), + func: () => {}, }) - const p2 = p.prefix('/test') - - expect(p2.zz$p.contract['~orpc'].route?.path).toBe(undefined) - - const p3 = os - .context<{ auth: boolean }>() - .route({ path: '/test1' }) - .func(() => { - return 'unnoq' - }) - - const p4 = p3.prefix('/test') - expect(p4.zz$p.contract['~orpc'].route?.path).toBe('/test/test1') -}) - -describe('use middleware', () => { - it('infer types', () => { - const p1 = os - .context<{ auth: boolean }>() - .use((_, __, meta) => { - return meta.next({ context: { postId: 'string' } }) - }) - .func(() => { - return 'unnoq' - }) - - const p2 = p1 - .use((input, context, meta) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf< - { auth: boolean } & { postId: string } - >() - expectTypeOf(meta).toEqualTypeOf>() - - return meta.next({ - context: { - userId: '1', - }, - }) - }) - .use((input, context, meta) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf< - { userId: string } & { postId: string } & { auth: boolean } - >() - expectTypeOf(meta).toEqualTypeOf>() - - return meta.next({}) - }) - - expectTypeOf(p2).toEqualTypeOf< - DecoratedProcedure< - { auth: boolean }, - { postId: string } & { userId: string }, - undefined, - undefined, - string - > - >() + it('works', () => { + expect(procedure).toSatisfy(isProcedure) + expect({}).not.toSatisfy(isProcedure) + expect(true).not.toSatisfy(isProcedure) }) - it('can map input', () => { - const mid = os.middleware((input: { id: number }, __, meta) => { - return meta.next({}) - }) - - os.input(z.object({ postId: z.number() })).use(mid, (input) => { - expectTypeOf(input).toEqualTypeOf<{ postId: number }>() - - return { - id: input.postId, - } - }) - - // @ts-expect-error mismatch input - os.input(z.object({ postId: z.number() })).use(mid) - - // @ts-expect-error mismatch input - os.input(z.object({ postId: z.number() })).use(mid, (input) => { - return { - wrong: input.postId, - } - }) - }) - - it('add middlewares to beginning', () => { - const mid1 = vi.fn() - const mid2 = vi.fn() - const mid3 = vi.fn() - - const p1 = os.use(mid1).func(() => 'unnoq') - const p2 = p1.use(mid2).use(mid3) - - expect(p2.zz$p.middlewares).toEqual([mid3, mid2, mid1]) - }) -}) - -describe('server action', () => { - it('only accept undefined context', () => { - expectTypeOf(os.func(() => {})).toMatchTypeOf<(...args: any[]) => any>() - expectTypeOf( - os.context<{ auth: boolean } | undefined>().func(() => {}), - ).toMatchTypeOf<(...args: any[]) => any>() - expectTypeOf( - os.context<{ auth: boolean }>().func(() => {}), - ).not.toMatchTypeOf<(...args: any[]) => any>() - }) - - it('infer types', () => { - const p = os - .input(z.object({ id: z.number() })) - .output(z.string()) - .func(() => 'string') - - expectTypeOf(p).toMatchTypeOf< - (input: { id: number }) => Promise - >() - - const p2 = os.input(z.object({ id: z.number() })).func(() => 12333) - - expectTypeOf(p2).toMatchTypeOf< - (input: { id: number }) => Promise - >() - }) - - it('works with input', async () => { - const p = os - .input(z.object({ id: z.number(), date: z.date() })) - .func(async (input, context) => { - expect(context).toBe(undefined) - return input - }) - - expect(await p({ id: 123, date: new Date('2022-01-01') })).toEqual({ - id: 123, - date: new Date('2022-01-01'), - }) + it('works with raw object', () => { + expect(Object.assign({}, procedure)).toSatisfy(isProcedure) }) }) diff --git a/packages/server/src/procedure.ts b/packages/server/src/procedure.ts index 12061b0af..54d163951 100644 --- a/packages/server/src/procedure.ts +++ b/packages/server/src/procedure.ts @@ -1,132 +1,8 @@ import type { Promisable } from '@orpc/shared' import type { Lazy } from './lazy' -import type { ProcedureCaller } from './procedure-caller' +import type { Middleware } from './middleware' import type { Context, MergeContext, Meta } from './types' -import { - type ContractProcedure, - DecoratedContractProcedure, - type HTTPPath, - isContractProcedure, - type RouteOptions, - type Schema, - type SchemaInput, - type SchemaOutput, -} from '@orpc/contract' -import { - decorateMiddleware, - type MapInputMiddleware, - type Middleware, -} from './middleware' -import { createProcedureCaller } from './procedure-caller' - -export class Procedure< - TContext extends Context, - TExtraContext extends Context, - TInputSchema extends Schema, - TOutputSchema extends Schema, - TFuncOutput extends SchemaOutput, -> { - constructor( - public zz$p: { - middlewares?: Middleware[] - contract: ContractProcedure - func: ProcedureFunc< - TContext, - TExtraContext, - TInputSchema, - TOutputSchema, - TFuncOutput - > - }, - ) {} -} - -export type ANY_PROCEDURE = Procedure -export type WELL_DEFINED_PROCEDURE = Procedure -export type ANY_LAZY_PROCEDURE = Lazy - -export type DecoratedProcedure< - TContext extends Context, - TExtraContext extends Context, - TInputSchema extends Schema, - TOutputSchema extends Schema, - TFuncOutput extends SchemaOutput, -> = Procedure< - TContext, - TExtraContext, - TInputSchema, - TOutputSchema, - TFuncOutput -> & { - prefix: ( - prefix: HTTPPath, - ) => DecoratedProcedure< - TContext, - TExtraContext, - TInputSchema, - TOutputSchema, - TFuncOutput - > - - route: ( - opts: RouteOptions, - ) => DecoratedProcedure< - TContext, - TExtraContext, - TInputSchema, - TOutputSchema, - TFuncOutput - > - - use: (< - UExtraContext extends - | Partial>> - | undefined = undefined, - >( - middleware: Middleware< - MergeContext, - UExtraContext, - SchemaOutput, - SchemaInput - >, - ) => DecoratedProcedure< - TContext, - MergeContext, - TInputSchema, - TOutputSchema, - TFuncOutput - >) & (< - UExtraContext extends - | Partial>> - | undefined = undefined, - UMappedInput = unknown, - >( - middleware: Middleware< - MergeContext, - UExtraContext, - UMappedInput, - SchemaInput - >, - mapInput: MapInputMiddleware< - SchemaOutput, - UMappedInput - >, - ) => DecoratedProcedure< - TContext, - MergeContext, - TInputSchema, - TOutputSchema, - TFuncOutput - >) -} & (undefined extends TContext - ? ProcedureCaller> - : unknown) +import { type ContractProcedure, isContractProcedure, type Schema, type SchemaInput, type SchemaOutput } from '@orpc/contract' export interface ProcedureFunc< TContext extends Context, @@ -142,93 +18,53 @@ export interface ProcedureFunc< ): Promisable> } -const DECORATED_PROCEDURE_SYMBOL = Symbol('DECORATED_PROCEDURE') - -export function decorateProcedure< +export interface ProcedureDef< TContext extends Context, TExtraContext extends Context, TInputSchema extends Schema, TOutputSchema extends Schema, TFuncOutput extends SchemaOutput, ->( - procedure: Procedure< - TContext, - TExtraContext, - TInputSchema, - TOutputSchema, - TFuncOutput - >, -): DecoratedProcedure< - TContext, - TExtraContext, - TInputSchema, - TOutputSchema, - TFuncOutput - > { - if (DECORATED_PROCEDURE_SYMBOL in procedure) { - return procedure as any - } - - return Object.assign(createProcedureCaller({ - procedure: procedure as any, - context: undefined as any, - }), { - [DECORATED_PROCEDURE_SYMBOL]: true, - zz$p: procedure.zz$p, - - prefix(prefix: HTTPPath) { - return decorateProcedure({ - zz$p: { - ...procedure.zz$p, - contract: DecoratedContractProcedure.decorate( - procedure.zz$p.contract, - ).prefix(prefix), - }, - }) - }, - - route(opts: RouteOptions) { - return decorateProcedure({ - zz$p: { - ...procedure.zz$p, - contract: DecoratedContractProcedure.decorate( - procedure.zz$p.contract, - ).route(opts), - }, - }) - }, +> { + middlewares?: Middleware[] + contract: ContractProcedure + func: ProcedureFunc +} - use( - middleware: Middleware, - mapInput?: MapInputMiddleware, - ) { - const middleware_ = mapInput - ? decorateMiddleware(middleware).mapInput(mapInput) - : middleware +export class Procedure< + TContext extends Context, + TExtraContext extends Context, + TInputSchema extends Schema, + TOutputSchema extends Schema, + TFuncOutput extends SchemaOutput, +> { + '~type' = 'Procedure' as const + '~orpc': ProcedureDef - return decorateProcedure({ - zz$p: { - ...procedure.zz$p, - middlewares: [middleware_, ...(procedure.zz$p.middlewares ?? [])], - }, - }) - }, - }) as any + constructor(def: ProcedureDef) { + this['~orpc'] = def + } } +export type ANY_PROCEDURE = Procedure +export type WELL_PROCEDURE = Procedure +export type ANY_LAZY_PROCEDURE = Lazy + export function isProcedure(item: unknown): item is ANY_PROCEDURE { - if (item instanceof Procedure) + if (item instanceof Procedure) { return true + } return ( (typeof item === 'object' || typeof item === 'function') && item !== null - && 'zz$p' in item - && typeof item.zz$p === 'object' - && item.zz$p !== null - && 'contract' in item.zz$p - && isContractProcedure(item.zz$p.contract) - && 'func' in item.zz$p - && typeof item.zz$p.func === 'function' + && '~type' in item + && item['~type'] === 'Procedure' + && '~orpc' in item + && typeof item['~orpc'] === 'object' + && item['~orpc'] !== null + && 'contract' in item['~orpc'] + && isContractProcedure(item['~orpc'].contract) + && 'func' in item['~orpc'] + && typeof item['~orpc'].func === 'function' ) } diff --git a/packages/server/src/router-builder.ts b/packages/server/src/router-builder.ts index d5f9f25cf..7c292fce4 100644 --- a/packages/server/src/router-builder.ts +++ b/packages/server/src/router-builder.ts @@ -211,7 +211,7 @@ function adaptProcedure(options: { let contract = DecoratedContractProcedure.decorate( options.procedure.zz$p.contract, - ).pushTag(...(options.tags ?? [])) + ).unshiftTag(...(options.tags ?? [])) if (options.prefix) { contract = contract.prefix(options.prefix) diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index 3f9a5b9ea..fc2f98c3b 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -1,6 +1,7 @@ -import type { WELL_DEFINED_PROCEDURE } from './procedure' +import type { WELL_PROCEDURE } from './procedure' -export type Context = Record | undefined +export type Context = Record | undefined +export type WELL_CONTEXT = Record | undefined export type MergeContext< TA extends Context, @@ -17,5 +18,5 @@ export interface Caller { export interface Meta extends CallerOptions { path: string[] - procedure: WELL_DEFINED_PROCEDURE + procedure: WELL_PROCEDURE } From 37440e4cf6e19af7ac75b459854bc017c89ff077 Mon Sep 17 00:00:00 2001 From: unnoq Date: Sat, 14 Dec 2024 11:22:39 +0700 Subject: [PATCH 02/51] wip --- packages/server/package.json | 3 +- packages/server/src/middleware.ts | 4 +- .../server/src/procedure-builder.test-d.ts | 165 ++++++++++++++++++ packages/server/src/procedure-builder.ts | 52 +++--- packages/server/src/procedure-decorated.ts | 2 +- packages/server/src/procedure.ts | 4 +- pnpm-lock.yaml | 3 + 7 files changed, 197 insertions(+), 36 deletions(-) create mode 100644 packages/server/src/procedure-builder.test-d.ts diff --git a/packages/server/package.json b/packages/server/package.json index 2e4163028..43761fe41 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -56,6 +56,7 @@ "@orpc/transformer": "workspace:*" }, "devDependencies": { - "@orpc/openapi": "workspace:*" + "@orpc/openapi": "workspace:*", + "zod": "^3.24.1" } } diff --git a/packages/server/src/middleware.ts b/packages/server/src/middleware.ts index c394d13bd..a7cff6338 100644 --- a/packages/server/src/middleware.ts +++ b/packages/server/src/middleware.ts @@ -43,7 +43,7 @@ export interface DecoratedMiddleware< TOutput, > extends Middleware { concat: (< - UExtraContext extends Context & (Partial> | undefined) = undefined, + UExtraContext extends Context & Partial> | undefined = undefined, UInput = unknown, >( middleware: Middleware< @@ -58,7 +58,7 @@ export interface DecoratedMiddleware< UInput & TInput, TOutput >) & (< - UExtraContext extends Context & (Partial> | undefined) = undefined, + UExtraContext extends Context & Partial> | undefined = undefined, UInput = TInput, UMappedInput = unknown, >( diff --git a/packages/server/src/procedure-builder.test-d.ts b/packages/server/src/procedure-builder.test-d.ts new file mode 100644 index 000000000..22e10583d --- /dev/null +++ b/packages/server/src/procedure-builder.test-d.ts @@ -0,0 +1,165 @@ +import type { RouteOptions } from '@orpc/contract' +import type { Middleware } from './middleware' +import type { DecoratedProcedure } from './procedure-decorated' +import type { ProcedureImplementer } from './procedure-implementer' +import type { WELL_CONTEXT } from './types' +import { ContractProcedure } from '@orpc/contract' +import { z } from 'zod' +import { ProcedureBuilder } from './procedure-builder' + +describe('self chainable', () => { + const builder = new ProcedureBuilder<{ id?: string }, undefined, undefined, undefined>({ + contract: new ContractProcedure({ + InputSchema: undefined, + OutputSchema: undefined, + }), + middlewares: [], + }) + + const schema = z.object({ id: z.string().transform(v => Number.parseInt(v)) }) + + it('route', () => { + expectTypeOf(builder.route).toEqualTypeOf((route: RouteOptions) => builder) + }) + + it('input', () => { + expectTypeOf(builder.input(schema)) + .toEqualTypeOf>() + + expectTypeOf(builder.input(schema, { id: '1' })) + .toEqualTypeOf>() + + // @ts-expect-error - invalid schema + builder.input({}) + + // @ts-expect-error - invalid example + builder.input(schema, {}) + + // @ts-expect-error - invalid example + builder.input(schema, { id: 1 }) + }) + + it('output', () => { + expectTypeOf(builder.output(schema)) + .toEqualTypeOf>() + + expectTypeOf(builder.output(schema, { id: 1 })) + .toEqualTypeOf>() + + // @ts-expect-error - invalid schema + builder.output({}) + + // @ts-expect-error - invalid example + builder.output(schema, {}) + + // @ts-expect-error - invalid example + builder.output(schema, { id: '1' }) + }) +}) + +it('to ProcedureImplementer', () => { + const schema = z.object({ id: z.string().transform(v => Number.parseInt(v)) }) + + const builder = new ProcedureBuilder<{ id?: string } | undefined, undefined, typeof schema, typeof schema>({ + contract: new ContractProcedure({ + InputSchema: schema, + OutputSchema: schema, + }), + middlewares: [], + }) + + it('use middleware', () => { + const implementer = builder.use(async (input, context, meta) => { + expectTypeOf(context).toEqualTypeOf<{ id?: string } | undefined>() + expectTypeOf(input).toEqualTypeOf<{ id: number }>() + + const result = await meta.next({}) + + expectTypeOf(result.output).toEqualTypeOf<{ id: string }>() + + return meta.next({ context: { id: '1', extra: true } }) + }) + + expectTypeOf(implementer).toEqualTypeOf< + ProcedureImplementer<{ id?: string } | undefined, { id: string, extra: boolean }, typeof schema, typeof schema> + >() + }) + + it('use middleware with map input', () => { + const mid: Middleware = (input, context, meta) => { + return meta.next({ + context: { id: 'string', extra: true }, + }) + } + + const implementer = builder.use(mid, (input) => { + expectTypeOf(input).toEqualTypeOf<{ id: number }>() + return input.id + }) + + expectTypeOf(implementer).toEqualTypeOf< + ProcedureImplementer<{ id?: string } | undefined, { id: string, extra: boolean }, typeof schema, typeof schema> + >() + + // @ts-expect-error - invalid input + builder.use(mid) + + // @ts-expect-error - invalid mapped input + builder.use(mid, input => input) + }) + + it('prevent conflict on context', () => { + builder.use((input, context, meta) => meta.next({})) + builder.use((input, context, meta) => meta.next({ context: { id: '1' } })) + builder.use((input, context, meta) => meta.next({ context: { id: '1', extra: true } })) + builder.use((input, context, meta) => meta.next({ context: { auth: true } })) + + builder.use((input, context, meta) => meta.next({}), () => 'anything') + builder.use((input, context, meta) => meta.next({ context: { id: '1' } }), () => 'anything') + builder.use((input, context, meta) => meta.next({ context: { id: '1', extra: true } }), () => 'anything') + builder.use((input, context, meta) => meta.next({ context: { auth: true } }), () => 'anything') + + // @ts-expect-error - conflict with context + builder.use((input, context, meta) => meta.next({ context: { id: 1 } })) + + // @ts-expect-error - conflict with context + builder.use((input, context, meta) => meta.next({ context: { id: 1, extra: true } })) + + // @ts-expect-error - conflict with context + builder.use((input, context, meta) => meta.next({ context: { id: 1 } }), () => 'anything') + + // @ts-expect-error - conflict with context + builder.use((input, context, meta) => meta.next({ context: { id: 1, extra: true } }), () => 'anything') + }) +}) + +it('to DecoratedProcedure', () => { + const schema = z.object({ id: z.string().transform(v => Number.parseInt(v)) }) + + const builder = new ProcedureBuilder<{ id?: string } | undefined, undefined, typeof schema, typeof schema>({ + contract: new ContractProcedure({ + InputSchema: schema, + OutputSchema: schema, + }), + middlewares: [], + }) + + it('func', () => { + const procedure = builder.func(async (input, context, meta) => { + expectTypeOf(context).toEqualTypeOf<{ id?: string } | undefined>() + expectTypeOf(input).toEqualTypeOf<{ id: number }>() + + return { id: '1' } + }) + + expectTypeOf(procedure).toEqualTypeOf< + DecoratedProcedure<{ id?: string } | undefined, undefined, typeof schema, typeof schema, { id: string }> + >() + + // @ts-expect-error - invalid output + builder.func(async (input, context, meta) => ({ id: 1 })) + + // @ts-expect-error - invalid output + builder.func(async (input, context, meta) => (true)) + }) +}) diff --git a/packages/server/src/procedure-builder.ts b/packages/server/src/procedure-builder.ts index 37abf13a9..a44c2ddbf 100644 --- a/packages/server/src/procedure-builder.ts +++ b/packages/server/src/procedure-builder.ts @@ -17,13 +17,13 @@ import { decorateProcedure } from './procedure-decorated' import { ProcedureImplementer } from './procedure-implementer' export interface ProcedureBuilderDef< - _TContext extends Context, - _TExtraContext extends Context, + TContext extends Context, + TExtraContext extends Context, TInputSchema extends Schema, TOutputSchema extends Schema, > { contract: ContractProcedure - middlewares?: Middleware[] + middlewares?: Middleware[] } export class ProcedureBuilder< @@ -39,9 +39,7 @@ export class ProcedureBuilder< this['~orpc'] = def } - route( - route: RouteOptions, - ): ProcedureBuilder { + route(route: RouteOptions): ProcedureBuilder { return new ProcedureBuilder({ ...this['~orpc'], contract: DecoratedContractProcedure @@ -50,10 +48,10 @@ export class ProcedureBuilder< }) } - input( - schema: USchema, - example?: SchemaInput, - ): ProcedureBuilder { + input( + schema: U, + example?: SchemaInput, + ): ProcedureBuilder { return new ProcedureBuilder({ ...this['~orpc'], contract: DecoratedContractProcedure @@ -62,10 +60,10 @@ export class ProcedureBuilder< }) } - output( - schema: USchema, - example?: SchemaOutput, - ): ProcedureBuilder { + output( + schema: U, + example?: SchemaOutput, + ): ProcedureBuilder { return new ProcedureBuilder({ ...this['~orpc'], contract: DecoratedContractProcedure @@ -74,40 +72,34 @@ export class ProcedureBuilder< }) } - use< - UExtraContext extends - | Partial>> - | undefined = undefined, - >( + use> | undefined = undefined>( middleware: Middleware< MergeContext, - UExtraContext, + U, SchemaOutput, SchemaInput >, ): ProcedureImplementer< TContext, - MergeContext, + MergeContext, TInputSchema, TOutputSchema > use< - UExtraContext extends - | Partial>> - | undefined = undefined, - UMappedInput = unknown, + UExtra extends Context & Partial> | undefined = undefined, + UInput = unknown, >( middleware: Middleware< MergeContext, - UExtraContext, - UMappedInput, + UExtra, + UInput, SchemaInput >, - mapInput: MapInputMiddleware, UMappedInput>, + mapInput: MapInputMiddleware, UInput>, ): ProcedureImplementer< TContext, - MergeContext, + MergeContext, TInputSchema, TOutputSchema > @@ -129,7 +121,7 @@ export class ProcedureBuilder< }).use(middleware, mapInput) } - func>( + func>( func: ProcedureFunc, ): DecoratedProcedure { return decorateProcedure(new Procedure({ diff --git a/packages/server/src/procedure-decorated.ts b/packages/server/src/procedure-decorated.ts index 35be7ab2f..49c0b5259 100644 --- a/packages/server/src/procedure-decorated.ts +++ b/packages/server/src/procedure-decorated.ts @@ -12,7 +12,7 @@ export type DecoratedProcedure< TExtraContext extends Context, TInputSchema extends Schema, TOutputSchema extends Schema, - TFuncOutput extends SchemaOutput, + TFuncOutput extends SchemaInput, > = & Procedure & { diff --git a/packages/server/src/procedure.ts b/packages/server/src/procedure.ts index 54d163951..ef7bdb9c8 100644 --- a/packages/server/src/procedure.ts +++ b/packages/server/src/procedure.ts @@ -9,7 +9,7 @@ export interface ProcedureFunc< TExtraContext extends Context, TInputSchema extends Schema, TOutputSchema extends Schema, - TOutput extends SchemaOutput, + TOutput extends SchemaInput, > { ( input: SchemaOutput, @@ -35,7 +35,7 @@ export class Procedure< TExtraContext extends Context, TInputSchema extends Schema, TOutputSchema extends Schema, - TFuncOutput extends SchemaOutput, + TFuncOutput extends SchemaInput, > { '~type' = 'Procedure' as const '~orpc': ProcedureDef diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a82e7c32f..46068357a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -328,6 +328,9 @@ importers: '@orpc/openapi': specifier: workspace:* version: link:../openapi + zod: + specifier: ^3.24.1 + version: 3.24.1 packages/shared: dependencies: From abc387a9fcaa89883f594e04c80cf23845f6b9d4 Mon Sep 17 00:00:00 2001 From: unnoq Date: Sat, 14 Dec 2024 15:31:03 +0700 Subject: [PATCH 03/51] procedure builder tests --- packages/contract/src/procedure.ts | 2 +- .../server/src/procedure-builder.test-d.ts | 4 +- packages/server/src/procedure-builder.test.ts | 286 ++++++------------ 3 files changed, 89 insertions(+), 203 deletions(-) diff --git a/packages/contract/src/procedure.ts b/packages/contract/src/procedure.ts index cfc30e7ce..6e29a435f 100644 --- a/packages/contract/src/procedure.ts +++ b/packages/contract/src/procedure.ts @@ -11,7 +11,7 @@ export interface RouteOptions { summary?: string description?: string deprecated?: boolean - tags?: string[] + tags?: readonly string[] } export interface ContractProcedureDef { diff --git a/packages/server/src/procedure-builder.test-d.ts b/packages/server/src/procedure-builder.test-d.ts index 22e10583d..517c98081 100644 --- a/packages/server/src/procedure-builder.test-d.ts +++ b/packages/server/src/procedure-builder.test-d.ts @@ -57,7 +57,7 @@ describe('self chainable', () => { }) }) -it('to ProcedureImplementer', () => { +describe('to ProcedureImplementer', () => { const schema = z.object({ id: z.string().transform(v => Number.parseInt(v)) }) const builder = new ProcedureBuilder<{ id?: string } | undefined, undefined, typeof schema, typeof schema>({ @@ -133,7 +133,7 @@ it('to ProcedureImplementer', () => { }) }) -it('to DecoratedProcedure', () => { +describe('to DecoratedProcedure', () => { const schema = z.object({ id: z.string().transform(v => Number.parseInt(v)) }) const builder = new ProcedureBuilder<{ id?: string } | undefined, undefined, typeof schema, typeof schema>({ diff --git a/packages/server/src/procedure-builder.test.ts b/packages/server/src/procedure-builder.test.ts index 12846a6cf..cd8de53f6 100644 --- a/packages/server/src/procedure-builder.test.ts +++ b/packages/server/src/procedure-builder.test.ts @@ -1,225 +1,111 @@ -import type { MiddlewareMeta } from './middleware' -import type { ProcedureImplementer } from './procedure-implementer' -import type { Meta } from './types' import { ContractProcedure } from '@orpc/contract' import { z } from 'zod' -import { os } from '.' -import { type DecoratedProcedure, isProcedure } from './procedure' +import { isProcedure } from './procedure' import { ProcedureBuilder } from './procedure-builder' +import { ProcedureImplementer } from './procedure-implementer' + +describe('self chainable', () => { + const builder = new ProcedureBuilder<{ id?: string }, undefined, undefined, undefined>({ + contract: new ContractProcedure({ + InputSchema: undefined, + OutputSchema: undefined, + }), + middlewares: [], + }) -const schema1 = z.object({ id: z.string() }) -const example1 = { id: '1' } -const schema2 = z.object({ name: z.string() }) -const example2 = { name: 'unnoq' } - -const builder = new ProcedureBuilder< - { auth: boolean }, - undefined, - undefined, - undefined ->({ - contract: new ContractProcedure({ - InputSchema: undefined, - OutputSchema: undefined, - }), -}) - -it('input', () => { - const builder2 = builder.input(schema1, example1) + const schema = z.object({ id: z.string().transform(v => Number.parseInt(v)) }) + const example = { id: '1' } + const out_example = { id: 1 } - expectTypeOf(builder2).toEqualTypeOf< - ProcedureBuilder<{ auth: boolean }, undefined, typeof schema1, undefined> - >() + it('route', () => { + const route = { method: 'GET', path: '/test', deprecated: true, description: 'des', summary: 'sum', tags: ['hi'] } as const + const routed = builder.route(route) - expect(builder2.zz$pb).toMatchObject({ - contract: { - '~orpc': { - InputSchema: schema1, - inputExample: example1, - }, - }, + expect(routed).not.toBe(builder) + expect(routed).toBeInstanceOf(ProcedureBuilder) + expect(routed['~orpc'].contract['~orpc'].route).toBe(route) }) -}) -it('output', () => { - const builder2 = builder.output(schema2, example2) + it('input', () => { + const input_ed = builder.input(schema, example) - expectTypeOf(builder2).toEqualTypeOf< - ProcedureBuilder<{ auth: boolean }, undefined, undefined, typeof schema2> - >() + expect(input_ed).not.toBe(builder) + expect(input_ed).toBeInstanceOf(ProcedureBuilder) + expect(input_ed['~orpc'].contract['~orpc'].InputSchema).toBe(schema) + expect(input_ed['~orpc'].contract['~orpc'].inputExample).toBe(example) + }) - expect(builder2.zz$pb).toMatchObject({ - contract: { - '~orpc': { - OutputSchema: schema2, - outputExample: example2, - }, - }, + it('output', () => { + const output_ed = builder.output(schema, out_example) + + expect(output_ed).not.toBe(builder) + expect(output_ed).toBeInstanceOf(ProcedureBuilder) + expect(output_ed['~orpc'].contract['~orpc'].OutputSchema).toBe(schema) + expect(output_ed['~orpc'].contract['~orpc'].outputExample).toBe(out_example) }) }) -it('route', () => { - const builder2 = builder.route({ - method: 'GET', - path: '/test', - deprecated: true, - description: 'des', - summary: 'sum', - tags: ['hi'], - }) +describe('to ProcedureImplementer', () => { + const schema = z.object({ id: z.string().transform(v => Number.parseInt(v)) }) - expectTypeOf(builder2).toEqualTypeOf< - ProcedureBuilder<{ auth: boolean }, undefined, undefined, undefined> - >() - - expect(builder2.zz$pb).toMatchObject({ - contract: { - '~orpc': { - route: { - method: 'GET', - path: '/test', - deprecated: true, - description: 'des', - summary: 'sum', - tags: ['hi'], - }, - }, - }, + const global_mid = vi.fn() + const builder = new ProcedureBuilder<{ id?: string } | undefined, undefined, typeof schema, typeof schema>({ + contract: new ContractProcedure({ + InputSchema: schema, + OutputSchema: schema, + }), + middlewares: [global_mid], }) -}) -describe('use middleware', () => { - it('infer types', () => { - const implementer = builder - .use((input, context, meta) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf<{ auth: boolean }>() - expectTypeOf(meta).toEqualTypeOf>() - - return meta.next({ - context: { - userId: '1', - }, - }) - }) - .use((input, context, meta) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf< - { userId: string } & { auth: boolean } - >() - expectTypeOf(meta).toEqualTypeOf>() - - return meta.next({}) - }) - - expectTypeOf(implementer).toEqualTypeOf< - ProcedureImplementer< - { auth: boolean }, - { userId: string }, - undefined, - undefined - > - >() + it('use middleware', () => { + const mid = vi.fn() + + const implementer = builder.use(mid) + + expect(implementer).toBeInstanceOf(ProcedureImplementer) + expect(implementer['~orpc'].middlewares).toEqual([global_mid, mid]) }) - it('map middleware input', () => { - // @ts-expect-error mismatch input - builder.use((input: { postId: string }) => { - return { context: { a: 'a' } } - }) - - builder.use( - (input: { postId: string }, _, meta) => { - return meta.next({ context: { a: 'a' } }) - }, - // @ts-expect-error mismatch input - input => ({ postId: 12455 }), - ) - - builder.use( - (input: { postId: string }, context, meta) => meta.next({}), - input => ({ postId: '12455' }), - ) - - const implementer = builder.input(schema1).use( - (input: { id: number }, _, meta) => { - return meta.next({ - context: { - userId555: '1', - }, - }) - }, - input => ({ id: Number.parseInt(input.id) }), - ) - - expectTypeOf(implementer).toEqualTypeOf< - ProcedureImplementer< - { auth: boolean }, - { userId555: string }, - typeof schema1, - undefined - > - >() + it('use middleware with map input', () => { + const mid = vi.fn() + const map_input = vi.fn() + + const implementer = builder.use(mid, map_input) + expect(implementer).toBeInstanceOf(ProcedureImplementer) + expect(implementer['~orpc'].middlewares).toEqual([global_mid, expect.any(Function)]) + + map_input.mockReturnValueOnce('__input__') + mid.mockReturnValueOnce('__mid__') + + expect((implementer as any)['~orpc'].middlewares[1]('input')).toBe('__mid__') + + expect(map_input).toBeCalledTimes(1) + expect(map_input).toBeCalledWith('input') + + expect(mid).toBeCalledTimes(1) + expect(mid).toBeCalledWith('__input__') }) }) -describe('handler', () => { - it('infer types', () => { - const handler = builder.func((input, context, meta) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf<{ auth: boolean }>() - expectTypeOf(meta).toEqualTypeOf() - }) - - expectTypeOf(handler).toEqualTypeOf< - DecoratedProcedure< - { auth: boolean }, - undefined, - undefined, - undefined, - void - > - >() - - expect(isProcedure(handler)).toBe(true) +describe('to DecoratedProcedure', () => { + const schema = z.object({ id: z.string().transform(v => Number.parseInt(v)) }) + + const global_mid = vi.fn() + const builder = new ProcedureBuilder<{ id?: string } | undefined, undefined, typeof schema, typeof schema>({ + contract: new ContractProcedure({ + InputSchema: schema, + OutputSchema: schema, + }), + middlewares: [global_mid], }) - it('combine middlewares', () => { - const mid1 = os.middleware((input, context, meta) => { - return meta.next({ - context: { - userId: '1', - }, - }) - }) - - const mid2 = os.middleware((_, __, meta) => meta.next({})) - - const handler = builder - .use(mid1) - .use(mid2) - .func((input, context, meta) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf< - { userId: string } & { auth: boolean } - >() - expectTypeOf(meta).toEqualTypeOf() - - return { - name: 'unnoq', - } - }) - - expectTypeOf(handler).toEqualTypeOf< - DecoratedProcedure< - { auth: boolean }, - { userId: string }, - undefined, - undefined, - { name: string } - > - >() - - expect(handler.zz$p.middlewares).toEqual([mid1, mid2]) + it('func', () => { + const func = vi.fn() + const procedure = builder.func(func) + + expect(procedure).toSatisfy(isProcedure) + + expect(procedure['~orpc'].func).toBe(func) + expect(procedure['~orpc'].middlewares).toEqual([global_mid]) }) }) From 88ce38a0e98542e01dbe6635cc9a1014764d1557 Mon Sep 17 00:00:00 2001 From: unnoq Date: Sat, 14 Dec 2024 16:23:37 +0700 Subject: [PATCH 04/51] procedure implementer tests --- .../server/src/procedure-builder.test-d.ts | 3 +- .../src/procedure-implementer.test-d.ts | 179 ++++++++++++ .../server/src/procedure-implementer.test.ts | 267 +++++------------- packages/server/src/procedure-implementer.ts | 45 ++- 4 files changed, 265 insertions(+), 229 deletions(-) create mode 100644 packages/server/src/procedure-implementer.test-d.ts diff --git a/packages/server/src/procedure-builder.test-d.ts b/packages/server/src/procedure-builder.test-d.ts index 517c98081..d7b2110b6 100644 --- a/packages/server/src/procedure-builder.test-d.ts +++ b/packages/server/src/procedure-builder.test-d.ts @@ -2,7 +2,7 @@ import type { RouteOptions } from '@orpc/contract' import type { Middleware } from './middleware' import type { DecoratedProcedure } from './procedure-decorated' import type { ProcedureImplementer } from './procedure-implementer' -import type { WELL_CONTEXT } from './types' +import type { Meta, WELL_CONTEXT } from './types' import { ContractProcedure } from '@orpc/contract' import { z } from 'zod' import { ProcedureBuilder } from './procedure-builder' @@ -148,6 +148,7 @@ describe('to DecoratedProcedure', () => { const procedure = builder.func(async (input, context, meta) => { expectTypeOf(context).toEqualTypeOf<{ id?: string } | undefined>() expectTypeOf(input).toEqualTypeOf<{ id: number }>() + expectTypeOf(meta).toEqualTypeOf() return { id: '1' } }) diff --git a/packages/server/src/procedure-implementer.test-d.ts b/packages/server/src/procedure-implementer.test-d.ts new file mode 100644 index 000000000..34b1a303b --- /dev/null +++ b/packages/server/src/procedure-implementer.test-d.ts @@ -0,0 +1,179 @@ +import type { DecoratedLazy } from './lazy' +import type { Middleware, MiddlewareMeta } from './middleware' +import type { Procedure } from './procedure' +import type { DecoratedProcedure } from './procedure-decorated' +import type { Meta, WELL_CONTEXT } from './types' +import { ContractProcedure } from '@orpc/contract' +import { z } from 'zod' +import { ProcedureImplementer } from './procedure-implementer' + +describe('self chainable', () => { + const global_mid = vi.fn() + const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) + const implementer = new ProcedureImplementer<{ id?: string }, undefined, typeof schema, typeof schema>({ + contract: new ContractProcedure({ + InputSchema: schema, + OutputSchema: schema, + }), + middlewares: [global_mid], + }) + + it('use middleware', () => { + const i = implementer + .use((input, context, meta) => { + expectTypeOf(input).toEqualTypeOf<{ val: number }>() + expectTypeOf(context).toEqualTypeOf<{ id?: string }>() + expectTypeOf(meta).toEqualTypeOf>() + + return meta.next({ + context: { + auth: true, + }, + }) + }) + .use((input, context, meta) => { + expectTypeOf(input).toEqualTypeOf<{ val: number }>() + expectTypeOf(context).toEqualTypeOf< + { id?: string } & { auth: boolean } + >() + expectTypeOf(meta).toEqualTypeOf>() + + return meta.next({}) + }) + + expectTypeOf(i).toEqualTypeOf< + ProcedureImplementer< + { id?: string }, + { auth: boolean }, + typeof schema, + typeof schema + > + >() + }) + + it('use middleware with map input', () => { + const mid: Middleware = (input, context, meta) => { + return meta.next({ + context: { id: 'string', extra: true }, + }) + } + + const i = implementer.use(mid, (input) => { + expectTypeOf(input).toEqualTypeOf<{ val: number }>() + return input.val + }) + + expectTypeOf(i).toEqualTypeOf< + ProcedureImplementer< + { id?: string }, + { id: string, extra: boolean }, + typeof schema, + typeof schema + > + >() + + // @ts-expect-error - invalid input + implementer.use(mid) + + // @ts-expect-error - invalid mapped input + implementer.use(mid, input => input) + }) + + it('prevent conflict on context', () => { + implementer.use((input, context, meta) => meta.next({})) + implementer.use((input, context, meta) => meta.next({ context: { id: '1' } })) + implementer.use((input, context, meta) => meta.next({ context: { id: '1', extra: true } })) + implementer.use((input, context, meta) => meta.next({ context: { auth: true } })) + + implementer.use((input, context, meta) => meta.next({}), () => 'anything') + implementer.use((input, context, meta) => meta.next({ context: { id: '1' } }), () => 'anything') + implementer.use((input, context, meta) => meta.next({ context: { id: '1', extra: true } }), () => 'anything') + implementer.use((input, context, meta) => meta.next({ context: { auth: true } }), () => 'anything') + + // @ts-expect-error - conflict with context + implementer.use((input, context, meta) => meta.next({ context: { id: 1 } })) + + // @ts-expect-error - conflict with context + implementer.use((input, context, meta) => meta.next({ context: { id: 1, extra: true } })) + + // @ts-expect-error - conflict with context + implementer.use((input, context, meta) => meta.next({ context: { id: 1 } }), () => 'anything') + + // @ts-expect-error - conflict with context + implementer.use((input, context, meta) => meta.next({ context: { id: 1, extra: true } }), () => 'anything') + }) +}) + +describe('to DecoratedProcedure', () => { + const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) + + const global_mid = vi.fn() + const implementer = new ProcedureImplementer<{ id?: string } | undefined, { db: string }, typeof schema, typeof schema>({ + contract: new ContractProcedure({ + InputSchema: schema, + OutputSchema: schema, + }), + middlewares: [global_mid], + }) + + it('func', () => { + const procedure = implementer.func((input, context, meta) => { + expectTypeOf(context).toEqualTypeOf<({ id?: string } & { db: string }) | { db: string }>() + expectTypeOf(input).toEqualTypeOf<{ val: number }>() + expectTypeOf(meta).toEqualTypeOf() + + return { val: '1' } + }) + + expectTypeOf(procedure).toEqualTypeOf< + DecoratedProcedure<{ id?: string } | undefined, { db: string }, typeof schema, typeof schema, { val: string }> + >() + + // @ts-expect-error - invalid output + implementer.func(() => ({ val: 1 })) + + // @ts-expect-error - invalid output + implementer.func(() => {}) + }) +}) + +describe('to DecoratedLazy', () => { + const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) + + const global_mid = vi.fn() + const implementer = new ProcedureImplementer<{ id?: string } | undefined, { db: string }, typeof schema, typeof schema>({ + contract: new ContractProcedure({ + InputSchema: schema, + OutputSchema: schema, + }), + middlewares: [global_mid], + }) + + it('lazy', () => { + const lazy = implementer.lazy(() => Promise.resolve({ + default: {} as Procedure<{ id?: string } | undefined, { db: string }, typeof schema, typeof schema, { val: string }>, + })) + + expectTypeOf(lazy).toEqualTypeOf< + DecoratedLazy< + Procedure<{ id?: string } | undefined, { db: string }, typeof schema, typeof schema, { val: string }> + > + >() + + // @ts-expect-error - invalid procedure + implementer.lazy(() => Promise.resolve({ default: {} })) + // @ts-expect-error - invalid procedure + implementer.lazy(() => Promise.resolve({ + default: {} as Procedure<{ id?: string } | undefined, { db: string }, undefined, typeof schema, { val: string }>, + })) + // @ts-expect-error - invalid procedure + implementer.lazy(() => Promise.resolve({ + default: {} as Procedure<{ id?: string } | undefined, { db: string }, typeof schema, undefined, { val: string }>, + })) + // @ts-expect-error - invalid procedure + implementer.lazy(() => Promise.resolve({ + // @ts-expect-error - invalid func output + default: {} as Procedure<{ id?: string } | undefined, { db: string }, typeof schema, typeof schema, { val: number }>, + })) + }) +}) diff --git a/packages/server/src/procedure-implementer.test.ts b/packages/server/src/procedure-implementer.test.ts index 6a4c985b7..cba8f9651 100644 --- a/packages/server/src/procedure-implementer.test.ts +++ b/packages/server/src/procedure-implementer.test.ts @@ -1,224 +1,85 @@ -import type { DecoratedProcedure, Meta, MiddlewareMeta } from '.' -import { DecoratedContractProcedure } from '@orpc/contract' +import { ContractProcedure } from '@orpc/contract' import { z } from 'zod' -import { isProcedure, os } from '.' +import { isProcedure } from './procedure' import { ProcedureImplementer } from './procedure-implementer' -const p1 = new DecoratedContractProcedure({ - InputSchema: undefined, - OutputSchema: undefined, - route: { - method: undefined, - path: undefined, - }, -}) -const implementer1 = new ProcedureImplementer< - { auth: boolean }, - undefined, - undefined, - undefined ->({ contract: p1 }) - -const schema1 = z.object({ id: z.string() }) -const schema2 = z.object({ name: z.string() }) - -const p2 = new DecoratedContractProcedure({ - InputSchema: schema1, - OutputSchema: schema2, - route: { - method: 'GET', - path: '/test', - }, -}) - -const implementer2 = new ProcedureImplementer< - { auth: boolean }, - undefined, - typeof schema1, - typeof schema2 ->({ contract: p2 }) - -describe('use middleware', () => { - it('infer types', () => { - const i = implementer1 - .use((input, context, meta) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf<{ auth: boolean }>() - expectTypeOf(meta).toEqualTypeOf>() - - return meta.next({ - context: { - userId: '1', - }, - }) - }) - .use((input, context, meta) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf< - { userId: string } & { auth: boolean } - >() - expectTypeOf(meta).toEqualTypeOf>() - - return meta.next({}) - }) - - expectTypeOf(i).toEqualTypeOf< - ProcedureImplementer< - { auth: boolean }, - { userId: string }, - undefined, - undefined - > - >() +describe('self chainable', () => { + const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) + const implementer = new ProcedureImplementer<{ id?: string }, undefined, typeof schema, typeof schema>({ + contract: new ContractProcedure({ + InputSchema: schema, + OutputSchema: schema, + }), }) - it('map middleware input', () => { - // @ts-expect-error mismatch input - implementer2.use((input: { postId: string }) => { - return { context: { a: 'a' } } - }) - - implementer2.use( - (input: { postId: string }, _, meta) => { - return meta.next({ context: { a: 'a' } }) - }, - // @ts-expect-error mismatch input - input => ({ postId: 12455 }), - ) - - implementer2.use( - (input: { postId: string }, context, meta) => meta.next({}), - input => ({ postId: '12455' }), - ) - - const i = implementer2.use( - (input: { id: number }, _, meta) => { - return meta.next({ - context: { - userIdd: '1', - }, - }) - }, - input => ({ id: Number.parseInt(input.id) }), - ) - - expectTypeOf(i).toEqualTypeOf< - ProcedureImplementer< - { auth: boolean }, - { userIdd: string }, - typeof schema1, - typeof schema2 - > - >() + it('use middleware', () => { + const mid1 = vi.fn() + const mid2 = vi.fn() + const i = implementer.use(mid1).use(mid2) + + expect(i).not.toBe(implementer) + expect(i).toBeInstanceOf(ProcedureImplementer) + expect(i['~orpc'].middlewares).toEqual([mid1, mid2]) }) -}) -describe('output schema', () => { - it('auto infer output schema if output schema is not specified', async () => { - const sr = os.func(() => ({ a: 1 })) + it('use middleware with map input', () => { + const mid = vi.fn() + const map = vi.fn() + + const i = implementer.use(mid, map) + + expect(i).not.toBe(implementer) + expect(i).toBeInstanceOf(ProcedureImplementer) + expect(i['~orpc'].middlewares).toEqual([expect.any(Function)]) - const result = await sr.zz$p.func({}, undefined, { - method: 'GET', - path: '/', - } as any) + map.mockReturnValueOnce('__input__') + mid.mockReturnValueOnce('__mid__') - expectTypeOf(result).toEqualTypeOf<{ a: number }>() + expect((i as any)['~orpc'].middlewares[0]('input')).toBe('__mid__') + + expect(map).toBeCalledTimes(1) + expect(map).toBeCalledWith('input') + + expect(mid).toBeCalledTimes(1) + expect(mid).toBeCalledWith('__input__') }) +}) - it('not infer output schema if output schema is specified', async () => { - const srb1 = new ProcedureImplementer({ - contract: new DecoratedContractProcedure({ - OutputSchema: z.unknown(), - InputSchema: undefined, - }), - }) +describe('to DecoratedProcedure', () => { + const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) - const sr = srb1.func(() => ({ b: 1 })) + const global_mid = vi.fn() + const implementer = new ProcedureImplementer<{ id?: string } | undefined, { db: string }, typeof schema, typeof schema>({ + contract: new ContractProcedure({ + InputSchema: schema, + OutputSchema: schema, + }), + middlewares: [global_mid], + }) - const result = await sr.zz$p.func({}, {}, { - method: 'GET', - path: '/', - } as any) + it('func', () => { + const func = vi.fn() + const procedure = implementer.func(func) - expectTypeOf(result).toEqualTypeOf() + expect(procedure).toSatisfy(isProcedure) + expect(procedure['~orpc'].func).toBe(func) + expect(procedure['~orpc'].middlewares).toEqual([global_mid]) }) }) -describe('handler', () => { - it('infer types', () => { - const handler = implementer1.func((input, context, meta) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf<{ auth: boolean }>() - expectTypeOf(meta).toEqualTypeOf() - - return { - name: 'unnoq', - } - }) - - expectTypeOf(handler).toEqualTypeOf< - DecoratedProcedure< - { auth: boolean }, - undefined, - undefined, - undefined, - { name: string } - > - >() - expect(isProcedure(handler)).toBe(true) - - implementer2.func((input, context, meta) => { - expectTypeOf(input).toEqualTypeOf<{ id: string }>() - expectTypeOf(context).toEqualTypeOf<{ auth: boolean }>() - expectTypeOf(meta).toEqualTypeOf() - - return { - name: 'unnoq', - } - }) - - // @ts-expect-error mismatch output - implementer2.func(() => {}) +describe('to DecoratedLazy', () => { + const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) + + const global_mid = vi.fn() + const implementer = new ProcedureImplementer<{ id?: string } | undefined, { db: string }, typeof schema, typeof schema>({ + contract: new ContractProcedure({ + InputSchema: schema, + OutputSchema: schema, + }), + middlewares: [global_mid], }) - it('combine middlewares', () => { - const mid1 = os.middleware((input, context, meta) => { - return meta.next({ - context: { - userId: '1', - }, - }) - }) - - const mid2 = os.middleware((input, context, meta) => { - return meta.next({ }) - }) - - const handler = implementer2 - .use(mid1) - .use(mid2) - .func((input, context, meta) => { - expectTypeOf(input).toEqualTypeOf<{ id: string }>() - expectTypeOf(context).toEqualTypeOf< - { auth: boolean } & { userId: string } - >() - expectTypeOf(meta).toEqualTypeOf() - - return { - name: 'unnoq', - } - }) - - expectTypeOf(handler).toEqualTypeOf< - DecoratedProcedure< - { auth: boolean }, - { userId: string }, - typeof schema1, - typeof schema2, - { name: string } - > - >() - - expect(handler.zz$p.middlewares).toEqual([mid1, mid2]) + it('lazy', { todo: true }, () => { + }) }) diff --git a/packages/server/src/procedure-implementer.ts b/packages/server/src/procedure-implementer.ts index 3ef6ace50..31c08e978 100644 --- a/packages/server/src/procedure-implementer.ts +++ b/packages/server/src/procedure-implementer.ts @@ -1,21 +1,22 @@ import type { ContractProcedure, Schema, SchemaInput, SchemaOutput } from '@orpc/contract' import type { DecoratedLazy } from './lazy' +import type { ANY_MAP_INPUT_MIDDLEWARE, ANY_MIDDLEWARE, MapInputMiddleware, Middleware } from './middleware' import type { ProcedureFunc } from './procedure' import type { DecoratedProcedure } from './procedure-decorated' import type { Context, MergeContext } from './types' -import { decorateMiddleware, type MapInputMiddleware, type Middleware } from './middleware' +import { decorateMiddleware } from './middleware' import { Procedure } from './procedure' import { decorateProcedure } from './procedure-decorated' import { RouterBuilder } from './router-builder' export type ProcedureImplementerDef< - _TContext extends Context, - _TExtraContext extends Context, + TContext extends Context, + TExtraContext extends Context, TInputSchema extends Schema, TOutputSchema extends Schema, > = { contract: ContractProcedure - middlewares?: Middleware[] + middlewares?: Middleware, SchemaInput>[] } export class ProcedureImplementer< @@ -31,51 +32,45 @@ export class ProcedureImplementer< this['~orpc'] = def } - use< - UExtraContext extends - | Partial>> - | undefined = undefined, - >( + use> | undefined = undefined>( middleware: Middleware< MergeContext, - UExtraContext, + U, SchemaOutput, SchemaInput >, ): ProcedureImplementer< TContext, - MergeContext, + MergeContext, TInputSchema, TOutputSchema > use< - UExtraContext extends - | Partial>> - | undefined = undefined, - UMappedInput = unknown, + UExtra extends Context & Partial> | undefined = undefined, + UInput = unknown, >( middleware: Middleware< MergeContext, - UExtraContext, - UMappedInput, + UExtra, + UInput, SchemaInput >, - mapInput: MapInputMiddleware, UMappedInput>, + mapInput: MapInputMiddleware, UInput>, ): ProcedureImplementer< TContext, - MergeContext, + MergeContext, TInputSchema, TOutputSchema > use( - middleware: Middleware, - mapInput?: MapInputMiddleware, + middleware: ANY_MIDDLEWARE, + mapInput?: ANY_MAP_INPUT_MIDDLEWARE, ): ProcedureImplementer { const mappedMiddleware = mapInput ? decorateMiddleware(middleware).mapInput(mapInput) - : decorateMiddleware(middleware) + : middleware return new ProcedureImplementer({ ...this['~orpc'], @@ -83,8 +78,8 @@ export class ProcedureImplementer< }) } - func>( - func: ProcedureFunc, + func>( + func: ProcedureFunc, ): DecoratedProcedure { return decorateProcedure(new Procedure({ middlewares: this['~orpc'].middlewares, @@ -93,7 +88,7 @@ export class ProcedureImplementer< })) } - lazy>>( + lazy>>( loader: () => Promise<{ default: U }>, ): DecoratedLazy { // TODO: replace with a more solid solution From ec79af7799f3d612babc3104ae783f23ead69b73 Mon Sep 17 00:00:00 2001 From: unnoq Date: Sat, 14 Dec 2024 20:33:03 +0700 Subject: [PATCH 05/51] wip --- packages/client/tests/e2e.test.ts | 2 +- packages/next/src/action-form.test.ts | 2 +- packages/next/src/action-safe.test.ts | 2 +- .../next/src/client/action-hooks.test.tsx | 6 +- .../src/client/action-safe-hooks.test.tsx | 2 +- .../openapi/src/fetch/server-handler.test.ts | 6 +- packages/react-query/tests/e2e.test.tsx | 2 +- packages/react/src/procedure-hooks.test.tsx | 10 +- packages/react/src/procedure-utils.test.tsx | 8 +- packages/server/src/fetch/handle.test.ts | 4 +- packages/server/src/fetch/handler.test.ts | 6 +- packages/server/src/middleware.ts | 3 +- .../server/src/procedure-caller.test-d.ts | 250 +++++++++ packages/server/src/procedure-caller.test.ts | 495 +++++++++++------- packages/server/src/procedure-caller.ts | 241 +++++---- packages/server/src/router-caller.test.ts | 12 +- packages/shared/src/value.ts | 5 +- packages/vue-query/tests/e2e.test.ts | 2 +- 18 files changed, 722 insertions(+), 336 deletions(-) create mode 100644 packages/server/src/procedure-caller.test-d.ts diff --git a/packages/client/tests/e2e.test.ts b/packages/client/tests/e2e.test.ts index 241a42e14..1e3f2b7df 100644 --- a/packages/client/tests/e2e.test.ts +++ b/packages/client/tests/e2e.test.ts @@ -8,7 +8,7 @@ describe('e2e', () => { it('works on error', () => { // @ts-expect-error - invalid input expect(client.user.find()).rejects.toThrowError( - 'Validation input failed', + 'Input validation failed', ) }) diff --git a/packages/next/src/action-form.test.ts b/packages/next/src/action-form.test.ts index 5ce89346a..2c3ad467a 100644 --- a/packages/next/src/action-form.test.ts +++ b/packages/next/src/action-form.test.ts @@ -27,7 +27,7 @@ describe('createFormAction', () => { procedure, }) - expect(formAction(new FormData())).rejects.toThrowError('Validation input failed') + expect(formAction(new FormData())).rejects.toThrowError('Input validation failed') }) it('hooks', async () => { diff --git a/packages/next/src/action-safe.test.ts b/packages/next/src/action-safe.test.ts index 37bb138df..c85817067 100644 --- a/packages/next/src/action-safe.test.ts +++ b/packages/next/src/action-safe.test.ts @@ -14,7 +14,7 @@ describe('createSafeAction', () => { // @ts-expect-error - invalid input expect(safe({ name: 123 })).resolves.toEqual([undefined, { code: 'BAD_REQUEST', - message: 'Validation input failed', + message: 'Input validation failed', issues: [ { code: 'invalid_type', diff --git a/packages/next/src/client/action-hooks.test.tsx b/packages/next/src/client/action-hooks.test.tsx index 3d10c2e4d..a572f7c7e 100644 --- a/packages/next/src/client/action-hooks.test.tsx +++ b/packages/next/src/client/action-hooks.test.tsx @@ -63,7 +63,7 @@ describe('useAction', () => { expect(result.current.isError).toBe(true) expect(result.current.input).toEqual({ value: 12334 }) expect(result.current.output).toEqual(undefined) - expect(result.current.error?.message).toEqual('Validation input failed') + expect(result.current.error?.message).toEqual('Input validation failed') }) it('return result on execute', async () => { @@ -86,7 +86,7 @@ describe('useAction', () => { const [output2, error2, status2] = await result.current.execute({ value: 123 }) expect(output2).toBe(undefined) - expect(error2?.message).toBe('Validation input failed') + expect(error2?.message).toBe('Input validation failed') expect(status2).toBe('error') await vi.waitFor(() => expect(result.current.status).toBe('error')) @@ -94,7 +94,7 @@ describe('useAction', () => { expect(result.current.isError).toBe(true) expect(result.current.input).toEqual({ value: 123 }) expect(result.current.output).toEqual(undefined) - expect(result.current.error?.message).toBe('Validation input failed') + expect(result.current.error?.message).toBe('Input validation failed') }) it('hooks', async () => { diff --git a/packages/next/src/client/action-safe-hooks.test.tsx b/packages/next/src/client/action-safe-hooks.test.tsx index 41310bc5c..e73e5ce5b 100644 --- a/packages/next/src/client/action-safe-hooks.test.tsx +++ b/packages/next/src/client/action-safe-hooks.test.tsx @@ -65,7 +65,7 @@ describe('useSafeAction', () => { expect(result.current.isPending).toBe(false) expect(result.current.isError).toBe(true) expect(result.current.output).toBe(undefined) - expect(result.current.error?.message).toEqual('Validation input failed') + expect(result.current.error?.message).toEqual('Input validation failed') expect(result.current.input).toEqual({ value: 12334 }) }) diff --git a/packages/openapi/src/fetch/server-handler.test.ts b/packages/openapi/src/fetch/server-handler.test.ts index af0f31439..4a0e945b2 100644 --- a/packages/openapi/src/fetch/server-handler.test.ts +++ b/packages/openapi/src/fetch/server-handler.test.ts @@ -118,7 +118,7 @@ describe.each(handlers)('openAPIServerHandler', (handler) => { expect(await response?.json()).toEqual({ code: 'BAD_REQUEST', status: 400, - message: 'Validation input failed', + message: 'Input validation failed', issues: [ { code: 'invalid_type', @@ -149,7 +149,7 @@ describe.each(handlers)('openAPIServerHandler', (handler) => { expect(await response?.json()).toEqual({ code: 'BAD_REQUEST', status: 400, - message: 'Validation input failed', + message: 'Input validation failed', issues: [ { code: 'invalid_type', @@ -180,7 +180,7 @@ describe.each(handlers)('openAPIServerHandler', (handler) => { expect(await response?.json()).toEqual({ code: 'BAD_REQUEST', status: 400, - message: 'Validation input failed', + message: 'Input validation failed', issues: [ { code: 'invalid_type', diff --git a/packages/react-query/tests/e2e.test.tsx b/packages/react-query/tests/e2e.test.tsx index 53c0d3e5f..4d073ba4d 100644 --- a/packages/react-query/tests/e2e.test.tsx +++ b/packages/react-query/tests/e2e.test.tsx @@ -23,7 +23,7 @@ describe('useQuery', () => { // @ts-expect-error -- invalid input const { result } = renderHook(() => useQuery(orpc.user.create.queryOptions({ input: {} }), queryClient)) - await vi.waitFor(() => expect(result.current.error).toEqual(new Error('Validation input failed'))) + await vi.waitFor(() => expect(result.current.error).toEqual(new Error('Input validation failed'))) expect(queryClient.getQueryData(orpc.ping.key({ type: 'query' }))).toEqual(undefined) }) diff --git a/packages/react/src/procedure-hooks.test.tsx b/packages/react/src/procedure-hooks.test.tsx index f3a8b3466..e9117c380 100644 --- a/packages/react/src/procedure-hooks.test.tsx +++ b/packages/react/src/procedure-hooks.test.tsx @@ -55,7 +55,7 @@ describe('useQuery', () => { await waitFor(() => expect(result.current.status).toBe('error')) expect((result.current.error as any).message).toEqual( - 'Validation input failed', + 'Input validation failed', ) }) }) @@ -143,7 +143,7 @@ describe('useInfiniteQuery', () => { await waitFor(() => expect(result.current.status).toBe('error')) expect((result.current.error as any).message).toEqual( - 'Validation input failed', + 'Input validation failed', ) }) }) @@ -186,7 +186,7 @@ describe('useSuspenseQuery', () => { await waitFor(() => expect(screen.getByTestId('error-boundary')).toHaveTextContent( - 'Validation input failed', + 'Input validation failed', ), ) }) @@ -274,7 +274,7 @@ describe('useSuspenseInfiniteQuery', () => { await waitFor(() => expect(screen.getByTestId('error-boundary')).toHaveTextContent( - 'Validation input failed', + 'Input validation failed', ), ) }) @@ -381,7 +381,7 @@ describe('useMutation', () => { await waitFor(() => expect((result.current.error as any)?.message).toEqual( - 'Validation input failed', + 'Input validation failed', ), ) }) diff --git a/packages/react/src/procedure-utils.test.tsx b/packages/react/src/procedure-utils.test.tsx index 40c10a3e8..799ec7c7a 100644 --- a/packages/react/src/procedure-utils.test.tsx +++ b/packages/react/src/procedure-utils.test.tsx @@ -33,7 +33,7 @@ describe('fetchQuery', () => { it('on error', () => { // @ts-expect-error invalid input expect(utils.fetchQuery({ id: {} })).rejects.toThrowError( - 'Validation input failed', + 'Input validation failed', ) }) }) @@ -70,7 +70,7 @@ describe('fetchInfiniteQuery', () => { expect( // @ts-expect-error invalid input utils.fetchInfiniteQuery({ input: { keyword: {} } }), - ).rejects.toThrowError('Validation input failed') + ).rejects.toThrowError('Input validation failed') }) }) @@ -168,7 +168,7 @@ describe('ensureQueryData', () => { it('on error', () => { // @ts-expect-error invalid input expect(utils.ensureQueryData({ id: {} })).rejects.toThrowError( - 'Validation input failed', + 'Input validation failed', ) }) }) @@ -205,7 +205,7 @@ describe('ensureInfiniteQuery', () => { expect( // @ts-expect-error invalid input utils.ensureInfiniteQueryData({ input: { keyword: {} } }), - ).rejects.toThrowError('Validation input failed') + ).rejects.toThrowError('Input validation failed') }) }) diff --git a/packages/server/src/fetch/handle.test.ts b/packages/server/src/fetch/handle.test.ts index 0bf292eb6..a2e3814a0 100644 --- a/packages/server/src/fetch/handle.test.ts +++ b/packages/server/src/fetch/handle.test.ts @@ -223,7 +223,7 @@ describe('procedure throw error', () => { expect(await response.json()).toEqual({ code: 'BAD_REQUEST', status: 400, - message: 'Validation input failed', + message: 'Input validation failed', issues: [ { code: 'invalid_type', @@ -259,7 +259,7 @@ describe('procedure throw error', () => { expect(await response.json()).toEqual({ code: 'INTERNAL_SERVER_ERROR', status: 500, - message: 'Validation output failed', + message: 'Output validation failed', }) }) }) diff --git a/packages/server/src/fetch/handler.test.ts b/packages/server/src/fetch/handler.test.ts index 73226d8d7..005bb20b9 100644 --- a/packages/server/src/fetch/handler.test.ts +++ b/packages/server/src/fetch/handler.test.ts @@ -125,7 +125,7 @@ describe('oRPCHandler', () => { data: { code: 'BAD_REQUEST', status: 400, - message: 'Validation input failed', + message: 'Input validation failed', issues: [ { code: 'invalid_type', @@ -159,7 +159,7 @@ describe('oRPCHandler', () => { data: { code: 'BAD_REQUEST', status: 400, - message: 'Validation input failed', + message: 'Input validation failed', issues: [ { code: 'invalid_type', @@ -193,7 +193,7 @@ describe('oRPCHandler', () => { data: { code: 'BAD_REQUEST', status: 400, - message: 'Validation input failed', + message: 'Input validation failed', issues: [ { code: 'invalid_type', diff --git a/packages/server/src/middleware.ts b/packages/server/src/middleware.ts index a7cff6338..e36c13753 100644 --- a/packages/server/src/middleware.ts +++ b/packages/server/src/middleware.ts @@ -1,5 +1,6 @@ import type { Promisable } from '@orpc/shared' import type { Context, MergeContext, Meta, WELL_CONTEXT } from './types' +import { mergeContext } from './utils' export type MiddlewareResult = Promisable<{ output: TOutput @@ -106,7 +107,7 @@ export function decorateMiddleware< const concatted = decorateMiddleware((input, context, meta, ...rest) => { const next: MiddlewareMeta['next'] = async (options) => { - return mapped(input, { ...context, ...options.context }, meta, ...rest) + return mapped(input, mergeContext(context, options.context), meta, ...rest) } const merged = middleware(input as any, context as any, { ...meta, next }, ...rest) diff --git a/packages/server/src/procedure-caller.test-d.ts b/packages/server/src/procedure-caller.test-d.ts new file mode 100644 index 000000000..254aef542 --- /dev/null +++ b/packages/server/src/procedure-caller.test-d.ts @@ -0,0 +1,250 @@ +import type { Caller, Meta, WELL_CONTEXT } from './types' +import { ContractProcedure } from '@orpc/contract' +import { z } from 'zod' +import { createLazy } from './lazy' +import { Procedure } from './procedure' +import { createProcedureCaller } from './procedure-caller' + +beforeEach(() => { + vi.resetAllMocks() +}) + +const schema = z.object({ val: z.string().transform(v => Number(v)) }) +const func = vi.fn() +const procedure = new Procedure({ + contract: new ContractProcedure({ + InputSchema: schema, + OutputSchema: schema, + }), + func, +}) +const procedureWithContext = new Procedure<{ userId?: string }, { db: string }, typeof schema, typeof schema, { val: string }>({ + contract: new ContractProcedure({ + InputSchema: schema, + OutputSchema: schema, + }), + func, +}) + +describe('createProcedureCaller', () => { + it('just a caller', () => { + const caller = createProcedureCaller({ + procedure, + }) + + expectTypeOf(caller).toEqualTypeOf>() + }) + + it('context can be optional and can be a sync or async function', () => { + createProcedureCaller({ + procedure, + }) + + createProcedureCaller({ + procedure, + context: undefined, + }) + + // @ts-expect-error - missing context + createProcedureCaller({ + procedure: procedureWithContext, + }) + + createProcedureCaller({ + procedure: procedureWithContext, + context: { userId: '123' }, + }) + + createProcedureCaller({ + procedure: procedureWithContext, + // @ts-expect-error invalid context + context: { userId: 123 }, + }) + + createProcedureCaller({ + procedure: procedureWithContext, + context: () => ({ userId: '123' }), + }) + + createProcedureCaller({ + procedure: procedureWithContext, + // @ts-expect-error invalid context + context: () => ({ userId: 123 }), + }) + + createProcedureCaller({ + procedure: procedureWithContext, + context: async () => ({ userId: '123' }), + }) + + createProcedureCaller({ + procedure: procedureWithContext, + // @ts-expect-error invalid context + context: async () => ({ userId: 123 }), + }) + }) + + it('accept hooks', () => { + createProcedureCaller({ + procedure, + + async execute(input, context, meta) { + expectTypeOf(input).toEqualTypeOf() + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(meta).toEqualTypeOf Promise<{ val: number }> }>() + + return { val: 123 } + }, + + onStart(state, context, meta) { + expectTypeOf(state).toEqualTypeOf<{ status: 'pending', input: unknown, output: undefined, error: undefined }>() + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(meta).toEqualTypeOf() + }, + + onSuccess(state, context, meta) { + expectTypeOf(state).toEqualTypeOf<{ status: 'success', input: unknown, output: { val: number }, error: undefined }>() + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(meta).toEqualTypeOf() + }, + + onError(state, context, meta) { + expectTypeOf(state).toEqualTypeOf<{ status: 'error', input: unknown, output: undefined, error: Error }>() + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(meta).toEqualTypeOf() + }, + + onFinish(state, context, meta) { + expectTypeOf(state).toEqualTypeOf<{ status: 'success', input: unknown, output: { val: number }, error: undefined } | { status: 'error', input: unknown, output: undefined, error: Error }>() + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(meta).toEqualTypeOf() + }, + }) + }) + + it('accept paths', () => { + createProcedureCaller({ + procedure, + path: ['users'], + }) + + createProcedureCaller({ + procedure, + // @ts-expect-error - invalid path + path: [123], + }) + }) +}) + +describe('createProcedure on invalid lazy procedure', () => { + const lazy = createLazy(() => Promise.resolve({ default: procedure })) + const lazyWithContext = createLazy(() => Promise.resolve({ default: procedureWithContext })) + + it('just a caller', () => { + const caller = createProcedureCaller({ + procedure: lazy, + }) + + expectTypeOf(caller).toEqualTypeOf>() + }) + + it('context can be optional and can be a sync or async function', () => { + createProcedureCaller({ + procedure: lazy, + }) + + createProcedureCaller({ + procedure: lazy, + context: undefined, + }) + + // @ts-expect-error - missing context + createProcedureCaller({ + procedure: lazyWithContext, + }) + + createProcedureCaller({ + procedure: lazyWithContext, + context: { userId: '123' }, + }) + + createProcedureCaller({ + procedure: lazyWithContext, + // @ts-expect-error invalid context + context: { userId: 123 }, + }) + + createProcedureCaller({ + procedure: lazyWithContext, + context: () => ({ userId: '123' }), + }) + + createProcedureCaller({ + procedure: lazyWithContext, + // @ts-expect-error invalid context + context: () => ({ userId: 123 }), + }) + + createProcedureCaller({ + procedure: lazyWithContext, + context: async () => ({ userId: '123' }), + }) + + createProcedureCaller({ + procedure: lazyWithContext, + // @ts-expect-error invalid context + context: async () => ({ userId: 123 }), + }) + }) + + it('accept hooks', () => { + createProcedureCaller({ + procedure: lazy, + + async execute(input, context, meta) { + expectTypeOf(input).toEqualTypeOf() + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(meta).toEqualTypeOf Promise<{ val: number }> }>() + + return { val: 123 } + }, + + onStart(state, context, meta) { + expectTypeOf(state).toEqualTypeOf<{ status: 'pending', input: unknown, output: undefined, error: undefined }>() + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(meta).toEqualTypeOf() + }, + + onSuccess(state, context, meta) { + expectTypeOf(state).toEqualTypeOf<{ status: 'success', input: unknown, output: { val: number }, error: undefined }>() + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(meta).toEqualTypeOf() + }, + + onError(state, context, meta) { + expectTypeOf(state).toEqualTypeOf<{ status: 'error', input: unknown, output: undefined, error: Error }>() + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(meta).toEqualTypeOf() + }, + + onFinish(state, context, meta) { + expectTypeOf(state).toEqualTypeOf<{ status: 'success', input: unknown, output: { val: number }, error: undefined } | { status: 'error', input: unknown, output: undefined, error: Error }>() + expectTypeOf(context).toEqualTypeOf() + expectTypeOf(meta).toEqualTypeOf() + }, + }) + }) + + it('accept paths', () => { + createProcedureCaller({ + procedure: lazy, + path: ['users'], + }) + + createProcedureCaller({ + procedure: lazy, + // @ts-expect-error - invalid path + path: [123], + }) + }) +}) diff --git a/packages/server/src/procedure-caller.test.ts b/packages/server/src/procedure-caller.test.ts index e042167d1..f7ceeb268 100644 --- a/packages/server/src/procedure-caller.test.ts +++ b/packages/server/src/procedure-caller.test.ts @@ -1,260 +1,387 @@ +import type { WELL_CONTEXT } from './types' +import { ContractProcedure } from '@orpc/contract' import { z } from 'zod' -import { createProcedureCaller, ORPCError, os } from '.' +import { createLazy, isLazy, loadLazy } from './lazy' +import { Procedure } from './procedure' +import { createProcedureCaller } from './procedure-caller' + +const schema = z.object({ val: z.string().transform(v => Number(v)) }) + +const func = vi.fn() +const mid = vi.fn() +const mid2 = vi.fn() + +const procedure = new Procedure({ + contract: new ContractProcedure({ + InputSchema: schema, + OutputSchema: schema, + }), + func, +}) -describe('createProcedureCaller', () => { - const path = ['ping'] - const context = { auth: true } +const procedureWithMiddleware = new Procedure<{ userId?: string }, { db: string }, typeof schema, typeof schema, { val: string }>({ + contract: new ContractProcedure({ + InputSchema: schema, + OutputSchema: schema, + }), + func, + middlewares: [mid], +}) - const osw = os.context<{ auth?: boolean }>() - const procedure = osw - .input(z.object({ value: z.string().transform(v => Number(v)) })) - .output(z.object({ value: z.number().transform(v => v.toString()) })) - .func((input, context, meta) => { - expect(context).toEqual(context) - expect(meta.path).toBe(path) +const procedureWithMultipleMiddleware = new Procedure<{ userId?: string }, { db: string }, typeof schema, typeof schema, { val: string }>({ + contract: new ContractProcedure({ + InputSchema: schema, + OutputSchema: schema, + }), + func, + middlewares: [mid, mid2], +}) - return input - }) +const procedureCases = [ + [ + 'default', + procedure, + procedureWithMiddleware, + procedureWithMultipleMiddleware, + ], + [ + 'lazy', + createLazy(() => Promise.resolve({ default: procedure })), + createLazy(() => Promise.resolve({ default: procedureWithMiddleware })), + createLazy(() => Promise.resolve({ default: procedureWithMultipleMiddleware })), + ], +] as const + +beforeEach(() => { + vi.resetAllMocks() +}) + +describe.each(procedureCases)('createProcedureCaller - %s', (_, procedure, procedureWithMiddleware, procedureWithMultipleMiddleware) => { + const unwrapLazy = async (val: any) => { + return isLazy(val) ? (await loadLazy(val)).default : val + } - it('infer context', () => { - createProcedureCaller({ + it('just a caller', async () => { + const caller = createProcedureCaller({ procedure, - // @ts-expect-error invalid context - context: { auth: 123 }, }) - createProcedureCaller({ + func.mockReturnValueOnce({ val: '123' }) + + await expect(caller({ val: '123' })).resolves.toEqual({ val: 123 }) + + expect(func).toHaveBeenCalledWith({ val: 123 }, undefined, { path: [], procedure: await unwrapLazy(procedure) }) + }) + + it('validate input and output', () => { + const caller = createProcedureCaller({ procedure, - context, }) + + // @ts-expect-error - invalid input + expect(caller({ val: 123 })).rejects.toThrow('Input validation failed') + + func.mockReturnValueOnce({ val: 1234 }) + expect(caller({ val: '1234' })).rejects.toThrow('Output validation failed') }) - it('with validate', async () => { + it('middleware can return output directly - single', async () => { const caller = createProcedureCaller({ - procedure, - context: async () => context, - path, + procedure: procedureWithMiddleware, + context: { userId: '123' }, }) - expectTypeOf(caller).toMatchTypeOf< - (input: { value: string }) => Promise<{ - value: string - }> - >() + mid.mockReturnValueOnce({ output: { val: '123' } }) - expect(await caller({ value: '123' })).toEqual({ value: '123' }) + await expect(caller({ val: '123' })).resolves.toEqual({ val: 123 }) - // @ts-expect-error - invalid input - expect(caller({ value: {} })).rejects.toThrowError( - 'Validation input failed', - ) + expect(func).toBeCalledTimes(0) + + expect(mid).toBeCalledTimes(1) + expect(mid).toHaveBeenCalledWith({ val: 123 }, { userId: '123' }, { + path: [], + procedure: await unwrapLazy(procedureWithMiddleware), + next: expect.any(Function), + output: expect.any(Function), + }) }) - it('without validate and schema', () => { - const procedure = osw.func(() => { - return { value: true } + it('middleware can return output directly - multiple', async () => { + const caller = createProcedureCaller({ + procedure: procedureWithMultipleMiddleware, + context: { userId: '123' }, + }) + + mid.mockImplementationOnce((input, context, meta) => { + return meta.next({ + context: { + extra: '__extra__', + }, + }) + }) + + mid2.mockReturnValueOnce({ output: { val: '1234567' } }) + + await expect(caller({ val: '123' })).resolves.toEqual({ val: 1234567 }) + + expect(func).toBeCalledTimes(0) + + expect(mid).toBeCalledTimes(1) + expect(mid).toHaveBeenCalledWith({ val: 123 }, { userId: '123' }, { + path: [], + procedure: await unwrapLazy(procedureWithMultipleMiddleware), + next: expect.any(Function), + output: expect.any(Function), + }) + expect(mid).toReturnWith(Promise.resolve({ output: { val: '1234567' }, context: { extra: '__extra__' } })) + + expect(mid2).toBeCalledTimes(1) + expect(mid2).toHaveBeenCalledWith({ val: 123 }, { userId: '123', extra: '__extra__' }, { + path: [], + procedure: await unwrapLazy(procedureWithMultipleMiddleware), + next: expect.any(Function), + output: expect.any(Function), }) + }) + it('output from middleware still be validated', async () => { const caller = createProcedureCaller({ - procedure, - context, + procedure: procedureWithMiddleware, + context: { userId: '123' }, }) - expectTypeOf(caller).toMatchTypeOf< - (value: unknown) => Promise<{ value: boolean }> - >() + mid.mockReturnValueOnce({ output: { val: 1234 } }) - expect(caller({ value: 123 })).resolves.toEqual({ value: true }) + await expect(caller({ val: '1234' })).rejects.toThrow('Output validation failed') }) - it('middlewares', () => { - const ref = { value: 0 } - - const mid1 = vi.fn( - osw.middleware(async (input: { id: string }, context, meta) => { - expect(input).toEqual({ id: '1' }) - - expect(ref.value).toBe(0) - ref.value++ - - try { - const result = await meta.next({ - context: { - userId: '1', - }, - }) - expect(ref.value).toBe(5) - ref.value++ - return result - } - finally { - expect(ref.value).toBe(6) - ref.value++ - } - }), - ) + it('middleware can add extra context - single', async () => { + const caller = createProcedureCaller({ + procedure: procedureWithMiddleware, + context: { userId: '123' }, + }) - const mid2 = vi.fn( - osw.middleware(async (input, context, meta) => { - expect(ref.value).toBe(1) - ref.value++ - - try { - const result = await meta.next({}) - expect(ref.value).toBe(3) - ref.value++ - return result - } - finally { - expect(ref.value).toBe(4) - ref.value++ - } - }), - ) + mid.mockImplementationOnce((input, context, meta) => { + return meta.next({ + context: { + extra: '__extra__', + }, + }) + }) + + func.mockReturnValueOnce({ val: '1234' }) + + await expect(caller({ val: '1234' })).resolves.toEqual({ val: 1234 }) + + expect(func).toBeCalledTimes(1) + expect(func).toHaveBeenCalledWith({ val: 1234 }, { userId: '123', extra: '__extra__' }, { + path: [], + procedure: await unwrapLazy(procedureWithMiddleware), + }) + }) - const ping = osw - .input(z.object({ id: z.string() })) - .use(mid1) - .use(mid2) - .func((input, context, meta) => { - expect(context).toEqual({ userId: '1', auth: false }) + it('middleware can add extra context - multiple', async () => { + const caller = createProcedureCaller({ + procedure: procedureWithMultipleMiddleware, + context: { userId: '123' }, + }) - expect(ref.value).toBe(2) - ref.value++ + mid.mockImplementationOnce((input, context, meta) => { + return meta.next({ + context: { + extra: '__extra__', + }, + }) + }) - return 'pong' + mid2.mockImplementationOnce((input, context, meta) => { + return meta.next({ + context: { + extra2: '__extra2__', + }, }) + }) + + func.mockReturnValueOnce({ val: '1234' }) + + await expect(caller({ val: '1234' })).resolves.toEqual({ val: 1234 }) + expect(func).toBeCalledTimes(1) + expect(func).toHaveBeenCalledWith({ val: 1234 }, { + userId: '123', + extra: '__extra__', + extra2: '__extra2__', + }, { + path: [], + procedure: await unwrapLazy(procedureWithMultipleMiddleware), + }) + }) + + it('middleware can override context - signal', async () => { const caller = createProcedureCaller({ - procedure: ping, - context: { auth: false }, + procedure: procedureWithMiddleware, + context: { userId: '123' }, }) - expect(caller({ id: '1' })).resolves.toEqual('pong') + mid.mockImplementationOnce((input, context, meta) => { + return meta.next({ + context: { + userId: '456', + }, + }) + }) + + func.mockReturnValueOnce({ val: '1234' }) + + await expect(caller({ val: '1234' })).resolves.toEqual({ val: 1234 }) + + expect(func).toBeCalledTimes(1) + expect(func).toHaveBeenCalledWith({ val: 1234 }, { userId: '456' }, { + path: [], + procedure: await unwrapLazy(procedureWithMiddleware), + }) }) - it('optional input when possible', async () => { - os.func(() => { })() - os.func(() => { })({}) - // @ts-expect-error input is required - expect(os.input(z.string()).func(() => { })()).rejects.toThrow() - os.input(z.string().optional()).func(() => { })() - // @ts-expect-error input is required - expect(os.input(z.object({})).func(() => { })()).rejects.toThrow() - os.input(z.object({}).optional()).func(() => { })() - os.input(z.unknown()).func(() => { })() - os.input(z.any()).func(() => { })() - // @ts-expect-error input is required - expect(os.input(z.boolean()).func(() => { })()).rejects.toThrow() + it('middleware can override context - multiple', async () => { + const caller = createProcedureCaller({ + procedure: procedureWithMultipleMiddleware, + context: { userId: '123' }, + }) + + mid.mockImplementationOnce((input, context, meta) => { + return meta.next({ + context: { + userId: '456', + extra: '1', + }, + }) + }) + + mid2.mockImplementationOnce((input, context, meta) => { + return meta.next({ + context: { + userId: '789', + extra: '2', + }, + }) + }) + + func.mockReturnValueOnce({ val: '1234' }) + + await expect(caller({ val: '1234' })).resolves.toEqual({ val: 1234 }) + + expect(func).toBeCalledTimes(1) + expect(func).toHaveBeenCalledWith({ val: 1234 }, { userId: '789', extra: '2' }, { + path: [], + procedure: await unwrapLazy(procedureWithMultipleMiddleware), + }) }) - it('hooks', async () => { + const contextCases = [ + ['directly value', { val: '__val__' }], + ['sync function value', () => ({ val: '__val__' })], + ['async function value', async () => ({ val: '__val__' })], + ] as const + + it.each(contextCases)('can accept %s', async (_, context) => { + func.mockReturnValue({ val: '1234' }) + + const caller1 = createProcedureCaller({ + procedure, + context, + }) + + await caller1({ val: '123' }) + expect(func).toBeCalledTimes(1) + expect(func).toHaveBeenCalledWith({ val: 123 }, { val: '__val__' }, { path: [], procedure: await unwrapLazy(procedure) }) + }) + + it.each(contextCases)('can accept hooks - context: %s', async (_, context) => { + const execute = vi.fn() const onStart = vi.fn() const onSuccess = vi.fn() const onError = vi.fn() const onFinish = vi.fn() - const onExecute = vi.fn() - const procedure = os.input(z.string()).func(() => 'output') - const context = { val: 'context' } const caller = createProcedureCaller({ procedure, context, - path: ['cc'], - execute: async (input, context, meta) => { - onExecute(input, context, meta) - try { - const output = await meta.next() - onSuccess(output, context, meta) - return output - } - catch (e) { - onError(e, context, meta) - throw e - } - }, + path: ['users'], + execute, onStart, onSuccess, onError, onFinish, }) + execute.mockImplementation((input, context, meta) => meta.next()) + func.mockReturnValueOnce({ val: '123' }) + + await caller({ val: '123' }) + const meta = { - path: ['cc'], - procedure, + path: ['users'], + procedure: await unwrapLazy(procedure), } - const metaFull = { + const contextValue = { val: '__val__' } + + expect(execute).toBeCalledTimes(1) + expect(execute).toHaveBeenCalledWith({ val: '123' }, contextValue, { ...meta, next: expect.any(Function), - } + }) - await caller('input') - expect(onExecute).toBeCalledTimes(1) - expect(onExecute).toHaveBeenCalledWith('input', context, metaFull) expect(onStart).toBeCalledTimes(1) - expect(onStart).toHaveBeenCalledWith({ input: 'input', status: 'pending' }, context, meta) - expect(onSuccess).toBeCalledTimes(2) - expect(onSuccess).toHaveBeenNthCalledWith(1, { output: 'output', input: 'input', status: 'success' }, context, meta) - expect(onSuccess).toHaveBeenNthCalledWith(2, 'output', context, metaFull) - expect(onError).not.toBeCalled() - expect(onFinish).toBeCalledTimes(1) - expect(onFinish).toBeCalledWith({ output: 'output', input: 'input', status: 'success' }, context, meta) - - onSuccess.mockClear() - onError.mockClear() - onFinish.mockClear() - onExecute.mockClear() - - // @ts-expect-error - invalid input - await expect(caller(123)).rejects.toThrowError( - 'Validation input failed', + expect(onStart).toHaveBeenCalledWith( + { status: 'pending', input: { val: '123' }, output: undefined, error: undefined }, + contextValue, + meta, ) - const meta2 = { - path: ['cc'], - procedure, - } + expect(onSuccess).toBeCalledTimes(1) + expect(onSuccess).toHaveBeenCalledWith( + { status: 'success', input: { val: '123' }, output: { val: 123 }, error: undefined }, + contextValue, + meta, + ) - const metaFull2 = { - ...meta2, - next: expect.any(Function), - } + expect(onError).toBeCalledTimes(0) + }) - const error2 = new ORPCError({ - message: 'Validation input failed', - code: 'BAD_REQUEST', - cause: expect.any(Error), + it('accept paths', async () => { + const onSuccess = vi.fn() + const caller = createProcedureCaller({ + procedure, + path: ['users'], + onSuccess, }) - expect(onExecute).toBeCalledTimes(1) - expect(onExecute).toHaveBeenCalledWith(123, context, metaFull2) - expect(onError).toBeCalledTimes(2) - expect(onError).toHaveBeenNthCalledWith(1, { input: 123, error: error2, status: 'error' }, context, meta2) - expect(onError).toHaveBeenNthCalledWith(2, error2, context, metaFull2) - expect(onSuccess).not.toBeCalled() - expect(onFinish).toBeCalledTimes(1) - expect(onFinish).toBeCalledWith({ input: 123, error: error2, status: 'error' }, context, meta2) - }) + func.mockReturnValueOnce({ val: '123' }) - it('abort signal', async () => { - const controller = new AbortController() - const signal = controller.signal + await caller({ val: '123' }) - const procedure = os - .use(async (_, __, meta) => { - expect(meta.signal).toBe(signal) + expect(func).toBeCalledTimes(1) + expect(func).toHaveBeenCalledWith({ val: 123 }, undefined, { path: ['users'], procedure: await unwrapLazy(procedure) }) - return meta.next({}) - }) - .func((_, __, meta) => { - expect(meta.signal).toBe(signal) - }) + expect(onSuccess).toBeCalledTimes(1) + expect(onSuccess).toHaveBeenCalledWith( + { status: 'success', input: { val: '123' }, output: { val: 123 }, error: undefined }, + undefined, + { path: ['users'], procedure: await unwrapLazy(procedure) }, + ) + }) +}) + +describe('createProcedure on invalid lazy procedure', () => { + it('should throw error', () => { + const lazy = createLazy(() => Promise.resolve({ default: 123 })) const caller = createProcedureCaller({ - procedure, + // @ts-expect-error - invalid lazy procedure + procedure: lazy, }) - await caller(undefined, { signal }) + expect(caller()).rejects.toThrow('Not found') }) }) diff --git a/packages/server/src/procedure-caller.ts b/packages/server/src/procedure-caller.ts index c9437cf75..83dcbd45e 100644 --- a/packages/server/src/procedure-caller.ts +++ b/packages/server/src/procedure-caller.ts @@ -1,75 +1,65 @@ -import type { SchemaInput, SchemaOutput } from '@orpc/contract' -import type { Hooks, PartialOnUndefinedDeep, Value } from '@orpc/shared' +import type { Schema, SchemaInput, SchemaOutput } from '@orpc/contract' +import type { Hooks, Value } from '@orpc/shared' import type { Lazy } from './lazy' import type { MiddlewareMeta } from './middleware' -import type { ANY_LAZY_PROCEDURE, ANY_PROCEDURE, Procedure, WELL_PROCEDURE } from './procedure' -import type { Caller, Context, Meta } from './types' +import type { + ANY_LAZY_PROCEDURE, + ANY_PROCEDURE, + Procedure, + WELL_PROCEDURE, +} from './procedure' +import type { Caller, Context, Meta, WELL_CONTEXT } from './types' + import { executeWithHooks, trim, value } from '@orpc/shared' import { ORPCError } from '@orpc/shared/error' import { isLazy, loadLazy } from './lazy' import { isProcedure } from './procedure' import { mergeContext } from './utils' +/** + * Options for creating a procedure caller with comprehensive type safety + */ export type CreateProcedureCallerOptions< - T extends ANY_PROCEDURE | ANY_LAZY_PROCEDURE, -> = T extends -| Procedure -| Lazy> ? { - procedure: T - - /** - * This is helpful for logging and analytics. - * - * @internal - */ - path?: string[] -} & PartialOnUndefinedDeep<{ - /** - * The context used when calling the procedure. - */ - context: Value -}> & Hooks, UContext, { path: string[], procedure: ANY_PROCEDURE }> - : never - -export type ProcedureCaller< - TProcedure extends ANY_PROCEDURE | ANY_LAZY_PROCEDURE, -> = TProcedure extends -| Procedure -| Lazy> - ? Caller, SchemaOutput> - : never + TContext extends Context, + TInputSchema extends Schema, + TOutputSchema extends Schema, + TFuncOutput extends SchemaInput, +> = + & { + procedure: + | Procedure + | Lazy> + + /** + * This is helpful for logging and analytics. + * + * @internal + */ + path?: string[] + } + & ({ + /** + * The context used when calling the procedure. + */ + context: Value + } | (undefined extends TContext ? { context?: undefined } : never)) + & Hooks, TContext, Meta> export function createProcedureCaller< - TProcedure extends ANY_PROCEDURE | ANY_LAZY_PROCEDURE, + TContext extends Context = WELL_CONTEXT, + TInputSchema extends Schema = undefined, + TOutputSchema extends Schema = undefined, + TFuncOutput extends SchemaInput = SchemaInput, >( - options: CreateProcedureCallerOptions, -): ProcedureCaller { - const caller: Caller = async (...args) => { - const [input, callerOptions] = args - + options: CreateProcedureCallerOptions, +): Caller, SchemaOutput> { + return async (...[input, callerOptions]) => { const path = options.path ?? [] const procedure = await loadProcedure(options.procedure) as WELL_PROCEDURE - const context = await value(options.context) - - const execute = async () => { - const validInput = await (async () => { - const schema = procedure.zz$p.contract['~orpc'].InputSchema - if (!schema) { - return input - } + const context = await value(options.context) as TContext - const result = await schema['~standard'].validate(input) - - if (result.issues) { - throw new ORPCError({ - message: 'Validation input failed', - code: 'BAD_REQUEST', - issues: result.issues, - }) - } - - return result.value - })() + const executeWithValidation = async () => { + const validInput = await validateInput(procedure, input) const meta: Meta = { path, @@ -77,86 +67,105 @@ export function createProcedureCaller< signal: callerOptions?.signal, } - const middlewares = procedure.zz$p.middlewares ?? [] - let currentMidIndex = 0 - let currentContext: Context = context - - const next: MiddlewareMeta['next'] = async (nextOptions) => { - const mid = middlewares[currentMidIndex] - currentMidIndex += 1 - currentContext = mergeContext(currentContext, nextOptions.context) - - if (mid) { - return await mid(validInput, currentContext, { - ...meta, - next, - output: output => ({ output, context: undefined }), - }) - } - else { - return { - output: await await procedure.zz$p.func(validInput, currentContext, meta), - context: currentContext, - } - } - } + const output = await executeMiddlewareChain( + procedure, + validInput, + context, + meta, + ) - const output = (await next({})).output - - const validOutput = await (async () => { - const schema = procedure.zz$p.contract['~orpc'].OutputSchema - if (!schema) { - return output - } - - const result = await schema['~standard'].validate(output) - if (result.issues) { - throw new ORPCError({ - message: 'Validation output failed', - code: 'INTERNAL_SERVER_ERROR', - }) - } - return result.value - })() - - return validOutput + return validateOutput(procedure, output) as SchemaOutput } - const output = await executeWithHooks({ + return executeWithHooks({ hooks: options, input, context, - meta: { - path, - procedure, - }, - execute, + meta: { path, procedure }, + execute: executeWithValidation, }) + } +} - return output +async function validateInput(procedure: WELL_PROCEDURE, input: unknown) { + const schema = procedure['~orpc'].contract['~orpc'].InputSchema + if (!schema) + return input + + const result = await schema['~standard'].validate(input) + if (result.issues) { + throw new ORPCError({ + message: 'Input validation failed', + code: 'BAD_REQUEST', + issues: result.issues, + }) } - return caller as any + return result.value } -export async function loadProcedure(procedure: ANY_PROCEDURE | ANY_LAZY_PROCEDURE): Promise { - let loadedProcedure: ANY_PROCEDURE +async function validateOutput(procedure: WELL_PROCEDURE, output: unknown) { + const schema = procedure['~orpc'].contract['~orpc'].OutputSchema + if (!schema) + return output - if (isLazy(procedure)) { - loadedProcedure = (await loadLazy(procedure)).default + const result = await schema['~standard'].validate(output) + if (result.issues) { + throw new ORPCError({ + message: 'Output validation failed', + code: 'INTERNAL_SERVER_ERROR', + issues: result.issues, + }) } - else { - loadedProcedure = procedure + + return result.value +} + +async function executeMiddlewareChain( + procedure: WELL_PROCEDURE, + input: unknown, + context: Context, + meta: Meta, +) { + const middlewares = procedure['~orpc'].middlewares ?? [] + let currentMidIndex = 0 + let currentContext = context + + const next: MiddlewareMeta['next'] = async (nextOptions) => { + const mid = middlewares[currentMidIndex] + currentMidIndex += 1 + currentContext = mergeContext(currentContext, nextOptions.context) + + if (mid) { + return await mid(input, currentContext, { + ...meta, + next, + output: output => ({ output, context: undefined }), + }) + } + + return { + output: await procedure['~orpc'].func(input, currentContext, meta), + context: currentContext, + } } + return (await next({})).output +} + +export async function loadProcedure(procedure: ANY_PROCEDURE | ANY_LAZY_PROCEDURE): Promise { + const loadedProcedure = isLazy(procedure) + ? (await loadLazy(procedure)).default + : procedure + if (!isProcedure(loadedProcedure)) { throw new ORPCError({ code: 'NOT_FOUND', message: 'Not found', cause: new Error(trim(` - This error should be caught by the typescript compiler. - But if you still see this error, it means that you trying to call a lazy router (expected to be a lazy procedure). - `)), + Attempted to call a lazy router or invalid procedure. + This should typically be caught by TypeScript compilation. + `)), }) } diff --git a/packages/server/src/router-caller.test.ts b/packages/server/src/router-caller.test.ts index 46c4cf36f..f361c60aa 100644 --- a/packages/server/src/router-caller.test.ts +++ b/packages/server/src/router-caller.test.ts @@ -133,22 +133,22 @@ describe('createRouterCaller', () => { // @ts-expect-error - invalid input expect(caller.ping({ value: new Date('2023-01-01') })).rejects.toThrowError( - 'Validation input failed', + 'Input validation failed', ) // @ts-expect-error - invalid input expect(caller.nested.ping({ value: true })).rejects.toThrowError( - 'Validation input failed', + 'Input validation failed', ) // @ts-expect-error - invalid input expect(caller.lazyRouter.ping({ value: true })).rejects.toThrowError( - 'Validation input failed', + 'Input validation failed', ) // @ts-expect-error - invalid input expect(caller.lazyRouter.lazyRouter.ping({ value: true })).rejects.toThrowError( - 'Validation input failed', + 'Input validation failed', ) }) @@ -252,7 +252,7 @@ describe('createRouterCaller', () => { // @ts-expect-error - invalid input await expect(caller.nested.procedure(123)).rejects.toThrowError( - 'Validation input failed', + 'Input validation failed', ) const meta2 = { @@ -261,7 +261,7 @@ describe('createRouterCaller', () => { } const error2 = new ORPCError({ - message: 'Validation input failed', + message: 'Input validation failed', code: 'BAD_REQUEST', cause: expect.any(Error), }) diff --git a/packages/shared/src/value.ts b/packages/shared/src/value.ts index 9b6ccf2fb..d259f077c 100644 --- a/packages/shared/src/value.ts +++ b/packages/shared/src/value.ts @@ -2,10 +2,9 @@ import type { Promisable } from 'type-fest' export type Value = T | (() => Promisable) -export function value>(value: T): -Promise ? U : never> { +export function value(value: Value): Promise { if (typeof value === 'function') { - return value() + return (value as any)() } return value as any diff --git a/packages/vue-query/tests/e2e.test.ts b/packages/vue-query/tests/e2e.test.ts index fddf58a6d..020da3187 100644 --- a/packages/vue-query/tests/e2e.test.ts +++ b/packages/vue-query/tests/e2e.test.ts @@ -37,7 +37,7 @@ describe('useQuery', () => { // @ts-expect-error -- invalid input const query = useQuery(orpc.user.create.queryOptions({ input: {} }), queryClient) - await vi.waitFor(() => expect(query.error.value).toEqual(new Error('Validation input failed'))) + await vi.waitFor(() => expect(query.error.value).toEqual(new Error('Input validation failed'))) expect(queryClient.getQueryData(orpc.ping.key({ type: 'query' }))).toEqual(undefined) }) From b3a0d91ab19bc0bdf8ba8585dea08e4441796a25 Mon Sep 17 00:00:00 2001 From: unnoq Date: Sat, 14 Dec 2024 21:52:28 +0700 Subject: [PATCH 06/51] refactor and 100% coverage for procedure caller --- .../server/src/procedure-caller.test-d.ts | 143 +------ packages/server/src/procedure-caller.test.ts | 378 +++++++++--------- packages/server/src/procedure-caller.ts | 14 +- packages/shared/src/hook.ts | 2 +- 4 files changed, 214 insertions(+), 323 deletions(-) diff --git a/packages/server/src/procedure-caller.test-d.ts b/packages/server/src/procedure-caller.test-d.ts index 254aef542..41798d626 100644 --- a/packages/server/src/procedure-caller.test-d.ts +++ b/packages/server/src/procedure-caller.test-d.ts @@ -1,32 +1,18 @@ +import type { Procedure } from './procedure' import type { Caller, Meta, WELL_CONTEXT } from './types' -import { ContractProcedure } from '@orpc/contract' import { z } from 'zod' import { createLazy } from './lazy' -import { Procedure } from './procedure' import { createProcedureCaller } from './procedure-caller' beforeEach(() => { vi.resetAllMocks() }) -const schema = z.object({ val: z.string().transform(v => Number(v)) }) -const func = vi.fn() -const procedure = new Procedure({ - contract: new ContractProcedure({ - InputSchema: schema, - OutputSchema: schema, - }), - func, -}) -const procedureWithContext = new Procedure<{ userId?: string }, { db: string }, typeof schema, typeof schema, { val: string }>({ - contract: new ContractProcedure({ - InputSchema: schema, - OutputSchema: schema, - }), - func, -}) - describe('createProcedureCaller', () => { + const schema = z.object({ val: z.string().transform(v => Number(v)) }) + const procedure = {} as Procedure + const procedureWithContext = {} as Procedure<{ userId?: string }, { db: string }, typeof schema, typeof schema, { val: string }> + it('just a caller', () => { const caller = createProcedureCaller({ procedure, @@ -136,115 +122,22 @@ describe('createProcedureCaller', () => { }) }) -describe('createProcedure on invalid lazy procedure', () => { +it('support lazy procedure', () => { + const schema = z.object({ val: z.string().transform(v => Number(v)) }) + const procedure = {} as Procedure<{ userId?: string }, undefined, typeof schema, typeof schema, { val: string }> const lazy = createLazy(() => Promise.resolve({ default: procedure })) - const lazyWithContext = createLazy(() => Promise.resolve({ default: procedureWithContext })) - - it('just a caller', () => { - const caller = createProcedureCaller({ - procedure: lazy, - }) - - expectTypeOf(caller).toEqualTypeOf>() - }) - - it('context can be optional and can be a sync or async function', () => { - createProcedureCaller({ - procedure: lazy, - }) - - createProcedureCaller({ - procedure: lazy, - context: undefined, - }) - - // @ts-expect-error - missing context - createProcedureCaller({ - procedure: lazyWithContext, - }) - - createProcedureCaller({ - procedure: lazyWithContext, - context: { userId: '123' }, - }) - - createProcedureCaller({ - procedure: lazyWithContext, - // @ts-expect-error invalid context - context: { userId: 123 }, - }) - - createProcedureCaller({ - procedure: lazyWithContext, - context: () => ({ userId: '123' }), - }) - createProcedureCaller({ - procedure: lazyWithContext, - // @ts-expect-error invalid context - context: () => ({ userId: 123 }), - }) - - createProcedureCaller({ - procedure: lazyWithContext, - context: async () => ({ userId: '123' }), - }) + const caller = createProcedureCaller({ + procedure: lazy, + context: async () => ({ userId: 'string' }), + path: ['users'], - createProcedureCaller({ - procedure: lazyWithContext, - // @ts-expect-error invalid context - context: async () => ({ userId: 123 }), - }) + onSuccess(state, context, meta) { + expectTypeOf(state).toEqualTypeOf<{ status: 'success', input: unknown, output: { val: number }, error: undefined }>() + expectTypeOf(context).toEqualTypeOf<{ userId: string }>() + expectTypeOf(meta).toEqualTypeOf() + }, }) - it('accept hooks', () => { - createProcedureCaller({ - procedure: lazy, - - async execute(input, context, meta) { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf() - expectTypeOf(meta).toEqualTypeOf Promise<{ val: number }> }>() - - return { val: 123 } - }, - - onStart(state, context, meta) { - expectTypeOf(state).toEqualTypeOf<{ status: 'pending', input: unknown, output: undefined, error: undefined }>() - expectTypeOf(context).toEqualTypeOf() - expectTypeOf(meta).toEqualTypeOf() - }, - - onSuccess(state, context, meta) { - expectTypeOf(state).toEqualTypeOf<{ status: 'success', input: unknown, output: { val: number }, error: undefined }>() - expectTypeOf(context).toEqualTypeOf() - expectTypeOf(meta).toEqualTypeOf() - }, - - onError(state, context, meta) { - expectTypeOf(state).toEqualTypeOf<{ status: 'error', input: unknown, output: undefined, error: Error }>() - expectTypeOf(context).toEqualTypeOf() - expectTypeOf(meta).toEqualTypeOf() - }, - - onFinish(state, context, meta) { - expectTypeOf(state).toEqualTypeOf<{ status: 'success', input: unknown, output: { val: number }, error: undefined } | { status: 'error', input: unknown, output: undefined, error: Error }>() - expectTypeOf(context).toEqualTypeOf() - expectTypeOf(meta).toEqualTypeOf() - }, - }) - }) - - it('accept paths', () => { - createProcedureCaller({ - procedure: lazy, - path: ['users'], - }) - - createProcedureCaller({ - procedure: lazy, - // @ts-expect-error - invalid path - path: [123], - }) - }) + expectTypeOf(caller).toEqualTypeOf>() }) diff --git a/packages/server/src/procedure-caller.test.ts b/packages/server/src/procedure-caller.test.ts index f7ceeb268..b72ed80db 100644 --- a/packages/server/src/procedure-caller.test.ts +++ b/packages/server/src/procedure-caller.test.ts @@ -7,9 +7,9 @@ import { createProcedureCaller } from './procedure-caller' const schema = z.object({ val: z.string().transform(v => Number(v)) }) -const func = vi.fn() -const mid = vi.fn() -const mid2 = vi.fn() +const func = vi.fn(() => ({ val: '123' })) +const mid1 = vi.fn((_, __, meta) => meta.next({})) +const mid2 = vi.fn((_, __, meta) => meta.next({})) const procedure = new Procedure({ contract: new ContractProcedure({ @@ -17,60 +17,36 @@ const procedure = new Procedure({ - contract: new ContractProcedure({ - InputSchema: schema, - OutputSchema: schema, - }), - func, - middlewares: [mid], -}) - -const procedureWithMultipleMiddleware = new Procedure<{ userId?: string }, { db: string }, typeof schema, typeof schema, { val: string }>({ - contract: new ContractProcedure({ - InputSchema: schema, - OutputSchema: schema, - }), - func, - middlewares: [mid, mid2], + middlewares: [mid1, mid2], }) const procedureCases = [ - [ - 'default', - procedure, - procedureWithMiddleware, - procedureWithMultipleMiddleware, - ], - [ - 'lazy', - createLazy(() => Promise.resolve({ default: procedure })), - createLazy(() => Promise.resolve({ default: procedureWithMiddleware })), - createLazy(() => Promise.resolve({ default: procedureWithMultipleMiddleware })), - ], + ['without lazy', procedure], + ['with lazy', createLazy(() => Promise.resolve({ default: procedure }))], ] as const beforeEach(() => { - vi.resetAllMocks() + vi.clearAllMocks() }) -describe.each(procedureCases)('createProcedureCaller - %s', (_, procedure, procedureWithMiddleware, procedureWithMultipleMiddleware) => { - const unwrapLazy = async (val: any) => { - return isLazy(val) ? (await loadLazy(val)).default : val - } +describe.each(procedureCases)('createProcedureCaller - case %s', async (_, procedure) => { + const unwrappedProcedure = isLazy(procedure) ? (await loadLazy(procedure)).default : procedure it('just a caller', async () => { const caller = createProcedureCaller({ procedure, }) - func.mockReturnValueOnce({ val: '123' }) - await expect(caller({ val: '123' })).resolves.toEqual({ val: 123 }) - expect(func).toHaveBeenCalledWith({ val: 123 }, undefined, { path: [], procedure: await unwrapLazy(procedure) }) + expect(func).toBeCalledTimes(1) + expect(func).toBeCalledWith({ val: 123 }, undefined, { path: [], procedure: unwrappedProcedure }) + + expect(mid1).toBeCalledTimes(1) + expect(mid1).toBeCalledWith({ val: 123 }, undefined, { path: [], procedure: unwrappedProcedure, next: expect.any(Function), output: expect.any(Function) }) + + expect(mid2).toBeCalledTimes(1) + expect(mid2).toBeCalledWith({ val: 123 }, undefined, { path: [], procedure: unwrappedProcedure, next: expect.any(Function), output: expect.any(Function) }) }) it('validate input and output', () => { @@ -81,115 +57,61 @@ describe.each(procedureCases)('createProcedureCaller - %s', (_, procedure, proce // @ts-expect-error - invalid input expect(caller({ val: 123 })).rejects.toThrow('Input validation failed') + // @ts-expect-error - invalid output func.mockReturnValueOnce({ val: 1234 }) expect(caller({ val: '1234' })).rejects.toThrow('Output validation failed') }) - it('middleware can return output directly - single', async () => { + it('middleware can return output directly', async () => { const caller = createProcedureCaller({ - procedure: procedureWithMiddleware, - context: { userId: '123' }, + procedure, }) - mid.mockReturnValueOnce({ output: { val: '123' } }) + mid1.mockReturnValueOnce({ output: { val: '990' } }) - await expect(caller({ val: '123' })).resolves.toEqual({ val: 123 }) + await expect(caller({ val: '123' })).resolves.toEqual({ val: 990 }) + expect(mid1).toBeCalledTimes(1) + expect(mid2).toBeCalledTimes(0) expect(func).toBeCalledTimes(0) - expect(mid).toBeCalledTimes(1) - expect(mid).toHaveBeenCalledWith({ val: 123 }, { userId: '123' }, { - path: [], - procedure: await unwrapLazy(procedureWithMiddleware), - next: expect.any(Function), - output: expect.any(Function), - }) - }) + vi.clearAllMocks() - it('middleware can return output directly - multiple', async () => { - const caller = createProcedureCaller({ - procedure: procedureWithMultipleMiddleware, - context: { userId: '123' }, - }) + mid2.mockReturnValueOnce({ output: { val: '9900' } }) - mid.mockImplementationOnce((input, context, meta) => { - return meta.next({ - context: { - extra: '__extra__', - }, - }) - }) - - mid2.mockReturnValueOnce({ output: { val: '1234567' } }) - - await expect(caller({ val: '123' })).resolves.toEqual({ val: 1234567 }) + await expect(caller({ val: '123' })).resolves.toEqual({ val: 9900 }) + expect(mid1).toBeCalledTimes(1) + expect(mid2).toBeCalledTimes(1) expect(func).toBeCalledTimes(0) - expect(mid).toBeCalledTimes(1) - expect(mid).toHaveBeenCalledWith({ val: 123 }, { userId: '123' }, { - path: [], - procedure: await unwrapLazy(procedureWithMultipleMiddleware), - next: expect.any(Function), - output: expect.any(Function), - }) - expect(mid).toReturnWith(Promise.resolve({ output: { val: '1234567' }, context: { extra: '__extra__' } })) - - expect(mid2).toBeCalledTimes(1) - expect(mid2).toHaveBeenCalledWith({ val: 123 }, { userId: '123', extra: '__extra__' }, { - path: [], - procedure: await unwrapLazy(procedureWithMultipleMiddleware), - next: expect.any(Function), - output: expect.any(Function), - }) + expect(mid1).toReturnWith(Promise.resolve({ output: { val: '9900' }, context: undefined })) }) it('output from middleware still be validated', async () => { const caller = createProcedureCaller({ - procedure: procedureWithMiddleware, + procedure, context: { userId: '123' }, }) - mid.mockReturnValueOnce({ output: { val: 1234 } }) - + mid1.mockReturnValueOnce({ output: { val: 990 } }) await expect(caller({ val: '1234' })).rejects.toThrow('Output validation failed') - }) - - it('middleware can add extra context - single', async () => { - const caller = createProcedureCaller({ - procedure: procedureWithMiddleware, - context: { userId: '123' }, - }) - - mid.mockImplementationOnce((input, context, meta) => { - return meta.next({ - context: { - extra: '__extra__', - }, - }) - }) - - func.mockReturnValueOnce({ val: '1234' }) - await expect(caller({ val: '1234' })).resolves.toEqual({ val: 1234 }) + vi.clearAllMocks() - expect(func).toBeCalledTimes(1) - expect(func).toHaveBeenCalledWith({ val: 1234 }, { userId: '123', extra: '__extra__' }, { - path: [], - procedure: await unwrapLazy(procedureWithMiddleware), - }) + mid2.mockReturnValueOnce({ output: { val: 9900 } }) + await expect(caller({ val: '1234' })).rejects.toThrow('Output validation failed') }) - it('middleware can add extra context - multiple', async () => { + it('middleware can add extra context - single', async () => { const caller = createProcedureCaller({ - procedure: procedureWithMultipleMiddleware, - context: { userId: '123' }, + procedure, }) - mid.mockImplementationOnce((input, context, meta) => { + mid1.mockImplementationOnce((input, context, meta) => { return meta.next({ context: { - extra: '__extra__', + extra1: '__extra1__', }, }) }) @@ -202,57 +124,28 @@ describe.each(procedureCases)('createProcedureCaller - %s', (_, procedure, proce }) }) - func.mockReturnValueOnce({ val: '1234' }) - - await expect(caller({ val: '1234' })).resolves.toEqual({ val: 1234 }) - - expect(func).toBeCalledTimes(1) - expect(func).toHaveBeenCalledWith({ val: 1234 }, { - userId: '123', - extra: '__extra__', - extra2: '__extra2__', - }, { - path: [], - procedure: await unwrapLazy(procedureWithMultipleMiddleware), - }) - }) - - it('middleware can override context - signal', async () => { - const caller = createProcedureCaller({ - procedure: procedureWithMiddleware, - context: { userId: '123' }, - }) - - mid.mockImplementationOnce((input, context, meta) => { - return meta.next({ - context: { - userId: '456', - }, - }) - }) + await expect(caller({ val: '123' })).resolves.toEqual({ val: 123 }) - func.mockReturnValueOnce({ val: '1234' }) + expect(mid1).toBeCalledTimes(1) + expect(mid1).toHaveBeenCalledWith(expect.any(Object), undefined, expect.any(Object)) - await expect(caller({ val: '1234' })).resolves.toEqual({ val: 1234 }) + expect(mid2).toBeCalledTimes(1) + expect(mid2).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({ extra1: '__extra1__' }), expect.any(Object)) expect(func).toBeCalledTimes(1) - expect(func).toHaveBeenCalledWith({ val: 1234 }, { userId: '456' }, { - path: [], - procedure: await unwrapLazy(procedureWithMiddleware), - }) + expect(func).toHaveBeenCalledWith(expect.any(Object), { extra1: '__extra1__', extra2: '__extra2__' }, expect.any(Object)) }) - it('middleware can override context - multiple', async () => { + it('middleware can override context', async () => { const caller = createProcedureCaller({ - procedure: procedureWithMultipleMiddleware, + procedure, context: { userId: '123' }, }) - mid.mockImplementationOnce((input, context, meta) => { + mid1.mockImplementationOnce((input, context, meta) => { return meta.next({ context: { - userId: '456', - extra: '1', + userId: '__override1__', }, }) }) @@ -260,21 +153,21 @@ describe.each(procedureCases)('createProcedureCaller - %s', (_, procedure, proce mid2.mockImplementationOnce((input, context, meta) => { return meta.next({ context: { - userId: '789', - extra: '2', + userId: '__override2__', }, }) }) - func.mockReturnValueOnce({ val: '1234' }) + await expect(caller({ val: '123' })).resolves.toEqual({ val: 123 }) - await expect(caller({ val: '1234' })).resolves.toEqual({ val: 1234 }) + expect(mid1).toBeCalledTimes(1) + expect(mid1).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({ userId: '123' }), expect.any(Object)) + + expect(mid2).toBeCalledTimes(1) + expect(mid2).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({ userId: '__override1__' }), expect.any(Object)) expect(func).toBeCalledTimes(1) - expect(func).toHaveBeenCalledWith({ val: 1234 }, { userId: '789', extra: '2' }, { - path: [], - procedure: await unwrapLazy(procedureWithMultipleMiddleware), - }) + expect(func).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({ userId: '__override2__' }), expect.any(Object)) }) const contextCases = [ @@ -283,21 +176,26 @@ describe.each(procedureCases)('createProcedureCaller - %s', (_, procedure, proce ['async function value', async () => ({ val: '__val__' })], ] as const - it.each(contextCases)('can accept %s', async (_, context) => { - func.mockReturnValue({ val: '1234' }) - - const caller1 = createProcedureCaller({ + it.each(contextCases)('can accept context: %s', async (_, context) => { + const caller = createProcedureCaller({ procedure, context, }) - await caller1({ val: '123' }) + await caller({ val: '123' }) + + expect(mid1).toBeCalledTimes(1) + expect(mid1).toBeCalledWith(expect.any(Object), { val: '__val__' }, expect.any(Object)) + + expect(mid2).toBeCalledTimes(1) + expect(mid2).toBeCalledWith(expect.any(Object), { val: '__val__' }, expect.any(Object)) + expect(func).toBeCalledTimes(1) - expect(func).toHaveBeenCalledWith({ val: 123 }, { val: '__val__' }, { path: [], procedure: await unwrapLazy(procedure) }) + expect(func).toBeCalledWith(expect.any(Object), { val: '__val__' }, expect.any(Object)) }) it.each(contextCases)('can accept hooks - context: %s', async (_, context) => { - const execute = vi.fn() + const execute = vi.fn((input, context, meta) => meta.next()) const onStart = vi.fn() const onSuccess = vi.fn() const onError = vi.fn() @@ -314,14 +212,11 @@ describe.each(procedureCases)('createProcedureCaller - %s', (_, procedure, proce onFinish, }) - execute.mockImplementation((input, context, meta) => meta.next()) - func.mockReturnValueOnce({ val: '123' }) - await caller({ val: '123' }) const meta = { path: ['users'], - procedure: await unwrapLazy(procedure), + procedure: unwrappedProcedure, } const contextValue = { val: '__val__' } @@ -357,31 +252,134 @@ describe.each(procedureCases)('createProcedureCaller - %s', (_, procedure, proce onSuccess, }) - func.mockReturnValueOnce({ val: '123' }) - await caller({ val: '123' }) + expect(mid1).toBeCalledTimes(1) + expect(mid1).toHaveBeenCalledWith(expect.any(Object), undefined, expect.objectContaining({ path: ['users'] })) + + expect(mid2).toBeCalledTimes(1) + expect(mid2).toHaveBeenCalledWith(expect.any(Object), undefined, expect.objectContaining({ path: ['users'] })) + expect(func).toBeCalledTimes(1) - expect(func).toHaveBeenCalledWith({ val: 123 }, undefined, { path: ['users'], procedure: await unwrapLazy(procedure) }) + expect(func).toHaveBeenCalledWith(expect.any(Object), undefined, expect.objectContaining({ path: ['users'] })) expect(onSuccess).toBeCalledTimes(1) - expect(onSuccess).toHaveBeenCalledWith( - { status: 'success', input: { val: '123' }, output: { val: 123 }, error: undefined }, - undefined, - { path: ['users'], procedure: await unwrapLazy(procedure) }, - ) + expect(onSuccess).toHaveBeenCalledWith(expect.any(Object), undefined, expect.objectContaining({ path: ['users'] })) }) -}) -describe('createProcedure on invalid lazy procedure', () => { - it('should throw error', () => { - const lazy = createLazy(() => Promise.resolve({ default: 123 })) + it('support signal', async () => { + const controller = new AbortController() + const signal = controller.signal + + const onSuccess = vi.fn() const caller = createProcedureCaller({ - // @ts-expect-error - invalid lazy procedure - procedure: lazy, + procedure, + onSuccess, + context: { userId: '123' }, }) - expect(caller()).rejects.toThrow('Not found') + await caller({ val: '123' }, { signal }) + + expect(mid1).toBeCalledTimes(1) + expect(mid1).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), expect.objectContaining({ signal })) + + expect(mid2).toBeCalledTimes(1) + expect(mid2).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), expect.objectContaining({ signal })) + + expect(func).toBeCalledTimes(1) + expect(func).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), expect.objectContaining({ signal })) + + expect(onSuccess).toBeCalledTimes(1) + expect(onSuccess).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), expect.objectContaining({ signal })) }) }) + +it('should throw error when invalid lazy procedure', () => { + const lazy = createLazy(() => Promise.resolve({ default: 123 })) + + const caller = createProcedureCaller({ + // @ts-expect-error - invalid lazy procedure + procedure: lazy, + }) + + expect(caller()).rejects.toThrow('Not found') +}) + +it('still work without middleware', async () => { + const procedure = new Procedure({ + contract: new ContractProcedure({ + InputSchema: schema, + OutputSchema: schema, + }), + func, + }) + + const caller = createProcedureCaller({ + procedure, + }) + + await expect(caller({ val: '123' })).resolves.toEqual({ val: 123 }) + + expect(func).toBeCalledTimes(1) + expect(func).toHaveBeenCalledWith({ val: 123 }, undefined, { path: [], procedure }) +}) + +it('still work without InputSchema', async () => { + const procedure = new Procedure({ + contract: new ContractProcedure({ + InputSchema: undefined, + OutputSchema: schema, + }), + func, + }) + + const caller = createProcedureCaller({ + procedure, + }) + + await expect(caller('anything')).resolves.toEqual({ val: 123 }) + + expect(func).toBeCalledTimes(1) + expect(func).toHaveBeenCalledWith('anything', undefined, { path: [], procedure }) +}) + +it('still work without OutputSchema', async () => { + const procedure = new Procedure({ + contract: new ContractProcedure({ + InputSchema: schema, + OutputSchema: undefined, + }), + func, + }) + + const caller = createProcedureCaller({ + procedure, + }) + + // @ts-expect-error - without output schema + func.mockReturnValueOnce('anything') + + await expect(caller({ val: '123' })).resolves.toEqual('anything') + + expect(func).toBeCalledTimes(1) + expect(func).toHaveBeenCalledWith({ val: 123 }, undefined, { path: [], procedure }) +}) + +it('has helper `output` in meta', async () => { + const caller = createProcedureCaller({ + procedure, + }) + + mid2.mockImplementationOnce((input, context, meta) => { + return meta.output({ val: '99990' }) + }) + + await expect(caller({ val: '123' })).resolves.toEqual({ val: 99990 }) + + expect(mid1).toBeCalledTimes(1) + expect(mid2).toBeCalledTimes(1) + expect(func).toBeCalledTimes(0) + + expect(mid1).toReturnWith(Promise.resolve({ output: { val: '99990' }, context: undefined })) +}) diff --git a/packages/server/src/procedure-caller.ts b/packages/server/src/procedure-caller.ts index 83dcbd45e..8a1471c5b 100644 --- a/packages/server/src/procedure-caller.ts +++ b/packages/server/src/procedure-caller.ts @@ -58,15 +58,15 @@ export function createProcedureCaller< const procedure = await loadProcedure(options.procedure) as WELL_PROCEDURE const context = await value(options.context) as TContext + const meta: Meta = { + path, + procedure, + signal: callerOptions?.signal, + } + const executeWithValidation = async () => { const validInput = await validateInput(procedure, input) - const meta: Meta = { - path, - procedure, - signal: callerOptions?.signal, - } - const output = await executeMiddlewareChain( procedure, validInput, @@ -81,7 +81,7 @@ export function createProcedureCaller< hooks: options, input, context, - meta: { path, procedure }, + meta, execute: executeWithValidation, }) } diff --git a/packages/shared/src/hook.ts b/packages/shared/src/hook.ts index 70ce31faf..906db8d10 100644 --- a/packages/shared/src/hook.ts +++ b/packages/shared/src/hook.ts @@ -17,7 +17,7 @@ export interface Hooks | OnErrorState, context: TContext, meta: TMeta) => Promisable> } -export async function executeWithHooks & { next?: never }) | undefined>( +export async function executeWithHooks & { next?: never }) | undefined>( options: { hooks?: Hooks input: TInput From 658a31b67c07c5824045e11603f3e509f01c27a8 Mon Sep 17 00:00:00 2001 From: unnoq Date: Sun, 15 Dec 2024 08:37:03 +0700 Subject: [PATCH 07/51] fix middleware types --- packages/server/src/procedure-builder.ts | 2 +- packages/server/src/procedure-caller.ts | 12 +++- packages/server/src/procedure-decorated.ts | 82 +++++++++++----------- packages/server/src/procedure.ts | 10 +-- 4 files changed, 57 insertions(+), 49 deletions(-) diff --git a/packages/server/src/procedure-builder.ts b/packages/server/src/procedure-builder.ts index a44c2ddbf..30d25dd0f 100644 --- a/packages/server/src/procedure-builder.ts +++ b/packages/server/src/procedure-builder.ts @@ -23,7 +23,7 @@ export interface ProcedureBuilderDef< TOutputSchema extends Schema, > { contract: ContractProcedure - middlewares?: Middleware[] + middlewares?: Middleware[] } export class ProcedureBuilder< diff --git a/packages/server/src/procedure-caller.ts b/packages/server/src/procedure-caller.ts index 8a1471c5b..13adb7cc4 100644 --- a/packages/server/src/procedure-caller.ts +++ b/packages/server/src/procedure-caller.ts @@ -16,6 +16,12 @@ import { isLazy, loadLazy } from './lazy' import { isProcedure } from './procedure' import { mergeContext } from './utils' +export type ProcedureCaller< + TInputSchema extends Schema, + TOutputSchema extends Schema, + TFuncOutput extends SchemaInput, +> = Caller, SchemaOutput> + /** * Options for creating a procedure caller with comprehensive type safety */ @@ -52,7 +58,7 @@ export function createProcedureCaller< TFuncOutput extends SchemaInput = SchemaInput, >( options: CreateProcedureCallerOptions, -): Caller, SchemaOutput> { +): ProcedureCaller { return async (...[input, callerOptions]) => { const path = options.path ?? [] const procedure = await loadProcedure(options.procedure) as WELL_PROCEDURE @@ -144,10 +150,12 @@ async function executeMiddlewareChain( }) } - return { + const result = { output: await procedure['~orpc'].func(input, currentContext, meta), context: currentContext, } + + return result as any } return (await next({})).output diff --git a/packages/server/src/procedure-decorated.ts b/packages/server/src/procedure-decorated.ts index 49c0b5259..bdd561494 100644 --- a/packages/server/src/procedure-decorated.ts +++ b/packages/server/src/procedure-decorated.ts @@ -28,48 +28,48 @@ export type DecoratedProcedure< unshiftMiddleware: (...middlewares: Middleware[]) => DecoratedProcedure - use: (< - UExtraContext extends - | Partial>> - | undefined = undefined, - >( - middleware: Middleware< - MergeContext, - UExtraContext, - SchemaOutput, - SchemaInput - >, - ) => DecoratedProcedure< - TContext, - MergeContext, - TInputSchema, - TOutputSchema, - TFuncOutput - >) & (< - UExtraContext extends - | Partial>> - | undefined = undefined, - UMappedInput = unknown, - >( - middleware: Middleware< - MergeContext, - UExtraContext, - UMappedInput, - SchemaInput - >, - mapInput: MapInputMiddleware< - SchemaOutput, - UMappedInput - >, - ) => DecoratedProcedure< - TContext, - MergeContext, - TInputSchema, - TOutputSchema, - TFuncOutput - >) + use: + & ( + > | undefined = undefined>( + middleware: Middleware< + MergeContext, + U, + SchemaOutput, + SchemaInput + >, + ) => DecoratedProcedure< + TContext, + MergeContext, + TInputSchema, + TOutputSchema, + TFuncOutput + > + ) + & ( + < + UExtra extends Context & Partial> | undefined = undefined, + UInput = unknown, + >( + middleware: Middleware< + MergeContext, + UExtra, + UInput, + SchemaInput + >, + mapInput: MapInputMiddleware< + SchemaOutput, + UInput + >, + ) => DecoratedProcedure< + TContext, + MergeContext, + TInputSchema, + TOutputSchema, + TFuncOutput + > + ) } - & (undefined extends TContext ? ProcedureCaller> : unknown) + & (undefined extends TContext ? ProcedureCaller : unknown) export function decorateProcedure< TContext extends Context, diff --git a/packages/server/src/procedure.ts b/packages/server/src/procedure.ts index ef7bdb9c8..8b0c03f0e 100644 --- a/packages/server/src/procedure.ts +++ b/packages/server/src/procedure.ts @@ -9,13 +9,13 @@ export interface ProcedureFunc< TExtraContext extends Context, TInputSchema extends Schema, TOutputSchema extends Schema, - TOutput extends SchemaInput, + TFuncOutput extends SchemaInput, > { ( input: SchemaOutput, context: MergeContext, meta: Meta, - ): Promisable> + ): Promisable> } export interface ProcedureDef< @@ -23,11 +23,11 @@ export interface ProcedureDef< TExtraContext extends Context, TInputSchema extends Schema, TOutputSchema extends Schema, - TFuncOutput extends SchemaOutput, + TFuncOutput extends SchemaInput, > { - middlewares?: Middleware[] + middlewares?: Middleware, any>[] contract: ContractProcedure - func: ProcedureFunc + func: ProcedureFunc } export class Procedure< From b4348182336ecc5ec63b745047e00c235c8708cb Mon Sep 17 00:00:00 2001 From: unnoq Date: Sun, 15 Dec 2024 08:47:58 +0700 Subject: [PATCH 08/51] tests for middleware with output is typed --- packages/server/src/procedure-builder.test-d.ts | 15 ++++++++++++++- .../server/src/procedure-implementer.test-d.ts | 14 ++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/server/src/procedure-builder.test-d.ts b/packages/server/src/procedure-builder.test-d.ts index d7b2110b6..fc5fca351 100644 --- a/packages/server/src/procedure-builder.test-d.ts +++ b/packages/server/src/procedure-builder.test-d.ts @@ -108,7 +108,7 @@ describe('to ProcedureImplementer', () => { builder.use(mid, input => input) }) - it('prevent conflict on context', () => { + it('use middleware prevent conflict on context', () => { builder.use((input, context, meta) => meta.next({})) builder.use((input, context, meta) => meta.next({ context: { id: '1' } })) builder.use((input, context, meta) => meta.next({ context: { id: '1', extra: true } })) @@ -131,6 +131,19 @@ describe('to ProcedureImplementer', () => { // @ts-expect-error - conflict with context builder.use((input, context, meta) => meta.next({ context: { id: 1, extra: true } }), () => 'anything') }) + + it('not allow use middleware with output is typed', () => { + const mid1 = {} as Middleware + const mid2 = {} as Middleware + const mid3 = {} as Middleware + + builder.use(mid1) + + // @ts-expect-error - required used any for output + builder.use(mid2) + // @ts-expect-error - typed output is not allow because builder is not know output yet + builder.use(mid3) + }) }) describe('to DecoratedProcedure', () => { diff --git a/packages/server/src/procedure-implementer.test-d.ts b/packages/server/src/procedure-implementer.test-d.ts index 34b1a303b..7e0441783 100644 --- a/packages/server/src/procedure-implementer.test-d.ts +++ b/packages/server/src/procedure-implementer.test-d.ts @@ -102,6 +102,20 @@ describe('self chainable', () => { // @ts-expect-error - conflict with context implementer.use((input, context, meta) => meta.next({ context: { id: 1, extra: true } }), () => 'anything') }) + + it('handle middleware with output is typed', () => { + const mid1 = {} as Middleware + const mid2 = {} as Middleware + const mid3 = {} as Middleware + const mid4 = {} as Middleware + + implementer.use(mid1) + implementer.use(mid2) + // @ts-expect-error - required used any for output + implementer.use(mid3) + // @ts-expect-error - output is not match + implementer.use(mid4) + }) }) describe('to DecoratedProcedure', () => { From c6c449dc7f3a965f377379f82632fc06521f452e Mon Sep 17 00:00:00 2001 From: unnoq Date: Sun, 15 Dec 2024 09:37:32 +0700 Subject: [PATCH 09/51] tests for procedure decorated --- packages/server/src/procedure-builder.ts | 2 +- .../server/src/procedure-decorated.test-d.ts | 157 +++++++++++++++--- .../server/src/procedure-decorated.test.ts | 129 ++++++++++++++ packages/server/src/procedure-decorated.ts | 37 +++-- packages/server/src/procedure-implementer.ts | 2 +- packages/server/src/procedure.ts | 2 +- 6 files changed, 286 insertions(+), 43 deletions(-) create mode 100644 packages/server/src/procedure-decorated.test.ts diff --git a/packages/server/src/procedure-builder.ts b/packages/server/src/procedure-builder.ts index 30d25dd0f..25b27f9c8 100644 --- a/packages/server/src/procedure-builder.ts +++ b/packages/server/src/procedure-builder.ts @@ -23,7 +23,7 @@ export interface ProcedureBuilderDef< TOutputSchema extends Schema, > { contract: ContractProcedure - middlewares?: Middleware[] + middlewares?: Middleware, TExtraContext, unknown, any>[] } export class ProcedureBuilder< diff --git a/packages/server/src/procedure-decorated.test-d.ts b/packages/server/src/procedure-decorated.test-d.ts index 04d3044fd..c827c70d5 100644 --- a/packages/server/src/procedure-decorated.test-d.ts +++ b/packages/server/src/procedure-decorated.test-d.ts @@ -1,19 +1,17 @@ -import { ContractProcedure } from '@orpc/contract' -import { Procedure } from './procedure' +import type { Middleware, MiddlewareMeta } from './middleware' +import type { Procedure } from './procedure' +import type { DecoratedProcedure } from './procedure-decorated' +import type { WELL_CONTEXT } from './types' +import { z } from 'zod' import { decorateProcedure } from './procedure-decorated' -const procedure = new Procedure({ - contract: new ContractProcedure({ - InputSchema: undefined, - OutputSchema: undefined, - }), - func: () => {}, -}) +const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) +const procedure = {} as Procedure<{ auth: boolean }, { db: string }, typeof schema, typeof schema, { val: string }> const decorated = decorateProcedure(procedure) -describe('prefix', () => { - it('works', () => { +describe('self chainable', () => { + it('prefix', () => { expectTypeOf(decorated.prefix('/test')).toEqualTypeOf() // @ts-expect-error - invalid prefix @@ -21,10 +19,8 @@ describe('prefix', () => { // @ts-expect-error - invalid prefix decorated.prefix(1) }) -}) -describe('route', () => { - it('works', () => { + it('route', () => { expectTypeOf(decorated.route({ path: '/test', method: 'GET' })).toEqualTypeOf() expectTypeOf(decorated.route({ path: '/test', @@ -42,10 +38,104 @@ describe('route', () => { // @ts-expect-error - invalid tags decorated.route({ tags: [1] }) }) -}) -describe('unshiftTag', () => { - it('works', () => { + it('use middleware', () => { + const i = decorated + .use((input, context, meta) => { + expectTypeOf(input).toEqualTypeOf<{ val: number }>() + expectTypeOf(context).toEqualTypeOf<{ auth: boolean } & { db: string }>() + expectTypeOf(meta).toEqualTypeOf>() + + return meta.next({ + context: { + dev: true, + }, + }) + }) + .use((input, context, meta) => { + expectTypeOf(input).toEqualTypeOf<{ val: number }>() + expectTypeOf(context).toEqualTypeOf<{ auth: boolean } & { db: string } & { dev: boolean }>() + expectTypeOf(meta).toEqualTypeOf>() + + return meta.next({}) + }) + + expectTypeOf(i).toEqualTypeOf< + DecoratedProcedure< + { auth: boolean }, + { db: string } & { dev: boolean }, + typeof schema, + typeof schema, + { val: string } + > + >() + }) + + it('use middleware with map input', () => { + const mid = {} as Middleware + + const i = decorated.use(mid, (input) => { + expectTypeOf(input).toEqualTypeOf<{ val: number }>() + return input.val + }) + + expectTypeOf(i).toEqualTypeOf< + DecoratedProcedure< + { auth: boolean }, + { db: string } & { extra: boolean }, + typeof schema, + typeof schema, + { val: string } + > + >() + + // @ts-expect-error - invalid input + decorated.use(mid) + + // @ts-expect-error - invalid mapped input + decorated.use(mid, input => input) + }) + + it('prevent conflict on context', () => { + decorated.use((input, context, meta) => meta.next({})) + decorated.use((input, context, meta) => meta.next({ context: { id: '1' } })) + decorated.use((input, context, meta) => meta.next({ context: { id: '1', extra: true } })) + decorated.use((input, context, meta) => meta.next({ context: { auth: true } })) + + decorated.use((input, context, meta) => meta.next({}), () => 'anything') + decorated.use((input, context, meta) => meta.next({ context: { id: '1' } }), () => 'anything') + decorated.use((input, context, meta) => meta.next({ context: { id: '1', extra: true } }), () => 'anything') + decorated.use((input, context, meta) => meta.next({ context: { auth: true } }), () => 'anything') + + // @ts-expect-error - conflict with context + decorated.use((input, context, meta) => meta.next({ context: { auth: 1 } })) + + // @ts-expect-error - conflict with context + decorated.use((input, context, meta) => meta.next({ context: { auth: 1, extra: true } })) + + // @ts-expect-error - conflict with context + decorated.use((input, context, meta) => meta.next({ context: { auth: 1 } }), () => 'anything') + + // @ts-expect-error - conflict with context + decorated.use((input, context, meta) => meta.next({ context: { auth: 1, extra: true } }), () => 'anything') + }) + + it('handle middleware with output is typed', () => { + const mid1 = {} as Middleware + const mid2 = {} as Middleware + const mid3 = {} as Middleware + const mid4 = {} as Middleware + + decorated.use(mid1) + decorated.use(mid2) + + // @ts-expect-error - required used any for output + decorated.use(mid3) + // @ts-expect-error - output is not match + decorated.use(mid4) + }) + + it('unshiftTag', () => { expectTypeOf(decorated.unshiftTag('test')).toEqualTypeOf() expectTypeOf(decorated.unshiftTag('test', 'test2', 'test3')).toEqualTypeOf() @@ -54,16 +144,37 @@ describe('unshiftTag', () => { // @ts-expect-error - invalid tag decorated.unshiftTag('123', 2) }) -}) -describe('unshiftMiddleware', () => { - it('works', () => { - expectTypeOf(decorated.unshiftMiddleware(() => {})).toEqualTypeOf() - expectTypeOf(decorated.unshiftMiddleware(() => {}, () => {})).toEqualTypeOf() + it('unshiftMiddleware', () => { + const mid1 = {} as Middleware + const mid2 = {} as Middleware<{ auth: boolean }, undefined, { val: number }, any> + const mid3 = {} as Middleware<{ auth: boolean }, { dev: boolean }, unknown, { val: string }> + + expectTypeOf(decorated.unshiftMiddleware(mid1)).toEqualTypeOf() + expectTypeOf(decorated.unshiftMiddleware(mid1, mid2)).toEqualTypeOf() + expectTypeOf(decorated.unshiftMiddleware(mid1, mid2, mid3)).toEqualTypeOf< + DecoratedProcedure<{ auth: boolean }, { dev: boolean } & { db: string }, typeof schema, typeof schema, { val: string }> + >() + + const mid4 = {} as Middleware<{ auth: 'invalid' }, undefined, unknown, any> + const mid5 = {} as Middleware<{ auth: boolean }, undefined, { val: string }, any> + const mid6 = {} as Middleware + const mid7 = {} as Middleware<{ db: string }, undefined, unknown, { val: string }> + + // @ts-expect-error - invalid middleware + decorated.unshiftMiddleware(mid4) + // @ts-expect-error - invalid middleware + decorated.unshiftMiddleware(mid5) + // @ts-expect-error - invalid middleware + decorated.unshiftMiddleware(mid6) + // @ts-expect-error - invalid middleware + decorated.unshiftMiddleware(mid7) + // @ts-expect-error - invalid middleware + decorated.unshiftMiddleware(mid4, mid5, mid6, mid7) // @ts-expect-error - invalid middleware decorated.unshiftMiddleware(1) // @ts-expect-error - invalid middleware - decorated.unshiftMiddleware(() => {}, 1) + decorated.unshiftMiddleware(() => { }, 1) }) }) diff --git a/packages/server/src/procedure-decorated.test.ts b/packages/server/src/procedure-decorated.test.ts new file mode 100644 index 000000000..fb7661f47 --- /dev/null +++ b/packages/server/src/procedure-decorated.test.ts @@ -0,0 +1,129 @@ +import { ContractProcedure } from '@orpc/contract' +import { z } from 'zod' +import { isProcedure, Procedure } from './procedure' +import { decorateProcedure } from './procedure-decorated' + +beforeEach(() => { + vi.clearAllMocks() +}) + +const func = vi.fn(() => ({ val: '123' })) +const mid = vi.fn((_, __, meta) => meta.next({})) + +const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) +const procedure = new Procedure<{ auth: boolean }, { db: string }, typeof schema, typeof schema, { val: string }>({ + contract: new ContractProcedure({ + InputSchema: schema, + OutputSchema: schema, + route: { path: '/test', method: 'GET', deprecated: true, description: 'des', summary: 'sum', tags: ['hi'] }, + inputExample: { val: 123 }, + outputExample: { val: 456 }, + }), + func, + middlewares: [mid], +}) + +const decorated = decorateProcedure(procedure) + +describe('self chainable', () => { + it('prefix', () => { + const prefixed = decorated.prefix('/test') + + expect(prefixed).not.toBe(decorated) + + expect(prefixed).toSatisfy(isProcedure) + expect(prefixed['~orpc'].contract['~orpc'].route?.path).toBe('/test/test') + }) + + it('route', () => { + const route = { path: '/test', method: 'GET', tags: ['hiu'] } as const + const routed = decorated.route(route) + + expect(routed).not.toBe(decorated) + expect(routed).toSatisfy(isProcedure) + expect(routed['~orpc'].contract['~orpc'].route).toBe(route) + }) + + it('use middleware', () => { + const extraMid = vi.fn() + + const applied = decorated.use(extraMid) + + expect(applied).not.toBe(decorated) + expect(applied).toSatisfy(isProcedure) + expect(applied['~orpc'].middlewares).toEqual([mid, extraMid]) + }) + + it('use middleware with map input', () => { + const extraMid = vi.fn() + const map = vi.fn() + + const applied = decorated.use(extraMid, map) + expect(applied).not.toBe(decorated) + expect(applied).toSatisfy(isProcedure) + expect(applied['~orpc'].middlewares).toEqual([mid, expect.any(Function)]) + + extraMid.mockReturnValueOnce('__extra__') + map.mockReturnValueOnce('__map__') + + expect((applied as any)['~orpc'].middlewares[1]('input')).toBe('__extra__') + + expect(map).toBeCalledTimes(1) + expect(map).toBeCalledWith('input') + + expect(extraMid).toBeCalledTimes(1) + expect(extraMid).toBeCalledWith('__map__') + }) + + it('unshiftTag', () => { + const tagged = decorated.unshiftTag('test', 'test2', 'test3') + expect(tagged).not.toBe(decorated) + expect(tagged).toSatisfy(isProcedure) + expect(tagged['~orpc'].contract['~orpc'].route?.tags).toEqual(['test', 'test2', 'test3', 'hi']) + }) + + it('unshiftMiddleware', () => { + const mid1 = vi.fn() + const mid2 = vi.fn() + + const applied = decorated.unshiftMiddleware(mid1, mid2) + expect(applied).not.toBe(decorated) + expect(applied).toSatisfy(isProcedure) + expect(applied['~orpc'].middlewares).toEqual([mid1, mid2, mid]) + }) +}) + +it('can use middleware when has no middleware', () => { + const decorated = decorateProcedure(new Procedure({ + contract: new ContractProcedure({ + InputSchema: undefined, + OutputSchema: undefined, + }), + func: () => { }, + })) + + const mid = vi.fn() + const applied = decorated.use(mid) + + expect(applied).not.toBe(decorated) + expect(applied).toSatisfy(isProcedure) + expect(applied['~orpc'].middlewares).toEqual([mid]) +}) + +it('can unshift middleware when has no middleware', () => { + const decorated = decorateProcedure(new Procedure({ + contract: new ContractProcedure({ + InputSchema: undefined, + OutputSchema: undefined, + }), + func: () => { }, + })) + + const mid1 = vi.fn() + const mid2 = vi.fn() + const applied = decorated.unshiftMiddleware(mid1, mid2) + + expect(applied).not.toBe(decorated) + expect(applied).toSatisfy(isProcedure) + expect(applied['~orpc'].middlewares).toEqual([mid1, mid2]) +}) diff --git a/packages/server/src/procedure-decorated.ts b/packages/server/src/procedure-decorated.ts index bdd561494..5153f17d1 100644 --- a/packages/server/src/procedure-decorated.ts +++ b/packages/server/src/procedure-decorated.ts @@ -1,5 +1,5 @@ import type { HTTPPath, RouteOptions, Schema, SchemaInput, SchemaOutput } from '@orpc/contract' -import type { MapInputMiddleware, Middleware } from './middleware' +import type { ANY_MIDDLEWARE, MapInputMiddleware, Middleware } from './middleware' import type { ProcedureCaller } from './procedure-caller' import type { Context, MergeContext } from './types' import { DecoratedContractProcedure } from '@orpc/contract' @@ -24,10 +24,6 @@ export type DecoratedProcedure< route: RouteOptions, ) => DecoratedProcedure - unshiftTag: (...tags: string[]) => DecoratedProcedure - - unshiftMiddleware: (...middlewares: Middleware[]) => DecoratedProcedure - use: & ( > | undefined = undefined>( @@ -68,6 +64,13 @@ export type DecoratedProcedure< TFuncOutput > ) + + unshiftTag: (...tags: string[]) => DecoratedProcedure + + unshiftMiddleware: > | undefined = undefined>( + ...middlewares: Middleware, SchemaInput>[] + ) => DecoratedProcedure, TInputSchema, TOutputSchema, TFuncOutput> + } & (undefined extends TContext ? ProcedureCaller : unknown) @@ -76,7 +79,7 @@ export function decorateProcedure< TExtraContext extends Context, TInputSchema extends Schema, TOutputSchema extends Schema, - TFuncOutput extends SchemaOutput, + TFuncOutput extends SchemaInput, >( procedure: Procedure< TContext, @@ -110,28 +113,28 @@ export function decorateProcedure< })) } - decorated.unshiftTag = (...tags) => { + decorated.use = (middleware: Middleware, mapInput?: MapInputMiddleware) => { + const middleware_ = mapInput + ? decorateMiddleware(middleware).mapInput(mapInput) + : middleware + return decorateProcedure(new Procedure({ ...procedure['~orpc'], - contract: DecoratedContractProcedure.decorate(procedure['~orpc'].contract).unshiftTag(...tags), - })) + middlewares: [...(procedure['~orpc'].middlewares ?? []), middleware_], + })) as any } - decorated.unshiftMiddleware = (...middlewares) => { + decorated.unshiftTag = (...tags) => { return decorateProcedure(new Procedure({ ...procedure['~orpc'], - middlewares: [...middlewares, ...(procedure['~orpc'].middlewares ?? [])], + contract: DecoratedContractProcedure.decorate(procedure['~orpc'].contract).unshiftTag(...tags), })) } - decorated.use = (middleware: Middleware, mapInput?: MapInputMiddleware) => { - const middleware_ = mapInput - ? decorateMiddleware(middleware).mapInput(mapInput) - : middleware - + decorated.unshiftMiddleware = (...middlewares: ANY_MIDDLEWARE[]) => { return decorateProcedure(new Procedure({ ...procedure['~orpc'], - middlewares: [middleware_, ...(procedure['~orpc'].middlewares ?? [])], + middlewares: [...middlewares, ...(procedure['~orpc'].middlewares ?? [])], })) as any } diff --git a/packages/server/src/procedure-implementer.ts b/packages/server/src/procedure-implementer.ts index 31c08e978..8667ab6fd 100644 --- a/packages/server/src/procedure-implementer.ts +++ b/packages/server/src/procedure-implementer.ts @@ -16,7 +16,7 @@ export type ProcedureImplementerDef< TOutputSchema extends Schema, > = { contract: ContractProcedure - middlewares?: Middleware, SchemaInput>[] + middlewares?: Middleware, TExtraContext, SchemaOutput, SchemaInput>[] } export class ProcedureImplementer< diff --git a/packages/server/src/procedure.ts b/packages/server/src/procedure.ts index 8b0c03f0e..63998def7 100644 --- a/packages/server/src/procedure.ts +++ b/packages/server/src/procedure.ts @@ -25,7 +25,7 @@ export interface ProcedureDef< TOutputSchema extends Schema, TFuncOutput extends SchemaInput, > { - middlewares?: Middleware, any>[] + middlewares?: Middleware, TExtraContext, SchemaOutput, any>[] contract: ContractProcedure func: ProcedureFunc } From 0916f3a15ee23562fdd999ebcfccaa022308622c Mon Sep 17 00:00:00 2001 From: unnoq Date: Sun, 15 Dec 2024 10:32:13 +0700 Subject: [PATCH 10/51] router --- packages/server/src/router-builder.ts | 33 +++- packages/server/src/router.test-d.ts | 231 ++++++++++++++++++++++---- packages/server/src/router.test.ts | 101 ----------- packages/server/src/router.ts | 80 +++------ 4 files changed, 248 insertions(+), 197 deletions(-) delete mode 100644 packages/server/src/router.test.ts diff --git a/packages/server/src/router-builder.ts b/packages/server/src/router-builder.ts index 7c292fce4..e809d07f9 100644 --- a/packages/server/src/router-builder.ts +++ b/packages/server/src/router-builder.ts @@ -1,6 +1,7 @@ -import type { DecoratedLazy, Lazy } from './lazy' -import type { ANY_LAZY_PROCEDURE, ANY_PROCEDURE, DecoratedProcedure } from './procedure' -import type { HandledRouter, Router } from './router' +import type { ANY_LAZY, DecoratedLazy, Lazy } from './lazy' +import type { ANY_LAZY_PROCEDURE, ANY_PROCEDURE, Procedure } from './procedure' +import type { DecoratedProcedure } from './procedure-decorated' +import type { Router } from './router' import type { Context, MergeContext } from './types' import { DecoratedContractProcedure, type HTTPPath } from '@orpc/contract' import { createLazy, decorateLazy, isLazy, loadLazy } from './lazy' @@ -9,7 +10,29 @@ import { type MapInputMiddleware, type Middleware, } from './middleware' -import { decorateProcedure, isProcedure } from './procedure' +import { isProcedure } from './procedure' + +export type AdaptedRouter> = { + [K in keyof TRouter]: TRouter[K] extends Procedure< + infer UContext, + infer UExtraContext, + infer UInputSchema, + infer UOutputSchema, + infer UFuncOutput + > + ? DecoratedProcedure< + UContext, + UExtraContext, + UInputSchema, + UOutputSchema, + UFuncOutput + > + : TRouter[K] extends ANY_LAZY + ? DecoratedLazy + : TRouter[K] extends Router + ? AdaptedRouter + : never +} export const LAZY_ROUTER_PREFIX_SYMBOL = Symbol('ORPC_LAZY_ROUTER_PREFIX') @@ -90,7 +113,7 @@ export class RouterBuilder< router>( router: URouter, - ): HandledRouter { + ): AdaptedRouter { const handled = adaptRouter({ routerOrChild: router, middlewares: this.zz$rb.middlewares, diff --git a/packages/server/src/router.test-d.ts b/packages/server/src/router.test-d.ts index edb4fd517..da63a1503 100644 --- a/packages/server/src/router.test-d.ts +++ b/packages/server/src/router.test-d.ts @@ -1,48 +1,207 @@ -import type { InferRouterInputs, InferRouterOutputs } from '.' +import type { Procedure } from './procedure' +import type { InferRouterInputs, InferRouterOutputs, Router } from './router' +import type { WELL_CONTEXT } from './types' +import { oc } from '@orpc/contract' import { z } from 'zod' -import { os } from '.' - -const router = os.router({ - ping: os - .input(z.object({ ping: z.string().transform(() => 1) })) - .output(z.object({ pong: z.number().transform(() => '1') })) - .func(() => ({ pong: 1 })), - user: { - find: os - .input(z.object({ find: z.number().transform(() => '1') })) - .func(() => ({ user: { id: 1 } })) - , +import { createLazy } from './lazy' + +const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) + +const ping = {} as Procedure<{ auth: boolean }, { db: string }, typeof schema, typeof schema, { val: string }> +const pong = {} as Procedure + +const router = { + ping: createLazy(() => Promise.resolve({ default: ping })), + pong, + nested: { + ping, + pong, }, -}) + lazy: createLazy(() => Promise.resolve({ default: { + ping: createLazy(() => Promise.resolve({ default: ping })), + pong, + nested: createLazy(() => Promise.resolve({ default: { + ping: createLazy(() => Promise.resolve({ default: ping })), + pong, + } })), + } })), +} it('InferRouterInputs', () => { type Inputs = InferRouterInputs - expectTypeOf().toEqualTypeOf<{ - ping: { - ping: string - } - user: { - find: { - find: number - } - } - }>() + expectTypeOf().toEqualTypeOf<{ val: string }>() + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf<{ val: string }>() + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf<{ val: string }>() + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf<{ val: string }>() + expectTypeOf().toEqualTypeOf() }) it('InferRouterOutputs', () => { type Outputs = InferRouterOutputs - expectTypeOf().toEqualTypeOf<{ - ping: { - pong: string - } - user: { - find: { - user: { - id: number - } - } - } - }>() + expectTypeOf().toEqualTypeOf<{ val: number }>() + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf<{ val: number }>() + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf<{ val: number }>() + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf<{ val: number }>() + expectTypeOf().toEqualTypeOf() +}) + +describe('Router', () => { + it('require match context', () => { + const ping = {} as Procedure<{ auth: boolean }, { db: string }, undefined, undefined, unknown> + const pong = {} as Procedure<{ auth: string }, undefined, undefined, undefined, unknown> + + const router: Router<{ auth: boolean, userId: string }, any> = { + ping, + // @ts-expect-error auth is not match + pong, + nested: { + ping, + // @ts-expect-error auth is not match + pong, + }, + + pingLazy: createLazy(() => Promise.resolve({ default: ping })), + // @ts-expect-error auth is not match + pongLazy: createLazy(() => Promise.resolve({ default: pong })), + + nestedLazy1: createLazy(() => Promise.resolve({ + default: { + ping, + }, + })), + + nestedLazy2: createLazy(() => Promise.resolve({ + default: { + ping: createLazy(() => Promise.resolve({ default: ping })), + }, + })), + + // @ts-expect-error auth is not match + nestedLazy3: createLazy(() => Promise.resolve({ + default: { + pong, + }, + })), + + // @ts-expect-error auth is not match + nestedLazy4: createLazy(() => Promise.resolve({ + default: { + nested: { + pong: createLazy(() => Promise.resolve({ default: pong })), + }, + }, + })), + + nestedLazy6: createLazy(() => Promise.resolve({ + default: { + nested: createLazy(() => Promise.resolve({ + default: { + pingLazy: createLazy(() => Promise.resolve({ default: ping })), + }, + })), + }, + })), + + // @ts-expect-error auth is not match + nestedLazy5: createLazy(() => Promise.resolve({ + default: { + nested: createLazy(() => Promise.resolve({ + default: { + pongLazy: createLazy(() => Promise.resolve({ default: pong })), + }, + })), + }, + })), + } + }) + + it('require match contract', () => { + const contract = oc.router({ + ping: oc.input(schema), + pong: oc.output(schema), + + nested: oc.router({ + ping: oc.input(schema), + pong: oc.output(schema), + }), + }) + + const ping = {} as Procedure<{ auth: boolean }, { db: string }, typeof schema, undefined, unknown> + const pong = {} as Procedure + + const router1: Router<{ auth: boolean, userId: string }, typeof contract> = { + ping, + pong, + nested: { + ping, + pong, + }, + } + + const router2: Router<{ auth: boolean, userId: string }, typeof contract> = { + ping, + pong: createLazy(() => Promise.resolve({ default: pong })), + nested: { + ping: createLazy(() => Promise.resolve({ default: ping })), + pong, + }, + } + + const router3: Router<{ auth: boolean, userId: string }, typeof contract> = { + ping: createLazy(() => Promise.resolve({ default: ping })), + pong, + nested: createLazy(() => Promise.resolve({ + default: { + ping: createLazy(() => Promise.resolve({ default: ping })), + pong, + }, + })), + } + + // @ts-expect-error missing + const router4: Router<{ auth: boolean, userId: string }, typeof contract> = {} + + const router39: Router<{ auth: boolean, userId: string }, typeof contract> = { + // @ts-expect-error wrong ping + ping: pong, + pong, + nested: { + ping, + // @ts-expect-error wrong pong + pong: ping, + }, + } + + const router565: Router<{ auth: boolean, userId: string }, typeof contract> = { + // @ts-expect-error wrong ping + ping: createLazy(() => Promise.resolve({ default: pong })), + pong, + nested: { + ping, + // @ts-expect-error wrong pong + pong: createLazy(() => Promise.resolve({ default: ping })), + }, + } + + const router343: Router<{ auth: boolean, userId: string }, typeof contract> = { + // @ts-expect-error wrong ping + ping: createLazy(() => Promise.resolve({ default: pong })), + pong, + // @ts-expect-error wrong nested + nested: createLazy(() => Promise.resolve({ + default: { + ping: createLazy(() => Promise.resolve({ default: ping })), + pong: createLazy(() => Promise.resolve({ default: ping })), + }, + })), + } + }) }) diff --git a/packages/server/src/router.test.ts b/packages/server/src/router.test.ts deleted file mode 100644 index aa76e57d5..000000000 --- a/packages/server/src/router.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { oc } from '@orpc/contract' -import { z } from 'zod' -import { os, type RouterWithContract } from '.' - -it('require procedure match context', () => { - const osw = os.context<{ auth: boolean, userId: string }>() - - osw.router({ - ping: osw.context<{ auth: boolean }>().func(() => { - return { pong: 'ping' } - }), - - // @ts-expect-error userId is not match - ping2: osw.context<{ userId: number }>().func(() => { - return { name: 'unnoq' } - }), - - nested: { - ping: osw.context<{ auth: boolean }>().func(() => { - return { pong: 'ping' } - }), - - // @ts-expect-error userId is not match - ping2: osw.context<{ userId: number }>().func(() => { - return { name: 'unnoq' } - }), - }, - }) -}) - -it('require match contract', () => { - const pingContract = oc.route({ method: 'GET', path: '/ping' }) - const pongContract = oc.input(z.string()).output(z.string()) - const ping = os.contract(pingContract).func(() => { - return 'ping' - }) - const pong = os.contract(pongContract).func(() => { - return 'pong' - }) - - const contract = oc.router({ - ping: pingContract, - pong: pongContract, - - nested: oc.router({ - ping: pingContract, - pong: pongContract, - }), - }) - - const _1: RouterWithContract = { - ping, - pong, - - nested: { - ping, - pong, - }, - } - - const _2: RouterWithContract = { - ping, - pong, - - nested: os.contract(contract.nested).router({ - ping, - pong, - }), - } - - const _3: RouterWithContract = { - ping, - pong, - - // @ts-expect-error missing nested.ping - nested: { - pong, - }, - } - - const _4: RouterWithContract = { - ping, - pong, - - nested: { - ping, - // @ts-expect-error nested.pong is mismatch - pong: os.func(() => 'ping'), - }, - } - - // @ts-expect-error missing pong - const _5: RouterWithContract = { - ping, - - nested: { - ping, - pong, - }, - } -}) diff --git a/packages/server/src/router.ts b/packages/server/src/router.ts index 244571dae..95b2984f8 100644 --- a/packages/server/src/router.ts +++ b/packages/server/src/router.ts @@ -1,77 +1,47 @@ -import type { - ContractProcedure, - ContractRouter, - SchemaInput, - SchemaOutput, -} from '@orpc/contract' -import type { ANY_LAZY, DecoratedLazy, Lazy } from './lazy' -import type { - DecoratedProcedure, - Procedure, -} from './procedure' - +import type { ContractProcedure, ContractRouter, SchemaInput, SchemaOutput } from '@orpc/contract' +import type { Lazy } from './lazy' +import type { Procedure } from './procedure' import type { Context } from './types' -export interface Router { - [k: string]: - | Procedure - | Lazy> - | Router - | Lazy> -} - -export type HandledRouter> = { - [K in keyof TRouter]: TRouter[K] extends Procedure< - infer UContext, - infer UExtraContext, - infer UInputSchema, - infer UOutputSchema, - infer UFuncOutput - > - ? DecoratedProcedure< - UContext, - UExtraContext, - UInputSchema, - UOutputSchema, - UFuncOutput - > - : TRouter[K] extends ANY_LAZY - ? DecoratedLazy - : TRouter[K] extends Router - ? HandledRouter - : never -} - -export type RouterWithContract< +export type Router< TContext extends Context, TContract extends ContractRouter, > = { - [K in keyof TContract]: TContract[K] extends ContractProcedure< - infer UInputSchema, - infer UOutputSchema - > - ? Procedure | Lazy> + [K in keyof TContract]: TContract[K] extends ContractProcedure + ? + | Procedure + | Lazy> : TContract[K] extends ContractRouter - ? RouterWithContract + ? Router | Lazy> : never } -export type InferRouterInputs> = { +export type ANY_ROUTER = Router + +export type InferRouterInputs = { [K in keyof T]: T[K] extends | Procedure | Lazy> ? SchemaInput - : T[K] extends Router + : T[K] extends ANY_ROUTER ? InferRouterInputs - : never + : T[K] extends Lazy + ? U extends ANY_ROUTER + ? InferRouterInputs + : never + : never } -export type InferRouterOutputs> = { +export type InferRouterOutputs = { [K in keyof T]: T[K] extends | Procedure | Lazy> ? SchemaOutput - : T[K] extends Router + : T[K] extends ANY_ROUTER ? InferRouterOutputs - : never + : T[K] extends Lazy + ? U extends ANY_ROUTER + ? InferRouterOutputs + : never + : never } From c472305b67e0686327e99f49b89060ae007cb1a6 Mon Sep 17 00:00:00 2001 From: unnoq Date: Sun, 15 Dec 2024 11:12:49 +0700 Subject: [PATCH 11/51] typed for router builder --- packages/server/src/router-builder.test-d.ts | 206 +++++++++++++++++++ packages/server/src/router-builder.test.ts | 6 +- packages/server/src/router-builder.ts | 97 ++++----- 3 files changed, 244 insertions(+), 65 deletions(-) create mode 100644 packages/server/src/router-builder.test-d.ts diff --git a/packages/server/src/router-builder.test-d.ts b/packages/server/src/router-builder.test-d.ts new file mode 100644 index 000000000..bf6fa3fdb --- /dev/null +++ b/packages/server/src/router-builder.test-d.ts @@ -0,0 +1,206 @@ +import type { DecoratedLazy, Lazy } from './lazy' +import type { Middleware } from './middleware' +import type { Procedure } from './procedure' +import type { AdaptedRouter, RouterBuilder } from './router-builder' +import type { WELL_CONTEXT } from './types' +import { z } from 'zod' +import { createLazy } from './lazy' +import { decorateProcedure } from './procedure-decorated' + +const builder = {} as RouterBuilder<{ auth: boolean }, { db: string }> + +describe('AdaptedRouter', () => { + const ping = {} as Procedure<{ auth: boolean }, { db: string }, undefined, undefined, unknown> + const pong = {} as Procedure + + it('without lazy', () => { + const router = { + ping, + pong, + nested: { + ping, + pong, + }, + } + + const adapted = {} as AdaptedRouter + + expectTypeOf(adapted.ping).toEqualTypeOf(decorateProcedure(ping)) + expectTypeOf(adapted.pong).toEqualTypeOf(decorateProcedure(pong)) + expectTypeOf(adapted.nested.ping).toEqualTypeOf(decorateProcedure(ping)) + expectTypeOf(adapted.nested.pong).toEqualTypeOf(decorateProcedure(pong)) + }) + + it('with lazy', () => { + const router = { + ping: createLazy(() => Promise.resolve({ default: ping })), + pong, + nested: createLazy(() => Promise.resolve({ + default: { + ping, + pong: createLazy(() => Promise.resolve({ default: pong })), + }, + })), + } + + const adapted = {} as AdaptedRouter + + expectTypeOf(adapted.ping).toEqualTypeOf>() + expectTypeOf(adapted.pong).toEqualTypeOf(decorateProcedure(pong)) + expectTypeOf(adapted.nested.ping).toEqualTypeOf>() + expectTypeOf(adapted.nested.pong).toEqualTypeOf>() + }) +}) + +describe('self chainable', () => { + it('prefix', () => { + expectTypeOf(builder.prefix('/test')).toEqualTypeOf() + + // @ts-expect-error - invalid prefix + builder.prefix('') + // @ts-expect-error - invalid prefix + builder.prefix(1) + }) + + it('tag', () => { + expectTypeOf(builder.tag('test')).toEqualTypeOf() + expectTypeOf(builder.tag('test', 'test2', 'test3')).toEqualTypeOf() + + // @ts-expect-error - invalid tag + builder.tag(1) + // @ts-expect-error - invalid tag + builder.tag('123', 2) + }) + + it('use middleware', () => { + const mid1 = {} as Middleware<{ auth: boolean }, undefined, unknown, unknown> + const mid2 = {} as Middleware<{ auth: boolean }, { dev: string }, unknown, unknown> + const mid3 = {} as Middleware<{ auth: boolean, db: string }, { dev: string }, unknown, unknown> + + expectTypeOf(builder.use(mid1)).toEqualTypeOf() + expectTypeOf(builder.use(mid2)).toEqualTypeOf< + RouterBuilder<{ auth: boolean }, { db: string } & { dev: string }> + >() + expectTypeOf(builder.use(mid3)).toEqualTypeOf< + RouterBuilder<{ auth: boolean }, { db: string } & { dev: string }> + >() + + const mid4 = {} as Middleware<{ auth: boolean }, { dev: string }, unknown, { val: string }> + const mid5 = {} as Middleware<{ auth: boolean }, { dev: string }, unknown, { val: number }> + const mid6 = {} as Middleware<{ auth: boolean }, { dev: string }, { val: number }, unknown> + + // @ts-expect-error - invalid middleware + builder.use(mid4) + // @ts-expect-error - invalid middleware + builder.use(mid5) + // @ts-expect-error - invalid middleware + builder.use(mid6) + // @ts-expect-error - invalid middleware + builder.use(true) + // @ts-expect-error - invalid middleware + builder.use(() => {}) + }) +}) + +describe('to AdaptedRouter', () => { + const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) + const ping = {} as Procedure<{ auth: boolean }, { db: string }, typeof schema, typeof schema, { val: string }> + const pong = {} as Procedure + + const wrongPing = {} as Procedure<{ auth: 'invalid' }, undefined, undefined, undefined, unknown> + + it('router without lazy', () => { + expectTypeOf(builder.router({ ping, pong, nested: { ping, pong } })).toEqualTypeOf< + AdaptedRouter<{ ping: typeof ping, pong: typeof pong, nested: { ping: typeof ping, pong: typeof pong } }> + >() + + builder.router({ ping }) + // @ts-expect-error - context is not match + builder.router({ wrongPing }) + }) + + it('router with lazy', () => { + expectTypeOf(builder.router({ + ping: createLazy(() => Promise.resolve({ default: ping })), + pong, + nested: createLazy(() => Promise.resolve({ + default: { + ping, + pong: createLazy(() => Promise.resolve({ default: pong })), + }, + })), + })).toEqualTypeOf< + AdaptedRouter<{ + ping: Lazy + pong: typeof pong + nested: Lazy<{ ping: typeof ping, pong: Lazy }> + }> + >() + + builder.router({ ping: createLazy(() => Promise.resolve({ default: ping })) }) + // @ts-expect-error - context is not match + builder.router({ wrongPing: createLazy(() => Promise.resolve({ default: wrongPing })) }) + }) +}) + +describe('to DecoratedLazy', () => { + const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) + const ping = {} as Procedure<{ auth: boolean }, { db: string }, typeof schema, typeof schema, { val: string }> + const pong = {} as Procedure + + const wrongPing = {} as Procedure<{ auth: 'invalid' }, undefined, undefined, undefined, unknown> + + it('router without lazy', () => { + expectTypeOf(builder.lazy(() => Promise.resolve({ + default: { + ping, + pong, + nested: { + ping, + pong, + }, + }, + }))).toEqualTypeOf< + DecoratedLazy<{ + ping: typeof ping + pong: typeof pong + nested: { + ping: typeof ping + pong: typeof pong + } + }> + >() + + builder.lazy(() => Promise.resolve({ default: { ping } })) + // @ts-expect-error - context is not match + builder.lazy(() => Promise.resolve({ default: { wrongPing } })) + }) + + it('router with lazy', () => { + expectTypeOf(builder.lazy(() => Promise.resolve({ + default: { + ping: createLazy(() => Promise.resolve({ default: ping })), + pong, + nested: createLazy(() => Promise.resolve({ + default: { + ping, + pong: createLazy(() => Promise.resolve({ default: pong })), + }, + })), + }, + }))).toEqualTypeOf< + DecoratedLazy<{ + ping: DecoratedLazy + pong: typeof pong + nested: { + ping: typeof ping + pong: DecoratedLazy + } + }> + >() + + builder.lazy(() => Promise.resolve({ default: { ping: createLazy(() => Promise.resolve({ default: ping })) } })) + // @ts-expect-error - context is not match + builder.lazy(() => Promise.resolve({ default: { wrongPing: createLazy(() => Promise.resolve({ default: wrongPing })) } })) + }) +}) diff --git a/packages/server/src/router-builder.test.ts b/packages/server/src/router-builder.test.ts index 13c89417c..7d2a17b82 100644 --- a/packages/server/src/router-builder.test.ts +++ b/packages/server/src/router-builder.test.ts @@ -55,7 +55,7 @@ describe('prefix', () => { describe('tags', () => { it('chainable tags', () => { - expect(builder.tags('1', '2').tags('3').tags('4').zz$rb.tags).toEqual([ + expect(builder.tag('1', '2').tag('3').tag('4').zz$rb.tags).toEqual([ '1', '2', '3', @@ -65,8 +65,8 @@ describe('tags', () => { it('router', async () => { const router = builder - .tags('api') - .tags('users') + .tag('api') + .tag('users') .router({ ping, pong, lazy, lazyRouter }) expect(router.ping.zz$p.contract['~orpc'].route?.tags).toEqual([ diff --git a/packages/server/src/router-builder.ts b/packages/server/src/router-builder.ts index e809d07f9..75d0c4f4f 100644 --- a/packages/server/src/router-builder.ts +++ b/packages/server/src/router-builder.ts @@ -1,18 +1,14 @@ import type { ANY_LAZY, DecoratedLazy, Lazy } from './lazy' +import type { Middleware } from './middleware' import type { ANY_LAZY_PROCEDURE, ANY_PROCEDURE, Procedure } from './procedure' import type { DecoratedProcedure } from './procedure-decorated' -import type { Router } from './router' +import type { ANY_ROUTER, Router } from './router' import type { Context, MergeContext } from './types' import { DecoratedContractProcedure, type HTTPPath } from '@orpc/contract' import { createLazy, decorateLazy, isLazy, loadLazy } from './lazy' -import { - decorateMiddleware, - type MapInputMiddleware, - type Middleware, -} from './middleware' import { isProcedure } from './procedure' -export type AdaptedRouter> = { +export type AdaptedRouter = { [K in keyof TRouter]: TRouter[K] extends Procedure< infer UContext, infer UExtraContext, @@ -29,91 +25,68 @@ export type AdaptedRouter> = { > : TRouter[K] extends ANY_LAZY ? DecoratedLazy - : TRouter[K] extends Router + : TRouter[K] extends ANY_ROUTER ? AdaptedRouter : never } +export type RouterBuilderDef = { + prefix?: HTTPPath + tags?: readonly string[] + middlewares?: Middleware, TExtraContext, unknown, any>[] +} + export const LAZY_ROUTER_PREFIX_SYMBOL = Symbol('ORPC_LAZY_ROUTER_PREFIX') export class RouterBuilder< TContext extends Context, TExtraContext extends Context, > { - constructor( - public zz$rb: { - prefix?: HTTPPath - tags?: string[] - middlewares?: Middleware[] - }, - ) { - if (zz$rb.prefix && zz$rb.prefix.includes('{')) { - throw new Error('Prefix cannot contain "{" for dynamic routing') + '~type' = 'RouterBuilder' as const + '~orpc': RouterBuilderDef + + constructor(def: RouterBuilderDef) { + this['~orpc'] = def + + if (def.prefix && def.prefix.includes('{')) { + throw new Error(` + Dynamic routing in prefix not supported yet. + Please remove "{" from "${def.prefix}". + `) } } prefix(prefix: HTTPPath): RouterBuilder { return new RouterBuilder({ - ...this.zz$rb, - prefix: `${this.zz$rb.prefix ?? ''}${prefix}`, + ...this['~orpc'], + prefix: `${this['~orpc'].prefix ?? ''}${prefix}`, }) } - tags(...tags: string[]): RouterBuilder { - if (!tags.length) - return this - + tag(...tags: string[]): RouterBuilder { return new RouterBuilder({ - ...this.zz$rb, - tags: [...(this.zz$rb.tags ?? []), ...tags], + ...this['~orpc'], + tags: [...(this['~orpc'].tags ?? []), ...tags], }) } - use< - UExtraContext extends - | Partial>> - | undefined = undefined, - >( + use> | undefined = undefined>( middleware: Middleware< MergeContext, - UExtraContext, + U, unknown, unknown >, - ): RouterBuilder> - - use< - UExtraContext extends - | Partial>> - | undefined = undefined, - UMappedInput = unknown, - >( - middleware: Middleware< - MergeContext, - UExtraContext, - UMappedInput, - unknown - >, - mapInput: MapInputMiddleware, - ): RouterBuilder> - - use( - middleware: Middleware, - mapInput?: MapInputMiddleware, - ): RouterBuilder { - const middleware_ = mapInput - ? decorateMiddleware(middleware).mapInput(mapInput) - : middleware - + ): RouterBuilder> { return new RouterBuilder({ - ...this.zz$rb, - middlewares: [...(this.zz$rb.middlewares || []), middleware_], + ...this['~orpc'], + middlewares: [...(this['~orpc'].middlewares ?? []), middleware as any], }) } - router>( - router: URouter, - ): AdaptedRouter { + router>( + router: U, + ): AdaptedRouter { const handled = adaptRouter({ routerOrChild: router, middlewares: this.zz$rb.middlewares, @@ -124,7 +97,7 @@ export class RouterBuilder< return handled as any } - lazy>( + lazy>( loader: () => Promise<{ default: U }>, ): DecoratedLazy { const lazy = adaptLazyRouter({ From a60ef9611ce1a6a5544f316d7a080e1ca7755954 Mon Sep 17 00:00:00 2001 From: unnoq Date: Mon, 16 Dec 2024 10:00:54 +0700 Subject: [PATCH 12/51] context restriction for router builder --- packages/server/src/router-builder.test-d.ts | 48 ++++++++++++++------ packages/server/src/router-builder.ts | 13 ++++-- 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/packages/server/src/router-builder.test-d.ts b/packages/server/src/router-builder.test-d.ts index bf6fa3fdb..16848ae05 100644 --- a/packages/server/src/router-builder.test-d.ts +++ b/packages/server/src/router-builder.test-d.ts @@ -1,11 +1,11 @@ import type { DecoratedLazy, Lazy } from './lazy' import type { Middleware } from './middleware' import type { Procedure } from './procedure' +import type { DecoratedProcedure } from './procedure-decorated' import type { AdaptedRouter, RouterBuilder } from './router-builder' import type { WELL_CONTEXT } from './types' import { z } from 'zod' import { createLazy } from './lazy' -import { decorateProcedure } from './procedure-decorated' const builder = {} as RouterBuilder<{ auth: boolean }, { db: string }> @@ -23,12 +23,20 @@ describe('AdaptedRouter', () => { }, } - const adapted = {} as AdaptedRouter + const adapted = {} as AdaptedRouter<{ log: true }, typeof router> - expectTypeOf(adapted.ping).toEqualTypeOf(decorateProcedure(ping)) - expectTypeOf(adapted.pong).toEqualTypeOf(decorateProcedure(pong)) - expectTypeOf(adapted.nested.ping).toEqualTypeOf(decorateProcedure(ping)) - expectTypeOf(adapted.nested.pong).toEqualTypeOf(decorateProcedure(pong)) + expectTypeOf(adapted.ping).toEqualTypeOf< + DecoratedProcedure<{ log: true } & { auth: boolean }, { db: string }, undefined, undefined, unknown> + >() + expectTypeOf(adapted.pong).toEqualTypeOf< + DecoratedProcedure<{ log: true } & WELL_CONTEXT, undefined, undefined, undefined, unknown> + >() + expectTypeOf(adapted.nested.ping).toEqualTypeOf< + DecoratedProcedure<{ log: true } & { auth: boolean }, { db: string }, undefined, undefined, unknown> + >() + expectTypeOf(adapted.nested.pong).toEqualTypeOf< + DecoratedProcedure<{ log: true } & WELL_CONTEXT, undefined, undefined, undefined, unknown> + >() }) it('with lazy', () => { @@ -43,10 +51,12 @@ describe('AdaptedRouter', () => { })), } - const adapted = {} as AdaptedRouter + const adapted = {} as AdaptedRouter<{ log: true }, typeof router> expectTypeOf(adapted.ping).toEqualTypeOf>() - expectTypeOf(adapted.pong).toEqualTypeOf(decorateProcedure(pong)) + expectTypeOf(adapted.pong).toEqualTypeOf< + DecoratedProcedure<{ log: true } & WELL_CONTEXT, undefined, undefined, undefined, unknown> + >() expectTypeOf(adapted.nested.ping).toEqualTypeOf>() expectTypeOf(adapted.nested.pong).toEqualTypeOf>() }) @@ -111,7 +121,14 @@ describe('to AdaptedRouter', () => { it('router without lazy', () => { expectTypeOf(builder.router({ ping, pong, nested: { ping, pong } })).toEqualTypeOf< - AdaptedRouter<{ ping: typeof ping, pong: typeof pong, nested: { ping: typeof ping, pong: typeof pong } }> + AdaptedRouter< + { auth: boolean }, + { + ping: typeof ping + pong: typeof pong + nested: { ping: typeof ping, pong: typeof pong } + } + > >() builder.router({ ping }) @@ -130,11 +147,14 @@ describe('to AdaptedRouter', () => { }, })), })).toEqualTypeOf< - AdaptedRouter<{ - ping: Lazy - pong: typeof pong - nested: Lazy<{ ping: typeof ping, pong: Lazy }> - }> + AdaptedRouter< + { auth: boolean }, + { + ping: Lazy + pong: typeof pong + nested: Lazy<{ ping: typeof ping, pong: Lazy }> + } + > >() builder.router({ ping: createLazy(() => Promise.resolve({ default: ping })) }) diff --git a/packages/server/src/router-builder.ts b/packages/server/src/router-builder.ts index 75d0c4f4f..ea358fe5a 100644 --- a/packages/server/src/router-builder.ts +++ b/packages/server/src/router-builder.ts @@ -8,7 +8,10 @@ import { DecoratedContractProcedure, type HTTPPath } from '@orpc/contract' import { createLazy, decorateLazy, isLazy, loadLazy } from './lazy' import { isProcedure } from './procedure' -export type AdaptedRouter = { +export type AdaptedRouter< + TContext extends Context, + TRouter extends Router, +> = { [K in keyof TRouter]: TRouter[K] extends Procedure< infer UContext, infer UExtraContext, @@ -17,16 +20,16 @@ export type AdaptedRouter = { infer UFuncOutput > ? DecoratedProcedure< - UContext, + TContext & UContext, UExtraContext, UInputSchema, UOutputSchema, UFuncOutput > : TRouter[K] extends ANY_LAZY - ? DecoratedLazy + ? DecoratedLazy // TODO: pass TContext here : TRouter[K] extends ANY_ROUTER - ? AdaptedRouter + ? AdaptedRouter : never } @@ -86,7 +89,7 @@ export class RouterBuilder< router>( router: U, - ): AdaptedRouter { + ): AdaptedRouter { const handled = adaptRouter({ routerOrChild: router, middlewares: this.zz$rb.middlewares, From c4d81de76753b80e5724242554a38203084c6042 Mon Sep 17 00:00:00 2001 From: unnoq Date: Mon, 16 Dec 2024 10:28:53 +0700 Subject: [PATCH 13/51] typesafe and prevent duplicate on unshift method --- .../contract/src/procedure-decorated.test.ts | 8 +++++- packages/contract/src/procedure-decorated.ts | 7 ++++-- .../server/src/procedure-decorated.test-d.ts | 25 +++++++++++++------ .../server/src/procedure-decorated.test.ts | 9 +++++++ packages/server/src/procedure-decorated.ts | 9 ++++--- 5 files changed, 44 insertions(+), 14 deletions(-) diff --git a/packages/contract/src/procedure-decorated.test.ts b/packages/contract/src/procedure-decorated.test.ts index b5efcc1c5..a795b0d55 100644 --- a/packages/contract/src/procedure-decorated.test.ts +++ b/packages/contract/src/procedure-decorated.test.ts @@ -52,7 +52,7 @@ describe('prefix', () => { }) }) -describe('pushTag', () => { +describe('unshiftTag', () => { const decorated = new DecoratedContractProcedure({ InputSchema: undefined, OutputSchema: undefined }) it('works', () => { @@ -70,6 +70,12 @@ describe('pushTag', () => { expect(tagged['~orpc']).not.toBe(decorated['~orpc']) expect(tagged).not.toBe(decorated) }) + + it('prevent duplicate', () => { + const tagged = decorated.unshiftTag('tag1', 'tag2') + const tagged2 = tagged.unshiftTag('tag1', 'tag3') + expect(tagged2['~orpc'].route?.tags).toEqual(['tag1', 'tag3', 'tag2']) + }) }) describe('input', () => { diff --git a/packages/contract/src/procedure-decorated.ts b/packages/contract/src/procedure-decorated.ts index 0f3eb9952..971e53410 100644 --- a/packages/contract/src/procedure-decorated.ts +++ b/packages/contract/src/procedure-decorated.ts @@ -40,12 +40,15 @@ export class DecoratedContractProcedure< }) } - unshiftTag(...tags: string[]): DecoratedContractProcedure { + unshiftTag(...tags: readonly string[]): DecoratedContractProcedure { return new DecoratedContractProcedure({ ...this['~orpc'], route: { ...this['~orpc'].route, - tags: [...tags, ...(this['~orpc'].route?.tags ?? [])], + tags: [ + ...tags, + ...this['~orpc'].route?.tags?.filter(tag => !tags.includes(tag)) ?? [], + ], }, }) } diff --git a/packages/server/src/procedure-decorated.test-d.ts b/packages/server/src/procedure-decorated.test-d.ts index c827c70d5..c6899ab3c 100644 --- a/packages/server/src/procedure-decorated.test-d.ts +++ b/packages/server/src/procedure-decorated.test-d.ts @@ -152,25 +152,34 @@ describe('self chainable', () => { expectTypeOf(decorated.unshiftMiddleware(mid1)).toEqualTypeOf() expectTypeOf(decorated.unshiftMiddleware(mid1, mid2)).toEqualTypeOf() - expectTypeOf(decorated.unshiftMiddleware(mid1, mid2, mid3)).toEqualTypeOf< - DecoratedProcedure<{ auth: boolean }, { dev: boolean } & { db: string }, typeof schema, typeof schema, { val: string }> - >() + expectTypeOf(decorated.unshiftMiddleware(mid1, mid2, mid3)).toEqualTypeOf() const mid4 = {} as Middleware<{ auth: 'invalid' }, undefined, unknown, any> const mid5 = {} as Middleware<{ auth: boolean }, undefined, { val: string }, any> const mid6 = {} as Middleware const mid7 = {} as Middleware<{ db: string }, undefined, unknown, { val: string }> + const mid8 = {} as Middleware - // @ts-expect-error - invalid middleware + // @ts-expect-error - context is not match decorated.unshiftMiddleware(mid4) - // @ts-expect-error - invalid middleware + // @ts-expect-error - input is not match decorated.unshiftMiddleware(mid5) - // @ts-expect-error - invalid middleware + // @ts-expect-error - output is not match decorated.unshiftMiddleware(mid6) - // @ts-expect-error - invalid middleware + // @ts-expect-error - context is not match decorated.unshiftMiddleware(mid7) + // @ts-expect-error - extra context is conflict with context + decorated.unshiftMiddleware(mid8) // @ts-expect-error - invalid middleware - decorated.unshiftMiddleware(mid4, mid5, mid6, mid7) + decorated.unshiftMiddleware(mid4, mid5, mid6, mid7, mid8) + + const mid9 = {} as Middleware + const mid10 = {} as Middleware + + decorated.unshiftMiddleware(mid9) + decorated.unshiftMiddleware(mid10) + // @ts-expect-error - extra context of mid10 is conflict with extra context of mid9 + decorated.unshiftMiddleware(mid9, mid10) // @ts-expect-error - invalid middleware decorated.unshiftMiddleware(1) diff --git a/packages/server/src/procedure-decorated.test.ts b/packages/server/src/procedure-decorated.test.ts index fb7661f47..01639e679 100644 --- a/packages/server/src/procedure-decorated.test.ts +++ b/packages/server/src/procedure-decorated.test.ts @@ -91,6 +91,15 @@ describe('self chainable', () => { expect(applied).toSatisfy(isProcedure) expect(applied['~orpc'].middlewares).toEqual([mid1, mid2, mid]) }) + + it('unshiftMiddleware - prevent duplicate', () => { + const mid1 = vi.fn() + const mid2 = vi.fn() + const mid3 = vi.fn() + + const applied = decorated.unshiftMiddleware(mid1, mid2).unshiftMiddleware(mid1, mid3, mid) + expect(applied['~orpc'].middlewares).toEqual([mid1, mid3, mid, mid2]) + }) }) it('can use middleware when has no middleware', () => { diff --git a/packages/server/src/procedure-decorated.ts b/packages/server/src/procedure-decorated.ts index 5153f17d1..d841244f1 100644 --- a/packages/server/src/procedure-decorated.ts +++ b/packages/server/src/procedure-decorated.ts @@ -68,8 +68,8 @@ export type DecoratedProcedure< unshiftTag: (...tags: string[]) => DecoratedProcedure unshiftMiddleware: > | undefined = undefined>( - ...middlewares: Middleware, SchemaInput>[] - ) => DecoratedProcedure, TInputSchema, TOutputSchema, TFuncOutput> + ...middlewares: readonly Middleware, SchemaInput>[] + ) => DecoratedProcedure } & (undefined extends TContext ? ProcedureCaller : unknown) @@ -134,7 +134,10 @@ export function decorateProcedure< decorated.unshiftMiddleware = (...middlewares: ANY_MIDDLEWARE[]) => { return decorateProcedure(new Procedure({ ...procedure['~orpc'], - middlewares: [...middlewares, ...(procedure['~orpc'].middlewares ?? [])], + middlewares: [ + ...middlewares, + ...procedure['~orpc'].middlewares?.filter(middleware => !middlewares.includes(middleware)) ?? [], + ], })) as any } From e9a1dde7c17d45286cebbe2f02eed60e848d6867 Mon Sep 17 00:00:00 2001 From: unnoq Date: Mon, 16 Dec 2024 10:36:29 +0700 Subject: [PATCH 14/51] router builder tests partial --- packages/server/src/router-builder.test.ts | 273 +++++++++++---------- 1 file changed, 137 insertions(+), 136 deletions(-) diff --git a/packages/server/src/router-builder.test.ts b/packages/server/src/router-builder.test.ts index 7d2a17b82..f086ca808 100644 --- a/packages/server/src/router-builder.test.ts +++ b/packages/server/src/router-builder.test.ts @@ -1,154 +1,155 @@ -import type { DecoratedLazy } from './lazy' -import { ContractProcedure } from '@orpc/contract' +import type { DecoratedLazy, Lazy } from './lazy' +import type { Procedure } from './procedure' +import type { AdaptedRouter } from './router-builder' +import type { WELL_CONTEXT } from './types' import { z } from 'zod' -import { decorateProcedure, isProcedure, os, Procedure } from '.' -import { createLazy, isLazy, LAZY_LOADER_SYMBOL } from './lazy' -import { LAZY_ROUTER_PREFIX_SYMBOL, RouterBuilder } from './router-builder' - -const builder = new RouterBuilder({}) -const ping = os - .route({ method: 'GET', path: '/ping', tags: ['ping'] }) - .func(() => 'ping') -const pong = os - .output(z.object({ id: z.string() })) - .func(() => ({ id: '123' })) - -const lazy = os.lazy(() => Promise.resolve({ - default: os.route({ - method: 'GET', - path: '/lazy', - tags: ['lazy'], - }).func(() => 'lazy'), -})) - -const lazyRouter = os.lazy(() => Promise.resolve({ - default: { - lazy, - lazyRouter: os.lazy(() => Promise.resolve({ default: { lazy } })), - }, -})) - -describe('prefix', () => { - it('chainable prefix', () => { - expect(builder.prefix('/1').prefix('/2').prefix('/3').zz$rb.prefix).toEqual( - '/1/2/3', - ) - }) +import { createLazy } from './lazy' +import { RouterBuilder } from './router-builder' - it('router', async () => { - const router = builder - .prefix('/api') - .prefix('/users') - .router({ ping, pong, lazy: builder.lazy(() => Promise.resolve({ default: { - ping, - lazy: os.route({ method: 'GET', path: '/lazy' }).func(() => 'lazy'), - } })) }) - - expect(router.ping.zz$p.contract['~orpc'].route?.path).toEqual('/api/users/ping') - expect(router.pong.zz$p.contract['~orpc'].route?.path).toEqual(undefined) - expect((await router.lazy.lazy[LAZY_LOADER_SYMBOL]()).default.zz$p.contract['~orpc'].route?.path).toEqual('/api/users/lazy') - expect((await (router.lazy as any)[LAZY_LOADER_SYMBOL]()).default.lazy.zz$p.contract['~orpc'].route?.path).toEqual('/api/users/lazy') - expect((router.lazy as any)[LAZY_ROUTER_PREFIX_SYMBOL]).toEqual('/api/users') - expect((router.lazy.lazy as any)[LAZY_ROUTER_PREFIX_SYMBOL]).toEqual('/api/users') - }) -}) +const mid1 = vi.fn() +const mid2 = vi.fn() -describe('tags', () => { - it('chainable tags', () => { - expect(builder.tag('1', '2').tag('3').tag('4').zz$rb.tags).toEqual([ - '1', - '2', - '3', - '4', - ]) - }) +const builder = new RouterBuilder<{ auth: boolean }, { db: string }>({ + middlewares: [mid1, mid2], + prefix: '/prefix', + tags: ['tag1', 'tag2'], +}) - it('router', async () => { - const router = builder - .tag('api') - .tag('users') - .router({ ping, pong, lazy, lazyRouter }) - - expect(router.ping.zz$p.contract['~orpc'].route?.tags).toEqual([ - 'ping', - 'api', - 'users', - ]) - expect(router.pong.zz$p.contract['~orpc'].route?.tags).toEqual(['api', 'users']) - - expect((await (router.lazy[LAZY_LOADER_SYMBOL]())).default.zz$p.contract['~orpc'].route?.tags).toEqual([ - 'lazy', - 'api', - 'users', - ]) - expect((await (router.lazyRouter.lazy[LAZY_LOADER_SYMBOL]())).default.zz$p.contract['~orpc'].route?.tags).toEqual([ - 'lazy', - 'api', - 'users', - ]) - expect((await (router.lazyRouter.lazyRouter.lazy[LAZY_LOADER_SYMBOL]())).default.zz$p.contract['~orpc'].route?.tags).toEqual([ - 'lazy', - 'api', - 'users', - ]) +describe('self chainable', () => { + it('prefix', () => { + const prefixed = builder.prefix('/test') + expect(prefixed).not.toBe(builder) + expect(prefixed).toBeInstanceOf(RouterBuilder) + expect(prefixed['~orpc'].prefix).toBe('/prefix/test') }) -}) -describe('middleware', () => { - const mid1 = vi.fn() - const mid2 = vi.fn() - const mid3 = vi.fn() - - it('chainable middleware', () => { - expect(builder.use(mid1).use(mid2).use(mid3).zz$rb.middlewares).toEqual([ - mid1, - mid2, - mid3, - ]) + it('tag', () => { + const tagged = builder.tag('test1', 'test2') + expect(tagged).not.toBe(builder) + expect(tagged).toBeInstanceOf(RouterBuilder) + expect(tagged['~orpc'].tags).toEqual(['tag1', 'tag2', 'test1', 'test2']) }) - it('router', async () => { - const router = builder.use(mid1).use(mid2).router({ ping, pong, lazy, lazyRouter }) + it('use middleware', () => { + const mid3 = vi.fn() + const mid4 = vi.fn() - expect(router.ping.zz$p.middlewares).toEqual([mid1, mid2]) - expect(router.pong.zz$p.middlewares).toEqual([mid1, mid2]) + const applied = builder.use(mid3).use(mid4) + expect(applied).not.toBe(builder) + expect(applied).toBeInstanceOf(RouterBuilder) + expect(applied['~orpc'].middlewares).toEqual([mid1, mid2, mid3, mid4]) + }) +}) - expect((await (router.lazy[LAZY_LOADER_SYMBOL]())).default.zz$p.middlewares).toEqual([mid1, mid2]) - expect((await (router.lazyRouter.lazy[LAZY_LOADER_SYMBOL]())).default.zz$p.middlewares).toEqual([mid1, mid2]) - expect((await (router.lazyRouter.lazyRouter.lazy[LAZY_LOADER_SYMBOL]())).default.zz$p.middlewares).toEqual([mid1, mid2]) +describe('to AdaptedRouter', () => { + const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) + const ping = {} as Procedure<{ auth: boolean }, { db: string }, typeof schema, typeof schema, { val: string }> + const pong = {} as Procedure + + const wrongPing = {} as Procedure<{ auth: 'invalid' }, undefined, undefined, undefined, unknown> + + it('router without lazy', () => { + expectTypeOf(builder.router({ ping, pong, nested: { ping, pong } })).toEqualTypeOf< + AdaptedRouter< + { auth: boolean }, + { + ping: typeof ping + pong: typeof pong + nested: { ping: typeof ping, pong: typeof pong } + } + > + >() + + builder.router({ ping }) + // @ts-expect-error - context is not match + builder.router({ wrongPing }) }) - it('decorate items', () => { - const ping = new Procedure({ - contract: new ContractProcedure({ - InputSchema: undefined, - OutputSchema: undefined, - }), - func: () => { }, - }) - - const decorated = decorateProcedure({ - zz$p: { - contract: new ContractProcedure({ - InputSchema: undefined, - OutputSchema: undefined, - }), - func: () => { }, - }, - }) + it('router with lazy', () => { + expectTypeOf(builder.router({ + ping: createLazy(() => Promise.resolve({ default: ping })), + pong, + nested: createLazy(() => Promise.resolve({ + default: { + ping, + pong: createLazy(() => Promise.resolve({ default: pong })), + }, + })), + })).toEqualTypeOf< + AdaptedRouter< + { auth: boolean }, + { + ping: Lazy + pong: typeof pong + nested: Lazy<{ ping: typeof ping, pong: Lazy }> + } + > + >() + + builder.router({ ping: createLazy(() => Promise.resolve({ default: ping })) }) + // @ts-expect-error - context is not match + builder.router({ wrongPing: createLazy(() => Promise.resolve({ default: wrongPing })) }) + }) +}) - const lazy = createLazy(() => Promise.resolve({ default: ping })) +describe('to DecoratedLazy', () => { + const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) + const ping = {} as Procedure<{ auth: boolean }, { db: string }, typeof schema, typeof schema, { val: string }> + const pong = {} as Procedure - const router = builder.router({ ping, nested: { ping }, lazy }) + const wrongPing = {} as Procedure<{ auth: 'invalid' }, undefined, undefined, undefined, unknown> - expectTypeOf(router).toEqualTypeOf<{ - ping: typeof decorated - nested: { ping: typeof decorated } - lazy: DecoratedLazy - }>() + it('router without lazy', () => { + expectTypeOf(builder.lazy(() => Promise.resolve({ + default: { + ping, + pong, + nested: { + ping, + pong, + }, + }, + }))).toEqualTypeOf< + DecoratedLazy<{ + ping: typeof ping + pong: typeof pong + nested: { + ping: typeof ping + pong: typeof pong + } + }> + >() + + builder.lazy(() => Promise.resolve({ default: { ping } })) + // @ts-expect-error - context is not match + builder.lazy(() => Promise.resolve({ default: { wrongPing } })) + }) - expect(router.ping).satisfies(isProcedure) - expect(router.nested.ping).satisfies(isProcedure) - expect(router.lazy).satisfies(isLazy) + it('router with lazy', () => { + expectTypeOf(builder.lazy(() => Promise.resolve({ + default: { + ping: createLazy(() => Promise.resolve({ default: ping })), + pong, + nested: createLazy(() => Promise.resolve({ + default: { + ping, + pong: createLazy(() => Promise.resolve({ default: pong })), + }, + })), + }, + }))).toEqualTypeOf< + DecoratedLazy<{ + ping: DecoratedLazy + pong: typeof pong + nested: { + ping: typeof ping + pong: DecoratedLazy + } + }> + >() + + builder.lazy(() => Promise.resolve({ default: { ping: createLazy(() => Promise.resolve({ default: ping })) } })) + // @ts-expect-error - context is not match + builder.lazy(() => Promise.resolve({ default: { wrongPing: createLazy(() => Promise.resolve({ default: wrongPing })) } })) }) }) From ee127d5447b17fd3f912bec6e34bf047e446da3d Mon Sep 17 00:00:00 2001 From: unnoq Date: Mon, 16 Dec 2024 15:29:38 +0700 Subject: [PATCH 15/51] lazy --- packages/server/src/lazy.test-d.ts | 151 ++++++++--------------------- packages/server/src/lazy.test.ts | 143 +++++++-------------------- packages/server/src/lazy.ts | 57 ++--------- 3 files changed, 84 insertions(+), 267 deletions(-) diff --git a/packages/server/src/lazy.test-d.ts b/packages/server/src/lazy.test-d.ts index 8899341da..32fc7be35 100644 --- a/packages/server/src/lazy.test-d.ts +++ b/packages/server/src/lazy.test-d.ts @@ -1,122 +1,53 @@ -import type { ANY_PROCEDURE, Router } from '.' -import type { Lazy } from './lazy' -import { z } from 'zod' -import { os } from '.' -import { createLazy, decorateLazy } from './lazy' +import type { ANY_LAZY, FlattenLazy, Lazy } from './lazy' +import type { Procedure } from './procedure' +import type { WELL_CONTEXT } from './types' +import { flatLazy, isLazy, lazy, unwrapLazy } from './lazy' -const router = { - ping: os.input(z.string()).func(() => 'pong'), - pong: os.func(() => 'ping'), -} -const lazyPing = createLazy(() => Promise.resolve({ default: router.ping })) -const lazyPong = createLazy(() => Promise.resolve({ default: router.pong })) -const lazyRouter = createLazy(() => Promise.resolve({ default: router })) -const nestedLazyRouter = createLazy(() => Promise.resolve({ default: lazyRouter })) -const complexLazyRouter = createLazy(() => Promise.resolve({ - default: { - ...router, - lazyRouter, - nestedLazyRouter, - }, -})) +const procedure = {} as Procedure -describe('DecoratedLazy', () => { - it('with procedure', () => { - const decorated = decorateLazy(lazyPing) +const router = { procedure } - type IsLazyProcedure = typeof decorated extends Lazy ? true : false - expectTypeOf().toEqualTypeOf() +it('lazy', () => { + expectTypeOf( + lazy(() => Promise.resolve({ default: procedure })), + ).toMatchTypeOf>() - expectTypeOf(decorated).toMatchTypeOf< - (input: string) => Promise - >() - - expectTypeOf(decorated('test')).toMatchTypeOf>() - }) - - it('with router', () => { - const decorated = decorateLazy(lazyRouter) - - type IsRouter = typeof decorated extends Router ? true : false - expectTypeOf().toEqualTypeOf() - - expectTypeOf(decorated).toMatchTypeOf<{ - ping: (input: string) => Promise - pong: () => Promise - }>() - - expectTypeOf(decorated.ping).toMatchTypeOf<(input: string) => Promise>() - expectTypeOf(decorated.ping('test')).toMatchTypeOf>() - - expectTypeOf(decorated.pong).toMatchTypeOf<() => Promise>() - expectTypeOf(decorated.pong()).toMatchTypeOf>() - }) - - it('with nested router', () => { - const decorated = decorateLazy(nestedLazyRouter) - - type IsRouter = typeof decorated extends Router ? true : false - expectTypeOf().toEqualTypeOf() - - expectTypeOf(decorated).toMatchTypeOf<{ - ping: (input: string) => Promise - pong: () => Promise - }>() - - expectTypeOf(decorated.ping).toMatchTypeOf<(input: string) => Promise>() - expectTypeOf(decorated.ping('test')).toMatchTypeOf>() - - expectTypeOf(decorated.pong).toMatchTypeOf<() => Promise>() - expectTypeOf(decorated.pong()).toMatchTypeOf>() - }) - - it('with complex router', () => { - const decorated = decorateLazy(complexLazyRouter) - - type IsRouter = typeof decorated extends Router ? true : false - expectTypeOf().toEqualTypeOf() - - expectTypeOf(decorated).toMatchTypeOf<{ - ping: (input: string) => Promise - pong: () => Promise - }>() - - expectTypeOf(decorated.nestedLazyRouter).toMatchTypeOf<{ - ping: (input: string) => Promise - pong: () => Promise - }>() - - expectTypeOf(decorated.nestedLazyRouter).toMatchTypeOf<{ - ping: (input: string) => Promise - pong: () => Promise - }>() - - expectTypeOf(decorated.ping).toMatchTypeOf<(input: string) => Promise>() - expectTypeOf(decorated.ping('test')).toMatchTypeOf>() + expectTypeOf( + lazy(() => Promise.resolve({ default: router })), + ).toMatchTypeOf>() +}) - expectTypeOf(decorated.pong).toMatchTypeOf<() => Promise>() - expectTypeOf(decorated.pong()).toMatchTypeOf>() +it('isLazy', () => { + const item = {} as unknown - expectTypeOf(decorated.lazyRouter).toMatchTypeOf<{ - ping: (input: string) => Promise - pong: () => Promise - }>() + if (isLazy(item)) { + expectTypeOf(item).toEqualTypeOf() + } +}) - expectTypeOf(decorated.lazyRouter.ping).toMatchTypeOf<(input: string) => Promise>() - expectTypeOf(decorated.lazyRouter.ping('test')).toMatchTypeOf>() +it('unwrapLazy', () => { + expectTypeOf( + unwrapLazy(lazy(() => Promise.resolve({ default: procedure }))), + ).toMatchTypeOf>() - expectTypeOf(decorated.lazyRouter.pong).toMatchTypeOf<() => Promise>() - expectTypeOf(decorated.lazyRouter.pong()).toMatchTypeOf>() + expectTypeOf( + unwrapLazy(lazy(() => Promise.resolve({ default: router }))), + ).toMatchTypeOf>() +}) - expectTypeOf(decorated.nestedLazyRouter).toMatchTypeOf<{ - ping: (input: string) => Promise - pong: () => Promise - }>() +it('FlattenLazy', () => { + expectTypeOf>>>().toMatchTypeOf>() + expectTypeOf < FlattenLazy>>>>().toMatchTypeOf>() +}) - expectTypeOf(decorated.nestedLazyRouter.ping).toMatchTypeOf<(input: string) => Promise>() - expectTypeOf(decorated.nestedLazyRouter.ping('test')).toMatchTypeOf>() +it('flatLazy', () => { + expectTypeOf( + flatLazy(lazy(() => Promise.resolve({ default: lazy(() => Promise.resolve({ default: procedure })) }))), + ).toMatchTypeOf>() - expectTypeOf(decorated.nestedLazyRouter.pong).toMatchTypeOf<() => Promise>() - expectTypeOf(decorated.nestedLazyRouter.pong()).toMatchTypeOf>() - }) + expectTypeOf( + flatLazy(lazy(() => Promise.resolve({ default: lazy(() => Promise.resolve({ + default: lazy(() => Promise.resolve({ default: router })), + })) }))), + ).toMatchTypeOf>() }) diff --git a/packages/server/src/lazy.test.ts b/packages/server/src/lazy.test.ts index 330dbd536..560938c7e 100644 --- a/packages/server/src/lazy.test.ts +++ b/packages/server/src/lazy.test.ts @@ -1,121 +1,48 @@ -import { describe, expect, it, vi } from 'vitest' -import { z } from 'zod' -import { os } from '.' -import { - createFlattenLazy, - createLazy, - decorateLazy, - isLazy, - LAZY_LOADER_SYMBOL, - loadLazy, -} from './lazy' - -describe('createLazy', () => { - it('should create a lazy object with a loader function', () => { - const mockLoader = vi.fn().mockResolvedValue({ default: 'test' }) - const lazyObj = createLazy(mockLoader) - - expect(lazyObj[LAZY_LOADER_SYMBOL]).toBe(mockLoader) - }) +import type { WELL_CONTEXT } from './types' +import { ContractProcedure } from '@orpc/contract' +import { flatLazy, isLazy, lazy, LAZY_LOADER_SYMBOL, unwrapLazy } from './lazy' +import { Procedure } from './procedure' + +const procedure = new Procedure({ + contract: new ContractProcedure({ + InputSchema: undefined, + OutputSchema: undefined, + }), + func: vi.fn(), + middlewares: [], }) -describe('loadLazy', () => { - it('should call the loader function and return the result', async () => { - const mockLoader = vi.fn().mockResolvedValue({ default: 'loaded value' }) - const lazyObj = createLazy(mockLoader) +const router = { procedure } - const result = await loadLazy(lazyObj) - - expect(mockLoader).toHaveBeenCalledOnce() - expect(result).toEqual({ default: 'loaded value' }) - }) -}) +it('lazy', () => { + const procedureLoader = () => Promise.resolve({ default: procedure }) + const routerLoader = () => Promise.resolve({ default: router }) -describe('isLazy', () => { - it('should return true for a lazy object', () => { - const lazyObj = createLazy(() => Promise.resolve({ default: 'test' })) - expect(isLazy(lazyObj)).toBe(true) - }) + expect(lazy(procedureLoader)).toSatisfy(isLazy) + expect(lazy(routerLoader)).toSatisfy(isLazy) - it('should return false for non-lazy objects', () => { - expect(isLazy(null)).toBe(false) - expect(isLazy(undefined)).toBe(false) - expect(isLazy({})).toBe(false) - expect(isLazy({ someOtherSymbol: () => { } })).toBe(false) - }) + expect(lazy(procedureLoader)[LAZY_LOADER_SYMBOL]).toBe(procedureLoader) + expect(lazy(routerLoader)[LAZY_LOADER_SYMBOL]).toBe(routerLoader) }) -describe('createFlattenLazy', () => { - it('should flatten nested lazy objects', async () => { - const innerMostLoader = vi.fn().mockResolvedValue({ default: 'final value' }) - const innerLoader = vi.fn().mockResolvedValue({ - default: createLazy(innerMostLoader), - }) - const outerLoader = vi.fn().mockResolvedValue({ - default: createLazy(innerLoader), - }) - - const flattenedLazy = createFlattenLazy(createLazy(outerLoader)) - - const result = await loadLazy(flattenedLazy) - - expect(outerLoader).toHaveBeenCalledOnce() - expect(innerLoader).toHaveBeenCalledOnce() - expect(innerMostLoader).toHaveBeenCalledOnce() - expect(result).toEqual({ default: 'final value' }) - }) - - it('should handle single-level lazy objects', async () => { - const loader = vi.fn().mockResolvedValue({ default: 'simple value' }) - const flattenedLazy = createFlattenLazy(createLazy(loader)) - - const result = await loadLazy(flattenedLazy) - - expect(loader).toHaveBeenCalledOnce() - expect(result).toEqual({ default: 'simple value' }) - }) +it('isLazy', () => { + expect(lazy(() => Promise.resolve({ default: procedure }))).toSatisfy(isLazy) + expect(lazy(() => Promise.resolve({ default: router }))).toSatisfy(isLazy) + expect({}).not.toSatisfy(isLazy) + expect(undefined).not.toSatisfy(isLazy) }) -describe('decorateLazy', () => { - const ping = os.input(z.string()).func(() => 'pong') - const pong = os.func(() => 'ping') - - const router = { - ping: createLazy(() => Promise.resolve({ default: ping })), - pong: createLazy(() => Promise.resolve({ default: pong })), - nested: { - ping: createLazy(() => Promise.resolve({ default: ping })), - pong: createLazy(() => Promise.resolve({ default: pong })), - }, - complex: createLazy(() => Promise.resolve({ - default: { - ping, - pong: createLazy(() => Promise.resolve({ default: pong })), - }, - })), - } - - it('should create a proxy for nested lazy loading', async () => { - const decoratedLazy = decorateLazy(createLazy(() => Promise.resolve({ default: router }))) +it('unwrapLazy', async () => { + const lazied = lazy(() => Promise.resolve({ default: 'root' })) - // Test method access - const methodResult = await decoratedLazy.ping('test') - expect(methodResult).toBe('pong') - - // Test nested method access - const nestedResult = await decoratedLazy.nested.pong('test') - expect(nestedResult).toBe('ping') - }) - - it('should create a proxy for complex lazy loading', async () => { - const decoratedLazy = decorateLazy(createLazy(() => Promise.resolve({ default: router }))) + expect(unwrapLazy(lazied)).resolves.toEqual({ default: 'root' }) + expect((await unwrapLazy(lazy(() => Promise.resolve({ default: lazied })))).default).toSatisfy(isLazy) +}) - // Test method access - const methodResult = await decoratedLazy.complex.ping('test') - expect(methodResult).toBe('pong') +it('flatLazy', () => { + const lazied = lazy(() => Promise.resolve({ default: 'root' })) - // Test nested method access - const nestedResult = await decoratedLazy.complex.pong('test') - expect(nestedResult).toBe('ping') - }) + expect(flatLazy(lazied)[LAZY_LOADER_SYMBOL]()).resolves.toEqual({ default: 'root' }) + expect(flatLazy(lazy(() => Promise.resolve({ default: lazied })))[LAZY_LOADER_SYMBOL]()).resolves.toEqual({ default: 'root' }) + expect(flatLazy(lazy(() => Promise.resolve({ default: lazy(() => Promise.resolve({ default: lazied })) })))[LAZY_LOADER_SYMBOL]()).resolves.toEqual({ default: 'root' }) }) diff --git a/packages/server/src/lazy.ts b/packages/server/src/lazy.ts index 460a524a9..c9880ed11 100644 --- a/packages/server/src/lazy.ts +++ b/packages/server/src/lazy.ts @@ -1,7 +1,3 @@ -import type { Procedure } from './procedure' -import type { ProcedureCaller } from './procedure-caller' -import { createProcedureCaller } from './procedure-caller' - export const LAZY_LOADER_SYMBOL: unique symbol = Symbol('ORPC_LAZY_LOADER') export interface Lazy { @@ -10,16 +6,12 @@ export interface Lazy { export type ANY_LAZY = Lazy -export function createLazy(loader: () => Promise<{ default: T }>): Lazy { +export function lazy(loader: () => Promise<{ default: T }>): Lazy { return { [LAZY_LOADER_SYMBOL]: loader, } } -export function loadLazy(lazy: Lazy): Promise<{ default: T }> { - return lazy[LAZY_LOADER_SYMBOL]() -} - export function isLazy(item: unknown): item is ANY_LAZY { return ( (typeof item === 'object' || typeof item === 'function') @@ -29,20 +21,24 @@ export function isLazy(item: unknown): item is ANY_LAZY { ) } +export function unwrapLazy(lazy: Lazy): Promise<{ default: T }> { + return lazy[LAZY_LOADER_SYMBOL]() +} + export type FlattenLazy = T extends Lazy ? FlattenLazy : Lazy -export function createFlattenLazy(lazy: Lazy): FlattenLazy { +export function flatLazy(lazy: Lazy): FlattenLazy { const flattenLoader = async () => { - let current = await loadLazy(lazy) + let current = await unwrapLazy(lazy) while (true) { if (!isLazy(current.default)) { break } - current = await loadLazy(current.default) + current = await unwrapLazy(current.default) } return current @@ -54,40 +50,3 @@ export function createFlattenLazy(lazy: Lazy): FlattenLazy { return flattenLazy as any } - -export type DecoratedLazy = T extends Lazy - ? DecoratedLazy - : ( - T extends Procedure ? Lazy & (undefined extends UContext ? ProcedureCaller : unknown) - : T extends Record - ? { - [K in keyof T]: DecoratedLazy - } /** Notice: this still a lazy, but type not work when I & Lazy, maybe it's a bug, should improve */ - : Lazy - ) - -export function decorateLazy(lazy: Lazy): DecoratedLazy { - const flattenLazy = createFlattenLazy(lazy) - - const procedureCaller = createProcedureCaller({ - procedure: flattenLazy as any, - context: undefined as any, - }) - - Object.assign(procedureCaller, flattenLazy) - - const recursive = new Proxy(procedureCaller, { - get(target, key) { - if (typeof key !== 'string') { - return Reflect.get(target, key) - } - - return decorateLazy(createLazy(async () => { - const current = await loadLazy(flattenLazy) - return { default: (current.default as any)[key] } - })) - }, - }) - - return recursive as any -} From 363ab50dd857a1f4caf9d65bf440536bca892183 Mon Sep 17 00:00:00 2001 From: unnoq Date: Mon, 16 Dec 2024 21:39:56 +0700 Subject: [PATCH 16/51] lazy decorated --- packages/server/src/lazy-decorated.test-d.ts | 111 ++++++++++++++++++ packages/server/src/lazy-decorated.test.ts | 92 +++++++++++++++ packages/server/src/lazy-decorated.ts | 45 +++++++ .../server/src/procedure-caller.test-d.ts | 6 +- packages/server/src/procedure-caller.test.ts | 10 +- packages/server/src/procedure-caller.ts | 12 +- 6 files changed, 259 insertions(+), 17 deletions(-) create mode 100644 packages/server/src/lazy-decorated.test-d.ts create mode 100644 packages/server/src/lazy-decorated.test.ts create mode 100644 packages/server/src/lazy-decorated.ts diff --git a/packages/server/src/lazy-decorated.test-d.ts b/packages/server/src/lazy-decorated.test-d.ts new file mode 100644 index 000000000..db4c0d9c1 --- /dev/null +++ b/packages/server/src/lazy-decorated.test-d.ts @@ -0,0 +1,111 @@ +import type { ANY_PROCEDURE, ANY_ROUTER, Caller, Procedure, WELL_CONTEXT } from '.' +import type { Lazy } from './lazy' +import type { DecoratedLazy } from './lazy-decorated' +import { z } from 'zod' +import { lazy } from './lazy' +import { decorateLazy } from './lazy-decorated' + +const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) + +const ping = {} as Procedure +const pong = {} as Procedure + +const lazyPing = lazy(() => Promise.resolve({ default: ping })) +const lazyPong = lazy(() => Promise.resolve({ default: pong })) + +const router = { + ping, + pong, + nested: { + ping, + pong, + }, +} + +const lazyRouter = lazy(() => Promise.resolve({ + default: { + ping: lazyPing, + pong, + nested: lazy(() => Promise.resolve({ + default: { + ping, + pong: lazyPong, + }, + })), + }, +})) + +describe('DecoratedLazy', () => { + it('with procedure', () => { + const decorated = {} as DecoratedLazy + + expectTypeOf(decorated).toMatchTypeOf>() + + expectTypeOf(decorated).toMatchTypeOf< + Caller + >() + }) + + it('with router', () => { + const decorated = {} as DecoratedLazy + + expectTypeOf(decorated).toMatchTypeOf>() + expectTypeOf({ router: decorated }).toMatchTypeOf() + + expectTypeOf(decorated.ping).toMatchTypeOf>() + expectTypeOf(decorated.ping).toMatchTypeOf >() + + expectTypeOf(decorated.pong).toMatchTypeOf>() + expectTypeOf(decorated.pong).toMatchTypeOf>() + + expectTypeOf(decorated.nested).toMatchTypeOf>() + expectTypeOf({ router: decorated.nested }).toMatchTypeOf() + + expectTypeOf(decorated.nested.ping).toMatchTypeOf>() + expectTypeOf(decorated.nested.ping).toMatchTypeOf>() + + expectTypeOf(decorated.nested.pong).toMatchTypeOf>() + expectTypeOf(decorated.nested.pong).toMatchTypeOf>() + }) + + it('flat lazy', () => { + expectTypeOf>().toEqualTypeOf>() + expectTypeOf>().toEqualTypeOf>() + expectTypeOf>>().toEqualTypeOf>() + + expectTypeOf['ping']>().toEqualTypeOf['ping']>() + expectTypeOf['pong']>().toEqualTypeOf['pong']>() + expectTypeOf['nested']['ping']>().toEqualTypeOf['nested']['ping']>() + expectTypeOf['nested']['pong']>().toEqualTypeOf['nested']['pong']>() + + // @ts-expect-error - lazy loader is diff + expectTypeOf['nested']>().toEqualTypeOf['nested']>() + }) + + it('not callable when context is required', () => { + const d1 = {} as DecoratedLazy + const d2 = {} as DecoratedLazy + const d3 = {} as DecoratedLazy> + const d4 = {} as DecoratedLazy> + + d1() + d3() + + // @ts-expect-error --- cannot call on router level + d2() + // @ts-expect-error --- context is required + d4() + }) +}) + +it('decorateLazy', () => { + expectTypeOf(decorateLazy(lazyPing)).toEqualTypeOf>() + expectTypeOf(decorateLazy(lazyPong)).toEqualTypeOf>() + expectTypeOf(decorateLazy(lazy(() => Promise.resolve({ default: router })))).toEqualTypeOf>() + + // @ts-expect-error - invalid lazy + decorateLazy(ping) + + // @ts-expect-error - invalid lazy + decorateLazy(router) +}) diff --git a/packages/server/src/lazy-decorated.test.ts b/packages/server/src/lazy-decorated.test.ts new file mode 100644 index 000000000..68673f89c --- /dev/null +++ b/packages/server/src/lazy-decorated.test.ts @@ -0,0 +1,92 @@ +import { ContractProcedure } from '@orpc/contract' +import { z } from 'zod' +import { isLazy, lazy, unwrapLazy } from './lazy' +import { decorateLazy } from './lazy-decorated' +import { Procedure } from './procedure' +import { createProcedureCaller } from './procedure-caller' + +vi.mock('./procedure-caller', () => ({ + createProcedureCaller: vi.fn(() => vi.fn()), +})) + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('decorated lazy', () => { + const schema = z.object({ val: z.string().transform(val => Number(val)) }) + + const ping = new Procedure({ + contract: new ContractProcedure({ + InputSchema: schema, + OutputSchema: undefined, + }), + func: vi.fn(), + middlewares: [], + }) + + const lazyPing = lazy(() => Promise.resolve({ default: ping })) + + it('still a lazy', async () => { + expect(decorateLazy(lazyPing)).toSatisfy(isLazy) + + expect((await unwrapLazy(decorateLazy(lazyPing))).default).toBe(ping) + + const l2 = lazy(() => Promise.resolve({ default: { ping } })) + expect(decorateLazy(l2)).toSatisfy(isLazy) + expect((await unwrapLazy(decorateLazy(l2))).default.ping).toBe(ping) + + const l3 = lazy(() => Promise.resolve({ default: { ping: lazyPing } })) + expect(decorateLazy(l3)).toSatisfy(isLazy) + expect((await unwrapLazy(decorateLazy(l3))).default.ping).toBe(lazyPing) + }) + + describe('callable', () => { + const nested = { ping: lazyPing } + const router = { nested } + /** decorated lazy is recursive proxy no does need to care what is the original on logic test, (typed will do it) */ + const lazied = lazy(() => Promise.resolve({ default: lazy(() => Promise.resolve({ default: router })) })) + + const controller = new AbortController() + const signal = controller.signal + + const caller = vi.fn(() => '__mocked__') + vi.mocked(createProcedureCaller).mockReturnValue(caller as any) + + it('on root', async () => { + const decorated = decorateLazy(lazied) as any + expect(decorated).toBeInstanceOf(Function) + + expect(createProcedureCaller).toHaveBeenCalledTimes(1) + expect(createProcedureCaller).toHaveBeenCalledWith({ + procedure: expect.any(Object), + context: undefined, + }) + expect(vi.mocked(createProcedureCaller).mock.calls[0]![0].procedure).toSatisfy(isLazy) + const unwrapped = await unwrapLazy(vi.mocked(createProcedureCaller).mock.calls[0]![0].procedure as any) + expect(unwrapped.default).toBe(router) + + expect(await decorated({ val: '1' }, { signal })).toBe('__mocked__') + expect(caller).toHaveBeenCalledTimes(1) + expect(caller).toHaveBeenCalledWith({ val: '1' }, { signal }) + }) + + it('on nested', async () => { + const decorated = decorateLazy(lazied).nested.ping as any + expect(decorated).toBeInstanceOf(Function) + + expect(createProcedureCaller).toHaveBeenCalledTimes(3) + expect(createProcedureCaller).toHaveBeenNthCalledWith(3, { + procedure: expect.any(Object), + context: undefined, + }) + expect(vi.mocked(createProcedureCaller).mock.calls[2]![0].procedure).toSatisfy(isLazy) + const unwrapped = await unwrapLazy(vi.mocked(createProcedureCaller).mock.calls[2]![0].procedure as any) + expect(unwrapped.default).toBe(ping) + + expect(await decorated({ val: '1' }, { signal })).toBe('__mocked__') + expect(caller).toHaveBeenCalledTimes(1) + expect(caller).toHaveBeenCalledWith({ val: '1' }, { signal }) + }) + }) +}) diff --git a/packages/server/src/lazy-decorated.ts b/packages/server/src/lazy-decorated.ts new file mode 100644 index 000000000..3a27cd00d --- /dev/null +++ b/packages/server/src/lazy-decorated.ts @@ -0,0 +1,45 @@ +import type { SchemaInput, SchemaOutput } from '@orpc/contract' +import type { AnyFunction } from '@orpc/shared' +import type { Procedure } from './procedure' +import type { Caller } from './types' +import { flatLazy, lazy, type Lazy, unwrapLazy } from './lazy' +import { createProcedureCaller } from './procedure-caller' + +export type DecoratedLazy = T extends Lazy + ? DecoratedLazy + : ( + T extends Procedure ? + & Lazy + & (undefined extends UContext ? Caller, SchemaOutput> : unknown) + : T extends AnyFunction ? Lazy + : T extends object ? { + [K in keyof T & string]: DecoratedLazy + } & Lazy + : Lazy + ) + +export function decorateLazy(lazied: Lazy): DecoratedLazy { + const flattenLazy = flatLazy(lazied) + + const procedureCaller = createProcedureCaller({ + procedure: flattenLazy as any, + context: undefined as any, + }) + + Object.assign(procedureCaller, flattenLazy) + + const recursive = new Proxy(procedureCaller, { + get(target, key) { + if (typeof key !== 'string') { + return Reflect.get(target, key) + } + + return decorateLazy(lazy(async () => { + const current = await unwrapLazy(flattenLazy) + return { default: (current.default as any)[key] } + })) + }, + }) + + return recursive as any +} diff --git a/packages/server/src/procedure-caller.test-d.ts b/packages/server/src/procedure-caller.test-d.ts index 41798d626..9f8428c77 100644 --- a/packages/server/src/procedure-caller.test-d.ts +++ b/packages/server/src/procedure-caller.test-d.ts @@ -1,7 +1,7 @@ import type { Procedure } from './procedure' import type { Caller, Meta, WELL_CONTEXT } from './types' import { z } from 'zod' -import { createLazy } from './lazy' +import { lazy } from './lazy' import { createProcedureCaller } from './procedure-caller' beforeEach(() => { @@ -125,10 +125,10 @@ describe('createProcedureCaller', () => { it('support lazy procedure', () => { const schema = z.object({ val: z.string().transform(v => Number(v)) }) const procedure = {} as Procedure<{ userId?: string }, undefined, typeof schema, typeof schema, { val: string }> - const lazy = createLazy(() => Promise.resolve({ default: procedure })) + const lazied = lazy(() => Promise.resolve({ default: procedure })) const caller = createProcedureCaller({ - procedure: lazy, + procedure: lazied, context: async () => ({ userId: 'string' }), path: ['users'], diff --git a/packages/server/src/procedure-caller.test.ts b/packages/server/src/procedure-caller.test.ts index b72ed80db..acd50ae06 100644 --- a/packages/server/src/procedure-caller.test.ts +++ b/packages/server/src/procedure-caller.test.ts @@ -1,7 +1,7 @@ import type { WELL_CONTEXT } from './types' import { ContractProcedure } from '@orpc/contract' import { z } from 'zod' -import { createLazy, isLazy, loadLazy } from './lazy' +import { isLazy, lazy, unwrapLazy } from './lazy' import { Procedure } from './procedure' import { createProcedureCaller } from './procedure-caller' @@ -22,7 +22,7 @@ const procedure = new Procedure Promise.resolve({ default: procedure }))], + ['with lazy', lazy(() => Promise.resolve({ default: procedure }))], ] as const beforeEach(() => { @@ -30,7 +30,7 @@ beforeEach(() => { }) describe.each(procedureCases)('createProcedureCaller - case %s', async (_, procedure) => { - const unwrappedProcedure = isLazy(procedure) ? (await loadLazy(procedure)).default : procedure + const unwrappedProcedure = isLazy(procedure) ? (await unwrapLazy(procedure)).default : procedure it('just a caller', async () => { const caller = createProcedureCaller({ @@ -296,11 +296,11 @@ describe.each(procedureCases)('createProcedureCaller - case %s', async (_, proce }) it('should throw error when invalid lazy procedure', () => { - const lazy = createLazy(() => Promise.resolve({ default: 123 })) + const lazied = lazy(() => Promise.resolve({ default: 123 })) const caller = createProcedureCaller({ // @ts-expect-error - invalid lazy procedure - procedure: lazy, + procedure: lazied, }) expect(caller()).rejects.toThrow('Not found') diff --git a/packages/server/src/procedure-caller.ts b/packages/server/src/procedure-caller.ts index 13adb7cc4..7bfde62ce 100644 --- a/packages/server/src/procedure-caller.ts +++ b/packages/server/src/procedure-caller.ts @@ -12,16 +12,10 @@ import type { Caller, Context, Meta, WELL_CONTEXT } from './types' import { executeWithHooks, trim, value } from '@orpc/shared' import { ORPCError } from '@orpc/shared/error' -import { isLazy, loadLazy } from './lazy' +import { isLazy, unwrapLazy } from './lazy' import { isProcedure } from './procedure' import { mergeContext } from './utils' -export type ProcedureCaller< - TInputSchema extends Schema, - TOutputSchema extends Schema, - TFuncOutput extends SchemaInput, -> = Caller, SchemaOutput> - /** * Options for creating a procedure caller with comprehensive type safety */ @@ -58,7 +52,7 @@ export function createProcedureCaller< TFuncOutput extends SchemaInput = SchemaInput, >( options: CreateProcedureCallerOptions, -): ProcedureCaller { +): Caller, SchemaOutput> { return async (...[input, callerOptions]) => { const path = options.path ?? [] const procedure = await loadProcedure(options.procedure) as WELL_PROCEDURE @@ -163,7 +157,7 @@ async function executeMiddlewareChain( export async function loadProcedure(procedure: ANY_PROCEDURE | ANY_LAZY_PROCEDURE): Promise { const loadedProcedure = isLazy(procedure) - ? (await loadLazy(procedure)).default + ? (await unwrapLazy(procedure)).default : procedure if (!isProcedure(loadedProcedure)) { From 1d229637e84b06337cb52c0076143d688764d007 Mon Sep 17 00:00:00 2001 From: unnoq Date: Tue, 17 Dec 2024 08:23:52 +0700 Subject: [PATCH 17/51] improve --- packages/server/src/lazy-decorated.test-d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/src/lazy-decorated.test-d.ts b/packages/server/src/lazy-decorated.test-d.ts index db4c0d9c1..03c58e5dc 100644 --- a/packages/server/src/lazy-decorated.test-d.ts +++ b/packages/server/src/lazy-decorated.test-d.ts @@ -1,4 +1,4 @@ -import type { ANY_PROCEDURE, ANY_ROUTER, Caller, Procedure, WELL_CONTEXT } from '.' +import type { ANY_PROCEDURE, ANY_ROUTER, Caller, DecoratedProcedure, Procedure, WELL_CONTEXT } from '.' import type { Lazy } from './lazy' import type { DecoratedLazy } from './lazy-decorated' import { z } from 'zod' @@ -8,7 +8,7 @@ import { decorateLazy } from './lazy-decorated' const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) const ping = {} as Procedure -const pong = {} as Procedure +const pong = {} as DecoratedProcedure const lazyPing = lazy(() => Promise.resolve({ default: ping })) const lazyPong = lazy(() => Promise.resolve({ default: pong })) From c6cd3e309ee6b93335a52164287a935fedfe14a8 Mon Sep 17 00:00:00 2001 From: unnoq Date: Tue, 17 Dec 2024 09:05:27 +0700 Subject: [PATCH 18/51] sync --- packages/server/src/procedure-decorated.ts | 5 +- packages/server/src/router-builder.test-d.ts | 97 +++++++-------- packages/server/src/router-builder.test.ts | 119 ------------------- packages/server/src/router-builder.ts | 41 +++++-- packages/server/src/router-implementer.ts | 19 ++- packages/server/src/router.test-d.ts | 62 +++++----- 6 files changed, 115 insertions(+), 228 deletions(-) diff --git a/packages/server/src/procedure-decorated.ts b/packages/server/src/procedure-decorated.ts index d841244f1..2d560f975 100644 --- a/packages/server/src/procedure-decorated.ts +++ b/packages/server/src/procedure-decorated.ts @@ -1,7 +1,6 @@ import type { HTTPPath, RouteOptions, Schema, SchemaInput, SchemaOutput } from '@orpc/contract' import type { ANY_MIDDLEWARE, MapInputMiddleware, Middleware } from './middleware' -import type { ProcedureCaller } from './procedure-caller' -import type { Context, MergeContext } from './types' +import type { Caller, Context, MergeContext } from './types' import { DecoratedContractProcedure } from '@orpc/contract' import { decorateMiddleware } from './middleware' import { Procedure } from './procedure' @@ -72,7 +71,7 @@ export type DecoratedProcedure< ) => DecoratedProcedure } - & (undefined extends TContext ? ProcedureCaller : unknown) + & (undefined extends TContext ? Caller, SchemaOutput> : unknown) export function decorateProcedure< TContext extends Context, diff --git a/packages/server/src/router-builder.test-d.ts b/packages/server/src/router-builder.test-d.ts index 16848ae05..dc66d2cbc 100644 --- a/packages/server/src/router-builder.test-d.ts +++ b/packages/server/src/router-builder.test-d.ts @@ -1,11 +1,12 @@ -import type { DecoratedLazy, Lazy } from './lazy' +import type { Lazy } from './lazy' +import type { DecoratedLazy } from './lazy-decorated' import type { Middleware } from './middleware' import type { Procedure } from './procedure' import type { DecoratedProcedure } from './procedure-decorated' import type { AdaptedRouter, RouterBuilder } from './router-builder' import type { WELL_CONTEXT } from './types' import { z } from 'zod' -import { createLazy } from './lazy' +import { lazy } from './lazy' const builder = {} as RouterBuilder<{ auth: boolean }, { db: string }> @@ -41,24 +42,30 @@ describe('AdaptedRouter', () => { it('with lazy', () => { const router = { - ping: createLazy(() => Promise.resolve({ default: ping })), + ping: lazy(() => Promise.resolve({ default: ping })), pong, - nested: createLazy(() => Promise.resolve({ + nested: lazy(() => Promise.resolve({ default: { ping, - pong: createLazy(() => Promise.resolve({ default: pong })), + pong: lazy(() => Promise.resolve({ default: pong })), }, })), } - const adapted = {} as AdaptedRouter<{ log: true }, typeof router> + const adapted = {} as AdaptedRouter<{ log: true } | undefined, typeof router> - expectTypeOf(adapted.ping).toEqualTypeOf>() + expectTypeOf(adapted.ping).toEqualTypeOf + >>() expectTypeOf(adapted.pong).toEqualTypeOf< - DecoratedProcedure<{ log: true } & WELL_CONTEXT, undefined, undefined, undefined, unknown> + DecoratedProcedure<({ log: true } | undefined) & WELL_CONTEXT, undefined, undefined, undefined, unknown> >() - expectTypeOf(adapted.nested.ping).toEqualTypeOf>() - expectTypeOf(adapted.nested.pong).toEqualTypeOf>() + expectTypeOf(adapted.nested.ping).toEqualTypeOf + >>() + expectTypeOf(adapted.nested.pong).toEqualTypeOf + >>() }) }) @@ -138,12 +145,12 @@ describe('to AdaptedRouter', () => { it('router with lazy', () => { expectTypeOf(builder.router({ - ping: createLazy(() => Promise.resolve({ default: ping })), + ping: lazy(() => Promise.resolve({ default: ping })), pong, - nested: createLazy(() => Promise.resolve({ + nested: lazy(() => Promise.resolve({ default: { ping, - pong: createLazy(() => Promise.resolve({ default: pong })), + pong: lazy(() => Promise.resolve({ default: pong })), }, })), })).toEqualTypeOf< @@ -157,9 +164,9 @@ describe('to AdaptedRouter', () => { > >() - builder.router({ ping: createLazy(() => Promise.resolve({ default: ping })) }) + builder.router({ ping: lazy(() => Promise.resolve({ default: ping })) }) // @ts-expect-error - context is not match - builder.router({ wrongPing: createLazy(() => Promise.resolve({ default: wrongPing })) }) + builder.router({ wrongPing: lazy(() => Promise.resolve({ default: wrongPing })) }) }) }) @@ -171,24 +178,17 @@ describe('to DecoratedLazy', () => { const wrongPing = {} as Procedure<{ auth: 'invalid' }, undefined, undefined, undefined, unknown> it('router without lazy', () => { - expectTypeOf(builder.lazy(() => Promise.resolve({ - default: { + const router = { + ping, + pong, + nested: { ping, pong, - nested: { - ping, - pong, - }, }, - }))).toEqualTypeOf< - DecoratedLazy<{ - ping: typeof ping - pong: typeof pong - nested: { - ping: typeof ping - pong: typeof pong - } - }> + } + + expectTypeOf(builder.lazy(() => Promise.resolve({ default: router }))).toEqualTypeOf< + DecoratedLazy> >() builder.lazy(() => Promise.resolve({ default: { ping } })) @@ -197,30 +197,23 @@ describe('to DecoratedLazy', () => { }) it('router with lazy', () => { - expectTypeOf(builder.lazy(() => Promise.resolve({ - default: { - ping: createLazy(() => Promise.resolve({ default: ping })), - pong, - nested: createLazy(() => Promise.resolve({ - default: { - ping, - pong: createLazy(() => Promise.resolve({ default: pong })), - }, - })), - }, - }))).toEqualTypeOf< - DecoratedLazy<{ - ping: DecoratedLazy - pong: typeof pong - nested: { - ping: typeof ping - pong: DecoratedLazy - } - }> + const router = { + ping: lazy(() => Promise.resolve({ default: ping })), + pong, + nested: lazy(() => Promise.resolve({ + default: { + ping, + pong: lazy(() => Promise.resolve({ default: pong })), + }, + })), + } + + expectTypeOf(builder.lazy(() => Promise.resolve({ default: router }))).toEqualTypeOf< + DecoratedLazy> >() - builder.lazy(() => Promise.resolve({ default: { ping: createLazy(() => Promise.resolve({ default: ping })) } })) + builder.lazy(() => Promise.resolve({ default: { ping: lazy(() => Promise.resolve({ default: ping })) } })) // @ts-expect-error - context is not match - builder.lazy(() => Promise.resolve({ default: { wrongPing: createLazy(() => Promise.resolve({ default: wrongPing })) } })) + builder.lazy(() => Promise.resolve({ default: { wrongPing: lazy(() => Promise.resolve({ default: wrongPing })) } })) }) }) diff --git a/packages/server/src/router-builder.test.ts b/packages/server/src/router-builder.test.ts index f086ca808..184c24489 100644 --- a/packages/server/src/router-builder.test.ts +++ b/packages/server/src/router-builder.test.ts @@ -1,9 +1,3 @@ -import type { DecoratedLazy, Lazy } from './lazy' -import type { Procedure } from './procedure' -import type { AdaptedRouter } from './router-builder' -import type { WELL_CONTEXT } from './types' -import { z } from 'zod' -import { createLazy } from './lazy' import { RouterBuilder } from './router-builder' const mid1 = vi.fn() @@ -40,116 +34,3 @@ describe('self chainable', () => { expect(applied['~orpc'].middlewares).toEqual([mid1, mid2, mid3, mid4]) }) }) - -describe('to AdaptedRouter', () => { - const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) - const ping = {} as Procedure<{ auth: boolean }, { db: string }, typeof schema, typeof schema, { val: string }> - const pong = {} as Procedure - - const wrongPing = {} as Procedure<{ auth: 'invalid' }, undefined, undefined, undefined, unknown> - - it('router without lazy', () => { - expectTypeOf(builder.router({ ping, pong, nested: { ping, pong } })).toEqualTypeOf< - AdaptedRouter< - { auth: boolean }, - { - ping: typeof ping - pong: typeof pong - nested: { ping: typeof ping, pong: typeof pong } - } - > - >() - - builder.router({ ping }) - // @ts-expect-error - context is not match - builder.router({ wrongPing }) - }) - - it('router with lazy', () => { - expectTypeOf(builder.router({ - ping: createLazy(() => Promise.resolve({ default: ping })), - pong, - nested: createLazy(() => Promise.resolve({ - default: { - ping, - pong: createLazy(() => Promise.resolve({ default: pong })), - }, - })), - })).toEqualTypeOf< - AdaptedRouter< - { auth: boolean }, - { - ping: Lazy - pong: typeof pong - nested: Lazy<{ ping: typeof ping, pong: Lazy }> - } - > - >() - - builder.router({ ping: createLazy(() => Promise.resolve({ default: ping })) }) - // @ts-expect-error - context is not match - builder.router({ wrongPing: createLazy(() => Promise.resolve({ default: wrongPing })) }) - }) -}) - -describe('to DecoratedLazy', () => { - const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) - const ping = {} as Procedure<{ auth: boolean }, { db: string }, typeof schema, typeof schema, { val: string }> - const pong = {} as Procedure - - const wrongPing = {} as Procedure<{ auth: 'invalid' }, undefined, undefined, undefined, unknown> - - it('router without lazy', () => { - expectTypeOf(builder.lazy(() => Promise.resolve({ - default: { - ping, - pong, - nested: { - ping, - pong, - }, - }, - }))).toEqualTypeOf< - DecoratedLazy<{ - ping: typeof ping - pong: typeof pong - nested: { - ping: typeof ping - pong: typeof pong - } - }> - >() - - builder.lazy(() => Promise.resolve({ default: { ping } })) - // @ts-expect-error - context is not match - builder.lazy(() => Promise.resolve({ default: { wrongPing } })) - }) - - it('router with lazy', () => { - expectTypeOf(builder.lazy(() => Promise.resolve({ - default: { - ping: createLazy(() => Promise.resolve({ default: ping })), - pong, - nested: createLazy(() => Promise.resolve({ - default: { - ping, - pong: createLazy(() => Promise.resolve({ default: pong })), - }, - })), - }, - }))).toEqualTypeOf< - DecoratedLazy<{ - ping: DecoratedLazy - pong: typeof pong - nested: { - ping: typeof ping - pong: DecoratedLazy - } - }> - >() - - builder.lazy(() => Promise.resolve({ default: { ping: createLazy(() => Promise.resolve({ default: ping })) } })) - // @ts-expect-error - context is not match - builder.lazy(() => Promise.resolve({ default: { wrongPing: createLazy(() => Promise.resolve({ default: wrongPing })) } })) - }) -}) diff --git a/packages/server/src/router-builder.ts b/packages/server/src/router-builder.ts index ea358fe5a..0a77676c2 100644 --- a/packages/server/src/router-builder.ts +++ b/packages/server/src/router-builder.ts @@ -1,11 +1,12 @@ -import type { ANY_LAZY, DecoratedLazy, Lazy } from './lazy' +import type { Lazy } from './lazy' +import type { DecoratedLazy } from './lazy-decorated' import type { Middleware } from './middleware' import type { ANY_LAZY_PROCEDURE, ANY_PROCEDURE, Procedure } from './procedure' import type { DecoratedProcedure } from './procedure-decorated' import type { ANY_ROUTER, Router } from './router' import type { Context, MergeContext } from './types' import { DecoratedContractProcedure, type HTTPPath } from '@orpc/contract' -import { createLazy, decorateLazy, isLazy, loadLazy } from './lazy' +import { isLazy, lazy, unwrapLazy } from './lazy' import { isProcedure } from './procedure' export type AdaptedRouter< @@ -26,8 +27,24 @@ export type AdaptedRouter< UOutputSchema, UFuncOutput > - : TRouter[K] extends ANY_LAZY - ? DecoratedLazy // TODO: pass TContext here + : TRouter[K] extends Lazy + ? U extends Procedure< + infer UContext, + infer UExtraContext, + infer UInputSchema, + infer UOutputSchema, + infer UFuncOutput + > + ? DecoratedLazy> + : U extends ANY_ROUTER + ? DecoratedLazy> + : never : TRouter[K] extends ANY_ROUTER ? AdaptedRouter : never @@ -102,15 +119,15 @@ export class RouterBuilder< lazy>( loader: () => Promise<{ default: U }>, - ): DecoratedLazy { - const lazy = adaptLazyRouter({ - current: createLazy(loader), + ): DecoratedLazy> { + const lazied = adaptLazyRouter({ + current: lazy(loader), middlewares: this.zz$rb.middlewares, tags: this.zz$rb.tags, prefix: this.zz$rb.prefix, }) - return lazy as any + return lazied as any } } @@ -153,7 +170,7 @@ function adaptLazyRouter(options: { prefix?: HTTPPath }): DecoratedLazy>> { const loader = async (): Promise<{ default: unknown }> => { - const current = (await loadLazy(options.current)).default + const current = (await unwrapLazy(options.current)).default return { default: adaptRouter({ @@ -169,7 +186,7 @@ function adaptLazyRouter(options: { lazyRouterPrefix = `${options.current[LAZY_ROUTER_PREFIX_SYMBOL]}${lazyRouterPrefix ?? ''}` as HTTPPath } - const decoratedLazy = Object.assign(decorateLazy(createLazy(loader)), { + const decoratedLazy = Object.assign(decorateLazy(lazy(loader)), { [LAZY_ROUTER_PREFIX_SYMBOL]: lazyRouterPrefix, }) @@ -181,8 +198,8 @@ function adaptLazyRouter(options: { return adaptLazyRouter({ ...options, - current: createLazy(async () => { - const current = (await loadLazy(options.current)).default + current: lazy(async () => { + const current = (await unwrapLazy(options.current)).default return { default: current[key] } }), }) diff --git a/packages/server/src/router-implementer.ts b/packages/server/src/router-implementer.ts index 93d509ebb..0ea48df31 100644 --- a/packages/server/src/router-implementer.ts +++ b/packages/server/src/router-implementer.ts @@ -1,9 +1,9 @@ -import type { DecoratedLazy } from './lazy' +import type { DecoratedLazy } from './lazy-decorated' import type { Middleware } from './middleware' -import type { HandledRouter, RouterWithContract } from './router' +import type { Router } from './router' +import type { AdaptedRouter } from './router-builder' import type { Context } from './types' import { type ContractProcedure, type ContractRouter, isContractProcedure } from '@orpc/contract' -import { createLazy, decorateLazy } from './lazy' import { ProcedureImplementer } from './procedure-implementer' import { RouterBuilder } from './router-builder' @@ -19,21 +19,18 @@ export class RouterImplementer< }, ) {} - router>( + router>( router: U, - ): HandledRouter { + ): AdaptedRouter { return Object.assign(new RouterBuilder({}).router(router), { [ROUTER_CONTRACT_SYMBOL]: this.zz$ri.contract, }) } - lazy>( + lazy>( loader: () => Promise<{ default: U }>, - ): DecoratedLazy { - const lazy = createLazy(loader) - const decorated = decorateLazy(lazy) - - return Object.assign(decorated, { + ): DecoratedLazy> { + return Object.assign(new RouterBuilder({}).lazy(loader), { [ROUTER_CONTRACT_SYMBOL]: this.zz$ri.contract, }) } diff --git a/packages/server/src/router.test-d.ts b/packages/server/src/router.test-d.ts index da63a1503..15eda961c 100644 --- a/packages/server/src/router.test-d.ts +++ b/packages/server/src/router.test-d.ts @@ -3,7 +3,7 @@ import type { InferRouterInputs, InferRouterOutputs, Router } from './router' import type { WELL_CONTEXT } from './types' import { oc } from '@orpc/contract' import { z } from 'zod' -import { createLazy } from './lazy' +import { lazy } from './lazy' const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) @@ -11,17 +11,17 @@ const ping = {} as Procedure<{ auth: boolean }, { db: string }, typeof schema, t const pong = {} as Procedure const router = { - ping: createLazy(() => Promise.resolve({ default: ping })), + ping: lazy(() => Promise.resolve({ default: ping })), pong, nested: { ping, pong, }, - lazy: createLazy(() => Promise.resolve({ default: { - ping: createLazy(() => Promise.resolve({ default: ping })), + lazy: lazy(() => Promise.resolve({ default: { + ping: lazy(() => Promise.resolve({ default: ping })), pong, - nested: createLazy(() => Promise.resolve({ default: { - ping: createLazy(() => Promise.resolve({ default: ping })), + nested: lazy(() => Promise.resolve({ default: { + ping: lazy(() => Promise.resolve({ default: ping })), pong, } })), } })), @@ -68,54 +68,54 @@ describe('Router', () => { pong, }, - pingLazy: createLazy(() => Promise.resolve({ default: ping })), + pingLazy: lazy(() => Promise.resolve({ default: ping })), // @ts-expect-error auth is not match - pongLazy: createLazy(() => Promise.resolve({ default: pong })), + pongLazy: lazy(() => Promise.resolve({ default: pong })), - nestedLazy1: createLazy(() => Promise.resolve({ + nestedLazy1: lazy(() => Promise.resolve({ default: { ping, }, })), - nestedLazy2: createLazy(() => Promise.resolve({ + nestedLazy2: lazy(() => Promise.resolve({ default: { - ping: createLazy(() => Promise.resolve({ default: ping })), + ping: lazy(() => Promise.resolve({ default: ping })), }, })), // @ts-expect-error auth is not match - nestedLazy3: createLazy(() => Promise.resolve({ + nestedLazy3: lazy(() => Promise.resolve({ default: { pong, }, })), // @ts-expect-error auth is not match - nestedLazy4: createLazy(() => Promise.resolve({ + nestedLazy4: lazy(() => Promise.resolve({ default: { nested: { - pong: createLazy(() => Promise.resolve({ default: pong })), + pong: lazy(() => Promise.resolve({ default: pong })), }, }, })), - nestedLazy6: createLazy(() => Promise.resolve({ + nestedLazy6: lazy(() => Promise.resolve({ default: { - nested: createLazy(() => Promise.resolve({ + nested: lazy(() => Promise.resolve({ default: { - pingLazy: createLazy(() => Promise.resolve({ default: ping })), + pingLazy: lazy(() => Promise.resolve({ default: ping })), }, })), }, })), // @ts-expect-error auth is not match - nestedLazy5: createLazy(() => Promise.resolve({ + nestedLazy5: lazy(() => Promise.resolve({ default: { - nested: createLazy(() => Promise.resolve({ + nested: lazy(() => Promise.resolve({ default: { - pongLazy: createLazy(() => Promise.resolve({ default: pong })), + pongLazy: lazy(() => Promise.resolve({ default: pong })), }, })), }, @@ -148,19 +148,19 @@ describe('Router', () => { const router2: Router<{ auth: boolean, userId: string }, typeof contract> = { ping, - pong: createLazy(() => Promise.resolve({ default: pong })), + pong: lazy(() => Promise.resolve({ default: pong })), nested: { - ping: createLazy(() => Promise.resolve({ default: ping })), + ping: lazy(() => Promise.resolve({ default: ping })), pong, }, } const router3: Router<{ auth: boolean, userId: string }, typeof contract> = { - ping: createLazy(() => Promise.resolve({ default: ping })), + ping: lazy(() => Promise.resolve({ default: ping })), pong, - nested: createLazy(() => Promise.resolve({ + nested: lazy(() => Promise.resolve({ default: { - ping: createLazy(() => Promise.resolve({ default: ping })), + ping: lazy(() => Promise.resolve({ default: ping })), pong, }, })), @@ -182,24 +182,24 @@ describe('Router', () => { const router565: Router<{ auth: boolean, userId: string }, typeof contract> = { // @ts-expect-error wrong ping - ping: createLazy(() => Promise.resolve({ default: pong })), + ping: lazy(() => Promise.resolve({ default: pong })), pong, nested: { ping, // @ts-expect-error wrong pong - pong: createLazy(() => Promise.resolve({ default: ping })), + pong: lazy(() => Promise.resolve({ default: ping })), }, } const router343: Router<{ auth: boolean, userId: string }, typeof contract> = { // @ts-expect-error wrong ping - ping: createLazy(() => Promise.resolve({ default: pong })), + ping: lazy(() => Promise.resolve({ default: pong })), pong, // @ts-expect-error wrong nested - nested: createLazy(() => Promise.resolve({ + nested: lazy(() => Promise.resolve({ default: { - ping: createLazy(() => Promise.resolve({ default: ping })), - pong: createLazy(() => Promise.resolve({ default: ping })), + ping: lazy(() => Promise.resolve({ default: ping })), + pong: lazy(() => Promise.resolve({ default: ping })), }, })), } From 680dbf764e52284fcb735ca84236fc6826dd62b2 Mon Sep 17 00:00:00 2001 From: unnoq Date: Tue, 17 Dec 2024 10:06:29 +0700 Subject: [PATCH 19/51] improve middleware types --- packages/server/src/procedure-builder.ts | 2 +- packages/server/src/procedure-implementer.ts | 5 +++-- packages/server/src/procedure.ts | 2 +- packages/server/src/router-builder.ts | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/server/src/procedure-builder.ts b/packages/server/src/procedure-builder.ts index 25b27f9c8..189a3ba06 100644 --- a/packages/server/src/procedure-builder.ts +++ b/packages/server/src/procedure-builder.ts @@ -23,7 +23,7 @@ export interface ProcedureBuilderDef< TOutputSchema extends Schema, > { contract: ContractProcedure - middlewares?: Middleware, TExtraContext, unknown, any>[] + middlewares?: Middleware, Partial | undefined, unknown, any>[] } export class ProcedureBuilder< diff --git a/packages/server/src/procedure-implementer.ts b/packages/server/src/procedure-implementer.ts index 8667ab6fd..5b98d844e 100644 --- a/packages/server/src/procedure-implementer.ts +++ b/packages/server/src/procedure-implementer.ts @@ -1,5 +1,5 @@ import type { ContractProcedure, Schema, SchemaInput, SchemaOutput } from '@orpc/contract' -import type { DecoratedLazy } from './lazy' +import type { DecoratedLazy } from './lazy-decorated' import type { ANY_MAP_INPUT_MIDDLEWARE, ANY_MIDDLEWARE, MapInputMiddleware, Middleware } from './middleware' import type { ProcedureFunc } from './procedure' import type { DecoratedProcedure } from './procedure-decorated' @@ -16,7 +16,7 @@ export type ProcedureImplementerDef< TOutputSchema extends Schema, > = { contract: ContractProcedure - middlewares?: Middleware, TExtraContext, SchemaOutput, SchemaInput>[] + middlewares?: Middleware, Partial | undefined, SchemaOutput, SchemaInput>[] } export class ProcedureImplementer< @@ -92,6 +92,7 @@ export class ProcedureImplementer< loader: () => Promise<{ default: U }>, ): DecoratedLazy { // TODO: replace with a more solid solution + // @ts-expect-error - invalid lazy return new RouterBuilder(this['~orpc']).lazy(loader as any) as any } } diff --git a/packages/server/src/procedure.ts b/packages/server/src/procedure.ts index 63998def7..00aa7b6b4 100644 --- a/packages/server/src/procedure.ts +++ b/packages/server/src/procedure.ts @@ -25,7 +25,7 @@ export interface ProcedureDef< TOutputSchema extends Schema, TFuncOutput extends SchemaInput, > { - middlewares?: Middleware, TExtraContext, SchemaOutput, any>[] + middlewares?: Middleware, Partial | undefined, SchemaOutput, any>[] contract: ContractProcedure func: ProcedureFunc } diff --git a/packages/server/src/router-builder.ts b/packages/server/src/router-builder.ts index 0a77676c2..fd6e919b3 100644 --- a/packages/server/src/router-builder.ts +++ b/packages/server/src/router-builder.ts @@ -53,7 +53,7 @@ export type AdaptedRouter< export type RouterBuilderDef = { prefix?: HTTPPath tags?: readonly string[] - middlewares?: Middleware, TExtraContext, unknown, any>[] + middlewares?: Middleware, Partial | undefined, unknown, any>[] } export const LAZY_ROUTER_PREFIX_SYMBOL = Symbol('ORPC_LAZY_ROUTER_PREFIX') From f9dd4f2cc38955522f2822347c33a992c39dc548 Mon Sep 17 00:00:00 2001 From: unnoq Date: Tue, 17 Dec 2024 14:53:36 +0700 Subject: [PATCH 20/51] improve merging middleware on unshiftMiddleware --- packages/contract/src/procedure-decorated.ts | 2 +- .../server/src/procedure-decorated.test.ts | 39 +++++++++++++++++-- packages/server/src/procedure-decorated.ts | 24 +++++++++--- 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/packages/contract/src/procedure-decorated.ts b/packages/contract/src/procedure-decorated.ts index 971e53410..ed9f4ba30 100644 --- a/packages/contract/src/procedure-decorated.ts +++ b/packages/contract/src/procedure-decorated.ts @@ -40,7 +40,7 @@ export class DecoratedContractProcedure< }) } - unshiftTag(...tags: readonly string[]): DecoratedContractProcedure { + unshiftTag(...tags: string[]): DecoratedContractProcedure { return new DecoratedContractProcedure({ ...this['~orpc'], route: { diff --git a/packages/server/src/procedure-decorated.test.ts b/packages/server/src/procedure-decorated.test.ts index 01639e679..d488080aa 100644 --- a/packages/server/src/procedure-decorated.test.ts +++ b/packages/server/src/procedure-decorated.test.ts @@ -92,13 +92,44 @@ describe('self chainable', () => { expect(applied['~orpc'].middlewares).toEqual([mid1, mid2, mid]) }) - it('unshiftMiddleware - prevent duplicate', () => { + describe('unshiftMiddleware --- prevent duplicate', () => { const mid1 = vi.fn() const mid2 = vi.fn() const mid3 = vi.fn() - - const applied = decorated.unshiftMiddleware(mid1, mid2).unshiftMiddleware(mid1, mid3, mid) - expect(applied['~orpc'].middlewares).toEqual([mid1, mid3, mid, mid2]) + const mid4 = vi.fn() + const mid5 = vi.fn() + + it('no duplicate', () => { + expect( + decorated.unshiftMiddleware(mid1, mid2)['~orpc'].middlewares, + ).toEqual([mid1, mid2, mid]) + }) + + it('case 1', () => { + expect( + decorated.unshiftMiddleware(mid1, mid2).unshiftMiddleware(mid1, mid3)['~orpc'].middlewares, + ).toEqual([mid1, mid3, mid2, mid]) + }) + + it('case 2', () => { + expect( + decorated.unshiftMiddleware(mid1, mid2, mid3, mid4).unshiftMiddleware(mid1, mid4, mid2, mid3)['~orpc'].middlewares, + ).toEqual([mid1, mid4, mid2, mid3, mid4, mid]) + }) + + it('case 3', () => { + expect( + decorated.unshiftMiddleware(mid1, mid5, mid2, mid3, mid4).unshiftMiddleware(mid1, mid4, mid2, mid3)['~orpc'].middlewares, + ).toEqual([mid1, mid4, mid2, mid3, mid5, mid2, mid3, mid4, mid]) + }) + + it('case 4', () => { + expect( + decorated + .unshiftMiddleware(mid2, mid2) + .unshiftMiddleware(mid1, mid2)['~orpc'].middlewares, + ).toEqual([mid1, mid2, mid2, mid]) + }) }) }) diff --git a/packages/server/src/procedure-decorated.ts b/packages/server/src/procedure-decorated.ts index 2d560f975..0e5a347f8 100644 --- a/packages/server/src/procedure-decorated.ts +++ b/packages/server/src/procedure-decorated.ts @@ -67,7 +67,7 @@ export type DecoratedProcedure< unshiftTag: (...tags: string[]) => DecoratedProcedure unshiftMiddleware: > | undefined = undefined>( - ...middlewares: readonly Middleware, SchemaInput>[] + ...middlewares: Middleware, SchemaInput>[] ) => DecoratedProcedure } @@ -131,13 +131,25 @@ export function decorateProcedure< } decorated.unshiftMiddleware = (...middlewares: ANY_MIDDLEWARE[]) => { + if (procedure['~orpc'].middlewares?.length) { + let exclusiveMinimum = -1 + + for (let i = 0; i < procedure['~orpc'].middlewares.length; i++) { + const index = middlewares.indexOf(procedure['~orpc'].middlewares[i]!) + + if (index <= exclusiveMinimum) { + middlewares.push(...procedure['~orpc'].middlewares.slice(i)) + break + } + + exclusiveMinimum = index + } + } + return decorateProcedure(new Procedure({ ...procedure['~orpc'], - middlewares: [ - ...middlewares, - ...procedure['~orpc'].middlewares?.filter(middleware => !middlewares.includes(middleware)) ?? [], - ], - })) as any + middlewares, + })) } return decorated From 226ef477a6886e03bfdb46cb01c96c4d948e4bda Mon Sep 17 00:00:00 2001 From: unnoq Date: Tue, 17 Dec 2024 15:01:09 +0700 Subject: [PATCH 21/51] remove lazy method --- .../src/procedure-implementer.test-d.ts | 43 ------------------- .../server/src/procedure-implementer.test.ts | 17 -------- packages/server/src/procedure-implementer.ts | 10 ----- 3 files changed, 70 deletions(-) diff --git a/packages/server/src/procedure-implementer.test-d.ts b/packages/server/src/procedure-implementer.test-d.ts index 7e0441783..681050479 100644 --- a/packages/server/src/procedure-implementer.test-d.ts +++ b/packages/server/src/procedure-implementer.test-d.ts @@ -1,6 +1,4 @@ -import type { DecoratedLazy } from './lazy' import type { Middleware, MiddlewareMeta } from './middleware' -import type { Procedure } from './procedure' import type { DecoratedProcedure } from './procedure-decorated' import type { Meta, WELL_CONTEXT } from './types' import { ContractProcedure } from '@orpc/contract' @@ -150,44 +148,3 @@ describe('to DecoratedProcedure', () => { implementer.func(() => {}) }) }) - -describe('to DecoratedLazy', () => { - const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) - - const global_mid = vi.fn() - const implementer = new ProcedureImplementer<{ id?: string } | undefined, { db: string }, typeof schema, typeof schema>({ - contract: new ContractProcedure({ - InputSchema: schema, - OutputSchema: schema, - }), - middlewares: [global_mid], - }) - - it('lazy', () => { - const lazy = implementer.lazy(() => Promise.resolve({ - default: {} as Procedure<{ id?: string } | undefined, { db: string }, typeof schema, typeof schema, { val: string }>, - })) - - expectTypeOf(lazy).toEqualTypeOf< - DecoratedLazy< - Procedure<{ id?: string } | undefined, { db: string }, typeof schema, typeof schema, { val: string }> - > - >() - - // @ts-expect-error - invalid procedure - implementer.lazy(() => Promise.resolve({ default: {} })) - // @ts-expect-error - invalid procedure - implementer.lazy(() => Promise.resolve({ - default: {} as Procedure<{ id?: string } | undefined, { db: string }, undefined, typeof schema, { val: string }>, - })) - // @ts-expect-error - invalid procedure - implementer.lazy(() => Promise.resolve({ - default: {} as Procedure<{ id?: string } | undefined, { db: string }, typeof schema, undefined, { val: string }>, - })) - // @ts-expect-error - invalid procedure - implementer.lazy(() => Promise.resolve({ - // @ts-expect-error - invalid func output - default: {} as Procedure<{ id?: string } | undefined, { db: string }, typeof schema, typeof schema, { val: number }>, - })) - }) -}) diff --git a/packages/server/src/procedure-implementer.test.ts b/packages/server/src/procedure-implementer.test.ts index cba8f9651..43bdf86b3 100644 --- a/packages/server/src/procedure-implementer.test.ts +++ b/packages/server/src/procedure-implementer.test.ts @@ -66,20 +66,3 @@ describe('to DecoratedProcedure', () => { expect(procedure['~orpc'].middlewares).toEqual([global_mid]) }) }) - -describe('to DecoratedLazy', () => { - const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) - - const global_mid = vi.fn() - const implementer = new ProcedureImplementer<{ id?: string } | undefined, { db: string }, typeof schema, typeof schema>({ - contract: new ContractProcedure({ - InputSchema: schema, - OutputSchema: schema, - }), - middlewares: [global_mid], - }) - - it('lazy', { todo: true }, () => { - - }) -}) diff --git a/packages/server/src/procedure-implementer.ts b/packages/server/src/procedure-implementer.ts index 5b98d844e..baed77646 100644 --- a/packages/server/src/procedure-implementer.ts +++ b/packages/server/src/procedure-implementer.ts @@ -1,5 +1,4 @@ import type { ContractProcedure, Schema, SchemaInput, SchemaOutput } from '@orpc/contract' -import type { DecoratedLazy } from './lazy-decorated' import type { ANY_MAP_INPUT_MIDDLEWARE, ANY_MIDDLEWARE, MapInputMiddleware, Middleware } from './middleware' import type { ProcedureFunc } from './procedure' import type { DecoratedProcedure } from './procedure-decorated' @@ -7,7 +6,6 @@ import type { Context, MergeContext } from './types' import { decorateMiddleware } from './middleware' import { Procedure } from './procedure' import { decorateProcedure } from './procedure-decorated' -import { RouterBuilder } from './router-builder' export type ProcedureImplementerDef< TContext extends Context, @@ -87,12 +85,4 @@ export class ProcedureImplementer< func, })) } - - lazy>>( - loader: () => Promise<{ default: U }>, - ): DecoratedLazy { - // TODO: replace with a more solid solution - // @ts-expect-error - invalid lazy - return new RouterBuilder(this['~orpc']).lazy(loader as any) as any - } } From 74859cdbb4bec9e43f2b1ec68547ff2f0452f36b Mon Sep 17 00:00:00 2001 From: unnoq Date: Tue, 17 Dec 2024 15:03:21 +0700 Subject: [PATCH 22/51] improve at context --- packages/server/src/router-builder.test-d.ts | 19 +++++++++---------- packages/server/src/router-builder.ts | 12 ++++++------ 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/packages/server/src/router-builder.test-d.ts b/packages/server/src/router-builder.test-d.ts index dc66d2cbc..5048640e8 100644 --- a/packages/server/src/router-builder.test-d.ts +++ b/packages/server/src/router-builder.test-d.ts @@ -23,20 +23,19 @@ describe('AdaptedRouter', () => { pong, }, } - - const adapted = {} as AdaptedRouter<{ log: true }, typeof router> + const adapted = {} as AdaptedRouter<{ log: true, auth: boolean }, typeof router> expectTypeOf(adapted.ping).toEqualTypeOf< - DecoratedProcedure<{ log: true } & { auth: boolean }, { db: string }, undefined, undefined, unknown> + DecoratedProcedure<{ log: true, auth: boolean }, { db: string }, undefined, undefined, unknown> >() expectTypeOf(adapted.pong).toEqualTypeOf< - DecoratedProcedure<{ log: true } & WELL_CONTEXT, undefined, undefined, undefined, unknown> + DecoratedProcedure<{ log: true, auth: boolean }, undefined, undefined, undefined, unknown> >() expectTypeOf(adapted.nested.ping).toEqualTypeOf< - DecoratedProcedure<{ log: true } & { auth: boolean }, { db: string }, undefined, undefined, unknown> + DecoratedProcedure<{ log: true, auth: boolean }, { db: string }, undefined, undefined, unknown> >() expectTypeOf(adapted.nested.pong).toEqualTypeOf< - DecoratedProcedure<{ log: true } & WELL_CONTEXT, undefined, undefined, undefined, unknown> + DecoratedProcedure<{ log: true, auth: boolean }, undefined, undefined, undefined, unknown> >() }) @@ -55,16 +54,16 @@ describe('AdaptedRouter', () => { const adapted = {} as AdaptedRouter<{ log: true } | undefined, typeof router> expectTypeOf(adapted.ping).toEqualTypeOf + DecoratedProcedure<{ log: true } | undefined, { db: string }, undefined, undefined, unknown> >>() expectTypeOf(adapted.pong).toEqualTypeOf< - DecoratedProcedure<({ log: true } | undefined) & WELL_CONTEXT, undefined, undefined, undefined, unknown> + DecoratedProcedure<{ log: true } | undefined, undefined, undefined, undefined, unknown> >() expectTypeOf(adapted.nested.ping).toEqualTypeOf + DecoratedProcedure<{ log: true } | undefined, { db: string }, undefined, undefined, unknown> >>() expectTypeOf(adapted.nested.pong).toEqualTypeOf + DecoratedProcedure<{ log: true } | undefined, undefined, undefined, undefined, unknown> >>() }) }) diff --git a/packages/server/src/router-builder.ts b/packages/server/src/router-builder.ts index fd6e919b3..d6dccc437 100644 --- a/packages/server/src/router-builder.ts +++ b/packages/server/src/router-builder.ts @@ -14,14 +14,14 @@ export type AdaptedRouter< TRouter extends Router, > = { [K in keyof TRouter]: TRouter[K] extends Procedure< - infer UContext, + any, infer UExtraContext, infer UInputSchema, infer UOutputSchema, infer UFuncOutput > ? DecoratedProcedure< - TContext & UContext, + TContext, UExtraContext, UInputSchema, UOutputSchema, @@ -29,14 +29,14 @@ export type AdaptedRouter< > : TRouter[K] extends Lazy ? U extends Procedure< - infer UContext, + any, infer UExtraContext, infer UInputSchema, infer UOutputSchema, infer UFuncOutput > ? DecoratedLazy>( + router, any>>( router: U, ): AdaptedRouter { const handled = adaptRouter({ @@ -117,7 +117,7 @@ export class RouterBuilder< return handled as any } - lazy>( + lazy, any>>( loader: () => Promise<{ default: U }>, ): DecoratedLazy> { const lazied = adaptLazyRouter({ From 103e5cbf33285da3ee6bf475fc12584c3505ed75 Mon Sep 17 00:00:00 2001 From: unnoq Date: Tue, 17 Dec 2024 16:37:20 +0700 Subject: [PATCH 23/51] router builder --- packages/server/src/lazy.ts | 4 +- packages/server/src/router-builder.test.ts | 243 ++++++++++++++++++++- packages/server/src/router-builder.ts | 171 ++++++--------- 3 files changed, 306 insertions(+), 112 deletions(-) diff --git a/packages/server/src/lazy.ts b/packages/server/src/lazy.ts index c9880ed11..506848edc 100644 --- a/packages/server/src/lazy.ts +++ b/packages/server/src/lazy.ts @@ -21,8 +21,8 @@ export function isLazy(item: unknown): item is ANY_LAZY { ) } -export function unwrapLazy(lazy: Lazy): Promise<{ default: T }> { - return lazy[LAZY_LOADER_SYMBOL]() +export function unwrapLazy>(lazy: T): Promise<{ default: T extends Lazy ? U : never }> { + return lazy[LAZY_LOADER_SYMBOL]() as any } export type FlattenLazy = T extends Lazy diff --git a/packages/server/src/router-builder.test.ts b/packages/server/src/router-builder.test.ts index 184c24489..f4d316190 100644 --- a/packages/server/src/router-builder.test.ts +++ b/packages/server/src/router-builder.test.ts @@ -1,4 +1,8 @@ -import { RouterBuilder } from './router-builder' +import { ContractProcedure } from '@orpc/contract' +import { z } from 'zod' +import { isLazy, lazy, unwrapLazy } from './lazy' +import { isProcedure, Procedure } from './procedure' +import { getLazyRouterPrefix, LAZY_ROUTER_PREFIX_SYMBOL, RouterBuilder } from './router-builder' const mid1 = vi.fn() const mid2 = vi.fn() @@ -9,6 +13,11 @@ const builder = new RouterBuilder<{ auth: boolean }, { db: string }>({ tags: ['tag1', 'tag2'], }) +it('prevent dynamic params on prefix', () => { + expect(() => builder.prefix('/{id}')).toThrowError() + expect(() => new RouterBuilder({ prefix: '/{id}' })).toThrowError() +}) + describe('self chainable', () => { it('prefix', () => { const prefixed = builder.prefix('/test') @@ -17,6 +26,15 @@ describe('self chainable', () => { expect(prefixed['~orpc'].prefix).toBe('/prefix/test') }) + it('prefix --- still work without pre prefix', () => { + const builder = new RouterBuilder({}) + + const prefixed = builder.prefix('/test') + expect(prefixed).not.toBe(builder) + expect(prefixed).toBeInstanceOf(RouterBuilder) + expect(prefixed['~orpc'].prefix).toBe('/test') + }) + it('tag', () => { const tagged = builder.tag('test1', 'test2') expect(tagged).not.toBe(builder) @@ -24,6 +42,15 @@ describe('self chainable', () => { expect(tagged['~orpc'].tags).toEqual(['tag1', 'tag2', 'test1', 'test2']) }) + it('tag --- still work without pre tag', () => { + const builder = new RouterBuilder({}) + + const tagged = builder.tag('test1', 'test2') + expect(tagged).not.toBe(builder) + expect(tagged).toBeInstanceOf(RouterBuilder) + expect(tagged['~orpc'].tags).toEqual(['test1', 'test2']) + }) + it('use middleware', () => { const mid3 = vi.fn() const mid4 = vi.fn() @@ -33,4 +60,218 @@ describe('self chainable', () => { expect(applied).toBeInstanceOf(RouterBuilder) expect(applied['~orpc'].middlewares).toEqual([mid1, mid2, mid3, mid4]) }) + + it('use middleware --- still work without pre middleware', () => { + const builder = new RouterBuilder({}) + + const applied = builder.use(mid1).use(mid2) + expect(applied).not.toBe(builder) + expect(applied).toBeInstanceOf(RouterBuilder) + expect(applied['~orpc'].middlewares).toEqual([mid1, mid2]) + }) +}) + +describe('adapt router', () => { + const pMid1 = vi.fn() + const pMid2 = vi.fn() + + const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) + const ping = new Procedure({ + contract: new ContractProcedure({ + InputSchema: schema, + OutputSchema: undefined, + route: { + tags: ['tag3', 'tag4'], + }, + }), + func: vi.fn(), + middlewares: [mid1, pMid1, pMid2], + }) + const pong = new Procedure({ + contract: new ContractProcedure({ + InputSchema: undefined, + OutputSchema: schema, + route: { + method: 'GET', + path: '/pong', + description: 'desc', + }, + }), + func: vi.fn(), + }) + + const router = { + ping, + pong, + nested: { + ping, + pong, + }, + } + + const routerWithLazy = { + ping: lazy(() => Promise.resolve({ default: ping })), + pong, + nested: lazy(() => Promise.resolve({ default: { + ping, + pong: lazy(() => Promise.resolve({ default: pong })), + } })), + } + + it('router without lazy', () => { + const adapted = builder.router(router) + + expect(adapted.ping).toSatisfy(isProcedure) + expect(typeof adapted.ping).toBe('function') + expect(adapted.ping['~orpc'].func).toBe(ping['~orpc'].func) + expect(adapted.ping['~orpc'].middlewares).toEqual([mid1, mid2, pMid1, pMid2]) + expect(adapted.ping['~orpc'].contract['~orpc'].route?.path).toBe(undefined) + expect(adapted.ping['~orpc'].contract['~orpc'].route?.method).toBe(undefined) + expect(adapted.ping['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2', 'tag3', 'tag4']) + + expect(adapted.pong).toSatisfy(isProcedure) + expect(typeof adapted.pong).toBe('function') + expect(adapted.pong['~orpc'].func).toBe(pong['~orpc'].func) + expect(adapted.pong['~orpc'].middlewares).toEqual([mid1, mid2]) + expect(adapted.pong['~orpc'].contract['~orpc'].route?.path).toBe('/prefix/pong') + expect(adapted.pong['~orpc'].contract['~orpc'].route?.method).toBe('GET') + expect(adapted.pong['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2']) + + expect(adapted.nested.ping).toSatisfy(isProcedure) + expect(typeof adapted.nested.ping).toBe('function') + expect(adapted.nested.ping['~orpc'].func).toBe(ping['~orpc'].func) + expect(adapted.nested.ping['~orpc'].middlewares).toEqual([mid1, mid2, pMid1, pMid2]) + expect(adapted.nested.ping['~orpc'].contract['~orpc'].route?.path).toBe(undefined) + expect(adapted.nested.ping['~orpc'].contract['~orpc'].route?.method).toBe(undefined) + expect(adapted.nested.ping['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2', 'tag3', 'tag4']) + + expect(adapted.nested.pong).toSatisfy(isProcedure) + expect(typeof adapted.nested.pong).toBe('function') + expect(adapted.nested.pong['~orpc'].func).toBe(pong['~orpc'].func) + expect(adapted.nested.pong['~orpc'].middlewares).toEqual([mid1, mid2]) + expect(adapted.nested.pong['~orpc'].contract['~orpc'].route?.path).toBe('/prefix/pong') + expect(adapted.nested.pong['~orpc'].contract['~orpc'].route?.method).toBe('GET') + expect(adapted.nested.pong['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2']) + }) + + it('router with lazy', async () => { + const adapted = builder.router(routerWithLazy) as any + + expect(adapted.ping[LAZY_ROUTER_PREFIX_SYMBOL]).toBe('/prefix') + expect(adapted.pong[LAZY_ROUTER_PREFIX_SYMBOL]).toBe(undefined) + expect(adapted.nested[LAZY_ROUTER_PREFIX_SYMBOL]).toBe('/prefix') + expect(adapted.nested.ping[LAZY_ROUTER_PREFIX_SYMBOL]).toBe('/prefix') + expect(adapted.nested.pong[LAZY_ROUTER_PREFIX_SYMBOL]).toBe('/prefix') + + expect(adapted.ping).toSatisfy(isLazy) + expect(typeof adapted.ping).toBe('function') + expect((await unwrapLazy(adapted.ping) as any).default).toSatisfy(isProcedure) + expect((await unwrapLazy(adapted.ping) as any).default['~orpc'].func).toBe(ping['~orpc'].func) + expect((await unwrapLazy(adapted.ping) as any).default['~orpc'].middlewares).toEqual([mid1, mid2, pMid1, pMid2]) + expect((await unwrapLazy(adapted.ping) as any).default['~orpc'].contract['~orpc'].route?.path).toBe(undefined) + expect((await unwrapLazy(adapted.ping) as any).default['~orpc'].contract['~orpc'].route?.method).toBe(undefined) + expect((await unwrapLazy(adapted.ping) as any).default['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2', 'tag3', 'tag4']) + + expect(adapted.pong).toSatisfy(isProcedure) + expect(typeof adapted.pong).toBe('function') + expect(adapted.pong['~orpc'].func).toBe(pong['~orpc'].func) + expect(adapted.pong['~orpc'].middlewares).toEqual([mid1, mid2]) + expect(adapted.pong['~orpc'].contract['~orpc'].route?.path).toBe('/prefix/pong') + expect(adapted.pong['~orpc'].contract['~orpc'].route?.method).toBe('GET') + expect(adapted.pong['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2']) + + expect(adapted.nested.ping).toSatisfy(isLazy) + expect(typeof adapted.nested.ping).toBe('function') + expect((await unwrapLazy(adapted.nested.ping) as any).default).toSatisfy(isProcedure) + expect((await unwrapLazy(adapted.nested.ping) as any).default['~orpc'].func).toBe(ping['~orpc'].func) + expect((await unwrapLazy(adapted.nested.ping) as any).default['~orpc'].middlewares).toEqual([mid1, mid2, pMid1, pMid2]) + expect((await unwrapLazy(adapted.nested.ping) as any).default['~orpc'].contract['~orpc'].route?.path).toBe(undefined) + expect((await unwrapLazy(adapted.nested.ping) as any).default['~orpc'].contract['~orpc'].route?.method).toBe(undefined) + expect((await unwrapLazy(adapted.nested.ping) as any).default['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2', 'tag3', 'tag4']) + + expect(adapted.nested.pong).toSatisfy(isLazy) + expect(typeof adapted.nested.pong).toBe('function') + expect((await unwrapLazy(adapted.nested.pong) as any).default).toSatisfy(isProcedure) + expect((await unwrapLazy(adapted.nested.pong) as any).default['~orpc'].func).toBe(pong['~orpc'].func) + expect((await unwrapLazy(adapted.nested.pong) as any).default['~orpc'].middlewares).toEqual([mid1, mid2]) + expect((await unwrapLazy(adapted.nested.pong) as any).default['~orpc'].contract['~orpc'].route?.path).toBe('/prefix/pong') + expect((await unwrapLazy(adapted.nested.pong) as any).default['~orpc'].contract['~orpc'].route?.method).toBe('GET') + expect((await unwrapLazy(adapted.nested.pong) as any).default['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2']) + }) + + it('router lazy with nested lazy', async () => { + const adapted = builder.lazy(() => Promise.resolve({ default: routerWithLazy })) as any + + expect(adapted.ping[LAZY_ROUTER_PREFIX_SYMBOL]).toBe('/prefix') + expect(adapted.pong[LAZY_ROUTER_PREFIX_SYMBOL]).toBe('/prefix') + expect(adapted.nested[LAZY_ROUTER_PREFIX_SYMBOL]).toBe('/prefix') + expect(adapted.nested.ping[LAZY_ROUTER_PREFIX_SYMBOL]).toBe('/prefix') + expect(adapted.nested.pong[LAZY_ROUTER_PREFIX_SYMBOL]).toBe('/prefix') + + expect(adapted.ping).toSatisfy(isLazy) + expect(typeof adapted.ping).toBe('function') + expect((await unwrapLazy(adapted.ping) as any).default).toSatisfy(isProcedure) + expect((await unwrapLazy(adapted.ping) as any).default['~orpc'].func).toBe(ping['~orpc'].func) + expect((await unwrapLazy(adapted.ping) as any).default['~orpc'].middlewares).toEqual([mid1, mid2, pMid1, pMid2]) + expect((await unwrapLazy(adapted.ping) as any).default['~orpc'].contract['~orpc'].route?.path).toBe(undefined) + expect((await unwrapLazy(adapted.ping) as any).default['~orpc'].contract['~orpc'].route?.method).toBe(undefined) + expect((await unwrapLazy(adapted.ping) as any).default['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2', 'tag3', 'tag4']) + + expect(adapted.pong).toSatisfy(isLazy) + expect(typeof adapted.pong).toBe('function') + expect((await unwrapLazy(adapted.pong) as any).default).toSatisfy(isProcedure) + expect((await unwrapLazy(adapted.pong) as any).default['~orpc'].func).toBe(pong['~orpc'].func) + expect((await unwrapLazy(adapted.pong) as any).default['~orpc'].middlewares).toEqual([mid1, mid2]) + expect((await unwrapLazy(adapted.pong) as any).default['~orpc'].contract['~orpc'].route?.path).toBe('/prefix/pong') + expect((await unwrapLazy(adapted.pong) as any).default['~orpc'].contract['~orpc'].route?.method).toBe('GET') + expect((await unwrapLazy(adapted.pong) as any).default['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2']) + + expect(adapted.nested.ping).toSatisfy(isLazy) + expect(typeof adapted.nested.ping).toBe('function') + expect((await unwrapLazy(adapted.nested.ping) as any).default).toSatisfy(isProcedure) + expect((await unwrapLazy(adapted.nested.ping) as any).default['~orpc'].func).toBe(ping['~orpc'].func) + expect((await unwrapLazy(adapted.nested.ping) as any).default['~orpc'].middlewares).toEqual([mid1, mid2, pMid1, pMid2]) + expect((await unwrapLazy(adapted.nested.ping) as any).default['~orpc'].contract['~orpc'].route?.path).toBe(undefined) + expect((await unwrapLazy(adapted.nested.ping) as any).default['~orpc'].contract['~orpc'].route?.method).toBe(undefined) + expect((await unwrapLazy(adapted.nested.ping) as any).default['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2', 'tag3', 'tag4']) + + expect(adapted.nested.pong).toSatisfy(isLazy) + expect(typeof adapted.nested.pong).toBe('function') + expect((await unwrapLazy(adapted.nested.pong) as any).default).toSatisfy(isProcedure) + expect((await unwrapLazy(adapted.nested.pong) as any).default['~orpc'].func).toBe(pong['~orpc'].func) + expect((await unwrapLazy(adapted.nested.pong) as any).default['~orpc'].middlewares).toEqual([mid1, mid2]) + expect((await unwrapLazy(adapted.nested.pong) as any).default['~orpc'].contract['~orpc'].route?.path).toBe('/prefix/pong') + expect((await unwrapLazy(adapted.nested.pong) as any).default['~orpc'].contract['~orpc'].route?.method).toBe('GET') + expect((await unwrapLazy(adapted.nested.pong) as any).default['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2']) + }) + + it('can concat LAZY_ROUTER_PREFIX_SYMBOL', () => { + const adapted = builder.prefix('/hi').router(builder.router(routerWithLazy)) as any + expect(adapted.ping[LAZY_ROUTER_PREFIX_SYMBOL]).toBe('/prefix/hi/prefix') + }) + + it('works with LAZY_ROUTER_PREFIX_SYMBOL when prefix is not set', () => { + const builderWithoutPrefix = new RouterBuilder({}) + const adapted = builderWithoutPrefix.router(routerWithLazy) as any + expect(adapted.ping[LAZY_ROUTER_PREFIX_SYMBOL]).toBe(undefined) + expect(adapted.pong[LAZY_ROUTER_PREFIX_SYMBOL]).toBe(undefined) + + const adapted2 = builderWithoutPrefix.router(builder.router(routerWithLazy) as any) as any + expect(adapted2.ping[LAZY_ROUTER_PREFIX_SYMBOL]).toBe('/prefix') + expect(adapted2.pong[LAZY_ROUTER_PREFIX_SYMBOL]).toBe(undefined) + }) + + it('getLazyRouterPrefix works', () => { + expect(getLazyRouterPrefix({})).toBe(undefined) + expect(getLazyRouterPrefix(undefined)).toBe(undefined) + expect(getLazyRouterPrefix(null)).toBe(undefined) + expect(getLazyRouterPrefix(builder.router(routerWithLazy).ping)).toBe('/prefix') + expect(getLazyRouterPrefix(builder.router(routerWithLazy).pong)).toBe(undefined) + }) + + it('deepSetLazyRouterPrefix not recursive on Symbol', () => { + const adapted = builder.router(routerWithLazy) as any + + expect(adapted.nested[Symbol('anything')]).toBe(undefined) + }) }) diff --git a/packages/server/src/router-builder.ts b/packages/server/src/router-builder.ts index d6dccc437..3dcf2e68c 100644 --- a/packages/server/src/router-builder.ts +++ b/packages/server/src/router-builder.ts @@ -1,13 +1,13 @@ +import type { HTTPPath } from '@orpc/contract' import type { Lazy } from './lazy' -import type { DecoratedLazy } from './lazy-decorated' -import type { Middleware } from './middleware' -import type { ANY_LAZY_PROCEDURE, ANY_PROCEDURE, Procedure } from './procedure' -import type { DecoratedProcedure } from './procedure-decorated' +import type { ANY_MIDDLEWARE, Middleware } from './middleware' +import type { ANY_PROCEDURE, Procedure } from './procedure' import type { ANY_ROUTER, Router } from './router' import type { Context, MergeContext } from './types' -import { DecoratedContractProcedure, type HTTPPath } from '@orpc/contract' import { isLazy, lazy, unwrapLazy } from './lazy' +import { type DecoratedLazy, decorateLazy } from './lazy-decorated' import { isProcedure } from './procedure' +import { type DecoratedProcedure, decorateProcedure } from './procedure-decorated' export type AdaptedRouter< TContext extends Context, @@ -107,137 +107,90 @@ export class RouterBuilder< router, any>>( router: U, ): AdaptedRouter { - const handled = adaptRouter({ - routerOrChild: router, - middlewares: this.zz$rb.middlewares, - tags: this.zz$rb.tags, - prefix: this.zz$rb.prefix, - }) - - return handled as any + const adapted = adapt(router, this['~orpc']) + return adapted as any } lazy, any>>( loader: () => Promise<{ default: U }>, ): DecoratedLazy> { - const lazied = adaptLazyRouter({ - current: lazy(loader), - middlewares: this.zz$rb.middlewares, - tags: this.zz$rb.tags, - prefix: this.zz$rb.prefix, - }) - - return lazied as any + const adapted = adapt(lazy(loader), this['~orpc']) + return adapted as any } } -function adaptRouter(options: { - routerOrChild: Router | Router[keyof Router] - middlewares?: Middleware[] - tags?: string[] - prefix?: HTTPPath -}) { - if (isProcedure(options.routerOrChild)) { - return adaptProcedure({ - ...options, - procedure: options.routerOrChild, - }) - } +function adapt( + item: ANY_ROUTER | ANY_ROUTER[string], + options: { + middlewares?: ANY_MIDDLEWARE[] + tags?: readonly string[] + prefix?: HTTPPath + }, +): unknown { + if (isLazy(item)) { + const adaptedLazy = decorateLazy(lazy(async () => { + const routerOrProcedure = (await unwrapLazy(item)).default as ANY_ROUTER | ANY_PROCEDURE + const adapted = adapt(routerOrProcedure, options) + + return { default: adapted } + })) + + const lazyPrefix = getLazyRouterPrefix(item) + if (options.prefix || lazyPrefix) { + const prefixed = deepSetLazyRouterPrefix(adaptedLazy, `${options.prefix ?? ''}${lazyPrefix ?? ''}` as any) + return prefixed + } - if (isLazy(options.routerOrChild)) { - return adaptLazyRouter({ - ...options, - current: options.routerOrChild, - }) + return adaptedLazy } - const handled: Record = {} + if (isProcedure(item)) { + let decorated = decorateProcedure(item) - for (const key in options.routerOrChild) { - handled[key] = adaptRouter({ - ...options, - routerOrChild: options.routerOrChild[key]!, - }) - } + if (options.tags?.length) { + decorated = decorated.unshiftTag(...options.tags) + } - return handled as any -} + if (options.prefix) { + decorated = decorated.prefix(options.prefix) + } -function adaptLazyRouter(options: { - current: ANY_LAZY_PROCEDURE | Lazy> - middlewares?: Middleware[] - tags?: string[] - prefix?: HTTPPath -}): DecoratedLazy>> { - const loader = async (): Promise<{ default: unknown }> => { - const current = (await unwrapLazy(options.current)).default - - return { - default: adaptRouter({ - ...options, - routerOrChild: current, - }), + if (options.middlewares?.length) { + decorated = decorated.unshiftMiddleware(...options.middlewares) } - } - let lazyRouterPrefix = options.prefix + return decorated + } - if (LAZY_ROUTER_PREFIX_SYMBOL in options.current && typeof options.current[LAZY_ROUTER_PREFIX_SYMBOL] === 'string') { - lazyRouterPrefix = `${options.current[LAZY_ROUTER_PREFIX_SYMBOL]}${lazyRouterPrefix ?? ''}` as HTTPPath + const adapted = {} as Record + for (const key in item) { + adapted[key] = adapt(item[key]!, options) } - const decoratedLazy = Object.assign(decorateLazy(lazy(loader)), { - [LAZY_ROUTER_PREFIX_SYMBOL]: lazyRouterPrefix, - }) + return adapted +} - const recursive = new Proxy(decoratedLazy, { +function deepSetLazyRouterPrefix(router: T, prefix: HTTPPath): T { + return new Proxy(router, { get(target, key) { - if (typeof key !== 'string') { - return Reflect.get(target, key) + if (key !== LAZY_ROUTER_PREFIX_SYMBOL) { + const val = Reflect.get(target, key) + if (val && (typeof val === 'object' || typeof val === 'function')) { + return deepSetLazyRouterPrefix(val, prefix) + } + + return val } - return adaptLazyRouter({ - ...options, - current: lazy(async () => { - const current = (await unwrapLazy(options.current)).default - return { default: current[key] } - }), - }) + return prefix }, }) - - return recursive as any } -function adaptProcedure(options: { - procedure: ANY_PROCEDURE - middlewares?: Middleware[] - tags?: string[] - prefix?: HTTPPath -}): DecoratedProcedure { - const builderMiddlewares = options.middlewares ?? [] - const procedureMiddlewares = options.procedure.zz$p.middlewares ?? [] - - const middlewares = [ - ...builderMiddlewares, - ...procedureMiddlewares.filter( - item => !builderMiddlewares.includes(item), - ), - ] - - let contract = DecoratedContractProcedure.decorate( - options.procedure.zz$p.contract, - ).unshiftTag(...(options.tags ?? [])) - - if (options.prefix) { - contract = contract.prefix(options.prefix) +export function getLazyRouterPrefix(router: unknown): HTTPPath | undefined { + if (router && (typeof router === 'object' || typeof router === 'function')) { + return (router as any)[LAZY_ROUTER_PREFIX_SYMBOL] } - return decorateProcedure({ - zz$p: { - ...options.procedure.zz$p, - contract, - middlewares, - }, - }) + return undefined } From 50eca8a50a1889eb63c625f7cb6120252a8d1134 Mon Sep 17 00:00:00 2001 From: unnoq Date: Wed, 18 Dec 2024 09:05:45 +0700 Subject: [PATCH 24/51] router caller --- packages/server/src/lazy-decorated.ts | 5 +- packages/server/src/lazy.ts | 10 +- packages/server/src/router-caller.test-d.ts | 129 ++++++++ packages/server/src/router-caller.test.ts | 326 ++++++-------------- packages/server/src/router-caller.ts | 82 ++--- 5 files changed, 267 insertions(+), 285 deletions(-) create mode 100644 packages/server/src/router-caller.test-d.ts diff --git a/packages/server/src/lazy-decorated.ts b/packages/server/src/lazy-decorated.ts index 3a27cd00d..692ce8898 100644 --- a/packages/server/src/lazy-decorated.ts +++ b/packages/server/src/lazy-decorated.ts @@ -1,8 +1,9 @@ import type { SchemaInput, SchemaOutput } from '@orpc/contract' import type { AnyFunction } from '@orpc/shared' +import type { ANY_LAZY, Lazy } from './lazy' import type { Procedure } from './procedure' import type { Caller } from './types' -import { flatLazy, lazy, type Lazy, unwrapLazy } from './lazy' +import { flatLazy, lazy, unwrapLazy } from './lazy' import { createProcedureCaller } from './procedure-caller' export type DecoratedLazy = T extends Lazy @@ -18,7 +19,7 @@ export type DecoratedLazy = T extends Lazy : Lazy ) -export function decorateLazy(lazied: Lazy): DecoratedLazy { +export function decorateLazy(lazied: T): DecoratedLazy { const flattenLazy = flatLazy(lazied) const procedureCaller = createProcedureCaller({ diff --git a/packages/server/src/lazy.ts b/packages/server/src/lazy.ts index 506848edc..d9c6f19b7 100644 --- a/packages/server/src/lazy.ts +++ b/packages/server/src/lazy.ts @@ -29,9 +29,9 @@ export type FlattenLazy = T extends Lazy ? FlattenLazy : Lazy -export function flatLazy(lazy: Lazy): FlattenLazy { +export function flatLazy(lazied: T): FlattenLazy { const flattenLoader = async () => { - let current = await unwrapLazy(lazy) + let current = await unwrapLazy(lazied) while (true) { if (!isLazy(current.default)) { @@ -44,9 +44,5 @@ export function flatLazy(lazy: Lazy): FlattenLazy { return current } - const flattenLazy = { - [LAZY_LOADER_SYMBOL]: flattenLoader, - } - - return flattenLazy as any + return lazy(flattenLoader) as any } diff --git a/packages/server/src/router-caller.test-d.ts b/packages/server/src/router-caller.test-d.ts new file mode 100644 index 000000000..256188f70 --- /dev/null +++ b/packages/server/src/router-caller.test-d.ts @@ -0,0 +1,129 @@ +import type { Procedure } from './procedure' +import type { Caller, Meta, WELL_CONTEXT } from './types' +import { z } from 'zod' +import { lazy } from './lazy' +import { createRouterCaller, type RouterCaller } from './router-caller' + +const schema = z.object({ val: z.string().transform(val => Number(val)) }) +const ping = {} as Procedure +const pong = {} as Procedure<{ auth: boolean }, undefined, undefined, undefined, unknown> + +const router = { + ping, + pong, + nested: { + ping, + pong, + }, +} + +const routerWithLazy = { + ping: lazy(() => Promise.resolve({ default: ping })), + pong, + nested: lazy(() => Promise.resolve({ default: { + ping, + pong: lazy(() => Promise.resolve({ default: pong })), + } })), +} + +describe('RouterCaller', () => { + it('router without lazy', () => { + const caller = {} as RouterCaller + + expectTypeOf(caller.ping).toEqualTypeOf< + Caller<{ val: string }, { val: number }> + >() + expectTypeOf(caller.pong).toEqualTypeOf< + Caller + >() + + expectTypeOf(caller.nested.ping).toEqualTypeOf< + Caller<{ val: string }, { val: number }> + >() + expectTypeOf(caller.nested.pong).toEqualTypeOf< + Caller + >() + }) + + it('support lazy', () => { + expectTypeOf>().toEqualTypeOf>() + }) +}) + +describe('createRouterCaller', () => { + it('return RouterCaller', () => { + const caller = createRouterCaller({ + router, + context: { auth: true }, + }) + + expectTypeOf(caller).toMatchTypeOf>() + + const caller2 = createRouterCaller({ + router: routerWithLazy, + context: { auth: true }, + }) + expectTypeOf(caller2).toMatchTypeOf>() + }) + + it('required context when needed', () => { + createRouterCaller({ + router: { ping }, + }) + + createRouterCaller({ + router: { pong }, + context: { auth: true }, + }) + + createRouterCaller({ + router: { pong }, + context: () => ({ auth: true }), + }) + + createRouterCaller({ + router: { pong }, + context: async () => ({ auth: true }), + }) + + createRouterCaller({ + router: { pong }, + // @ts-expect-error --- invalid context + context: { auth: 'invalid' }, + }) + + // @ts-expect-error --- missing context + createRouterCaller({ + router: { pong }, + }) + }) + + it('support hooks', () => { + createRouterCaller({ + router, + context: { auth: true }, + onSuccess: async ({ output }, context, meta) => { + expectTypeOf(output).toEqualTypeOf() + expectTypeOf(context).toEqualTypeOf & { + auth: boolean + }>() + expectTypeOf(meta).toEqualTypeOf() + }, + }) + }) + + it('support base path', () => { + createRouterCaller({ + router: { ping }, + context: { auth: true }, + path: ['users'], + }) + + createRouterCaller({ + router: { ping }, + context: { auth: true }, + // @ts-expect-error --- invalid path + path: [123], + }) + }) +}) diff --git a/packages/server/src/router-caller.test.ts b/packages/server/src/router-caller.test.ts index f361c60aa..bd6e70870 100644 --- a/packages/server/src/router-caller.test.ts +++ b/packages/server/src/router-caller.test.ts @@ -1,198 +1,95 @@ +import { ContractProcedure } from '@orpc/contract' import { z } from 'zod' -import { createRouterCaller, ORPCError, os } from '.' +import { lazy, unwrapLazy } from './lazy' +import { Procedure } from './procedure' +import { createProcedureCaller } from './procedure-caller' +import { createRouterCaller } from './router-caller' -describe('createRouterCaller', () => { - const internal = false - const context = { auth: true } - - const osw = os.context<{ auth?: boolean }>() - - const ping = osw - .input(z.object({ value: z.string().transform(v => Number(v)) })) - .output(z.object({ value: z.number().transform(v => v.toString()) })) - .func((input, context, meta) => { - expect(context).toEqual(context) - - return input - }) +vi.mock('./procedure-caller', () => ({ + createProcedureCaller: vi.fn(() => vi.fn(() => '__mocked__')), +})) - const pong = osw.func((_, context, meta) => { - expect(context).toEqual(context) +beforeEach(() => { + vi.clearAllMocks() +}) - return { value: true } +describe('createRouterCaller', () => { + const schema = z.object({ val: z.string().transform(v => Number(v)) }) + const ping = new Procedure({ + contract: new ContractProcedure({ + InputSchema: schema, + OutputSchema: schema, + }), + func: vi.fn(() => ({ val: '123' })), + }) + const pong = new Procedure({ + contract: new ContractProcedure({ + InputSchema: undefined, + OutputSchema: undefined, + }), + func: vi.fn(() => ('output')), }) - const lazyRouter = osw.lazy(() => Promise.resolve({ - default: { - ping: osw.lazy(() => Promise.resolve({ default: ping })), - pong, - lazyRouter: osw.lazy(() => Promise.resolve({ default: { ping, pong } })), - }, - })) - - const router = osw.router({ - ping, + const router = { + ping: lazy(() => Promise.resolve({ default: ping })), pong, - nested: { + nested: lazy(() => Promise.resolve({ default: { ping, - pong, - }, - lazyRouter, + pong: lazy(() => Promise.resolve({ default: pong })), + } })), + } + + const caller = createRouterCaller({ + router, + context: { auth: true }, + path: ['users'], }) - it('infer context', () => { - createRouterCaller({ - router, - // @ts-expect-error invalid context - context: { auth: 123 }, - }) - - createRouterCaller({ - router, - context, - }) - }) - - it('with validate', () => { - const caller = createRouterCaller({ - router, - context, - }) - - expectTypeOf(caller.ping).toMatchTypeOf< - (input: { value: string }) => Promise<{ - value: string - }> - >() - - expectTypeOf(caller.pong).toMatchTypeOf< - (input: unknown) => Promise<{ - value: boolean - }> - >() - - expectTypeOf(caller.nested.ping).toMatchTypeOf< - (input: { value: string }) => Promise<{ - value: string - }> - >() - - expectTypeOf(caller.nested.pong).toMatchTypeOf< - (input: unknown) => Promise<{ - value: boolean - }> - >() - - expectTypeOf(caller.lazyRouter.ping).toMatchTypeOf< - (input: { value: string }) => Promise<{ - value: string - }> - >() - - expectTypeOf(caller.lazyRouter.pong).toMatchTypeOf< - (input: unknown) => Promise<{ - value: boolean - }> - >() - - expectTypeOf(caller.lazyRouter.lazyRouter.ping).toMatchTypeOf< - (input: { value: string }) => Promise<{ - value: string - }> - >() - - expectTypeOf(caller.lazyRouter.lazyRouter.pong).toMatchTypeOf< - (input: unknown) => Promise<{ - value: boolean - }> - >() - - expect(caller.ping({ value: '123' })).resolves.toEqual({ value: '123' }) - expect(caller.pong({ value: '123' })).resolves.toEqual({ value: true }) - - expect(caller.nested.ping({ value: '123' })).resolves.toEqual({ - value: '123', - }) - expect(caller.nested.pong({ value: '123' })).resolves.toEqual({ - value: true, - }) + it('works', () => { + expect(caller.pong({ val: '123' })).toEqual('__mocked__') - expect(caller.lazyRouter.ping({ value: '123' })).resolves.toEqual({ - value: '123', - }) - expect(caller.lazyRouter.pong({ value: '123' })).resolves.toEqual({ - value: true, - }) + expect(createProcedureCaller).toBeCalledTimes(1) + expect(createProcedureCaller).toBeCalledWith(expect.objectContaining({ + procedure: pong, + context: { auth: true }, + path: ['users', 'pong'], + })) - expect(caller.lazyRouter.lazyRouter.ping({ value: '123' })).resolves.toEqual({ - value: '123', - }) - expect(caller.lazyRouter.lazyRouter.pong({ value: '123' })).resolves.toEqual({ - value: true, - }) + expect(vi.mocked(createProcedureCaller).mock.results[0]?.value).toBeCalledTimes(1) + expect(vi.mocked(createProcedureCaller).mock.results[0]?.value).toBeCalledWith({ val: '123' }) + }) - // @ts-expect-error - invalid input - expect(caller.ping({ value: new Date('2023-01-01') })).rejects.toThrowError( - 'Input validation failed', - ) + it('work with lazy', async () => { + expect(caller.ping({ val: '123' })).toEqual('__mocked__') - // @ts-expect-error - invalid input - expect(caller.nested.ping({ value: true })).rejects.toThrowError( - 'Input validation failed', - ) + expect(createProcedureCaller).toBeCalledTimes(2) + expect(createProcedureCaller).toHaveBeenNthCalledWith(2, expect.objectContaining({ + procedure: expect.any(Function), + context: { auth: true }, + path: ['users', 'ping'], + })) - // @ts-expect-error - invalid input - expect(caller.lazyRouter.ping({ value: true })).rejects.toThrowError( - 'Input validation failed', - ) + expect((await unwrapLazy(vi.mocked(createProcedureCaller as any).mock.calls[1]![0].procedure)).default).toBe(ping) - // @ts-expect-error - invalid input - expect(caller.lazyRouter.lazyRouter.ping({ value: true })).rejects.toThrowError( - 'Input validation failed', - ) + expect(vi.mocked(createProcedureCaller).mock.results[1]?.value).toBeCalledTimes(1) + expect(vi.mocked(createProcedureCaller).mock.results[1]?.value).toBeCalledWith({ val: '123' }) }) - it('path', () => { - const ping = osw.func((_, __, { path }) => { - return path - }) + it('work with nested lazy', async () => { + expect(caller.nested.ping({ val: '123' })).toEqual('__mocked__') - const lazyRouter = osw.lazy(() => Promise.resolve({ - default: { - ping: osw.lazy(() => Promise.resolve({ default: ping })), - lazyRouter: osw.lazy(() => Promise.resolve({ default: { ping } })), - }, + expect(createProcedureCaller).toBeCalledTimes(5) + expect(createProcedureCaller).toHaveBeenNthCalledWith(5, expect.objectContaining({ + procedure: expect.any(Function), + context: { auth: true }, + path: ['users', 'nested', 'ping'], })) - const router = osw.router({ - ping, - nested: { - ping, - child: { - ping, - }, - }, - lazyRouter, - }) + const lazied = vi.mocked(createProcedureCaller as any).mock.calls[4]![0].procedure + expect(await unwrapLazy(lazied)).toEqual({ default: ping }) - const caller = createRouterCaller({ - router, - context, - }) - - expect(caller.ping('')).resolves.toEqual(['ping']) - expect(caller.nested.ping('')).resolves.toEqual(['nested', 'ping']) - expect(caller.nested.child.ping('')).resolves.toEqual([ - 'nested', - 'child', - 'ping', - ]) - expect(caller.lazyRouter.ping()).resolves.toEqual(['lazyRouter', 'ping']) - expect(caller.lazyRouter.lazyRouter.ping('')).resolves.toEqual([ - 'lazyRouter', - 'lazyRouter', - 'ping', - ]) + expect(vi.mocked(createProcedureCaller).mock.results[4]?.value).toBeCalledTimes(1) + expect(vi.mocked(createProcedureCaller).mock.results[4]?.value).toBeCalledWith({ val: '123' }) }) it('hooks', async () => { @@ -200,79 +97,34 @@ describe('createRouterCaller', () => { const onSuccess = vi.fn() const onError = vi.fn() const onFinish = vi.fn() - const onExecute = vi.fn() - - const procedure = os.input(z.string()).func(() => 'output') + const execute = vi.fn() - const context = { val: 'context' } const caller = createRouterCaller({ - router: { procedure, nested: { procedure } }, - context, - execute: async (input, context, meta) => { - onStart(input, context, meta) - onExecute(input, context, meta) - try { - const output = await meta.next() - onSuccess(output, context, meta) - return output - } - catch (e) { - onError(e, context, meta) - throw e - } - }, + router, + context: { auth: true }, onStart, onSuccess, onError, onFinish, + execute, }) - const meta = { - path: ['procedure'], - procedure, - } - - await caller.procedure('input') - expect(onStart).toBeCalledTimes(2) - expect(onStart).toHaveBeenNthCalledWith(1, 'input', context, { ...meta, next: expect.any(Function) }) - expect(onStart).toHaveBeenNthCalledWith(2, { input: 'input', status: 'pending' }, context, meta) - expect(onExecute).toBeCalledTimes(1) - expect(onExecute).toHaveBeenCalledWith('input', context, { ...meta, next: expect.any(Function) }) - expect(onSuccess).toBeCalledTimes(2) - expect(onSuccess).toHaveBeenNthCalledWith(1, { output: 'output', input: 'input', status: 'success' }, context, meta) - expect(onSuccess).toHaveBeenNthCalledWith(2, 'output', context, { ...meta, next: expect.any(Function) }) - expect(onError).not.toBeCalled() - expect(onFinish).toBeCalledTimes(1) - expect(onFinish).toBeCalledWith({ output: 'output', input: 'input', status: 'success' }, context, meta) - - onSuccess.mockClear() - onError.mockClear() - onFinish.mockClear() - onExecute.mockClear() - - // @ts-expect-error - invalid input - await expect(caller.nested.procedure(123)).rejects.toThrowError( - 'Input validation failed', - ) - - const meta2 = { - path: ['nested', 'procedure'], - procedure, - } + expect(caller.pong({ val: '123' })).toEqual('__mocked__') - const error2 = new ORPCError({ - message: 'Input validation failed', - code: 'BAD_REQUEST', - cause: expect.any(Error), - }) + expect(createProcedureCaller).toBeCalledTimes(1) + expect(createProcedureCaller).toHaveBeenCalledWith(expect.objectContaining({ + procedure: pong, + context: { auth: true }, + path: ['pong'], + onStart, + onSuccess, + onError, + onFinish, + execute, + })) + }) - expect(onExecute).toBeCalledTimes(1) - expect(onExecute).toHaveBeenCalledWith(123, context, { ...meta2, next: expect.any(Function) }) - expect(onError).toBeCalledTimes(2) - expect(onError).toHaveBeenNthCalledWith(1, { input: 123, error: error2, status: 'error' }, context, meta2) - expect(onError).toHaveBeenNthCalledWith(2, error2, context, { ...meta2, next: expect.any(Function) }) - expect(onSuccess).not.toBeCalled() - expect(onFinish).toBeCalledTimes(1) - expect(onFinish).toBeCalledWith({ input: 123, error: error2, status: 'error' }, context, meta2) + it('not recursive on symbol', () => { + expect((caller as any)[Symbol('something')]).toBeUndefined() }) }) diff --git a/packages/server/src/router-caller.ts b/packages/server/src/router-caller.ts index b2cccadcf..724e067ac 100644 --- a/packages/server/src/router-caller.ts +++ b/packages/server/src/router-caller.ts @@ -1,47 +1,49 @@ +import type { SchemaInput, SchemaOutput } from '@orpc/contract' import type { Hooks, Merge, Value } from '@orpc/shared' -import type { ANY_LAZY_PROCEDURE, ANY_PROCEDURE } from './procedure' -import type { Router } from './router' +import type { Lazy } from './lazy' +import type { Procedure } from './procedure' +import type { ANY_ROUTER, Router } from './router' +import type { Caller, Meta } from './types' import { isLazy } from './lazy' +import { decorateLazy } from './lazy-decorated' import { isProcedure } from './procedure' -import { createProcedureCaller, type ProcedureCaller } from './procedure-caller' +import { createProcedureCaller } from './procedure-caller' -export interface CreateRouterCallerOptions< - TRouter extends Router, -> extends Hooks< - unknown, - unknown, - TRouter extends Router ? UContext : never, - { path: string[], procedure: ANY_PROCEDURE } - > { - router: TRouter - - /** - * The context used when calling the procedure. - */ - context: Value< - TRouter extends Router ? UContext : never - > - - /** - * This is helpful for logging and analytics. - * - * @internal - */ - basePath?: string[] -} +export type CreateRouterCallerOptions< + TRouter extends ANY_ROUTER, +> = + & { + router: TRouter + /** + * This is helpful for logging and analytics. + * + * @internal + */ + path?: string[] + } + & (TRouter extends Router + ? undefined extends UContext ? { context?: Value } : { context: Value } + : never) + & Hooks ? UContext : never, Meta> export type RouterCaller< - TRouter extends Router, + TRouter extends ANY_ROUTER, > = { - [K in keyof TRouter]: TRouter[K] extends ANY_PROCEDURE | ANY_LAZY_PROCEDURE - ? ProcedureCaller - : TRouter[K] extends Router + [K in keyof TRouter]: TRouter[K] extends + | Procedure + | Lazy> + ? Caller, SchemaOutput> + : TRouter[K] extends ANY_ROUTER ? RouterCaller - : never + : TRouter[K] extends Lazy + ? U extends ANY_ROUTER + ? RouterCaller + : never + : never } export function createRouterCaller< - TRouter extends Router, + TRouter extends ANY_ROUTER, >( options: CreateRouterCallerOptions, ): RouterCaller { @@ -49,16 +51,18 @@ export function createRouterCaller< } function createRouterCallerInternal( - options: Merge>, { - router: Router | Router[keyof Router] + options: Merge, { + router: ANY_ROUTER | ANY_ROUTER[string] }>, ) { + const router = isLazy(options.router) ? decorateLazy(options.router) : options.router + const procedureCaller = isLazy(options.router) || isProcedure(options.router) ? createProcedureCaller({ ...options, - procedure: options.router as any, + procedure: router as any, context: options.context, - path: options.basePath, + path: options.path, }) : {} @@ -68,12 +72,12 @@ function createRouterCallerInternal( return Reflect.get(target, key) } - const next = (options.router as any)[key] + const next = (router as any)[key] return createRouterCallerInternal({ ...options, router: next, - basePath: [...(options.basePath ?? []), key], + path: [...(options.path ?? []), key], }) }, }) From d005b75af1a0d17d2b8f021b337e4e27680a352b Mon Sep 17 00:00:00 2001 From: unnoq Date: Wed, 18 Dec 2024 14:01:40 +0700 Subject: [PATCH 25/51] router implementer --- .../src/implementer-chainable.test-d.ts | 121 +++++++++++ .../server/src/implementer-chainable.test.ts | 116 ++++++++++ packages/server/src/implementer-chainable.ts | 71 ++++++ packages/server/src/router-builder.test-d.ts | 2 +- .../server/src/router-implementer.test-d.ts | 105 +++++++++ .../server/src/router-implementer.test.ts | 202 ++++++++---------- packages/server/src/router-implementer.ts | 106 +++++---- 7 files changed, 553 insertions(+), 170 deletions(-) create mode 100644 packages/server/src/implementer-chainable.test-d.ts create mode 100644 packages/server/src/implementer-chainable.test.ts create mode 100644 packages/server/src/implementer-chainable.ts create mode 100644 packages/server/src/router-implementer.test-d.ts diff --git a/packages/server/src/implementer-chainable.test-d.ts b/packages/server/src/implementer-chainable.test-d.ts new file mode 100644 index 000000000..63630b156 --- /dev/null +++ b/packages/server/src/implementer-chainable.test-d.ts @@ -0,0 +1,121 @@ +import type { ChainableImplementer } from './implementer-chainable' +import type { Middleware } from './middleware' +import type { ProcedureImplementer } from './procedure-implementer' +import type { RouterImplementer } from './router-implementer' +import type { WELL_CONTEXT } from './types' +import { oc } from '@orpc/contract' +import { z } from 'zod' +import { createChainableImplementer } from './implementer-chainable' + +const schema = z.object({ val: z.string().transform(val => Number(val)) }) + +const ping = oc.input(schema).output(schema) +const pong = oc.route({ method: 'GET', path: '/ping' }) + +const contract = oc.router({ + ping, + pong, + nested: { + ping, + pong, + }, +}) + +describe('ChainableImplementer', () => { + it('with procedure', () => { + expectTypeOf(createChainableImplementer(ping)).toEqualTypeOf< + ProcedureImplementer + >() + + expectTypeOf(createChainableImplementer(pong)).toEqualTypeOf< + ProcedureImplementer + >() + }) + + it('with router', () => { + const implementer = createChainableImplementer(contract) + + expectTypeOf(implementer).toMatchTypeOf< + Omit, '~type' | '~orpc'> + >() + + expectTypeOf(implementer.ping).toEqualTypeOf< + ProcedureImplementer + >() + + expectTypeOf(implementer.pong).toEqualTypeOf< + ProcedureImplementer + >() + + expectTypeOf(implementer.nested).toMatchTypeOf< + Omit, '~type' | '~orpc'> + >() + + expectTypeOf(implementer.nested.ping).toEqualTypeOf< + ProcedureImplementer + >() + + expectTypeOf(implementer.nested.pong).toEqualTypeOf< + ProcedureImplementer + >() + }) + + it('not expose properties of router implementer', () => { + const implementer = createChainableImplementer(contract) + + expectTypeOf(implementer).not.toHaveProperty('~orpc') + expectTypeOf(implementer).not.toHaveProperty('~type') + expectTypeOf(implementer.router).not.toHaveProperty('~orpc') + expectTypeOf(implementer.router).not.toHaveProperty('~type') + }) + + it('works on conflicted', () => { + const contract = oc.router({ + use: ping, + router: { + use: ping, + router: pong, + }, + }) + + const implementer = createChainableImplementer(contract) + + expectTypeOf(implementer).toMatchTypeOf< + Omit, '~type' | '~orpc'> + >() + + expectTypeOf(implementer.use).toMatchTypeOf< + ProcedureImplementer + >() + + expectTypeOf(implementer.router).toMatchTypeOf< + Omit, '~type' | '~orpc'> + >() + + expectTypeOf(implementer.router.use).toMatchTypeOf< + ProcedureImplementer + >() + + expectTypeOf(implementer.router.router).toMatchTypeOf< + ProcedureImplementer + >() + }) +}) + +describe('createChainableImplementer', () => { + it('with procedure', () => { + const implementer = createChainableImplementer(ping) + expectTypeOf(implementer).toEqualTypeOf>() + }) + + it('with router', () => { + const implementer = createChainableImplementer(contract) + expectTypeOf(implementer).toEqualTypeOf>() + }) + + it('with middlewares', () => { + const mid = {} as Middleware<{ auth: boolean }, { db: string }, unknown, unknown> + const implementer = createChainableImplementer(contract, [mid]) + expectTypeOf(implementer).toEqualTypeOf>() + }) +}) diff --git a/packages/server/src/implementer-chainable.test.ts b/packages/server/src/implementer-chainable.test.ts new file mode 100644 index 000000000..f2bc10b00 --- /dev/null +++ b/packages/server/src/implementer-chainable.test.ts @@ -0,0 +1,116 @@ +import { oc } from '@orpc/contract' +import { z } from 'zod' +import { createChainableImplementer } from './implementer-chainable' +import { ProcedureImplementer } from './procedure-implementer' +import { RouterImplementer } from './router-implementer' + +describe('createChainableImplementer', () => { + const schema = z.object({ val: z.string().transform(val => Number(val)) }) + + const ping = oc.input(schema).output(schema) + const pong = oc.route({ method: 'GET', path: '/ping' }) + + const contract = oc.router({ + ping, + pong, + nested: { + ping, + pong, + }, + }) + + const mid1 = vi.fn() + const mid2 = vi.fn() + const mid3 = vi.fn() + + it('with procedure', () => { + const implementer = createChainableImplementer(ping, [mid1, mid2]) + + expect(implementer).toBeInstanceOf(ProcedureImplementer) + expect(implementer['~orpc'].middlewares).toEqual([mid1, mid2]) + expect(implementer['~orpc'].contract).toBe(ping) + }) + + it('with router', () => { + const implementer = createChainableImplementer(contract, [mid1, mid2]) + + expect(implementer.use(mid3)['~orpc'].middlewares).toEqual([mid1, mid2, mid3]) + expect(implementer.use(mid3)['~orpc'].contract).toBe(contract) + + expect(implementer.ping).toBeInstanceOf(ProcedureImplementer) + expect(implementer.ping['~orpc'].middlewares).toEqual([mid1, mid2]) + expect(implementer.ping['~orpc'].contract).toBe(ping) + + expect(implementer.pong).toBeInstanceOf(ProcedureImplementer) + expect(implementer.pong['~orpc'].middlewares).toEqual([mid1, mid2]) + expect(implementer.pong['~orpc'].contract).toBe(pong) + + expect(implementer.nested.use(mid3)['~orpc'].middlewares).toEqual([mid1, mid2, mid3]) + expect(implementer.nested.use(mid3)['~orpc'].contract).toBe(contract.nested) + + expect(implementer.nested.ping).toBeInstanceOf(ProcedureImplementer) + expect(implementer.nested.ping['~orpc'].middlewares).toEqual([mid1, mid2]) + expect(implementer.nested.ping['~orpc'].contract).toBe(contract.nested.ping) + + expect(implementer.nested.pong).toBeInstanceOf(ProcedureImplementer) + expect(implementer.nested.pong['~orpc'].middlewares).toEqual([mid1, mid2]) + expect(implementer.nested.pong['~orpc'].contract).toBe(contract.nested.pong) + }) + + describe('on conflicted', () => { + const contract = oc.router({ + 'use': ping, + 'router': { + use: ping, + router: pong, + }, + '~orpc': { + use: ping, + router: pong, + }, + '~type': { + use: ping, + router: pong, + }, + }) + + const implementer = createChainableImplementer(contract, [mid1, mid2]) + + it('still works', () => { + expect(implementer.use(mid3)['~orpc'].middlewares).toEqual([mid1, mid2, mid3]) + expect(implementer.use(mid3)['~orpc'].contract).toBe(contract) + + expect(implementer.use).toBeTypeOf('function') + expect(implementer.use.use(mid3)).toBeInstanceOf(ProcedureImplementer) + expect(implementer.use.use(mid3)['~orpc'].middlewares).toEqual([mid1, mid2, mid3]) + expect(implementer.use['~orpc'].contract).toBe(ping) + + expect(implementer.router).toBeTypeOf('function') + expect(implementer.router.use(mid3)).toBeInstanceOf(RouterImplementer) + expect(implementer.router.use(mid3)['~orpc'].middlewares).toEqual([mid1, mid2, mid3]) + expect(implementer.router.use(mid3)['~orpc'].contract).toBe(contract.router) + + expect(implementer.router.router).toBeTypeOf('function') + expect(implementer.router.router.use(mid3)).toBeInstanceOf(ProcedureImplementer) + expect(implementer.router.router.use(mid3)['~orpc'].middlewares).toEqual([mid1, mid2, mid3]) + expect(implementer.router.router['~orpc'].contract).toBe(contract.router.router) + + expect(implementer.router.use).toBeTypeOf('function') + expect(implementer.router.use.use(mid3)).toBeInstanceOf(ProcedureImplementer) + expect(implementer.router.use.use(mid3)['~orpc'].middlewares).toEqual([mid1, mid2, mid3]) + expect(implementer.router.use['~orpc'].contract).toBe(contract.router.use) + + expect(implementer['~orpc'].use).toBeTypeOf('function') + expect(implementer['~orpc'].use.use(mid3)).toBeInstanceOf(ProcedureImplementer) + expect(implementer['~orpc'].use.use(mid3)['~orpc'].middlewares).toEqual([mid1, mid2, mid3]) + expect(implementer['~orpc'].use['~orpc'].contract).toBe(contract.router.use) + }) + + it('not recursive on symbol', () => { + expect((implementer as any)[Symbol('something')]).toBeUndefined() + expect((implementer.use as any)[Symbol('something')]).toBeUndefined() + expect((implementer.router as any)[Symbol('something')]).toBeUndefined() + expect((implementer.router.use as any)[Symbol('something')]).toBeUndefined() + }) + }) +}) diff --git a/packages/server/src/implementer-chainable.ts b/packages/server/src/implementer-chainable.ts new file mode 100644 index 000000000..5f752c005 --- /dev/null +++ b/packages/server/src/implementer-chainable.ts @@ -0,0 +1,71 @@ +import type { Middleware } from './middleware' +import type { Context, MergeContext, WELL_CONTEXT } from './types' +import { type ANY_CONTRACT_PROCEDURE, type ContractProcedure, type ContractRouter, isContractProcedure } from '@orpc/contract' +import { ProcedureImplementer } from './procedure-implementer' +import { RouterImplementer } from './router-implementer' + +export type ChainableImplementer< + TContext extends Context, + TExtraContext extends Context, + TContract extends ContractRouter | ANY_CONTRACT_PROCEDURE, +> = TContract extends ContractProcedure + ? ProcedureImplementer + : TContract extends ContractRouter ? { + [K in keyof TContract]: ChainableImplementer + } & Omit, '~type' | '~orpc'> + : never + +export function createChainableImplementer< + TContext extends Context = WELL_CONTEXT, + TExtraContext extends Context = undefined, + TContract extends ContractRouter | ANY_CONTRACT_PROCEDURE = any, +>( + contract: TContract, + middlewares?: Middleware, Partial | undefined, unknown, any>[], +): ChainableImplementer { + if (isContractProcedure(contract)) { + const implementer = new ProcedureImplementer({ + contract, + middlewares, + }) + + return implementer as any + } + + const chainable: Record = {} + + for (const key in contract) { + chainable[key] = createChainableImplementer(contract[key]!, middlewares) + } + + const routerImplementer = new RouterImplementer({ contract, middlewares }) + + const merged = new Proxy(chainable, { + get(target, key) { + const next = Reflect.get(target, key) + const method = Reflect.get(routerImplementer, key) + + if (typeof key !== 'string' || typeof method !== 'function') { + return next + } + + return new Proxy(method.bind(routerImplementer), { + get(target, key) { + // TODO: create own utils for object callable proxy + if ( + typeof key !== 'string' + || !next + || (typeof next !== 'object' && typeof next !== 'function') + || !(key in next) + ) { + return Reflect.get(target, key) + } + + return next[key] + }, + }) + }, + }) + + return merged as any +} diff --git a/packages/server/src/router-builder.test-d.ts b/packages/server/src/router-builder.test-d.ts index 5048640e8..0157c304f 100644 --- a/packages/server/src/router-builder.test-d.ts +++ b/packages/server/src/router-builder.test-d.ts @@ -103,7 +103,7 @@ describe('self chainable', () => { const mid4 = {} as Middleware<{ auth: boolean }, { dev: string }, unknown, { val: string }> const mid5 = {} as Middleware<{ auth: boolean }, { dev: string }, unknown, { val: number }> - const mid6 = {} as Middleware<{ auth: boolean }, { dev: string }, { val: number }, unknown> + const mid6 = {} as Middleware<{ auth: 'invalid' }, undefined, any, unknown> // @ts-expect-error - invalid middleware builder.use(mid4) diff --git a/packages/server/src/router-implementer.test-d.ts b/packages/server/src/router-implementer.test-d.ts new file mode 100644 index 000000000..96b08080d --- /dev/null +++ b/packages/server/src/router-implementer.test-d.ts @@ -0,0 +1,105 @@ +import type { DecoratedLazy } from './lazy-decorated' +import type { Middleware } from './middleware' +import type { AdaptedRouter } from './router-builder' +import type { RouterImplementer } from './router-implementer' +import { oc } from '@orpc/contract' +import { z } from 'zod' +import { lazy } from './lazy' +import { Procedure } from './procedure' + +const schema = z.object({ val: z.string().transform(val => Number(val)) }) + +const ping = oc.input(schema).output(schema) +const pong = oc.route({ method: 'GET', path: '/ping' }) + +const contract = oc.router({ + ping, + pong, + nested: { + ping, + pong, + }, +}) + +const pingImpl = new Procedure({ + contract: ping, + func: vi.fn(), +}) + +const pongImpl = new Procedure({ + contract: pong, + func: vi.fn(), +}) + +const router = { + ping: pingImpl, + pong: pongImpl, + nested: { + ping: pingImpl, + pong: pongImpl, + }, +} + +const routerWithLazy = { + ping: lazy(() => Promise.resolve({ default: pingImpl })), + pong: pongImpl, + nested: lazy(() => Promise.resolve({ + default: { + ping: pingImpl, + pong: lazy(() => Promise.resolve({ default: pongImpl })), + }, + })), +} + +const implementer = {} as RouterImplementer<{ auth: boolean }, { db: string }, typeof contract> + +describe('self chainable', () => { + it('use middleware', () => { + const mid1 = {} as Middleware<{ auth: boolean }, undefined, unknown, unknown> + const mid2 = {} as Middleware<{ auth: boolean }, { dev: string }, unknown, unknown> + const mid3 = {} as Middleware<{ auth: boolean, db: string }, { dev: string }, unknown, unknown> + + expectTypeOf(implementer.use(mid1)).toEqualTypeOf() + expectTypeOf(implementer.use(mid2)).toEqualTypeOf< + RouterImplementer<{ auth: boolean }, { db: string } & { dev: string }, typeof contract> + >() + expectTypeOf(implementer.use(mid3)).toEqualTypeOf< + RouterImplementer<{ auth: boolean }, { db: string } & { dev: string }, typeof contract> + >() + + const mid4 = {} as Middleware<{ auth: boolean }, { dev: string }, unknown, { val: string }> + const mid5 = {} as Middleware<{ auth: boolean }, { dev: string }, unknown, { val: number }> + const mid6 = {} as Middleware<{ auth: 'invalid' }, undefined, any, any> + + // @ts-expect-error - invalid middleware + implementer.use(mid4) + // @ts-expect-error - invalid middleware + implementer.use(mid5) + // @ts-expect-error - invalid middleware + implementer.use(mid6) + // @ts-expect-error - invalid middleware + implementer.use(true) + // @ts-expect-error - invalid middleware + implementer.use(() => {}) + }) +}) + +it('to AdaptedRouter', () => { + expectTypeOf(implementer.router(router)).toMatchTypeOf< + AdaptedRouter<{ auth: boolean }, typeof router> + >() + + expectTypeOf(implementer.router(routerWithLazy)).toMatchTypeOf< + AdaptedRouter<{ auth: boolean }, typeof routerWithLazy> + >() +}) + +it('to AdaptedLazy', () => { + expectTypeOf(implementer.lazy(() => Promise.resolve({ default: router }))).toMatchTypeOf< + DecoratedLazy> + >() + + expectTypeOf(implementer.lazy(() => Promise.resolve({ default: routerWithLazy }))).toMatchTypeOf< + DecoratedLazy> + >() +}) diff --git a/packages/server/src/router-implementer.test.ts b/packages/server/src/router-implementer.test.ts index c9182491d..5749ba9ad 100644 --- a/packages/server/src/router-implementer.test.ts +++ b/packages/server/src/router-implementer.test.ts @@ -1,138 +1,116 @@ import { oc } from '@orpc/contract' import { z } from 'zod' -import { os, RouterImplementer } from '.' - -const cp1 = oc.input(z.string()).output(z.string()) -const cp2 = oc.output(z.string()) -const cp3 = oc.route({ method: 'GET', path: '/test' }) -const cr = oc.router({ - p1: cp1, - nested: oc.router({ - p2: cp2, - }), - nested2: { - p3: cp3, - }, +import { Procedure } from './procedure' +import { RouterBuilder } from './router-builder' +import { ROUTER_CONTRACT_SYMBOL, RouterImplementer } from './router-implementer' + +vi.mock('./router-builder', () => ({ + RouterBuilder: vi.fn(() => ({ + router: vi.fn(() => ({ mocked: true })), + lazy: vi.fn(() => ({ mocked: true })), + })), +})) + +beforeEach(() => { + vi.clearAllMocks() }) -const osw = os.context<{ auth: boolean }>().contract(cr) +const schema = z.object({ val: z.string().transform(val => Number(val)) }) + +const ping = oc.input(schema).output(schema) +const pong = oc.route({ method: 'GET', path: '/ping' }) -const p1 = osw.p1.func(() => { - return 'unnoq' +const contract = oc.router({ + ping, + pong, + nested: { + ping, + pong, + }, }) -const p2 = osw.nested.p2.func(() => { - return 'unnoq' +const pingImpl = new Procedure({ + contract: ping, + func: vi.fn(), }) -const p3 = osw.nested2.p3.func(() => { - return 'unnoq' +const pongImpl = new Procedure({ + contract: pong, + func: vi.fn(), }) -it('required all procedure match', () => { - const implementer = new RouterImplementer<{ auth: boolean }, typeof cr>({ - contract: cr, - }) +const router = { + ping: pingImpl, + pong: pongImpl, + nested: { + ping: pingImpl, + pong: pongImpl, + }, +} - implementer.router({ - p1, - nested: { - p2: os.contract(cp2).func(() => ''), - }, - nested2: { - p3, - }, - }) +const mid = vi.fn() +const implementer = new RouterImplementer({ + contract, + middlewares: [mid], +}) - implementer.router({ - p1, - nested: { - p2: os.contract(cp2).func(() => ''), - }, - nested2: { - p3: os.context<{ auth: boolean }>().lazy(() => Promise.resolve({ default: p3 })), - }, - }) +describe('self chainable', () => { + it('use middleware', () => { + const mid1 = vi.fn() + const mid2 = vi.fn() + const mid3 = vi.fn() - implementer.router({ - p1, - nested: { - p2: os.contract(cp2).func(() => ''), - }, - nested2: osw.nested2.lazy(() => Promise.resolve({ default: { - p3, - } })), - }) + const implementer = new RouterImplementer({ + contract, + }) + + const applied1 = implementer.use(mid1) + expect(applied1).not.toBe(implementer) + expect(applied1).toBeInstanceOf(RouterImplementer) + expect(applied1['~orpc'].middlewares).toEqual([mid1]) - implementer.router({ - p1, - nested: { - p2: os.contract(cp2).lazy(() => Promise.resolve({ default: os.output(z.string()).func(() => { - return '' - }) })), - }, - nested2: osw.nested2.lazy(() => Promise.resolve({ - default: { - p3: osw.nested2.p3.lazy(() => Promise.resolve({ default: p3 })), - }, - })), + const applied2 = applied1.use(mid2).use(mid3) + expect(applied2['~orpc'].middlewares).toEqual([mid1, mid2, mid3]) }) +}) + +describe('to AdaptedRouter', () => { + it('works', () => { + expect(implementer.router(router)).toEqual({ mocked: true }) - implementer.lazy(() => Promise.resolve({ - default: { - p1, - nested: { - p2: os.contract(cp2).func(() => ''), - }, - nested2: osw.nested2.lazy(() => Promise.resolve({ - default: { - p3, - }, - })), - }, - })) - - implementer.router({ - // @ts-expect-error p1 is mismatch - p1: os.func(() => { }), - nested: { - p2, - }, - nested2: { - p3, - }, + expect(RouterBuilder).toBeCalledTimes(1) + expect(RouterBuilder).toBeCalledWith(expect.objectContaining({ + middlewares: [mid], + })) + + const builder = vi.mocked(RouterBuilder).mock.results[0]?.value + expect(vi.mocked(builder.router)).toBeCalledTimes(1) + expect(vi.mocked(builder.router)).toBeCalledWith(router) }) - implementer.router({ - // @ts-expect-error p1 is mismatch - p1: p2, - nested: { - p2, - }, - nested2: { - p3, - }, + it('attach contract', () => { + const adapted = implementer.router(router) as any + expect(adapted[ROUTER_CONTRACT_SYMBOL]).toBe(contract) }) +}) + +describe('to AdaptedLazy', () => { + it('works', () => { + const loader = () => Promise.resolve({ default: router }) + expect(implementer.lazy(loader)).toEqual({ mocked: true }) - // @ts-expect-error required all procedure match - implementer.router({}) + expect(RouterBuilder).toBeCalledTimes(1) + expect(RouterBuilder).toBeCalledWith(expect.objectContaining({ + middlewares: [mid], + })) - implementer.router({ - p1, - nested: { - p2, - }, - // @ts-expect-error missing p3 - nested2: {}, + const builder = vi.mocked(RouterBuilder).mock.results[0]?.value + expect(vi.mocked(builder.lazy)).toBeCalledTimes(1) + expect(vi.mocked(builder.lazy)).toBeCalledWith(loader) }) - implementer.router({ - p1, - nested: { - p2, - }, - nested2: { - p3: p3.prefix('/test'), - }, + it('attach contract', () => { + const adapted = implementer.lazy(() => Promise.resolve({ default: router })) as any + expect(adapted[ROUTER_CONTRACT_SYMBOL]).toBe(contract) }) }) diff --git a/packages/server/src/router-implementer.ts b/packages/server/src/router-implementer.ts index 0ea48df31..019e43517 100644 --- a/packages/server/src/router-implementer.ts +++ b/packages/server/src/router-implementer.ts @@ -1,81 +1,73 @@ +import type { ContractRouter } from '@orpc/contract' import type { DecoratedLazy } from './lazy-decorated' import type { Middleware } from './middleware' import type { Router } from './router' import type { AdaptedRouter } from './router-builder' -import type { Context } from './types' -import { type ContractProcedure, type ContractRouter, isContractProcedure } from '@orpc/contract' -import { ProcedureImplementer } from './procedure-implementer' +import type { Context, MergeContext } from './types' import { RouterBuilder } from './router-builder' export const ROUTER_CONTRACT_SYMBOL = Symbol('ORPC_ROUTER_CONTRACT') +export interface RouterImplementerDef< + TContext extends Context, + TExtraContext extends Context, + TContract extends ContractRouter, +> { + middlewares?: Middleware, Partial | undefined, unknown, any>[] + contract: TContract +} + export class RouterImplementer< TContext extends Context, + TExtraContext extends Context, TContract extends ContractRouter, > { - constructor( - public zz$ri: { - contract: TContract - }, - ) {} + '~type' = 'RouterImplementer' as const + '~orpc': RouterImplementerDef - router>( - router: U, - ): AdaptedRouter { - return Object.assign(new RouterBuilder({}).router(router), { - [ROUTER_CONTRACT_SYMBOL]: this.zz$ri.contract, - }) + constructor(def: RouterImplementerDef) { + this['~orpc'] = def } - lazy>( - loader: () => Promise<{ default: U }>, - ): DecoratedLazy> { - return Object.assign(new RouterBuilder({}).lazy(loader), { - [ROUTER_CONTRACT_SYMBOL]: this.zz$ri.contract, + use> | undefined = undefined>( + middleware: Middleware< + MergeContext, + U, + unknown, + unknown + >, + ): RouterImplementer, TContract> { + return new RouterImplementer({ + ...this['~orpc'], + middlewares: [...(this['~orpc'].middlewares ?? []), middleware as any], }) } -} -export type ChainedRouterImplementer< - TContext extends Context, - TContract extends ContractRouter, - TExtraContext extends Context, -> = { - [K in keyof TContract]: TContract[K] extends ContractProcedure< - infer UInputSchema, - infer UOutputSchema - > - ? ProcedureImplementer - : TContract[K] extends ContractRouter - ? ChainedRouterImplementer - : never -} & RouterImplementer - -export function chainRouterImplementer< - TContext extends Context, - TContract extends ContractRouter, - TExtraContext extends Context, ->( - contract: TContract, - middlewares?: Middleware[], -): ChainedRouterImplementer { - const result: Record = {} + router, TContract>>( + router: U, + ): AdaptedRouter { + const adapted = new RouterBuilder(this['~orpc']).router(router) - for (const key in contract) { - const item = contract[key] + const contracted = this.attachContract(adapted) - if (isContractProcedure(item)) { - result[key] = new ProcedureImplementer({ - contract: item, - middlewares, - }) - } - else { - result[key] = chainRouterImplementer(item as ContractRouter, middlewares) - } + return contracted } - const implementer = new RouterImplementer({ contract }) + lazy, TContract>>( + loader: () => Promise<{ default: U }>, + ): DecoratedLazy> { + const adapted = new RouterBuilder(this['~orpc']).lazy(loader) - return Object.assign(implementer, result) as any + const contracted = this.attachContract(adapted) + + return contracted + } + + private attachContract( + router: T, + ): T { + return Object.defineProperty(router, ROUTER_CONTRACT_SYMBOL, { + value: this['~orpc'].contract, + }) + } } From b0a3383bc9a80093ae154d2668b2f88e864da659 Mon Sep 17 00:00:00 2001 From: unnoq Date: Wed, 18 Dec 2024 15:10:20 +0700 Subject: [PATCH 26/51] builder --- packages/server/src/builder.test-d.ts | 209 ++++++++++++ packages/server/src/builder.test.ts | 443 +++++++------------------- packages/server/src/builder.ts | 241 +++++--------- 3 files changed, 403 insertions(+), 490 deletions(-) create mode 100644 packages/server/src/builder.test-d.ts diff --git a/packages/server/src/builder.test-d.ts b/packages/server/src/builder.test-d.ts new file mode 100644 index 000000000..3cfd86485 --- /dev/null +++ b/packages/server/src/builder.test-d.ts @@ -0,0 +1,209 @@ +import type { ChainableImplementer } from './implementer-chainable' +import type { DecoratedLazy } from './lazy-decorated' +import type { DecoratedMiddleware, Middleware, MiddlewareMeta } from './middleware' +import type { ANY_PROCEDURE, Procedure } from './procedure' +import type { ProcedureBuilder } from './procedure-builder' +import type { DecoratedProcedure } from './procedure-decorated' +import type { AdaptedRouter, RouterBuilder } from './router-builder' +import type { Meta, WELL_CONTEXT } from './types' +import { oc } from '@orpc/contract' +import { z } from 'zod' +import { Builder } from './builder' + +const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) + +const builder = new Builder<{ auth: boolean }, { db: string }>({}) + +describe('self chainable', () => { + it('define context', () => { + expectTypeOf(builder.context()).toEqualTypeOf>() + expectTypeOf(builder.context<{ db: string }>()).toEqualTypeOf>() + expectTypeOf(builder.context<{ auth: boolean }>()).toEqualTypeOf>() + }) + + it('use middleware', () => { + expectTypeOf( + builder.use({} as Middleware<{ auth: boolean }, undefined, unknown, unknown>), + ).toEqualTypeOf>() + expectTypeOf( + builder.use({} as Middleware<{ auth: boolean }, { dev: string }, unknown, unknown>), + ).toEqualTypeOf>() + expectTypeOf( + builder.use({} as Middleware), + ).toEqualTypeOf>() + + // @ts-expect-error - context is not match + builder.use({} as Middleware<{ auth: 'invalid' }, undefined, unknown, unknown>) + + // @ts-expect-error - extra context is conflict with context + builder.use({} as Middleware) + + // @ts-expect-error - expected input is not match with unknown + builder.use({} as Middleware) + + // @ts-expect-error - expected output is not match with unknown + builder.use({} as Middleware) + + // @ts-expect-error - invalid middleware + builder.use(() => {}) + }) +}) + +describe('create middleware', () => { + it('works', () => { + const mid = builder.middleware((input, context, meta) => { + expectTypeOf(input).toEqualTypeOf() + expectTypeOf(context).toEqualTypeOf<{ auth: boolean } & { db: string }>() + expectTypeOf(meta).toEqualTypeOf>() + + return meta.next({ + context: { + dev: true, + }, + }) + }) + + expectTypeOf(mid).toEqualTypeOf< + DecoratedMiddleware<{ auth: boolean } & { db: string }, { dev: boolean }, unknown, any> + >() + + // @ts-expect-error - conflict extra context and context + builder.middleware((input, context, meta) => meta.next({ + context: { + auth: 'invalid', + }, + })) + }) +}) + +describe('to ProcedureBuilder', () => { + it('route', () => { + expectTypeOf(builder.route({ path: '/test', method: 'GET' })).toEqualTypeOf< + ProcedureBuilder<{ auth: boolean }, { db: string }, undefined, undefined> + >() + + // @ts-expect-error - invalid path + builder.route({ path: '' }) + + // @ts-expect-error - invalid method + builder.route({ method: '' }) + }) + + it('input', () => { + expectTypeOf(builder.input(schema, { val: '123' })).toEqualTypeOf< + ProcedureBuilder<{ auth: boolean }, { db: string }, typeof schema, undefined> + >() + + builder.input(schema) + // @ts-expect-error - invalid example + builder.input(schema, { val: 123 }) + // @ts-expect-error - invalid schema + builder.input({}) + }) + + it('output', () => { + expectTypeOf(builder.output(schema, { val: 123 })).toEqualTypeOf< + ProcedureBuilder<{ auth: boolean }, { db: string }, undefined, typeof schema> + >() + + builder.output(schema) + // @ts-expect-error - invalid example + builder.output(schema, { val: '123' }) + // @ts-expect-error - invalid schema + builder.output({}) + }) +}) + +describe('to DecoratedProcedure', () => { + it('func', () => { + expectTypeOf(builder.func((input, context, meta) => { + expectTypeOf(input).toEqualTypeOf() + expectTypeOf(context).toEqualTypeOf<{ auth: boolean } & { db: string }>() + expectTypeOf(meta).toEqualTypeOf() + + return 456 + })).toMatchTypeOf< + DecoratedProcedure<{ auth: boolean }, { db: string }, undefined, undefined, number> + >() + }) +}) + +describe('to RouterBuilder', () => { + it('prefix', () => { + expectTypeOf(builder.prefix('/test')).toEqualTypeOf< + RouterBuilder<{ auth: boolean }, { db: string }> + >() + + // @ts-expect-error invalid prefix + builder.prefix('') + }) + + it('tags', () => { + expectTypeOf(builder.tags('test', 'test2')).toEqualTypeOf< + RouterBuilder<{ auth: boolean }, { db: string }> + >() + + // @ts-expect-error invalid tags + builder.tags(123) + }) +}) + +it('to AdaptedRouter', () => { + const ping = {} as Procedure<{ auth: boolean, db: string }, undefined, undefined, undefined, unknown> + const router = { + ping, + nested: { + ping, + }, + } + expectTypeOf(builder.router(router)).toEqualTypeOf< + AdaptedRouter<{ auth: boolean }, typeof router> + >() + + // @ts-expect-error - context is not match + builder.router({ ping: {} as Procedure<{ invalid: true }, undefined, undefined, undefined, unknown> }) +}) + +it('to DecoratedLazy', () => { + const ping = {} as Procedure<{ auth: boolean, db: string }, undefined, undefined, undefined, unknown> + const router = { + ping, + nested: { + ping, + }, + } + + expectTypeOf( + builder.lazy(() => Promise.resolve({ default: router })), + ).toEqualTypeOf< + DecoratedLazy> + >() + + // @ts-expect-error - context is not match + builder.lazy(() => Promise.resolve({ default: { + ping: {} as Procedure<{ invalid: true }, undefined, undefined, undefined, unknown>, + } })) +}) + +it('to ChainableImplementer', () => { + const schema = z.object({ val: z.string().transform(val => Number(val)) }) + + const ping = oc.input(schema).output(schema) + const pong = oc.route({ method: 'GET', path: '/ping' }) + + const contract = oc.router({ + ping, + pong, + nested: { + ping, + pong, + }, + }) + + expectTypeOf(builder.contract(contract)).toEqualTypeOf< + ChainableImplementer<{ auth: boolean }, { db: string }, typeof contract> + >() + + /// @ts-expect-error - context is not match + builder.contract({} as ANY_PROCEDURE) +}) diff --git a/packages/server/src/builder.test.ts b/packages/server/src/builder.test.ts index d693215a2..d14fb50aa 100644 --- a/packages/server/src/builder.test.ts +++ b/packages/server/src/builder.test.ts @@ -1,373 +1,176 @@ -import type { - Builder, - DecoratedMiddleware, - DecoratedProcedure, - Meta, - MiddlewareMeta, -} from '.' -import { oc } from '@orpc/contract' import { z } from 'zod' -import { - isProcedure, - os, - ProcedureBuilder, - ProcedureImplementer, - RouterImplementer, -} from '.' +import { Builder } from './builder' +import { createChainableImplementer } from './implementer-chainable' +import { isProcedure } from './procedure' +import { ProcedureBuilder } from './procedure-builder' import { RouterBuilder } from './router-builder' -it('context method', () => { - expectTypeOf< - typeof os extends Builder ? TContext : never - >().toEqualTypeOf>() +vi.mock('./router-builder', () => ({ + RouterBuilder: vi.fn(() => ({ + router: vi.fn(() => ({ mocked: true })), + lazy: vi.fn(() => ({ mocked: true })), + })), +})) - const os2 = os.context<{ foo: 'bar' }>() +vi.mock('./implementer-chainable', () => ({ + createChainableImplementer: vi.fn(() => ({ mocked: true })), +})) - expectTypeOf< - typeof os2 extends Builder ? TContext : never - >().toEqualTypeOf<{ foo: 'bar' }>() +beforeEach(() => { + vi.clearAllMocks() +}) - const os3 = os.context<{ foo: 'bar' }>().context() +const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) - expectTypeOf< - typeof os3 extends Builder ? TContext : never - >().toEqualTypeOf<{ foo: 'bar' }>() +const mid = vi.fn() +const builder = new Builder({ + middlewares: [mid], }) -describe('use middleware', () => { - type Context = { auth: boolean } - - const osw = os.context() +describe('self chainable', () => { + it('define context', () => { + const applied = builder.context() - it('infer types', () => { - osw.use((input, context, meta) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf() - expectTypeOf(meta).toEqualTypeOf>() + expect(applied).not.toBe(builder) + expect(applied).toBeInstanceOf(Builder) - return meta.next({}) - }) + expect(applied['~orpc'].middlewares).toEqual(undefined) }) - it('can map context', () => { - osw - .use((_, __, meta) => { - return meta.next({ context: { userId: '1' } }) - }) - .use((_, context, meta) => { - expectTypeOf(context).toMatchTypeOf() + it('use middleware', () => { + const builder = new Builder({ + }) - return meta.next({}) - }) - }) + const mid1 = vi.fn() + const mid2 = vi.fn() + const mid3 = vi.fn() - it('can map input', () => { - osw - // @ts-expect-error mismatch input - .use((input: { postId: string }) => {}) - .use( - (input: { postId: string }, _, meta) => { - return meta.next({ context: { user: '1' } }) - }, - (input) => { - expectTypeOf(input).toEqualTypeOf() - return { postId: '1' } - }, - ) - .func((_, context) => { - expectTypeOf(context).toMatchTypeOf<{ user: string }>() - }) + const applied = builder.use(mid1).use(mid2).use(mid3) + expect(applied).not.toBe(builder) + expect(applied).toBeInstanceOf(Builder) + expect(applied['~orpc'].middlewares).toEqual([mid1, mid2, mid3]) }) }) describe('create middleware', () => { - it('infer types', () => { - const mid = os - .context<{ auth: boolean }>() - .middleware((input, context, meta) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf<{ auth: boolean }>() - expectTypeOf(meta).toEqualTypeOf>() - - return meta.next({ }) - }) - - expectTypeOf(mid).toEqualTypeOf< - DecoratedMiddleware<{ auth: boolean }, undefined, unknown, any> - >() - }) + it('works', () => { + const fn = vi.fn() + const mid = builder.middleware(fn) as any - it('map context', () => { - const mid = os.context<{ auth: boolean }>().middleware((_, __, meta) => { - return meta.next({ context: { userId: '1' } }) - }) + fn.mockReturnValueOnce('__mocked__') + expect(mid).toBeTypeOf('function') + expect(mid(1, 2, 3)).toBe('__mocked__') - expectTypeOf(mid).toEqualTypeOf< - DecoratedMiddleware< - { auth: boolean }, - { userId: string }, - unknown, - any - > - >() + expect(fn).toBeCalledTimes(1) + expect(fn).toBeCalledWith(1, 2, 3) }) }) -it('router method', () => { - const pingContract = oc.input(z.string()).output(z.string()) - const userFindContract = oc - .input(z.object({ id: z.string() })) - .output(z.object({ name: z.string() })) - - const contract = oc.router({ - ping: pingContract, - user: { - find: userFindContract, - }, +describe('to ProcedureBuilder', () => { + it('route', () => { + const route = { path: '/test', method: 'GET', description: '124', tags: ['hi ho'] } as const + const result = builder.route(route) - user2: oc.router({ - find: userFindContract, - }), - - router: userFindContract, + expect(result).instanceOf(ProcedureBuilder) + expect(result['~orpc'].middlewares).toEqual([mid]) + expect(result['~orpc'].contract['~orpc'].route).toBe(route) }) - const osw = os.contract(contract) - - expect(osw.ping).instanceOf(ProcedureImplementer) - expect(osw.ping.zz$pi.contract).toEqual(pingContract) + it('input', () => { + const example = { val: '123' } + const result = builder.input(schema, example) - expect(osw.user).instanceOf(RouterImplementer) + expect(result).instanceOf(ProcedureBuilder) + expect(result['~orpc'].middlewares).toEqual([mid]) + expect(result['~orpc'].contract['~orpc'].InputSchema).toBe(schema) + expect(result['~orpc'].contract['~orpc'].inputExample).toBe(example) + }) - expect(osw.user.find).instanceOf(ProcedureImplementer) - expect(osw.user.find.zz$pi.contract).toEqual(userFindContract) + it('output', () => { + const example = { val: 123 } + const result = builder.output(schema, example) - // Because of the router keyword is special, we can't use instanceof - expect(osw.router.zz$pi.contract).toEqual(userFindContract) - expect( - osw.router.func(() => { - return { name: '' } - }), - ).toSatisfy(isProcedure) + expect(result).instanceOf(ProcedureBuilder) + expect(result['~orpc'].middlewares).toEqual([mid]) + expect(result['~orpc'].contract['~orpc'].OutputSchema).toBe(schema) + expect(result['~orpc'].contract['~orpc'].outputExample).toBe(example) + }) }) -describe('define procedure builder', () => { - const osw = os.context<{ auth: boolean }>() - const schema1 = z.object({}) - const example1 = {} - const schema2 = z.object({ a: z.string() }) - const example2 = { a: '' } - - it('input method', () => { - const builder = osw.input(schema1, example1) - - expectTypeOf(builder).toEqualTypeOf< - ProcedureBuilder<{ auth: boolean }, undefined, typeof schema1, undefined> - >() - - expect(builder).instanceOf(ProcedureBuilder) - expect(builder.zz$pb.middlewares).toBe(undefined) - expect(builder.zz$pb).toMatchObject({ - contract: { - '~orpc': { - InputSchema: schema1, - inputExample: example1, - }, - }, - }) - }) +describe('to DecoratedProcedure', () => { + it('func', () => { + const fn = vi.fn() + const result = builder.func(fn) - it('output method', () => { - const builder = osw.output(schema2, example2) - - expectTypeOf(builder).toEqualTypeOf< - ProcedureBuilder<{ auth: boolean }, undefined, undefined, typeof schema2> - >() - - expect(builder).instanceOf(ProcedureBuilder) - expect(builder.zz$pb.middlewares).toBe(undefined) - expect(builder.zz$pb).toMatchObject({ - contract: { - '~orpc': { - OutputSchema: schema2, - outputExample: example2, - }, - }, - }) + expect(result).toSatisfy(isProcedure) + expect(result['~orpc'].middlewares).toEqual([mid]) + expect(result['~orpc'].func).toBe(fn) }) +}) - it('route method', () => { - const builder = osw.route({ - method: 'GET', - path: '/test', - deprecated: true, - description: 'des', - summary: 'sum', - tags: ['cccc'], - }) +describe('to RouterBuilder', () => { + it('prefix', () => { + vi.mocked(RouterBuilder).mockReturnValueOnce({ mocked: true } as any) + expect(builder.prefix('/test')).toEqual({ mocked: true }) - expectTypeOf(builder).toEqualTypeOf< - ProcedureBuilder<{ auth: boolean }, undefined, undefined, undefined> - >() - - expect(builder).instanceOf(ProcedureBuilder) - expect(builder.zz$pb.middlewares).toBe(undefined) - expect(builder.zz$pb).toMatchObject({ - contract: { - '~orpc': { - route: { - method: 'GET', - path: '/test', - deprecated: true, - description: 'des', - summary: 'sum', - tags: ['cccc'], - }, - }, - }, - }) + expect(RouterBuilder).toBeCalledTimes(1) + expect(RouterBuilder).toBeCalledWith(expect.objectContaining({ + middlewares: [mid], + prefix: '/test', + })) }) - it('with middlewares', () => { - const mid = os.middleware((_, __, meta) => { - return meta.next({ - context: { - userId: 'string', - }, - }) - }) - - const mid2 = os.middleware((_, __, meta) => { - return meta.next({ - context: { - mid2: true, - }, - }) - }) + it('tags', () => { + vi.mocked(RouterBuilder).mockReturnValueOnce({ mocked: true } as any) + expect(builder.tags('tag1', 'tag2')).toEqual({ mocked: true }) - const osw = os.context<{ auth: boolean }>().use(mid).use(mid2) - - const builder1 = osw.input(schema1) - const builder2 = osw.output(schema2) - const builder3 = osw.route({ method: 'GET', path: '/test' }) - - expectTypeOf(builder1).toEqualTypeOf< - ProcedureBuilder< - { auth: boolean }, - { userId: string } & { mid2: boolean }, - typeof schema1, - undefined - > - >() - - expectTypeOf(builder2).toEqualTypeOf< - ProcedureBuilder< - { auth: boolean }, - { userId: string } & { mid2: boolean }, - undefined, - typeof schema2 - > - >() - - expectTypeOf(builder3).toEqualTypeOf< - ProcedureBuilder< - { auth: boolean }, - { userId: string } & { mid2: boolean }, - undefined, - undefined - > - >() - - expect(builder1.zz$pb.middlewares).toEqual([mid, mid2]) - expect(builder2.zz$pb.middlewares).toEqual([mid, mid2]) - expect(builder3.zz$pb.middlewares).toEqual([mid, mid2]) + expect(RouterBuilder).toBeCalledTimes(1) + expect(RouterBuilder).toBeCalledWith(expect.objectContaining({ + middlewares: [mid], + tags: ['tag1', 'tag2'], + })) }) }) -describe('handler method', () => { - it('without middlewares', () => { - const osw = os.context<{ auth: boolean }>() - - const procedure = osw.func((input, context, meta) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toEqualTypeOf<{ auth: boolean }>() - expectTypeOf(meta).toEqualTypeOf() - }) - - expectTypeOf(procedure).toEqualTypeOf< - DecoratedProcedure< - { auth: boolean }, - undefined, - undefined, - undefined, - void - > - >() - - expect(isProcedure(procedure)).toBe(true) - expect(procedure.zz$p.middlewares).toBe(undefined) - }) - - it('with middlewares', () => { - const mid = os.middleware((_, __, meta) => { - return meta.next({ - context: { - userId: 'string', - }, - }) - }) +it('to AdaptedRouter', () => { + const ping = vi.fn() as any + const router = { + ping, + nested: { + ping, + }, + } - const osw = os.context<{ auth: boolean }>().use(mid) + expect(builder.router(router)).toEqual({ mocked: true }) - const procedure = osw.func((input, context, meta) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(context).toMatchTypeOf<{ auth: boolean }>() - expectTypeOf(meta).toEqualTypeOf() - }) + expect(RouterBuilder).toBeCalledTimes(1) + expect(RouterBuilder).toBeCalledWith(expect.objectContaining({ + middlewares: [mid], + })) - expectTypeOf(procedure).toEqualTypeOf< - DecoratedProcedure< - { auth: boolean }, - { userId: string }, - undefined, - undefined, - void - > - >() - - expect(isProcedure(procedure)).toBe(true) - expect(procedure.zz$p.middlewares).toEqual([mid]) - }) + const routerBuilder = vi.mocked(RouterBuilder).mock.results[0]?.value + expect(vi.mocked(routerBuilder.router)).toBeCalledTimes(1) + expect(vi.mocked(routerBuilder.router)).toBeCalledWith(router) }) -it('prefix', () => { - const builder = os - .context<{ auth: boolean }>() - .use((_, __, meta) => { - return meta.next({ context: { userId: '1' } }) - }) - .prefix('/api') +it('to DecoratedLazy', () => { + const loader = vi.fn() as any - expectTypeOf(builder).toEqualTypeOf< - RouterBuilder<{ auth: boolean }, { userId: string }> - >() + expect(builder.lazy(loader)).toEqual({ mocked: true }) + expect(RouterBuilder).toBeCalledTimes(1) + expect(RouterBuilder).toBeCalledWith(expect.objectContaining({ + middlewares: [mid], + })) - expect(builder).instanceOf(RouterBuilder) - expect(builder.zz$rb.prefix).toEqual('/api') + const routerBuilder = vi.mocked(RouterBuilder).mock.results[0]?.value + expect(vi.mocked(routerBuilder.lazy)).toBeCalledTimes(1) + expect(vi.mocked(routerBuilder.lazy)).toBeCalledWith(loader) }) -it('tags', () => { - const builder = os - .context<{ auth: boolean }>() - .use((_, __, meta) => { - return meta.next({ context: { userId: '1' } }) - }) - .tags('user', 'user2') - - expectTypeOf(builder).toEqualTypeOf< - RouterBuilder<{ auth: boolean }, { userId: string }> - >() +it('to ChainableImplementer', () => { + const contract = vi.fn() as any - expect(builder).instanceOf(RouterBuilder) - expect(builder.zz$rb.tags).toEqual(['user', 'user2']) + expect(builder.contract(contract)).toEqual({ mocked: true }) + expect(createChainableImplementer).toBeCalledTimes(1) + expect(createChainableImplementer).toBeCalledWith(contract, [mid]) }) diff --git a/packages/server/src/builder.ts b/packages/server/src/builder.ts index f8dda80b8..ef504f8a6 100644 --- a/packages/server/src/builder.ts +++ b/packages/server/src/builder.ts @@ -1,104 +1,69 @@ -import type { - ANY_CONTRACT_PROCEDURE, - ContractRouter, - HTTPPath, - RouteOptions, - Schema, - SchemaInput, - SchemaOutput, -} from '@orpc/contract' -import type { IsEqual } from '@orpc/shared' -import type { DecoratedLazy } from './lazy' -import type { DecoratedProcedure, Procedure, ProcedureFunc } from './procedure' -import type { HandledRouter, Router } from './router' -import type { Context, MergeContext } from './types' -import { - ContractProcedure, - isContractProcedure, -} from '@orpc/contract' -import { - type DecoratedMiddleware, - decorateMiddleware, - type MapInputMiddleware, - type Middleware, -} from './middleware' -import { decorateProcedure } from './procedure' +import type { ANY_CONTRACT_PROCEDURE, ContractRouter, HTTPPath, RouteOptions, Schema, SchemaInput, SchemaOutput } from '@orpc/contract' +import type { DecoratedLazy } from './lazy-decorated' +import type { Router } from './router' +import type { AdaptedRouter } from './router-builder' +import type { Context, MergeContext, WELL_CONTEXT } from './types' +import { ContractProcedure } from '@orpc/contract' +import { type ChainableImplementer, createChainableImplementer } from './implementer-chainable' +import { type DecoratedMiddleware, decorateMiddleware, type Middleware } from './middleware' +import { Procedure, type ProcedureFunc } from './procedure' import { ProcedureBuilder } from './procedure-builder' -import { ProcedureImplementer } from './procedure-implementer' +import { type DecoratedProcedure, decorateProcedure } from './procedure-decorated' import { RouterBuilder } from './router-builder' -import { - type ChainedRouterImplementer, - chainRouterImplementer, -} from './router-implementer' + +export interface BuilderDef { + middlewares?: Middleware, Partial | undefined, unknown, any>[] +} export class Builder { - constructor( - public zz$b: { - middlewares?: Middleware[] - } = {}, - ) { } + '~type' = 'Builder' as const + '~orpc': BuilderDef - /** - * Self chainable - */ + constructor(def: BuilderDef) { + this['~orpc'] = def + } - context(): IsEqual extends true - ? Builder - : Builder { - return this as any + context(): Builder { + return new Builder({}) } - use< - UExtraContext extends - | Partial>> - | undefined = undefined, - >( + use> | undefined = undefined>( middleware: Middleware< MergeContext, - UExtraContext, + U, unknown, unknown >, - ): Builder> + ): Builder> { + return new Builder({ + ...this['~orpc'], + middlewares: [...(this['~orpc'].middlewares ?? []), middleware as any], + }) + } - use< - UExtraContext extends - | Partial>> - | undefined = undefined, - UMappedInput = unknown, + middleware< + UExtraContext extends Context & Partial> | undefined = undefined, + TInput = unknown, + TOutput = any, >( middleware: Middleware< MergeContext, UExtraContext, - UMappedInput, - unknown + TInput, + TOutput >, - mapInput: MapInputMiddleware, - ): Builder> - - use( - middleware: Middleware, - mapInput?: MapInputMiddleware, - ): Builder { - const middleware_ = mapInput - ? decorateMiddleware(middleware).mapInput(mapInput) - : middleware - - return new Builder({ - ...this.zz$b, - middlewares: [...(this.zz$b.middlewares || []), middleware_], - }) + ): DecoratedMiddleware< + MergeContext, + UExtraContext, + TInput, + TOutput + > { + return decorateMiddleware(middleware) } - /** - * Convert to ContractProcedureBuilder - */ - - route( - route: RouteOptions, - ): ProcedureBuilder { + route(route: RouteOptions): ProcedureBuilder { return new ProcedureBuilder({ - middlewares: this.zz$b.middlewares, + middlewares: this['~orpc'].middlewares, contract: new ContractProcedure({ route, InputSchema: undefined, @@ -112,7 +77,7 @@ export class Builder { example?: SchemaInput, ): ProcedureBuilder { return new ProcedureBuilder({ - middlewares: this.zz$b.middlewares, + middlewares: this['~orpc'].middlewares, contract: new ContractProcedure({ OutputSchema: undefined, InputSchema: schema, @@ -126,7 +91,7 @@ export class Builder { example?: SchemaOutput, ): ProcedureBuilder { return new ProcedureBuilder({ - middlewares: this.zz$b.middlewares, + middlewares: this['~orpc'].middlewares, contract: new ContractProcedure({ InputSchema: undefined, OutputSchema: schema, @@ -135,112 +100,48 @@ export class Builder { }) } - /** - * Convert to Procedure - */ func( - func: ProcedureFunc< - TContext, - TExtraContext, - undefined, - undefined, - UFuncOutput - >, - ): DecoratedProcedure< - TContext, - TExtraContext, - undefined, - undefined, - UFuncOutput - > { - return decorateProcedure({ - zz$p: { - middlewares: this.zz$b.middlewares, - contract: new ContractProcedure({ - InputSchema: undefined, - OutputSchema: undefined, - }), - func, - }, - }) - } - - /** - * Convert to ProcedureImplementer | RouterBuilder - */ - - contract( - contract: UContract, - ): UContract extends ContractProcedure< - infer UInputSchema, - infer UOutputSchema - > - ? ProcedureImplementer - : UContract extends ContractRouter - ? ChainedRouterImplementer - : never { - if (isContractProcedure(contract)) { - return new ProcedureImplementer({ - contract, - middlewares: this.zz$b.middlewares, - }) as any - } - - return chainRouterImplementer( - contract as ContractRouter, - this.zz$b.middlewares, - ) as any - } - - /** - * Create ExtendedMiddleware - */ - - // TODO: TOutput always any, infer not work at all, because TOutput used inside middleware params, - // solution (maybe): create new generic for .output() method - middleware( - middleware: Middleware< - MergeContext, - UExtraContext, - TInput, - TOutput - >, - ): DecoratedMiddleware< - MergeContext, - UExtraContext, - TInput, - TOutput - > { - return decorateMiddleware(middleware) + func: ProcedureFunc, + ): DecoratedProcedure { + return decorateProcedure(new Procedure({ + middlewares: this['~orpc'].middlewares, + contract: new ContractProcedure({ + InputSchema: undefined, + OutputSchema: undefined, + }), + func, + })) } prefix(prefix: HTTPPath): RouterBuilder { return new RouterBuilder({ - ...this.zz$b, + middlewares: this['~orpc'].middlewares, prefix, }) } tags(...tags: string[]): RouterBuilder { return new RouterBuilder({ - ...this.zz$b, + middlewares: this['~orpc'].middlewares, tags, }) } - /** - * Create DecoratedRouter - */ - router>( - router: URouter, - ): HandledRouter { - return new RouterBuilder(this.zz$b).router(router) + router, any>>( + router: U, + ): AdaptedRouter { + return new RouterBuilder(this['~orpc']).router(router) } - lazy | Procedure>( + lazy, any>>( loader: () => Promise<{ default: U }>, - ): DecoratedLazy { - // TODO: replace with a more solid solution - return new RouterBuilder(this.zz$b).lazy(loader as any) as any + ): DecoratedLazy> { + return new RouterBuilder(this['~orpc']).lazy(loader) + } + + contract( + contract: U, + ): ChainableImplementer { + return createChainableImplementer(contract, this['~orpc'].middlewares) } } From 03935d182d7dc56f3bd73608c1c6e1442fa6c87c Mon Sep 17 00:00:00 2001 From: unnoq Date: Wed, 18 Dec 2024 15:13:53 +0700 Subject: [PATCH 27/51] reindex --- packages/server/src/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 692471a52..bdbabb478 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -2,7 +2,9 @@ import type { WELL_CONTEXT } from './types' import { Builder } from './builder' export * from './builder' +export * from './implementer-chainable' export * from './lazy' +export * from './lazy-decorated' export * from './middleware' export * from './procedure' export * from './procedure-builder' @@ -17,4 +19,4 @@ export * from './types' export * from './utils' export * from '@orpc/shared/error' -export const os = new Builder() +export const os = new Builder({}) From a53f2b9ed22dd6680e7485451ba257a63034c5b6 Mon Sep 17 00:00:00 2001 From: unnoq Date: Wed, 18 Dec 2024 15:27:42 +0700 Subject: [PATCH 28/51] separate middleware decorated --- packages/server/src/builder.test-d.ts | 3 +- packages/server/src/builder.ts | 4 +- packages/server/src/index.ts | 1 + .../server/src/middleware-decorated.test-d.ts | 89 +++++++++++++++++++ ...e.test.ts => middleware-decorated.test.ts} | 2 +- packages/server/src/middleware-decorated.ts | 87 ++++++++++++++++++ packages/server/src/middleware.test-d.ts | 89 +------------------ packages/server/src/middleware.ts | 87 +----------------- packages/server/src/procedure-decorated.ts | 2 +- packages/server/src/procedure-implementer.ts | 2 +- 10 files changed, 187 insertions(+), 179 deletions(-) create mode 100644 packages/server/src/middleware-decorated.test-d.ts rename packages/server/src/{middleware.test.ts => middleware-decorated.test.ts} (97%) create mode 100644 packages/server/src/middleware-decorated.ts diff --git a/packages/server/src/builder.test-d.ts b/packages/server/src/builder.test-d.ts index 3cfd86485..f4c376393 100644 --- a/packages/server/src/builder.test-d.ts +++ b/packages/server/src/builder.test-d.ts @@ -1,6 +1,7 @@ import type { ChainableImplementer } from './implementer-chainable' import type { DecoratedLazy } from './lazy-decorated' -import type { DecoratedMiddleware, Middleware, MiddlewareMeta } from './middleware' +import type { Middleware, MiddlewareMeta } from './middleware' +import type { DecoratedMiddleware } from './middleware-decorated' import type { ANY_PROCEDURE, Procedure } from './procedure' import type { ProcedureBuilder } from './procedure-builder' import type { DecoratedProcedure } from './procedure-decorated' diff --git a/packages/server/src/builder.ts b/packages/server/src/builder.ts index ef504f8a6..e095f193a 100644 --- a/packages/server/src/builder.ts +++ b/packages/server/src/builder.ts @@ -1,11 +1,13 @@ import type { ANY_CONTRACT_PROCEDURE, ContractRouter, HTTPPath, RouteOptions, Schema, SchemaInput, SchemaOutput } from '@orpc/contract' import type { DecoratedLazy } from './lazy-decorated' +import type { Middleware } from './middleware' +import type { DecoratedMiddleware } from './middleware-decorated' import type { Router } from './router' import type { AdaptedRouter } from './router-builder' import type { Context, MergeContext, WELL_CONTEXT } from './types' import { ContractProcedure } from '@orpc/contract' import { type ChainableImplementer, createChainableImplementer } from './implementer-chainable' -import { type DecoratedMiddleware, decorateMiddleware, type Middleware } from './middleware' +import { decorateMiddleware } from './middleware-decorated' import { Procedure, type ProcedureFunc } from './procedure' import { ProcedureBuilder } from './procedure-builder' import { type DecoratedProcedure, decorateProcedure } from './procedure-decorated' diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index bdbabb478..6d7ac3d7d 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -6,6 +6,7 @@ export * from './implementer-chainable' export * from './lazy' export * from './lazy-decorated' export * from './middleware' +export * from './middleware-decorated' export * from './procedure' export * from './procedure-builder' export * from './procedure-caller' diff --git a/packages/server/src/middleware-decorated.test-d.ts b/packages/server/src/middleware-decorated.test-d.ts new file mode 100644 index 000000000..afb06ef7b --- /dev/null +++ b/packages/server/src/middleware-decorated.test-d.ts @@ -0,0 +1,89 @@ +import type { Middleware, MiddlewareMeta } from './middleware' +import type { DecoratedMiddleware } from './middleware-decorated' +import type { WELL_CONTEXT } from './types' +import { decorateMiddleware } from './middleware-decorated' + +describe('decorateMiddleware', () => { + const decorated = decorateMiddleware( + (input: { name: string }, context: { user?: string }, meta) => meta.next({ context: { auth: true as const, user: 'string' } }), + ) + + it('assignable to middleware', () => { + const decorated = decorateMiddleware((input: { input: 'input' }, context, meta) => meta.next({})) + const mid: Middleware = decorated + + const decorated2 = decorateMiddleware((input, context, meta: MiddlewareMeta<'output'>) => meta.next({ context: { extra: true } })) + const mid2: Middleware = decorated2 + }) + + it('can map input', () => { + const mapped = decorated.mapInput((input: 'something') => ({ name: input })) + + expectTypeOf(mapped).toEqualTypeOf< + DecoratedMiddleware<{ user?: string }, { auth: true, user: string }, 'something', unknown> + >() + }) + + it('can concat', () => { + const mapped = decorated.concat( + (input: { age: number }, context, meta) => meta.next({ context: { db: true } }), + ) + + expectTypeOf(mapped).toEqualTypeOf< + DecoratedMiddleware< + { user?: string }, + { auth: true, user: string } & { db: boolean }, + { name: string } & { age: number }, + unknown + > + >() + }) + + it('can concat with map input', () => { + const mapped = decorated.concat( + (input: { age: number }, context, meta) => meta.next({ context: { db: true } }), + (input: { year: number }) => ({ age: 123 }), + ) + + expectTypeOf(mapped).toEqualTypeOf< + DecoratedMiddleware< + { user?: string }, + { auth: true, user: string } & { db: boolean }, + { name: string } & { year: number }, + unknown + > + >() + + decorated.concat( + (input: { age: number }, context, meta) => meta.next({ context: { db: true } }), + // @ts-expect-error - invalid return input + (input: { year: number }) => ({ age: '123' }), + ) + }) + + it('can concat and prevent conflict on context', () => { + const mapped = decorated.concat( + (input, context, meta) => meta.next({ context: { db: true } }), + ) + + expectTypeOf(mapped).toEqualTypeOf< + DecoratedMiddleware< + { user?: string }, + { auth: true, user: string } & { db: boolean }, + { name: string }, + unknown + > + >() + + decorated.concat( + // @ts-expect-error - user is not assignable to existing user context + (input, context, meta) => meta.next({ context: { user: true } }), + ) + + decorated.concat( + // @ts-expect-error - user is not assignable to existing user context + (input, context, meta) => meta.next({ context: { user: true } }), + () => 'anything', + ) + }) +}) diff --git a/packages/server/src/middleware.test.ts b/packages/server/src/middleware-decorated.test.ts similarity index 97% rename from packages/server/src/middleware.test.ts rename to packages/server/src/middleware-decorated.test.ts index 9e9a9317a..e14df7ae8 100644 --- a/packages/server/src/middleware.test.ts +++ b/packages/server/src/middleware-decorated.test.ts @@ -1,4 +1,4 @@ -import { decorateMiddleware } from './middleware' +import { decorateMiddleware } from './middleware-decorated' describe('decorateMiddleware', () => { it('just a function', () => { diff --git a/packages/server/src/middleware-decorated.ts b/packages/server/src/middleware-decorated.ts new file mode 100644 index 000000000..eb28bbd00 --- /dev/null +++ b/packages/server/src/middleware-decorated.ts @@ -0,0 +1,87 @@ +import type { ANY_MAP_INPUT_MIDDLEWARE, ANY_MIDDLEWARE, MapInputMiddleware, Middleware, MiddlewareMeta } from './middleware' +import type { Context, MergeContext, WELL_CONTEXT } from './types' +import { mergeContext } from './utils' + +export interface DecoratedMiddleware< + TContext extends Context, + TExtraContext extends Context, + TInput, + TOutput, +> extends Middleware { + concat: (< + UExtraContext extends Context & Partial> | undefined = undefined, + UInput = unknown, + >( + middleware: Middleware< + MergeContext, + UExtraContext, + UInput & TInput, + TOutput + >, + ) => DecoratedMiddleware< + TContext, + MergeContext, + UInput & TInput, + TOutput + >) & (< + UExtraContext extends Context & Partial> | undefined = undefined, + UInput = TInput, + UMappedInput = unknown, + >( + middleware: Middleware< + MergeContext, + UExtraContext, + UMappedInput, + TOutput + >, + mapInput: MapInputMiddleware, + ) => DecoratedMiddleware< + TContext, + MergeContext, + UInput & TInput, + TOutput + >) + + mapInput: ( + map: MapInputMiddleware, + ) => DecoratedMiddleware +} + +export function decorateMiddleware< + TContext extends Context = WELL_CONTEXT, + TExtraContext extends Context = undefined, + TInput = unknown, + TOutput = unknown, +>( + middleware: Middleware, +): DecoratedMiddleware { + const decorated = middleware as DecoratedMiddleware + + decorated.mapInput = (mapInput) => { + const mapped = decorateMiddleware( + (input, ...rest) => middleware(mapInput(input as any), ...rest as [any, any]), + ) + + return mapped as any + } + + decorated.concat = (concatMiddleware: ANY_MIDDLEWARE, mapInput?: ANY_MAP_INPUT_MIDDLEWARE) => { + const mapped = mapInput + ? decorateMiddleware(concatMiddleware).mapInput(mapInput) + : concatMiddleware + + const concatted = decorateMiddleware((input, context, meta, ...rest) => { + const next: MiddlewareMeta['next'] = async (options) => { + return mapped(input, mergeContext(context, options.context), meta, ...rest) + } + + const merged = middleware(input as any, context as any, { ...meta, next }, ...rest) + + return merged + }) + + return concatted as any + } + + return decorated +} diff --git a/packages/server/src/middleware.test-d.ts b/packages/server/src/middleware.test-d.ts index 8cdb9fc29..dc4fd5a51 100644 --- a/packages/server/src/middleware.test-d.ts +++ b/packages/server/src/middleware.test-d.ts @@ -1,6 +1,4 @@ -import type { DecoratedMiddleware, Middleware, MiddlewareMeta } from './middleware' -import type { WELL_CONTEXT } from './types' -import { decorateMiddleware } from './middleware' +import type { Middleware, MiddlewareMeta } from './middleware' describe('middleware', () => { it('just a function', () => { @@ -86,88 +84,3 @@ describe('middleware', () => { >() }) }) - -describe('decorateMiddleware', () => { - const decorated = decorateMiddleware( - (input: { name: string }, context: { user?: string }, meta) => meta.next({ context: { auth: true as const, user: 'string' } }), - ) - - it('assignable to middleware', () => { - const decorated = decorateMiddleware((input: { input: 'input' }, context, meta) => meta.next({})) - const mid: Middleware = decorated - - const decorated2 = decorateMiddleware((input, context, meta: MiddlewareMeta<'output'>) => meta.next({ context: { extra: true } })) - const mid2: Middleware = decorated2 - }) - - it('can map input', () => { - const mapped = decorated.mapInput((input: 'something') => ({ name: input })) - - expectTypeOf(mapped).toEqualTypeOf< - DecoratedMiddleware<{ user?: string }, { auth: true, user: string }, 'something', unknown> - >() - }) - - it('can concat', () => { - const mapped = decorated.concat( - (input: { age: number }, context, meta) => meta.next({ context: { db: true } }), - ) - - expectTypeOf(mapped).toEqualTypeOf< - DecoratedMiddleware< - { user?: string }, - { auth: true, user: string } & { db: boolean }, - { name: string } & { age: number }, - unknown - > - >() - }) - - it('can concat with map input', () => { - const mapped = decorated.concat( - (input: { age: number }, context, meta) => meta.next({ context: { db: true } }), - (input: { year: number }) => ({ age: 123 }), - ) - - expectTypeOf(mapped).toEqualTypeOf< - DecoratedMiddleware< - { user?: string }, - { auth: true, user: string } & { db: boolean }, - { name: string } & { year: number }, - unknown - > - >() - - decorated.concat( - (input: { age: number }, context, meta) => meta.next({ context: { db: true } }), - // @ts-expect-error - invalid return input - (input: { year: number }) => ({ age: '123' }), - ) - }) - - it('can concat and prevent conflict on context', () => { - const mapped = decorated.concat( - (input, context, meta) => meta.next({ context: { db: true } }), - ) - - expectTypeOf(mapped).toEqualTypeOf< - DecoratedMiddleware< - { user?: string }, - { auth: true, user: string } & { db: boolean }, - { name: string }, - unknown - > - >() - - decorated.concat( - // @ts-expect-error - user is not assignable to existing user context - (input, context, meta) => meta.next({ context: { user: true } }), - ) - - decorated.concat( - // @ts-expect-error - user is not assignable to existing user context - (input, context, meta) => meta.next({ context: { user: true } }), - () => 'anything', - ) - }) -}) diff --git a/packages/server/src/middleware.ts b/packages/server/src/middleware.ts index e36c13753..3db7d9ff5 100644 --- a/packages/server/src/middleware.ts +++ b/packages/server/src/middleware.ts @@ -1,6 +1,5 @@ import type { Promisable } from '@orpc/shared' -import type { Context, MergeContext, Meta, WELL_CONTEXT } from './types' -import { mergeContext } from './utils' +import type { Context, Meta } from './types' export type MiddlewareResult = Promisable<{ output: TOutput @@ -36,87 +35,3 @@ export interface MapInputMiddleware { } export type ANY_MAP_INPUT_MIDDLEWARE = MapInputMiddleware - -export interface DecoratedMiddleware< - TContext extends Context, - TExtraContext extends Context, - TInput, - TOutput, -> extends Middleware { - concat: (< - UExtraContext extends Context & Partial> | undefined = undefined, - UInput = unknown, - >( - middleware: Middleware< - MergeContext, - UExtraContext, - UInput & TInput, - TOutput - >, - ) => DecoratedMiddleware< - TContext, - MergeContext, - UInput & TInput, - TOutput - >) & (< - UExtraContext extends Context & Partial> | undefined = undefined, - UInput = TInput, - UMappedInput = unknown, - >( - middleware: Middleware< - MergeContext, - UExtraContext, - UMappedInput, - TOutput - >, - mapInput: MapInputMiddleware, - ) => DecoratedMiddleware< - TContext, - MergeContext, - UInput & TInput, - TOutput - >) - - mapInput: ( - map: MapInputMiddleware, - ) => DecoratedMiddleware -} - -export function decorateMiddleware< - TContext extends Context = WELL_CONTEXT, - TExtraContext extends Context = undefined, - TInput = unknown, - TOutput = unknown, ->( - middleware: Middleware, -): DecoratedMiddleware { - const decorated = middleware as DecoratedMiddleware - - decorated.mapInput = (mapInput) => { - const mapped = decorateMiddleware( - (input, ...rest) => middleware(mapInput(input as any), ...rest as [any, any]), - ) - - return mapped as any - } - - decorated.concat = (concatMiddleware: ANY_MIDDLEWARE, mapInput?: ANY_MAP_INPUT_MIDDLEWARE) => { - const mapped = mapInput - ? decorateMiddleware(concatMiddleware).mapInput(mapInput) - : concatMiddleware - - const concatted = decorateMiddleware((input, context, meta, ...rest) => { - const next: MiddlewareMeta['next'] = async (options) => { - return mapped(input, mergeContext(context, options.context), meta, ...rest) - } - - const merged = middleware(input as any, context as any, { ...meta, next }, ...rest) - - return merged - }) - - return concatted as any - } - - return decorated -} diff --git a/packages/server/src/procedure-decorated.ts b/packages/server/src/procedure-decorated.ts index 0e5a347f8..9410fe2c9 100644 --- a/packages/server/src/procedure-decorated.ts +++ b/packages/server/src/procedure-decorated.ts @@ -2,7 +2,7 @@ import type { HTTPPath, RouteOptions, Schema, SchemaInput, SchemaOutput } from ' import type { ANY_MIDDLEWARE, MapInputMiddleware, Middleware } from './middleware' import type { Caller, Context, MergeContext } from './types' import { DecoratedContractProcedure } from '@orpc/contract' -import { decorateMiddleware } from './middleware' +import { decorateMiddleware } from './middleware-decorated' import { Procedure } from './procedure' import { createProcedureCaller } from './procedure-caller' diff --git a/packages/server/src/procedure-implementer.ts b/packages/server/src/procedure-implementer.ts index baed77646..2322bc846 100644 --- a/packages/server/src/procedure-implementer.ts +++ b/packages/server/src/procedure-implementer.ts @@ -3,7 +3,7 @@ import type { ANY_MAP_INPUT_MIDDLEWARE, ANY_MIDDLEWARE, MapInputMiddleware, Midd import type { ProcedureFunc } from './procedure' import type { DecoratedProcedure } from './procedure-decorated' import type { Context, MergeContext } from './types' -import { decorateMiddleware } from './middleware' +import { decorateMiddleware } from './middleware-decorated' import { Procedure } from './procedure' import { decorateProcedure } from './procedure-decorated' From d6a668eda873c744305d8f212215408274480100 Mon Sep 17 00:00:00 2001 From: unnoq Date: Wed, 18 Dec 2024 16:40:54 +0700 Subject: [PATCH 29/51] procedure can be a router --- .../contract/src/procedure-decorated.test.ts | 2 +- packages/contract/src/router-builder.ts | 29 +++++----- packages/contract/src/router.ts | 26 ++++----- packages/server/src/implementer-chainable.ts | 13 ++--- packages/server/src/lazy-decorated.ts | 16 +++--- packages/server/src/lazy.ts | 2 + packages/server/src/router-builder.ts | 38 ++++--------- packages/server/src/router-caller.ts | 2 +- packages/server/src/router.ts | 56 +++++++------------ packages/server/src/types.ts | 2 + 10 files changed, 74 insertions(+), 112 deletions(-) diff --git a/packages/contract/src/procedure-decorated.test.ts b/packages/contract/src/procedure-decorated.test.ts index a795b0d55..51213f4fb 100644 --- a/packages/contract/src/procedure-decorated.test.ts +++ b/packages/contract/src/procedure-decorated.test.ts @@ -62,7 +62,7 @@ describe('unshiftTag', () => { const tagged2 = tagged.unshiftTag('tag3') expect(tagged2).toBeInstanceOf(DecoratedContractProcedure) - expect(tagged2['~orpc']).toEqual({ route: { tags: ['tag1', 'tag2', 'tag3'] } }) + expect(tagged2['~orpc']).toEqual({ route: { tags: ['tag3', 'tag1', 'tag2'] } }) }) it('not reference', () => { diff --git a/packages/contract/src/router-builder.ts b/packages/contract/src/router-builder.ts index dd96097f6..88e156e30 100644 --- a/packages/contract/src/router-builder.ts +++ b/packages/contract/src/router-builder.ts @@ -40,27 +40,24 @@ export class ContractRouterBuilder { } router(router: T): AdaptedContractRouter { - const adapted: ContractRouter = {} + if (isContractProcedure(router)) { + let decorated = DecoratedContractProcedure.decorate(router) - for (const key in router) { - const item = router[key] + if (this['~orpc'].tags) { + decorated = decorated.unshiftTag(...this['~orpc'].tags) + } - if (isContractProcedure(item)) { - let decorated = DecoratedContractProcedure.decorate(item) + if (this['~orpc'].prefix) { + decorated = decorated.prefix(this['~orpc'].prefix) + } - if (this['~orpc'].tags) { - decorated = decorated.unshiftTag(...this['~orpc'].tags) - } + return decorated as any + } - if (this['~orpc'].prefix) { - decorated = decorated.prefix(this['~orpc'].prefix) - } + const adapted: ContractRouter = {} - adapted[key] = decorated - } - else { - adapted[key] = this.router(item as ContractRouter) - } + for (const key in router) { + adapted[key] = this.router(router[key]!) } return adapted as any diff --git a/packages/contract/src/router.ts b/packages/contract/src/router.ts index 8dbb26fbe..f37b73ffe 100644 --- a/packages/contract/src/router.ts +++ b/packages/contract/src/router.ts @@ -1,22 +1,20 @@ import type { ANY_CONTRACT_PROCEDURE, ContractProcedure } from './procedure' import type { SchemaInput, SchemaOutput } from './types' -export interface ContractRouter { - [k: string]: ANY_CONTRACT_PROCEDURE | ContractRouter +export type ContractRouter = ANY_CONTRACT_PROCEDURE | { + [k: string]: ContractRouter } -export type InferContractRouterInputs = { - [K in keyof T]: T[K] extends ContractProcedure +export type InferContractRouterInputs = + T extends ContractProcedure ? SchemaInput - : T[K] extends ContractRouter - ? InferContractRouterInputs - : never -} + : { + [K in keyof T]: T[K] extends ContractRouter ? InferContractRouterInputs : never + } -export type InferContractRouterOutputs = { - [K in keyof T]: T[K] extends ContractProcedure +export type InferContractRouterOutputs = + T extends ContractProcedure ? SchemaOutput - : T[K] extends ContractRouter - ? InferContractRouterOutputs - : never -} + : { + [K in keyof T]: T[K] extends ContractRouter ? InferContractRouterOutputs : never + } diff --git a/packages/server/src/implementer-chainable.ts b/packages/server/src/implementer-chainable.ts index 5f752c005..b30f5fc1e 100644 --- a/packages/server/src/implementer-chainable.ts +++ b/packages/server/src/implementer-chainable.ts @@ -1,24 +1,23 @@ import type { Middleware } from './middleware' import type { Context, MergeContext, WELL_CONTEXT } from './types' -import { type ANY_CONTRACT_PROCEDURE, type ContractProcedure, type ContractRouter, isContractProcedure } from '@orpc/contract' +import { type ContractProcedure, type ContractRouter, isContractProcedure } from '@orpc/contract' import { ProcedureImplementer } from './procedure-implementer' import { RouterImplementer } from './router-implementer' export type ChainableImplementer< TContext extends Context, TExtraContext extends Context, - TContract extends ContractRouter | ANY_CONTRACT_PROCEDURE, -> = TContract extends ContractProcedure + TContract extends ContractRouter, +> = TContract extends ContractProcedure ? ProcedureImplementer - : TContract extends ContractRouter ? { - [K in keyof TContract]: ChainableImplementer + : { + [K in keyof TContract]: TContract[K] extends ContractRouter ? ChainableImplementer : never } & Omit, '~type' | '~orpc'> - : never export function createChainableImplementer< TContext extends Context = WELL_CONTEXT, TExtraContext extends Context = undefined, - TContract extends ContractRouter | ANY_CONTRACT_PROCEDURE = any, + TContract extends ContractRouter = any, >( contract: TContract, middlewares?: Middleware, Partial | undefined, unknown, any>[], diff --git a/packages/server/src/lazy-decorated.ts b/packages/server/src/lazy-decorated.ts index 692ce8898..3116d5ffa 100644 --- a/packages/server/src/lazy-decorated.ts +++ b/packages/server/src/lazy-decorated.ts @@ -1,5 +1,4 @@ import type { SchemaInput, SchemaOutput } from '@orpc/contract' -import type { AnyFunction } from '@orpc/shared' import type { ANY_LAZY, Lazy } from './lazy' import type { Procedure } from './procedure' import type { Caller } from './types' @@ -9,14 +8,13 @@ import { createProcedureCaller } from './procedure-caller' export type DecoratedLazy = T extends Lazy ? DecoratedLazy : ( - T extends Procedure ? - & Lazy - & (undefined extends UContext ? Caller, SchemaOutput> : unknown) - : T extends AnyFunction ? Lazy - : T extends object ? { - [K in keyof T & string]: DecoratedLazy - } & Lazy - : Lazy + T extends Procedure + ? + & Lazy + & (undefined extends UContext ? Caller, SchemaOutput> : unknown) + : { + [K in keyof T]: DecoratedLazy + } & Lazy ) export function decorateLazy(lazied: T): DecoratedLazy { diff --git a/packages/server/src/lazy.ts b/packages/server/src/lazy.ts index d9c6f19b7..f3f7779f2 100644 --- a/packages/server/src/lazy.ts +++ b/packages/server/src/lazy.ts @@ -4,6 +4,8 @@ export interface Lazy { [LAZY_LOADER_SYMBOL]: () => Promise<{ default: T }> } +export type Lazyable = T | Lazy + export type ANY_LAZY = Lazy export function lazy(loader: () => Promise<{ default: T }>): Lazy { diff --git a/packages/server/src/router-builder.ts b/packages/server/src/router-builder.ts index 3dcf2e68c..3741e93be 100644 --- a/packages/server/src/router-builder.ts +++ b/packages/server/src/router-builder.ts @@ -4,16 +4,17 @@ import type { ANY_MIDDLEWARE, Middleware } from './middleware' import type { ANY_PROCEDURE, Procedure } from './procedure' import type { ANY_ROUTER, Router } from './router' import type { Context, MergeContext } from './types' -import { isLazy, lazy, unwrapLazy } from './lazy' +import { flatLazy, isLazy, lazy, unwrapLazy } from './lazy' import { type DecoratedLazy, decorateLazy } from './lazy-decorated' import { isProcedure } from './procedure' import { type DecoratedProcedure, decorateProcedure } from './procedure-decorated' export type AdaptedRouter< TContext extends Context, - TRouter extends Router, -> = { - [K in keyof TRouter]: TRouter[K] extends Procedure< + TRouter extends ANY_ROUTER, +> = TRouter extends Lazy + ? DecoratedLazy> + : TRouter extends Procedure< any, infer UExtraContext, infer UInputSchema, @@ -27,28 +28,9 @@ export type AdaptedRouter< UOutputSchema, UFuncOutput > - : TRouter[K] extends Lazy - ? U extends Procedure< - any, - infer UExtraContext, - infer UInputSchema, - infer UOutputSchema, - infer UFuncOutput - > - ? DecoratedLazy> - : U extends ANY_ROUTER - ? DecoratedLazy> - : never - : TRouter[K] extends ANY_ROUTER - ? AdaptedRouter - : never -} + : { + [K in keyof TRouter]: TRouter[K] extends ANY_ROUTER ? AdaptedRouter : never + } export type RouterBuilderDef = { prefix?: HTTPPath @@ -114,13 +96,13 @@ export class RouterBuilder< lazy, any>>( loader: () => Promise<{ default: U }>, ): DecoratedLazy> { - const adapted = adapt(lazy(loader), this['~orpc']) + const adapted = adapt(flatLazy(lazy(loader)), this['~orpc']) return adapted as any } } function adapt( - item: ANY_ROUTER | ANY_ROUTER[string], + item: ANY_ROUTER, options: { middlewares?: ANY_MIDDLEWARE[] tags?: readonly string[] diff --git a/packages/server/src/router-caller.ts b/packages/server/src/router-caller.ts index 724e067ac..8ec221b54 100644 --- a/packages/server/src/router-caller.ts +++ b/packages/server/src/router-caller.ts @@ -52,7 +52,7 @@ export function createRouterCaller< function createRouterCallerInternal( options: Merge, { - router: ANY_ROUTER | ANY_ROUTER[string] + router: ANY_ROUTER }>, ) { const router = isLazy(options.router) ? decorateLazy(options.router) : options.router diff --git a/packages/server/src/router.ts b/packages/server/src/router.ts index 95b2984f8..6e8d14350 100644 --- a/packages/server/src/router.ts +++ b/packages/server/src/router.ts @@ -1,47 +1,31 @@ import type { ContractProcedure, ContractRouter, SchemaInput, SchemaOutput } from '@orpc/contract' -import type { Lazy } from './lazy' +import type { Lazy, Lazyable } from './lazy' import type { Procedure } from './procedure' import type { Context } from './types' export type Router< TContext extends Context, TContract extends ContractRouter, -> = { - [K in keyof TContract]: TContract[K] extends ContractProcedure - ? - | Procedure - | Lazy> - : TContract[K] extends ContractRouter - ? Router | Lazy> - : never -} +> = TContract extends ContractProcedure + ? Lazyable> + : Lazyable<{ + [K in keyof TContract]: TContract[K] extends ContractRouter ? Router : never + }> export type ANY_ROUTER = Router -export type InferRouterInputs = { - [K in keyof T]: T[K] extends - | Procedure - | Lazy> - ? SchemaInput - : T[K] extends ANY_ROUTER - ? InferRouterInputs - : T[K] extends Lazy - ? U extends ANY_ROUTER - ? InferRouterInputs - : never - : never -} +export type InferRouterInputs = + T extends Lazy ? InferRouterInputs + : T extends Procedure + ? SchemaInput + : { + [K in keyof T]: T[K] extends ANY_ROUTER ? InferRouterInputs : never + } -export type InferRouterOutputs = { - [K in keyof T]: T[K] extends - | Procedure - | Lazy> - ? SchemaOutput - : T[K] extends ANY_ROUTER - ? InferRouterOutputs - : T[K] extends Lazy - ? U extends ANY_ROUTER - ? InferRouterOutputs - : never - : never -} +export type InferRouterOutputs = + T extends Lazy ? InferRouterOutputs + : T extends Procedure + ? SchemaOutput + : { + [K in keyof T]: T[K] extends ANY_ROUTER ? InferRouterOutputs : never + } diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index fc2f98c3b..95d063741 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -1,3 +1,5 @@ +/// + import type { WELL_PROCEDURE } from './procedure' export type Context = Record | undefined From 54c8c1e4cc752194bc5d64c353c8ff5bcac19f5a Mon Sep 17 00:00:00 2001 From: unnoq Date: Wed, 18 Dec 2024 16:45:44 +0700 Subject: [PATCH 30/51] improve dedupe in unshiftMiddleware --- packages/server/src/procedure-decorated.test.ts | 8 ++++++++ packages/server/src/procedure-decorated.ts | 8 ++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/server/src/procedure-decorated.test.ts b/packages/server/src/procedure-decorated.test.ts index d488080aa..d4914eab5 100644 --- a/packages/server/src/procedure-decorated.test.ts +++ b/packages/server/src/procedure-decorated.test.ts @@ -130,6 +130,14 @@ describe('self chainable', () => { .unshiftMiddleware(mid1, mid2)['~orpc'].middlewares, ).toEqual([mid1, mid2, mid2, mid]) }) + + it('case 5', () => { + expect( + decorated + .unshiftMiddleware(mid2, mid2) + .unshiftMiddleware(mid1, mid2, mid2)['~orpc'].middlewares, + ).toEqual([mid1, mid2, mid2, mid]) + }) }) }) diff --git a/packages/server/src/procedure-decorated.ts b/packages/server/src/procedure-decorated.ts index 9410fe2c9..b0a267747 100644 --- a/packages/server/src/procedure-decorated.ts +++ b/packages/server/src/procedure-decorated.ts @@ -132,17 +132,17 @@ export function decorateProcedure< decorated.unshiftMiddleware = (...middlewares: ANY_MIDDLEWARE[]) => { if (procedure['~orpc'].middlewares?.length) { - let exclusiveMinimum = -1 + let min = 0 for (let i = 0; i < procedure['~orpc'].middlewares.length; i++) { - const index = middlewares.indexOf(procedure['~orpc'].middlewares[i]!) + const index = middlewares.indexOf(procedure['~orpc'].middlewares[i]!, min) - if (index <= exclusiveMinimum) { + if (index === -1) { middlewares.push(...procedure['~orpc'].middlewares.slice(i)) break } - exclusiveMinimum = index + min = index + 1 } } From c1d90e7cd056ae5674aa1eff45b086991ba769f0 Mon Sep 17 00:00:00 2001 From: unnoq Date: Wed, 18 Dec 2024 17:32:44 +0700 Subject: [PATCH 31/51] improve --- packages/server/src/router-builder.ts | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/packages/server/src/router-builder.ts b/packages/server/src/router-builder.ts index 3741e93be..58cf0bdb3 100644 --- a/packages/server/src/router-builder.ts +++ b/packages/server/src/router-builder.ts @@ -1,5 +1,5 @@ import type { HTTPPath } from '@orpc/contract' -import type { Lazy } from './lazy' +import type { FlattenLazy, Lazy } from './lazy' import type { ANY_MIDDLEWARE, Middleware } from './middleware' import type { ANY_PROCEDURE, Procedure } from './procedure' import type { ANY_ROUTER, Router } from './router' @@ -14,20 +14,8 @@ export type AdaptedRouter< TRouter extends ANY_ROUTER, > = TRouter extends Lazy ? DecoratedLazy> - : TRouter extends Procedure< - any, - infer UExtraContext, - infer UInputSchema, - infer UOutputSchema, - infer UFuncOutput - > - ? DecoratedProcedure< - TContext, - UExtraContext, - UInputSchema, - UOutputSchema, - UFuncOutput - > + : TRouter extends Procedure + ? DecoratedProcedure : { [K in keyof TRouter]: TRouter[K] extends ANY_ROUTER ? AdaptedRouter : never } @@ -95,7 +83,7 @@ export class RouterBuilder< lazy, any>>( loader: () => Promise<{ default: U }>, - ): DecoratedLazy> { + ): AdaptedRouter> { const adapted = adapt(flatLazy(lazy(loader)), this['~orpc']) return adapted as any } From 56641224bb089ecc07471bf9e31c2f559166b723 Mon Sep 17 00:00:00 2001 From: unnoq Date: Wed, 18 Dec 2024 19:35:07 +0700 Subject: [PATCH 32/51] tests for procedure works as a router --- .../contract/src/router-builder.test-d.ts | 2 ++ packages/contract/src/router.test-d.ts | 4 +++ packages/server/src/router-builder.test-d.ts | 28 ++++++++++++++++++- packages/server/src/router-builder.test.ts | 22 +++++++++++++++ packages/server/src/router.test-d.ts | 11 ++++++++ 5 files changed, 66 insertions(+), 1 deletion(-) diff --git a/packages/contract/src/router-builder.test-d.ts b/packages/contract/src/router-builder.test-d.ts index 5cf1b6819..5733db98d 100644 --- a/packages/contract/src/router-builder.test-d.ts +++ b/packages/contract/src/router-builder.test-d.ts @@ -61,6 +61,8 @@ describe('router', () => { const routed = builder.router(router) expectTypeOf(routed).toEqualTypeOf>() + + expectTypeOf(builder.router(ping)).toEqualTypeOf>() }) it('throw error on invalid router', () => { diff --git a/packages/contract/src/router.test-d.ts b/packages/contract/src/router.test-d.ts index 74c20a0cc..88cc62f9c 100644 --- a/packages/contract/src/router.test-d.ts +++ b/packages/contract/src/router.test-d.ts @@ -30,6 +30,10 @@ const router = { } describe('ContractRouter', () => { + it('procedure also is a contract router', () => { + const _: ContractRouter = ping + }) + it('just an object and accepts both procedures and decorated procedures', () => { const _: ContractRouter = router }) diff --git a/packages/server/src/router-builder.test-d.ts b/packages/server/src/router-builder.test-d.ts index 0157c304f..5d73d8aef 100644 --- a/packages/server/src/router-builder.test-d.ts +++ b/packages/server/src/router-builder.test-d.ts @@ -66,6 +66,16 @@ describe('AdaptedRouter', () => { DecoratedProcedure<{ log: true } | undefined, undefined, undefined, undefined, unknown> >>() }) + + it('with procedure', () => { + expectTypeOf>().toEqualTypeOf< + DecoratedProcedure<{ log: boolean }, { db: string }, undefined, undefined, unknown> + >() + + expectTypeOf < AdaptedRouter<{ log: boolean }, Lazy>>().toEqualTypeOf< + DecoratedLazy> + >() + }) }) describe('self chainable', () => { @@ -167,9 +177,25 @@ describe('to AdaptedRouter', () => { // @ts-expect-error - context is not match builder.router({ wrongPing: lazy(() => Promise.resolve({ default: wrongPing })) }) }) + + it('procedure as a router', () => { + expectTypeOf(builder.router(ping)).toEqualTypeOf< + AdaptedRouter< + { auth: boolean }, + typeof ping + > + >() + + expectTypeOf(builder.router(lazy(() => Promise.resolve({ default: ping })))).toEqualTypeOf< + AdaptedRouter< + { auth: boolean }, + Lazy + > + >() + }) }) -describe('to DecoratedLazy', () => { +describe('to Decorated Adapted Lazy', () => { const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) const ping = {} as Procedure<{ auth: boolean }, { db: string }, typeof schema, typeof schema, { val: string }> const pong = {} as Procedure diff --git a/packages/server/src/router-builder.test.ts b/packages/server/src/router-builder.test.ts index f4d316190..a67f30202 100644 --- a/packages/server/src/router-builder.test.ts +++ b/packages/server/src/router-builder.test.ts @@ -245,6 +245,28 @@ describe('adapt router', () => { expect((await unwrapLazy(adapted.nested.pong) as any).default['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2']) }) + it('support procedure as a router', async () => { + const adapted = builder.router(ping) + + expect(adapted).toSatisfy(isProcedure) + expect(typeof adapted).toBe('function') + expect(adapted['~orpc'].func).toBe(ping['~orpc'].func) + expect(adapted['~orpc'].middlewares).toEqual([mid1, mid2, pMid1, pMid2]) + expect(adapted['~orpc'].contract['~orpc'].route?.path).toBe(undefined) + expect(adapted['~orpc'].contract['~orpc'].route?.method).toBe(undefined) + expect(adapted['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2', 'tag3', 'tag4']) + + const adaptedLazy = builder.router(lazy(() => Promise.resolve({ default: ping }))) + + expect(adaptedLazy).toSatisfy(isLazy) + expect(typeof adaptedLazy).toBe('function') + expect((await unwrapLazy(adaptedLazy) as any).default).toSatisfy(isProcedure) + expect((await unwrapLazy(adaptedLazy) as any).default['~orpc'].func).toBe(ping['~orpc'].func) + expect((await unwrapLazy(adaptedLazy) as any).default['~orpc'].middlewares).toEqual([mid1, mid2, pMid1, pMid2]) + expect((await unwrapLazy(adaptedLazy) as any).default['~orpc'].contract['~orpc'].route?.path).toBe(undefined) + expect((await unwrapLazy(adaptedLazy) as any).default['~orpc'].contract['~orpc'].route?.method).toBe(undefined) + }) + it('can concat LAZY_ROUTER_PREFIX_SYMBOL', () => { const adapted = builder.prefix('/hi').router(builder.router(routerWithLazy)) as any expect(adapted.ping[LAZY_ROUTER_PREFIX_SYMBOL]).toBe('/prefix/hi/prefix') diff --git a/packages/server/src/router.test-d.ts b/packages/server/src/router.test-d.ts index 15eda961c..ea7438b8f 100644 --- a/packages/server/src/router.test-d.ts +++ b/packages/server/src/router.test-d.ts @@ -204,4 +204,15 @@ describe('Router', () => { })), } }) + + it('support procedure as a router', () => { + const router1: Router<{ auth: boolean, userId: string }, any> = {} as Procedure<{ auth: boolean }, { db: string }, typeof schema, undefined, unknown> + // @ts-expect-error - invalid context + const router2: Router<{ auth: boolean, userId: string }, any> = {} as Procedure<{ auth: boolean, dev: boolean }, { db: string }, typeof schema, undefined, unknown> + + const pingContract = oc.input(schema) + const router3: Router<{ auth: boolean, userId: string }, typeof pingContract> = {} as Procedure<{ auth: boolean }, { db: string }, typeof schema, undefined, unknown> + // @ts-expect-error - mismatch contract + const router4: Router<{ auth: boolean, userId: string }, typeof pingContract> = {} as Procedure<{ auth: boolean }, { db: string }, undefined, undefined, unknown> + }) }) From 57f6953728c3669ca07e3d04dd3b4d51d15e12f3 Mon Sep 17 00:00:00 2001 From: unnoq Date: Wed, 18 Dec 2024 20:43:19 +0700 Subject: [PATCH 33/51] safe object callable --- packages/server/src/implementer-chainable.ts | 25 ++++++---------- packages/shared/src/index.ts | 1 + packages/shared/src/proxy.ts | 30 ++++++++++++++++++++ 3 files changed, 39 insertions(+), 17 deletions(-) create mode 100644 packages/shared/src/proxy.ts diff --git a/packages/server/src/implementer-chainable.ts b/packages/server/src/implementer-chainable.ts index b30f5fc1e..643773ad2 100644 --- a/packages/server/src/implementer-chainable.ts +++ b/packages/server/src/implementer-chainable.ts @@ -1,6 +1,7 @@ import type { Middleware } from './middleware' import type { Context, MergeContext, WELL_CONTEXT } from './types' import { type ContractProcedure, type ContractRouter, isContractProcedure } from '@orpc/contract' +import { createCallableObject } from '@orpc/shared' import { ProcedureImplementer } from './procedure-implementer' import { RouterImplementer } from './router-implementer' @@ -31,38 +32,28 @@ export function createChainableImplementer< return implementer as any } - const chainable: Record = {} + const chainable = {} as ChainableImplementer for (const key in contract) { - chainable[key] = createChainableImplementer(contract[key]!, middlewares) + (chainable as any)[key] = createChainableImplementer(contract[key]!, middlewares) } const routerImplementer = new RouterImplementer({ contract, middlewares }) const merged = new Proxy(chainable, { get(target, key) { - const next = Reflect.get(target, key) + const next = Reflect.get(target, key) as ChainableImplementer | undefined const method = Reflect.get(routerImplementer, key) if (typeof key !== 'string' || typeof method !== 'function') { return next } - return new Proxy(method.bind(routerImplementer), { - get(target, key) { - // TODO: create own utils for object callable proxy - if ( - typeof key !== 'string' - || !next - || (typeof next !== 'object' && typeof next !== 'function') - || !(key in next) - ) { - return Reflect.get(target, key) - } + if (!next) { + return method.bind(routerImplementer) + } - return next[key] - }, - }) + return createCallableObject(next, method.bind(routerImplementer)) }, }) diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index cebe4c6d9..5fd079d1c 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -3,6 +3,7 @@ export * from './function' export * from './hook' export * from './json' export * from './object' +export * from './proxy' export * from './value' export { isPlainObject } from 'is-what' diff --git a/packages/shared/src/proxy.ts b/packages/shared/src/proxy.ts new file mode 100644 index 000000000..df4ea9bec --- /dev/null +++ b/packages/shared/src/proxy.ts @@ -0,0 +1,30 @@ +import type { AnyFunction } from './function' + +export function createCallableObject(obj: TObject, func: TFunc): TObject & TFunc { + const proxy = new Proxy(func, { + has(target, key) { + return Reflect.has(obj, key) || Reflect.has(target, key) + }, + ownKeys(target) { + return Array.from(new Set(Reflect.ownKeys(obj).concat(...Reflect.ownKeys(target)))) + }, + get(target, key) { + if (!Reflect.has(target, key) || Reflect.has(obj, key)) { + return Reflect.get(obj, key) + } + + return Reflect.get(target, key) + }, + defineProperty(_, key, descriptor) { + return Reflect.defineProperty(obj, key, descriptor) + }, + set(_, key, value) { + return Reflect.set(obj, key, value) + }, + deleteProperty(target, key) { + return Reflect.deleteProperty(target, key) && Reflect.deleteProperty(obj, key) + }, + }) + + return proxy as any +} From 6ac0f1b4f57cfb674659e54a548a9cbe3f18022d Mon Sep 17 00:00:00 2001 From: unnoq Date: Wed, 18 Dec 2024 21:19:21 +0700 Subject: [PATCH 34/51] group hidden mechanism --- packages/server/src/hidden.ts | 42 +++++++++++++++++++ packages/server/src/index.ts | 1 + packages/server/src/router-builder.test.ts | 35 ++++++++-------- packages/server/src/router-builder.ts | 28 +------------ .../server/src/router-implementer.test.ts | 7 ++-- packages/server/src/router-implementer.ts | 19 +++------ 6 files changed, 70 insertions(+), 62 deletions(-) create mode 100644 packages/server/src/hidden.ts diff --git a/packages/server/src/hidden.ts b/packages/server/src/hidden.ts new file mode 100644 index 000000000..59198c9cd --- /dev/null +++ b/packages/server/src/hidden.ts @@ -0,0 +1,42 @@ +import type { ContractRouter, HTTPPath } from '@orpc/contract' + +const ROUTER_CONTRACT_SYMBOL = Symbol('ORPC_ROUTER_CONTRACT') + +export function setRouterContract(obj: T, contract: ContractRouter): T { + return new Proxy(obj, { + get(target, key) { + if (key === ROUTER_CONTRACT_SYMBOL) { + return contract + } + + return Reflect.get(target, key) + }, + }) +} + +export function getRouterContract(obj: object): ContractRouter | undefined { + return (obj as any)[ROUTER_CONTRACT_SYMBOL] +} + +const LAZY_ROUTER_PREFIX_SYMBOL = Symbol('ORPC_LAZY_ROUTER_PREFIX') + +export function deepSetLazyRouterPrefix(router: T, prefix: HTTPPath): T { + return new Proxy(router, { + get(target, key) { + if (key !== LAZY_ROUTER_PREFIX_SYMBOL) { + const val = Reflect.get(target, key) + if (val && (typeof val === 'object' || typeof val === 'function')) { + return deepSetLazyRouterPrefix(val, prefix) + } + + return val + } + + return prefix + }, + }) +} + +export function getLazyRouterPrefix(obj: object): HTTPPath | undefined { + return (obj as any)[LAZY_ROUTER_PREFIX_SYMBOL] +} diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 6d7ac3d7d..92c41230d 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -2,6 +2,7 @@ import type { WELL_CONTEXT } from './types' import { Builder } from './builder' export * from './builder' +export * from './hidden' export * from './implementer-chainable' export * from './lazy' export * from './lazy-decorated' diff --git a/packages/server/src/router-builder.test.ts b/packages/server/src/router-builder.test.ts index a67f30202..0a5ad57f0 100644 --- a/packages/server/src/router-builder.test.ts +++ b/packages/server/src/router-builder.test.ts @@ -1,8 +1,9 @@ import { ContractProcedure } from '@orpc/contract' import { z } from 'zod' +import { getLazyRouterPrefix } from './hidden' import { isLazy, lazy, unwrapLazy } from './lazy' import { isProcedure, Procedure } from './procedure' -import { getLazyRouterPrefix, LAZY_ROUTER_PREFIX_SYMBOL, RouterBuilder } from './router-builder' +import { RouterBuilder } from './router-builder' const mid1 = vi.fn() const mid2 = vi.fn() @@ -157,11 +158,11 @@ describe('adapt router', () => { it('router with lazy', async () => { const adapted = builder.router(routerWithLazy) as any - expect(adapted.ping[LAZY_ROUTER_PREFIX_SYMBOL]).toBe('/prefix') - expect(adapted.pong[LAZY_ROUTER_PREFIX_SYMBOL]).toBe(undefined) - expect(adapted.nested[LAZY_ROUTER_PREFIX_SYMBOL]).toBe('/prefix') - expect(adapted.nested.ping[LAZY_ROUTER_PREFIX_SYMBOL]).toBe('/prefix') - expect(adapted.nested.pong[LAZY_ROUTER_PREFIX_SYMBOL]).toBe('/prefix') + expect(getLazyRouterPrefix(adapted.ping)).toBe('/prefix') + expect(getLazyRouterPrefix(adapted.pong)).toBe(undefined) + expect(getLazyRouterPrefix(adapted.nested)).toBe('/prefix') + expect(getLazyRouterPrefix(adapted.nested.ping)).toBe('/prefix') + expect(getLazyRouterPrefix(adapted.nested.pong)).toBe('/prefix') expect(adapted.ping).toSatisfy(isLazy) expect(typeof adapted.ping).toBe('function') @@ -202,11 +203,11 @@ describe('adapt router', () => { it('router lazy with nested lazy', async () => { const adapted = builder.lazy(() => Promise.resolve({ default: routerWithLazy })) as any - expect(adapted.ping[LAZY_ROUTER_PREFIX_SYMBOL]).toBe('/prefix') - expect(adapted.pong[LAZY_ROUTER_PREFIX_SYMBOL]).toBe('/prefix') - expect(adapted.nested[LAZY_ROUTER_PREFIX_SYMBOL]).toBe('/prefix') - expect(adapted.nested.ping[LAZY_ROUTER_PREFIX_SYMBOL]).toBe('/prefix') - expect(adapted.nested.pong[LAZY_ROUTER_PREFIX_SYMBOL]).toBe('/prefix') + expect(getLazyRouterPrefix(adapted.ping)).toBe('/prefix') + expect(getLazyRouterPrefix(adapted.pong)).toBe('/prefix') + expect(getLazyRouterPrefix(adapted.nested)).toBe('/prefix') + expect(getLazyRouterPrefix(adapted.nested.ping)).toBe('/prefix') + expect(getLazyRouterPrefix(adapted.nested.pong)).toBe('/prefix') expect(adapted.ping).toSatisfy(isLazy) expect(typeof adapted.ping).toBe('function') @@ -269,24 +270,22 @@ describe('adapt router', () => { it('can concat LAZY_ROUTER_PREFIX_SYMBOL', () => { const adapted = builder.prefix('/hi').router(builder.router(routerWithLazy)) as any - expect(adapted.ping[LAZY_ROUTER_PREFIX_SYMBOL]).toBe('/prefix/hi/prefix') + expect(getLazyRouterPrefix(adapted.ping)).toBe('/prefix/hi/prefix') }) it('works with LAZY_ROUTER_PREFIX_SYMBOL when prefix is not set', () => { const builderWithoutPrefix = new RouterBuilder({}) const adapted = builderWithoutPrefix.router(routerWithLazy) as any - expect(adapted.ping[LAZY_ROUTER_PREFIX_SYMBOL]).toBe(undefined) - expect(adapted.pong[LAZY_ROUTER_PREFIX_SYMBOL]).toBe(undefined) + expect(getLazyRouterPrefix(adapted.ping)).toBe(undefined) + expect(getLazyRouterPrefix(adapted.pong)).toBe(undefined) const adapted2 = builderWithoutPrefix.router(builder.router(routerWithLazy) as any) as any - expect(adapted2.ping[LAZY_ROUTER_PREFIX_SYMBOL]).toBe('/prefix') - expect(adapted2.pong[LAZY_ROUTER_PREFIX_SYMBOL]).toBe(undefined) + expect(getLazyRouterPrefix(adapted2.ping)).toBe('/prefix') + expect(getLazyRouterPrefix(adapted2.pong)).toBe(undefined) }) it('getLazyRouterPrefix works', () => { expect(getLazyRouterPrefix({})).toBe(undefined) - expect(getLazyRouterPrefix(undefined)).toBe(undefined) - expect(getLazyRouterPrefix(null)).toBe(undefined) expect(getLazyRouterPrefix(builder.router(routerWithLazy).ping)).toBe('/prefix') expect(getLazyRouterPrefix(builder.router(routerWithLazy).pong)).toBe(undefined) }) diff --git a/packages/server/src/router-builder.ts b/packages/server/src/router-builder.ts index 58cf0bdb3..ca367f103 100644 --- a/packages/server/src/router-builder.ts +++ b/packages/server/src/router-builder.ts @@ -4,6 +4,7 @@ import type { ANY_MIDDLEWARE, Middleware } from './middleware' import type { ANY_PROCEDURE, Procedure } from './procedure' import type { ANY_ROUTER, Router } from './router' import type { Context, MergeContext } from './types' +import { deepSetLazyRouterPrefix, getLazyRouterPrefix } from './hidden' import { flatLazy, isLazy, lazy, unwrapLazy } from './lazy' import { type DecoratedLazy, decorateLazy } from './lazy-decorated' import { isProcedure } from './procedure' @@ -26,8 +27,6 @@ export type RouterBuilderDef, Partial | undefined, unknown, any>[] } -export const LAZY_ROUTER_PREFIX_SYMBOL = Symbol('ORPC_LAZY_ROUTER_PREFIX') - export class RouterBuilder< TContext extends Context, TExtraContext extends Context, @@ -139,28 +138,3 @@ function adapt( return adapted } - -function deepSetLazyRouterPrefix(router: T, prefix: HTTPPath): T { - return new Proxy(router, { - get(target, key) { - if (key !== LAZY_ROUTER_PREFIX_SYMBOL) { - const val = Reflect.get(target, key) - if (val && (typeof val === 'object' || typeof val === 'function')) { - return deepSetLazyRouterPrefix(val, prefix) - } - - return val - } - - return prefix - }, - }) -} - -export function getLazyRouterPrefix(router: unknown): HTTPPath | undefined { - if (router && (typeof router === 'object' || typeof router === 'function')) { - return (router as any)[LAZY_ROUTER_PREFIX_SYMBOL] - } - - return undefined -} diff --git a/packages/server/src/router-implementer.test.ts b/packages/server/src/router-implementer.test.ts index 5749ba9ad..47c776f0b 100644 --- a/packages/server/src/router-implementer.test.ts +++ b/packages/server/src/router-implementer.test.ts @@ -1,8 +1,9 @@ import { oc } from '@orpc/contract' import { z } from 'zod' +import { getRouterContract } from './hidden' import { Procedure } from './procedure' import { RouterBuilder } from './router-builder' -import { ROUTER_CONTRACT_SYMBOL, RouterImplementer } from './router-implementer' +import { RouterImplementer } from './router-implementer' vi.mock('./router-builder', () => ({ RouterBuilder: vi.fn(() => ({ @@ -90,7 +91,7 @@ describe('to AdaptedRouter', () => { it('attach contract', () => { const adapted = implementer.router(router) as any - expect(adapted[ROUTER_CONTRACT_SYMBOL]).toBe(contract) + expect(getRouterContract(adapted)).toBe(contract) }) }) @@ -111,6 +112,6 @@ describe('to AdaptedLazy', () => { it('attach contract', () => { const adapted = implementer.lazy(() => Promise.resolve({ default: router })) as any - expect(adapted[ROUTER_CONTRACT_SYMBOL]).toBe(contract) + expect(getRouterContract(adapted)).toBe(contract) }) }) diff --git a/packages/server/src/router-implementer.ts b/packages/server/src/router-implementer.ts index 019e43517..ea9a18690 100644 --- a/packages/server/src/router-implementer.ts +++ b/packages/server/src/router-implementer.ts @@ -1,13 +1,12 @@ import type { ContractRouter } from '@orpc/contract' -import type { DecoratedLazy } from './lazy-decorated' +import type { FlattenLazy } from './lazy' import type { Middleware } from './middleware' import type { Router } from './router' import type { AdaptedRouter } from './router-builder' import type { Context, MergeContext } from './types' +import { setRouterContract } from './hidden' import { RouterBuilder } from './router-builder' -export const ROUTER_CONTRACT_SYMBOL = Symbol('ORPC_ROUTER_CONTRACT') - export interface RouterImplementerDef< TContext extends Context, TExtraContext extends Context, @@ -48,26 +47,18 @@ export class RouterImplementer< ): AdaptedRouter { const adapted = new RouterBuilder(this['~orpc']).router(router) - const contracted = this.attachContract(adapted) + const contracted = setRouterContract(adapted, this['~orpc'].contract) return contracted } lazy, TContract>>( loader: () => Promise<{ default: U }>, - ): DecoratedLazy> { + ): AdaptedRouter> { const adapted = new RouterBuilder(this['~orpc']).lazy(loader) - const contracted = this.attachContract(adapted) + const contracted = setRouterContract(adapted, this['~orpc'].contract) return contracted } - - private attachContract( - router: T, - ): T { - return Object.defineProperty(router, ROUTER_CONTRACT_SYMBOL, { - value: this['~orpc'].contract, - }) - } } From 9c859818979138857746064012d3e8084f27cd55 Mon Sep 17 00:00:00 2001 From: unnoq Date: Thu, 19 Dec 2024 15:01:40 +0700 Subject: [PATCH 35/51] router caller now works with procedure as router, and more --- packages/server/src/lazy-decorated.test.ts | 23 +++-- packages/server/src/lazy-decorated.ts | 36 +++---- packages/server/src/lazy.test-d.ts | 6 +- packages/server/src/lazy.test.ts | 6 +- packages/server/src/lazy.ts | 8 +- packages/server/src/procedure-caller.test.ts | 5 +- packages/server/src/procedure-caller.ts | 26 +++--- packages/server/src/router-builder.test.ts | 96 +++++++++---------- packages/server/src/router-builder.ts | 6 +- packages/server/src/router-caller.test-d.ts | 4 + packages/server/src/router-caller.test.ts | 48 +++++++--- packages/server/src/router-caller.ts | 63 ++++++------- packages/server/src/router.test-d.ts | 25 ++++- packages/server/src/router.test.ts | 98 ++++++++++++++++++++ packages/server/src/router.ts | 63 +++++++++++-- 15 files changed, 358 insertions(+), 155 deletions(-) create mode 100644 packages/server/src/router.test.ts diff --git a/packages/server/src/lazy-decorated.test.ts b/packages/server/src/lazy-decorated.test.ts index 68673f89c..a6285ad6d 100644 --- a/packages/server/src/lazy-decorated.test.ts +++ b/packages/server/src/lazy-decorated.test.ts @@ -1,6 +1,6 @@ import { ContractProcedure } from '@orpc/contract' import { z } from 'zod' -import { isLazy, lazy, unwrapLazy } from './lazy' +import { isLazy, lazy, unlazy } from './lazy' import { decorateLazy } from './lazy-decorated' import { Procedure } from './procedure' import { createProcedureCaller } from './procedure-caller' @@ -30,15 +30,26 @@ describe('decorated lazy', () => { it('still a lazy', async () => { expect(decorateLazy(lazyPing)).toSatisfy(isLazy) - expect((await unwrapLazy(decorateLazy(lazyPing))).default).toBe(ping) + expect((await unlazy(decorateLazy(lazyPing))).default).toBe(ping) const l2 = lazy(() => Promise.resolve({ default: { ping } })) expect(decorateLazy(l2)).toSatisfy(isLazy) - expect((await unwrapLazy(decorateLazy(l2))).default.ping).toBe(ping) + expect((await unlazy(decorateLazy(l2))).default.ping).toBe(ping) const l3 = lazy(() => Promise.resolve({ default: { ping: lazyPing } })) expect(decorateLazy(l3)).toSatisfy(isLazy) - expect((await unwrapLazy(decorateLazy(l3))).default.ping).toBe(lazyPing) + expect((await unlazy(decorateLazy(l3))).default.ping).toBe(lazyPing) + }) + + it('throw error on load child of lazy that does not exist', () => { + const decorated = decorateLazy(lazy(() => Promise.resolve({ default: { ping: { pong: lazyPing } } }))) as any + + const child = decorated.ping.pong.peng.pang.p + + expect(child).toBeInstanceOf(Function) + expect(child).toSatisfy(isLazy) + + expect(unlazy(child)).rejects.toThrow(`Not found`) }) describe('callable', () => { @@ -63,7 +74,7 @@ describe('decorated lazy', () => { context: undefined, }) expect(vi.mocked(createProcedureCaller).mock.calls[0]![0].procedure).toSatisfy(isLazy) - const unwrapped = await unwrapLazy(vi.mocked(createProcedureCaller).mock.calls[0]![0].procedure as any) + const unwrapped = await unlazy(vi.mocked(createProcedureCaller).mock.calls[0]![0].procedure as any) expect(unwrapped.default).toBe(router) expect(await decorated({ val: '1' }, { signal })).toBe('__mocked__') @@ -81,7 +92,7 @@ describe('decorated lazy', () => { context: undefined, }) expect(vi.mocked(createProcedureCaller).mock.calls[2]![0].procedure).toSatisfy(isLazy) - const unwrapped = await unwrapLazy(vi.mocked(createProcedureCaller).mock.calls[2]![0].procedure as any) + const unwrapped = await unlazy(vi.mocked(createProcedureCaller).mock.calls[2]![0].procedure as any) expect(unwrapped.default).toBe(ping) expect(await decorated({ val: '1' }, { signal })).toBe('__mocked__') diff --git a/packages/server/src/lazy-decorated.ts b/packages/server/src/lazy-decorated.ts index 3116d5ffa..1d4cbf236 100644 --- a/packages/server/src/lazy-decorated.ts +++ b/packages/server/src/lazy-decorated.ts @@ -1,28 +1,31 @@ import type { SchemaInput, SchemaOutput } from '@orpc/contract' -import type { ANY_LAZY, Lazy } from './lazy' +import type { Lazy } from './lazy' import type { Procedure } from './procedure' import type { Caller } from './types' -import { flatLazy, lazy, unwrapLazy } from './lazy' +import { flatLazy } from './lazy' import { createProcedureCaller } from './procedure-caller' +import { type ANY_ROUTER, getRouterChild } from './router' export type DecoratedLazy = T extends Lazy ? DecoratedLazy - : ( - T extends Procedure - ? - & Lazy - & (undefined extends UContext ? Caller, SchemaOutput> : unknown) - : { - [K in keyof T]: DecoratedLazy - } & Lazy + : + & Lazy + & ( + T extends Procedure + ? undefined extends UContext + ? Caller, SchemaOutput> + : unknown + : { + [K in keyof T]: T[K] extends object ? DecoratedLazy : never + } ) -export function decorateLazy(lazied: T): DecoratedLazy { +export function decorateLazy>(lazied: T): DecoratedLazy { const flattenLazy = flatLazy(lazied) const procedureCaller = createProcedureCaller({ - procedure: flattenLazy as any, - context: undefined as any, + procedure: flattenLazy, + context: undefined, }) Object.assign(procedureCaller, flattenLazy) @@ -33,10 +36,9 @@ export function decorateLazy(lazied: T): DecoratedLazy { return Reflect.get(target, key) } - return decorateLazy(lazy(async () => { - const current = await unwrapLazy(flattenLazy) - return { default: (current.default as any)[key] } - })) + const next = getRouterChild(flattenLazy, key) + + return decorateLazy(next) }, }) diff --git a/packages/server/src/lazy.test-d.ts b/packages/server/src/lazy.test-d.ts index 32fc7be35..4094e9459 100644 --- a/packages/server/src/lazy.test-d.ts +++ b/packages/server/src/lazy.test-d.ts @@ -1,7 +1,7 @@ import type { ANY_LAZY, FlattenLazy, Lazy } from './lazy' import type { Procedure } from './procedure' import type { WELL_CONTEXT } from './types' -import { flatLazy, isLazy, lazy, unwrapLazy } from './lazy' +import { flatLazy, isLazy, lazy, unlazy } from './lazy' const procedure = {} as Procedure @@ -27,11 +27,11 @@ it('isLazy', () => { it('unwrapLazy', () => { expectTypeOf( - unwrapLazy(lazy(() => Promise.resolve({ default: procedure }))), + unlazy(lazy(() => Promise.resolve({ default: procedure }))), ).toMatchTypeOf>() expectTypeOf( - unwrapLazy(lazy(() => Promise.resolve({ default: router }))), + unlazy(lazy(() => Promise.resolve({ default: router }))), ).toMatchTypeOf>() }) diff --git a/packages/server/src/lazy.test.ts b/packages/server/src/lazy.test.ts index 560938c7e..cb7aa52e0 100644 --- a/packages/server/src/lazy.test.ts +++ b/packages/server/src/lazy.test.ts @@ -1,6 +1,6 @@ import type { WELL_CONTEXT } from './types' import { ContractProcedure } from '@orpc/contract' -import { flatLazy, isLazy, lazy, LAZY_LOADER_SYMBOL, unwrapLazy } from './lazy' +import { flatLazy, isLazy, lazy, LAZY_LOADER_SYMBOL, unlazy } from './lazy' import { Procedure } from './procedure' const procedure = new Procedure({ @@ -35,8 +35,8 @@ it('isLazy', () => { it('unwrapLazy', async () => { const lazied = lazy(() => Promise.resolve({ default: 'root' })) - expect(unwrapLazy(lazied)).resolves.toEqual({ default: 'root' }) - expect((await unwrapLazy(lazy(() => Promise.resolve({ default: lazied })))).default).toSatisfy(isLazy) + expect(unlazy(lazied)).resolves.toEqual({ default: 'root' }) + expect((await unlazy(lazy(() => Promise.resolve({ default: lazied })))).default).toSatisfy(isLazy) }) it('flatLazy', () => { diff --git a/packages/server/src/lazy.ts b/packages/server/src/lazy.ts index f3f7779f2..253079da1 100644 --- a/packages/server/src/lazy.ts +++ b/packages/server/src/lazy.ts @@ -23,8 +23,8 @@ export function isLazy(item: unknown): item is ANY_LAZY { ) } -export function unwrapLazy>(lazy: T): Promise<{ default: T extends Lazy ? U : never }> { - return lazy[LAZY_LOADER_SYMBOL]() as any +export function unlazy(lazied: Lazyable): Promise<{ default: T }> { + return isLazy(lazied) ? lazied[LAZY_LOADER_SYMBOL]() : Promise.resolve({ default: lazied }) } export type FlattenLazy = T extends Lazy @@ -33,14 +33,14 @@ export type FlattenLazy = T extends Lazy export function flatLazy(lazied: T): FlattenLazy { const flattenLoader = async () => { - let current = await unwrapLazy(lazied) + let current = await unlazy(lazied) while (true) { if (!isLazy(current.default)) { break } - current = await unwrapLazy(current.default) + current = await unlazy(current.default) } return current diff --git a/packages/server/src/procedure-caller.test.ts b/packages/server/src/procedure-caller.test.ts index acd50ae06..43f5bda6f 100644 --- a/packages/server/src/procedure-caller.test.ts +++ b/packages/server/src/procedure-caller.test.ts @@ -1,7 +1,7 @@ import type { WELL_CONTEXT } from './types' import { ContractProcedure } from '@orpc/contract' import { z } from 'zod' -import { isLazy, lazy, unwrapLazy } from './lazy' +import { isLazy, lazy, unlazy } from './lazy' import { Procedure } from './procedure' import { createProcedureCaller } from './procedure-caller' @@ -30,7 +30,7 @@ beforeEach(() => { }) describe.each(procedureCases)('createProcedureCaller - case %s', async (_, procedure) => { - const unwrappedProcedure = isLazy(procedure) ? (await unwrapLazy(procedure)).default : procedure + const unwrappedProcedure = isLazy(procedure) ? (await unlazy(procedure)).default : procedure it('just a caller', async () => { const caller = createProcedureCaller({ @@ -299,7 +299,6 @@ it('should throw error when invalid lazy procedure', () => { const lazied = lazy(() => Promise.resolve({ default: 123 })) const caller = createProcedureCaller({ - // @ts-expect-error - invalid lazy procedure procedure: lazied, }) diff --git a/packages/server/src/procedure-caller.ts b/packages/server/src/procedure-caller.ts index 7bfde62ce..b7d9b014d 100644 --- a/packages/server/src/procedure-caller.ts +++ b/packages/server/src/procedure-caller.ts @@ -1,19 +1,19 @@ import type { Schema, SchemaInput, SchemaOutput } from '@orpc/contract' import type { Hooks, Value } from '@orpc/shared' -import type { Lazy } from './lazy' +import type { Lazy, Lazyable } from './lazy' import type { MiddlewareMeta } from './middleware' -import type { - ANY_LAZY_PROCEDURE, - ANY_PROCEDURE, - Procedure, - WELL_PROCEDURE, -} from './procedure' import type { Caller, Context, Meta, WELL_CONTEXT } from './types' import { executeWithHooks, trim, value } from '@orpc/shared' import { ORPCError } from '@orpc/shared/error' -import { isLazy, unwrapLazy } from './lazy' -import { isProcedure } from './procedure' +import { isLazy, unlazy } from './lazy' +import { + type ANY_LAZY_PROCEDURE, + type ANY_PROCEDURE, + isProcedure, + type Procedure, + type WELL_PROCEDURE, +} from './procedure' import { mergeContext } from './utils' /** @@ -27,8 +27,8 @@ export type CreateProcedureCallerOptions< > = & { procedure: - | Procedure - | Lazy> + | Lazyable> + | Lazy /** * This is helpful for logging and analytics. @@ -155,9 +155,9 @@ async function executeMiddlewareChain( return (await next({})).output } -export async function loadProcedure(procedure: ANY_PROCEDURE | ANY_LAZY_PROCEDURE): Promise { +export async function loadProcedure(procedure: ANY_PROCEDURE | ANY_LAZY_PROCEDURE | Lazy): Promise { const loadedProcedure = isLazy(procedure) - ? (await unwrapLazy(procedure)).default + ? (await unlazy(procedure)).default : procedure if (!isProcedure(loadedProcedure)) { diff --git a/packages/server/src/router-builder.test.ts b/packages/server/src/router-builder.test.ts index 0a5ad57f0..9aa117154 100644 --- a/packages/server/src/router-builder.test.ts +++ b/packages/server/src/router-builder.test.ts @@ -1,7 +1,7 @@ import { ContractProcedure } from '@orpc/contract' import { z } from 'zod' import { getLazyRouterPrefix } from './hidden' -import { isLazy, lazy, unwrapLazy } from './lazy' +import { isLazy, lazy, unlazy } from './lazy' import { isProcedure, Procedure } from './procedure' import { RouterBuilder } from './router-builder' @@ -166,12 +166,12 @@ describe('adapt router', () => { expect(adapted.ping).toSatisfy(isLazy) expect(typeof adapted.ping).toBe('function') - expect((await unwrapLazy(adapted.ping) as any).default).toSatisfy(isProcedure) - expect((await unwrapLazy(adapted.ping) as any).default['~orpc'].func).toBe(ping['~orpc'].func) - expect((await unwrapLazy(adapted.ping) as any).default['~orpc'].middlewares).toEqual([mid1, mid2, pMid1, pMid2]) - expect((await unwrapLazy(adapted.ping) as any).default['~orpc'].contract['~orpc'].route?.path).toBe(undefined) - expect((await unwrapLazy(adapted.ping) as any).default['~orpc'].contract['~orpc'].route?.method).toBe(undefined) - expect((await unwrapLazy(adapted.ping) as any).default['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2', 'tag3', 'tag4']) + expect((await unlazy(adapted.ping) as any).default).toSatisfy(isProcedure) + expect((await unlazy(adapted.ping) as any).default['~orpc'].func).toBe(ping['~orpc'].func) + expect((await unlazy(adapted.ping) as any).default['~orpc'].middlewares).toEqual([mid1, mid2, pMid1, pMid2]) + expect((await unlazy(adapted.ping) as any).default['~orpc'].contract['~orpc'].route?.path).toBe(undefined) + expect((await unlazy(adapted.ping) as any).default['~orpc'].contract['~orpc'].route?.method).toBe(undefined) + expect((await unlazy(adapted.ping) as any).default['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2', 'tag3', 'tag4']) expect(adapted.pong).toSatisfy(isProcedure) expect(typeof adapted.pong).toBe('function') @@ -183,21 +183,21 @@ describe('adapt router', () => { expect(adapted.nested.ping).toSatisfy(isLazy) expect(typeof adapted.nested.ping).toBe('function') - expect((await unwrapLazy(adapted.nested.ping) as any).default).toSatisfy(isProcedure) - expect((await unwrapLazy(adapted.nested.ping) as any).default['~orpc'].func).toBe(ping['~orpc'].func) - expect((await unwrapLazy(adapted.nested.ping) as any).default['~orpc'].middlewares).toEqual([mid1, mid2, pMid1, pMid2]) - expect((await unwrapLazy(adapted.nested.ping) as any).default['~orpc'].contract['~orpc'].route?.path).toBe(undefined) - expect((await unwrapLazy(adapted.nested.ping) as any).default['~orpc'].contract['~orpc'].route?.method).toBe(undefined) - expect((await unwrapLazy(adapted.nested.ping) as any).default['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2', 'tag3', 'tag4']) + expect((await unlazy(adapted.nested.ping) as any).default).toSatisfy(isProcedure) + expect((await unlazy(adapted.nested.ping) as any).default['~orpc'].func).toBe(ping['~orpc'].func) + expect((await unlazy(adapted.nested.ping) as any).default['~orpc'].middlewares).toEqual([mid1, mid2, pMid1, pMid2]) + expect((await unlazy(adapted.nested.ping) as any).default['~orpc'].contract['~orpc'].route?.path).toBe(undefined) + expect((await unlazy(adapted.nested.ping) as any).default['~orpc'].contract['~orpc'].route?.method).toBe(undefined) + expect((await unlazy(adapted.nested.ping) as any).default['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2', 'tag3', 'tag4']) expect(adapted.nested.pong).toSatisfy(isLazy) expect(typeof adapted.nested.pong).toBe('function') - expect((await unwrapLazy(adapted.nested.pong) as any).default).toSatisfy(isProcedure) - expect((await unwrapLazy(adapted.nested.pong) as any).default['~orpc'].func).toBe(pong['~orpc'].func) - expect((await unwrapLazy(adapted.nested.pong) as any).default['~orpc'].middlewares).toEqual([mid1, mid2]) - expect((await unwrapLazy(adapted.nested.pong) as any).default['~orpc'].contract['~orpc'].route?.path).toBe('/prefix/pong') - expect((await unwrapLazy(adapted.nested.pong) as any).default['~orpc'].contract['~orpc'].route?.method).toBe('GET') - expect((await unwrapLazy(adapted.nested.pong) as any).default['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2']) + expect((await unlazy(adapted.nested.pong) as any).default).toSatisfy(isProcedure) + expect((await unlazy(adapted.nested.pong) as any).default['~orpc'].func).toBe(pong['~orpc'].func) + expect((await unlazy(adapted.nested.pong) as any).default['~orpc'].middlewares).toEqual([mid1, mid2]) + expect((await unlazy(adapted.nested.pong) as any).default['~orpc'].contract['~orpc'].route?.path).toBe('/prefix/pong') + expect((await unlazy(adapted.nested.pong) as any).default['~orpc'].contract['~orpc'].route?.method).toBe('GET') + expect((await unlazy(adapted.nested.pong) as any).default['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2']) }) it('router lazy with nested lazy', async () => { @@ -211,39 +211,39 @@ describe('adapt router', () => { expect(adapted.ping).toSatisfy(isLazy) expect(typeof adapted.ping).toBe('function') - expect((await unwrapLazy(adapted.ping) as any).default).toSatisfy(isProcedure) - expect((await unwrapLazy(adapted.ping) as any).default['~orpc'].func).toBe(ping['~orpc'].func) - expect((await unwrapLazy(adapted.ping) as any).default['~orpc'].middlewares).toEqual([mid1, mid2, pMid1, pMid2]) - expect((await unwrapLazy(adapted.ping) as any).default['~orpc'].contract['~orpc'].route?.path).toBe(undefined) - expect((await unwrapLazy(adapted.ping) as any).default['~orpc'].contract['~orpc'].route?.method).toBe(undefined) - expect((await unwrapLazy(adapted.ping) as any).default['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2', 'tag3', 'tag4']) + expect((await unlazy(adapted.ping) as any).default).toSatisfy(isProcedure) + expect((await unlazy(adapted.ping) as any).default['~orpc'].func).toBe(ping['~orpc'].func) + expect((await unlazy(adapted.ping) as any).default['~orpc'].middlewares).toEqual([mid1, mid2, pMid1, pMid2]) + expect((await unlazy(adapted.ping) as any).default['~orpc'].contract['~orpc'].route?.path).toBe(undefined) + expect((await unlazy(adapted.ping) as any).default['~orpc'].contract['~orpc'].route?.method).toBe(undefined) + expect((await unlazy(adapted.ping) as any).default['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2', 'tag3', 'tag4']) expect(adapted.pong).toSatisfy(isLazy) expect(typeof adapted.pong).toBe('function') - expect((await unwrapLazy(adapted.pong) as any).default).toSatisfy(isProcedure) - expect((await unwrapLazy(adapted.pong) as any).default['~orpc'].func).toBe(pong['~orpc'].func) - expect((await unwrapLazy(adapted.pong) as any).default['~orpc'].middlewares).toEqual([mid1, mid2]) - expect((await unwrapLazy(adapted.pong) as any).default['~orpc'].contract['~orpc'].route?.path).toBe('/prefix/pong') - expect((await unwrapLazy(adapted.pong) as any).default['~orpc'].contract['~orpc'].route?.method).toBe('GET') - expect((await unwrapLazy(adapted.pong) as any).default['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2']) + expect((await unlazy(adapted.pong) as any).default).toSatisfy(isProcedure) + expect((await unlazy(adapted.pong) as any).default['~orpc'].func).toBe(pong['~orpc'].func) + expect((await unlazy(adapted.pong) as any).default['~orpc'].middlewares).toEqual([mid1, mid2]) + expect((await unlazy(adapted.pong) as any).default['~orpc'].contract['~orpc'].route?.path).toBe('/prefix/pong') + expect((await unlazy(adapted.pong) as any).default['~orpc'].contract['~orpc'].route?.method).toBe('GET') + expect((await unlazy(adapted.pong) as any).default['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2']) expect(adapted.nested.ping).toSatisfy(isLazy) expect(typeof adapted.nested.ping).toBe('function') - expect((await unwrapLazy(adapted.nested.ping) as any).default).toSatisfy(isProcedure) - expect((await unwrapLazy(adapted.nested.ping) as any).default['~orpc'].func).toBe(ping['~orpc'].func) - expect((await unwrapLazy(adapted.nested.ping) as any).default['~orpc'].middlewares).toEqual([mid1, mid2, pMid1, pMid2]) - expect((await unwrapLazy(adapted.nested.ping) as any).default['~orpc'].contract['~orpc'].route?.path).toBe(undefined) - expect((await unwrapLazy(adapted.nested.ping) as any).default['~orpc'].contract['~orpc'].route?.method).toBe(undefined) - expect((await unwrapLazy(adapted.nested.ping) as any).default['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2', 'tag3', 'tag4']) + expect((await unlazy(adapted.nested.ping) as any).default).toSatisfy(isProcedure) + expect((await unlazy(adapted.nested.ping) as any).default['~orpc'].func).toBe(ping['~orpc'].func) + expect((await unlazy(adapted.nested.ping) as any).default['~orpc'].middlewares).toEqual([mid1, mid2, pMid1, pMid2]) + expect((await unlazy(adapted.nested.ping) as any).default['~orpc'].contract['~orpc'].route?.path).toBe(undefined) + expect((await unlazy(adapted.nested.ping) as any).default['~orpc'].contract['~orpc'].route?.method).toBe(undefined) + expect((await unlazy(adapted.nested.ping) as any).default['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2', 'tag3', 'tag4']) expect(adapted.nested.pong).toSatisfy(isLazy) expect(typeof adapted.nested.pong).toBe('function') - expect((await unwrapLazy(adapted.nested.pong) as any).default).toSatisfy(isProcedure) - expect((await unwrapLazy(adapted.nested.pong) as any).default['~orpc'].func).toBe(pong['~orpc'].func) - expect((await unwrapLazy(adapted.nested.pong) as any).default['~orpc'].middlewares).toEqual([mid1, mid2]) - expect((await unwrapLazy(adapted.nested.pong) as any).default['~orpc'].contract['~orpc'].route?.path).toBe('/prefix/pong') - expect((await unwrapLazy(adapted.nested.pong) as any).default['~orpc'].contract['~orpc'].route?.method).toBe('GET') - expect((await unwrapLazy(adapted.nested.pong) as any).default['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2']) + expect((await unlazy(adapted.nested.pong) as any).default).toSatisfy(isProcedure) + expect((await unlazy(adapted.nested.pong) as any).default['~orpc'].func).toBe(pong['~orpc'].func) + expect((await unlazy(adapted.nested.pong) as any).default['~orpc'].middlewares).toEqual([mid1, mid2]) + expect((await unlazy(adapted.nested.pong) as any).default['~orpc'].contract['~orpc'].route?.path).toBe('/prefix/pong') + expect((await unlazy(adapted.nested.pong) as any).default['~orpc'].contract['~orpc'].route?.method).toBe('GET') + expect((await unlazy(adapted.nested.pong) as any).default['~orpc'].contract['~orpc'].route?.tags).toEqual(['tag1', 'tag2']) }) it('support procedure as a router', async () => { @@ -261,11 +261,11 @@ describe('adapt router', () => { expect(adaptedLazy).toSatisfy(isLazy) expect(typeof adaptedLazy).toBe('function') - expect((await unwrapLazy(adaptedLazy) as any).default).toSatisfy(isProcedure) - expect((await unwrapLazy(adaptedLazy) as any).default['~orpc'].func).toBe(ping['~orpc'].func) - expect((await unwrapLazy(adaptedLazy) as any).default['~orpc'].middlewares).toEqual([mid1, mid2, pMid1, pMid2]) - expect((await unwrapLazy(adaptedLazy) as any).default['~orpc'].contract['~orpc'].route?.path).toBe(undefined) - expect((await unwrapLazy(adaptedLazy) as any).default['~orpc'].contract['~orpc'].route?.method).toBe(undefined) + expect((await unlazy(adaptedLazy) as any).default).toSatisfy(isProcedure) + expect((await unlazy(adaptedLazy) as any).default['~orpc'].func).toBe(ping['~orpc'].func) + expect((await unlazy(adaptedLazy) as any).default['~orpc'].middlewares).toEqual([mid1, mid2, pMid1, pMid2]) + expect((await unlazy(adaptedLazy) as any).default['~orpc'].contract['~orpc'].route?.path).toBe(undefined) + expect((await unlazy(adaptedLazy) as any).default['~orpc'].contract['~orpc'].route?.method).toBe(undefined) }) it('can concat LAZY_ROUTER_PREFIX_SYMBOL', () => { diff --git a/packages/server/src/router-builder.ts b/packages/server/src/router-builder.ts index ca367f103..5f6cfead0 100644 --- a/packages/server/src/router-builder.ts +++ b/packages/server/src/router-builder.ts @@ -5,7 +5,7 @@ import type { ANY_PROCEDURE, Procedure } from './procedure' import type { ANY_ROUTER, Router } from './router' import type { Context, MergeContext } from './types' import { deepSetLazyRouterPrefix, getLazyRouterPrefix } from './hidden' -import { flatLazy, isLazy, lazy, unwrapLazy } from './lazy' +import { flatLazy, isLazy, lazy, unlazy } from './lazy' import { type DecoratedLazy, decorateLazy } from './lazy-decorated' import { isProcedure } from './procedure' import { type DecoratedProcedure, decorateProcedure } from './procedure-decorated' @@ -95,10 +95,10 @@ function adapt( tags?: readonly string[] prefix?: HTTPPath }, -): unknown { +): ANY_ROUTER { if (isLazy(item)) { const adaptedLazy = decorateLazy(lazy(async () => { - const routerOrProcedure = (await unwrapLazy(item)).default as ANY_ROUTER | ANY_PROCEDURE + const routerOrProcedure = (await unlazy(item)).default as ANY_ROUTER | ANY_PROCEDURE const adapted = adapt(routerOrProcedure, options) return { default: adapted } diff --git a/packages/server/src/router-caller.test-d.ts b/packages/server/src/router-caller.test-d.ts index 256188f70..5a9f8b506 100644 --- a/packages/server/src/router-caller.test-d.ts +++ b/packages/server/src/router-caller.test-d.ts @@ -48,6 +48,10 @@ describe('RouterCaller', () => { it('support lazy', () => { expectTypeOf>().toEqualTypeOf>() }) + + it('support procedure as router', () => { + expectTypeOf>().toEqualTypeOf>() + }) }) describe('createRouterCaller', () => { diff --git a/packages/server/src/router-caller.test.ts b/packages/server/src/router-caller.test.ts index bd6e70870..14d1e3bf3 100644 --- a/packages/server/src/router-caller.test.ts +++ b/packages/server/src/router-caller.test.ts @@ -1,6 +1,6 @@ import { ContractProcedure } from '@orpc/contract' import { z } from 'zod' -import { lazy, unwrapLazy } from './lazy' +import { lazy, unlazy } from './lazy' import { Procedure } from './procedure' import { createProcedureCaller } from './procedure-caller' import { createRouterCaller } from './router-caller' @@ -62,34 +62,54 @@ describe('createRouterCaller', () => { it('work with lazy', async () => { expect(caller.ping({ val: '123' })).toEqual('__mocked__') - expect(createProcedureCaller).toBeCalledTimes(2) - expect(createProcedureCaller).toHaveBeenNthCalledWith(2, expect.objectContaining({ - procedure: expect.any(Function), + expect(createProcedureCaller).toBeCalledTimes(1) + expect(createProcedureCaller).toHaveBeenNthCalledWith(1, expect.objectContaining({ + procedure: expect.any(Object), context: { auth: true }, path: ['users', 'ping'], })) - expect((await unwrapLazy(vi.mocked(createProcedureCaller as any).mock.calls[1]![0].procedure)).default).toBe(ping) + expect((await unlazy(vi.mocked(createProcedureCaller as any).mock.calls[0]![0].procedure)).default).toBe(ping) - expect(vi.mocked(createProcedureCaller).mock.results[1]?.value).toBeCalledTimes(1) - expect(vi.mocked(createProcedureCaller).mock.results[1]?.value).toBeCalledWith({ val: '123' }) + expect(vi.mocked(createProcedureCaller).mock.results[0]?.value).toBeCalledTimes(1) + expect(vi.mocked(createProcedureCaller).mock.results[0]?.value).toBeCalledWith({ val: '123' }) }) it('work with nested lazy', async () => { expect(caller.nested.ping({ val: '123' })).toEqual('__mocked__') - expect(createProcedureCaller).toBeCalledTimes(5) - expect(createProcedureCaller).toHaveBeenNthCalledWith(5, expect.objectContaining({ - procedure: expect.any(Function), + expect(createProcedureCaller).toBeCalledTimes(2) + expect(createProcedureCaller).toHaveBeenNthCalledWith(2, expect.objectContaining({ + procedure: expect.any(Object), context: { auth: true }, path: ['users', 'nested', 'ping'], })) - const lazied = vi.mocked(createProcedureCaller as any).mock.calls[4]![0].procedure - expect(await unwrapLazy(lazied)).toEqual({ default: ping }) + const lazied = vi.mocked(createProcedureCaller as any).mock.calls[1]![0].procedure + expect(await unlazy(lazied)).toEqual({ default: ping }) + + expect(vi.mocked(createProcedureCaller).mock.results[1]?.value).toBeCalledTimes(1) + expect(vi.mocked(createProcedureCaller).mock.results[1]?.value).toBeCalledWith({ val: '123' }) + }) - expect(vi.mocked(createProcedureCaller).mock.results[4]?.value).toBeCalledTimes(1) - expect(vi.mocked(createProcedureCaller).mock.results[4]?.value).toBeCalledWith({ val: '123' }) + it('work with procedure as router', () => { + const caller = createRouterCaller({ + router: ping, + context: { auth: true }, + path: ['users'], + }) + + expect(caller({ val: '123' })).toEqual('__mocked__') + + expect(createProcedureCaller).toBeCalledTimes(1) + expect(createProcedureCaller).toHaveBeenCalledWith(expect.objectContaining({ + procedure: ping, + context: { auth: true }, + path: ['users'], + })) + + expect(vi.mocked(createProcedureCaller).mock.results[0]?.value).toBeCalledTimes(1) + expect(vi.mocked(createProcedureCaller).mock.results[0]?.value).toBeCalledWith({ val: '123' }) }) it('hooks', async () => { diff --git a/packages/server/src/router-caller.ts b/packages/server/src/router-caller.ts index 8ec221b54..72f470725 100644 --- a/packages/server/src/router-caller.ts +++ b/packages/server/src/router-caller.ts @@ -1,19 +1,27 @@ import type { SchemaInput, SchemaOutput } from '@orpc/contract' -import type { Hooks, Merge, Value } from '@orpc/shared' +import type { Hooks, Value } from '@orpc/shared' import type { Lazy } from './lazy' import type { Procedure } from './procedure' -import type { ANY_ROUTER, Router } from './router' import type { Caller, Meta } from './types' import { isLazy } from './lazy' -import { decorateLazy } from './lazy-decorated' import { isProcedure } from './procedure' import { createProcedureCaller } from './procedure-caller' +import { type ANY_ROUTER, getRouterChild, type Router } from './router' + +export type RouterCaller = T extends Lazy + ? RouterCaller + : T extends Procedure + ? Caller, SchemaOutput> + : { + [K in keyof T]: T[K] extends ANY_ROUTER ? RouterCaller : never + } export type CreateRouterCallerOptions< TRouter extends ANY_ROUTER, > = & { - router: TRouter + router: TRouter | Lazy + /** * This is helpful for logging and analytics. * @@ -26,41 +34,26 @@ export type CreateRouterCallerOptions< : never) & Hooks ? UContext : never, Meta> -export type RouterCaller< - TRouter extends ANY_ROUTER, -> = { - [K in keyof TRouter]: TRouter[K] extends - | Procedure - | Lazy> - ? Caller, SchemaOutput> - : TRouter[K] extends ANY_ROUTER - ? RouterCaller - : TRouter[K] extends Lazy - ? U extends ANY_ROUTER - ? RouterCaller - : never - : never -} - export function createRouterCaller< TRouter extends ANY_ROUTER, >( options: CreateRouterCallerOptions, ): RouterCaller { - return createRouterCallerInternal(options) as any -} + if (isProcedure(options.router)) { + const caller = createProcedureCaller({ + ...options, + procedure: options.router, + context: options.context, + path: options.path, + }) -function createRouterCallerInternal( - options: Merge, { - router: ANY_ROUTER - }>, -) { - const router = isLazy(options.router) ? decorateLazy(options.router) : options.router + return caller as any + } - const procedureCaller = isLazy(options.router) || isProcedure(options.router) + const procedureCaller = isLazy(options.router) ? createProcedureCaller({ ...options, - procedure: router as any, + procedure: options.router, context: options.context, path: options.path, }) @@ -72,9 +65,13 @@ function createRouterCallerInternal( return Reflect.get(target, key) } - const next = (router as any)[key] + const next = getRouterChild(options.router, key) + + if (!next) { + return Reflect.get(target, key) + } - return createRouterCallerInternal({ + return createRouterCaller({ ...options, router: next, path: [...(options.path ?? []), key], @@ -82,5 +79,5 @@ function createRouterCallerInternal( }, }) - return recursive + return recursive as any } diff --git a/packages/server/src/router.test-d.ts b/packages/server/src/router.test-d.ts index ea7438b8f..f5dd7f760 100644 --- a/packages/server/src/router.test-d.ts +++ b/packages/server/src/router.test-d.ts @@ -1,9 +1,11 @@ +import type { ANY_LAZY, Lazy } from './lazy' import type { Procedure } from './procedure' -import type { InferRouterInputs, InferRouterOutputs, Router } from './router' +import type { ANY_ROUTER, InferRouterInputs, InferRouterOutputs, Router } from './router' import type { WELL_CONTEXT } from './types' import { oc } from '@orpc/contract' import { z } from 'zod' import { lazy } from './lazy' +import { getRouterChild } from './router' const schema = z.object({ val: z.string().transform(v => Number.parseInt(v)) }) @@ -216,3 +218,24 @@ describe('Router', () => { const router4: Router<{ auth: boolean, userId: string }, typeof pingContract> = {} as Procedure<{ auth: boolean }, { db: string }, undefined, undefined, unknown> }) }) + +describe('getRouterChild', () => { + it('works', () => { + getRouterChild({}) + getRouterChild(router) + getRouterChild(lazy(() => Promise.resolve({ default: router }))) + getRouterChild(lazy(() => Promise.resolve({ default: undefined }))) + + // @ts-expect-error --- invalid router + getRouterChild(1) + + expectTypeOf(getRouterChild({})).toEqualTypeOf | undefined>() + }) + + it('return lazy if router is lazy', () => { + expectTypeOf( + getRouterChild(lazy(() => Promise.resolve({ default: router })), 'a', 'b'), + ) + .toMatchTypeOf() + }) +}) diff --git a/packages/server/src/router.test.ts b/packages/server/src/router.test.ts new file mode 100644 index 000000000..fc73fdad0 --- /dev/null +++ b/packages/server/src/router.test.ts @@ -0,0 +1,98 @@ +import { ContractProcedure } from '@orpc/contract' +import { z } from 'zod' +import { isLazy, lazy, unlazy } from './lazy' +import { Procedure } from './procedure' +import { getRouterChild } from './router' + +describe('getRouterChild', () => { + const schema = z.object({ val: z.string().transform(val => Number(val)) }) + + const ping = new Procedure({ + contract: new ContractProcedure({ + InputSchema: schema, + OutputSchema: schema, + }), + func: vi.fn(() => ({ val: '123' })), + }) + const pong = new Procedure({ + contract: new ContractProcedure({ + InputSchema: undefined, + OutputSchema: undefined, + }), + func: vi.fn(() => ('output')), + }) + + it('with procedure as router', () => { + expect(getRouterChild(ping)).toBe(ping) + expect(getRouterChild(ping, '~orpc')).toBe(undefined) + expect(getRouterChild(ping, '~type')).toBe(undefined) + }) + + it('with router', () => { + const router = { + ping, + pong, + nested: { + ping, + pong, + }, + } + + expect(getRouterChild(router, 'ping')).toBe(ping) + expect(getRouterChild(router, 'pong')).toBe(pong) + expect(getRouterChild(router, 'nested')).toBe(router.nested) + expect(getRouterChild(router, 'nested', 'ping')).toBe(ping) + expect(getRouterChild(router, 'nested', 'pong')).toBe(pong) + expect(getRouterChild(router, 'nested', '~orpc')).toBe(undefined) + expect(getRouterChild(router, 'nested', 'ping', '~orpc')).toBe(undefined) + expect(getRouterChild(router, 'nested', 'pue', '~orpc', 'peng', 'pue')).toBe(undefined) + }) + + it('with lazy router', async () => { + const lazyPing = lazy(() => Promise.resolve({ default: ping })) + const lazyPong = lazy(() => Promise.resolve({ default: pong })) + + const lazyNested = lazy(() => Promise.resolve({ + default: { + ping, + pong: lazyPong, + nested2: lazy(() => Promise.resolve({ + default: { + ping, + pong: lazyPong, + }, + })), + }, + })) + + const router = { + ping: lazyPing, + pong, + nested: lazyNested, + } + + expect(await unlazy(getRouterChild(router, 'ping'))).toEqual({ default: ping }) + expect(getRouterChild(router, 'pong')).toBe(pong) + + expect(getRouterChild(router, 'nested')).toSatisfy(isLazy) + expect(getRouterChild(router, 'nested', 'ping')).toSatisfy(isLazy) + expect(getRouterChild(router, 'nested', 'pong')).toSatisfy(isLazy) + + expect(getRouterChild(router, 'nested')).toBe(lazyNested) + expect(await unlazy(getRouterChild(router, 'nested', 'ping'))).toEqual({ default: ping }) + expect(await unlazy(getRouterChild(router, 'nested', 'pong'))).toEqual({ default: pong }) + + expect(getRouterChild(router, 'nested', '~orpc')).toSatisfy(isLazy) + expect(await unlazy(getRouterChild(router, 'nested', '~orpc'))).toEqual({ default: undefined }) + + expect(await unlazy(getRouterChild(router, 'nested', 'nested2', 'pong'))).toEqual({ default: pong }) + expect(await unlazy(getRouterChild(router, 'nested', 'nested2', 'peo', 'pue', 'cu', 'la'))).toEqual({ default: undefined }) + }) + + it('support Lazy', async () => { + const lazied = lazy(() => Promise.resolve({ default: undefined })) + + expect(await unlazy(getRouterChild(lazied, 'ping'))).toEqual({ default: undefined }) + expect(await unlazy(getRouterChild(lazied, 'ping', '~orpc'))).toEqual({ default: undefined }) + }) +}) diff --git a/packages/server/src/router.ts b/packages/server/src/router.ts index 6e8d14350..3a4bc8fa1 100644 --- a/packages/server/src/router.ts +++ b/packages/server/src/router.ts @@ -1,16 +1,20 @@ import type { ContractProcedure, ContractRouter, SchemaInput, SchemaOutput } from '@orpc/contract' -import type { Lazy, Lazyable } from './lazy' -import type { Procedure } from './procedure' +import type { ANY_LAZY, Lazy, Lazyable } from './lazy' +import type { ANY_PROCEDURE, Procedure } from './procedure' import type { Context } from './types' +import { flatLazy, isLazy, lazy, unlazy } from './lazy' +import { isProcedure } from './procedure' export type Router< TContext extends Context, TContract extends ContractRouter, -> = TContract extends ContractProcedure - ? Lazyable> - : Lazyable<{ - [K in keyof TContract]: TContract[K] extends ContractRouter ? Router : never - }> +> = Lazyable< + TContract extends ContractProcedure + ? Procedure + : { + [K in keyof TContract]: TContract[K] extends ContractRouter ? Router : never + } +> export type ANY_ROUTER = Router @@ -29,3 +33,48 @@ export type InferRouterOutputs = : { [K in keyof T]: T[K] extends ANY_ROUTER ? InferRouterOutputs : never } + +export function getRouterChild< + T extends ANY_ROUTER | Lazy, +>(router: T, ...path: string[]): T extends ANY_LAZY + ? Lazy | undefined> + : ANY_ROUTER | Lazy | undefined { + let current: ANY_ROUTER | Lazy | undefined = router + + for (let i = 0; i < path.length; i++) { + const segment = path[i]! + + if (!current) { + return undefined as any + } + + if (isProcedure(current)) { + return undefined as any + } + + if (!isLazy(current)) { + current = current[segment] + + continue + } + + const lazied = current + const rest = path.slice(i) + + const newLazy = lazy(async () => { + const unwrapped = await unlazy(lazied) + + if (!unwrapped.default) { + return unwrapped + } + + const next = getRouterChild(unwrapped.default, ...rest) + + return { default: next } + }) + + return flatLazy(newLazy) + } + + return current as any +} From 612ddfa59aa189504f6eb4d28e5296c29574643a Mon Sep 17 00:00:00 2001 From: unnoq Date: Thu, 19 Dec 2024 15:29:16 +0700 Subject: [PATCH 36/51] fix --- packages/server/src/lazy-decorated.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/src/lazy-decorated.test.ts b/packages/server/src/lazy-decorated.test.ts index a6285ad6d..00dd6d99b 100644 --- a/packages/server/src/lazy-decorated.test.ts +++ b/packages/server/src/lazy-decorated.test.ts @@ -41,7 +41,7 @@ describe('decorated lazy', () => { expect((await unlazy(decorateLazy(l3))).default.ping).toBe(lazyPing) }) - it('throw error on load child of lazy that does not exist', () => { + it('return undefined when not exists child', () => { const decorated = decorateLazy(lazy(() => Promise.resolve({ default: { ping: { pong: lazyPing } } }))) as any const child = decorated.ping.pong.peng.pang.p @@ -49,7 +49,7 @@ describe('decorated lazy', () => { expect(child).toBeInstanceOf(Function) expect(child).toSatisfy(isLazy) - expect(unlazy(child)).rejects.toThrow(`Not found`) + expect(unlazy(child)).resolves.toEqual({ default: undefined }) }) describe('callable', () => { From de6138a738592a8fa5b4e55d1e66ba90b232c6ca Mon Sep 17 00:00:00 2001 From: unnoq Date: Thu, 19 Dec 2024 15:53:46 +0700 Subject: [PATCH 37/51] fetch - handle request --- .../server/src/fetch/handle-request.test-d.ts | 71 ++ .../server/src/fetch/handle-request.test.ts | 94 +++ .../fetch/{handle.ts => handle-request.ts} | 8 +- packages/server/src/fetch/handle.test.ts | 670 ------------------ packages/server/src/fetch/index.ts | 2 +- packages/server/src/fetch/types.ts | 51 +- 6 files changed, 192 insertions(+), 704 deletions(-) create mode 100644 packages/server/src/fetch/handle-request.test-d.ts create mode 100644 packages/server/src/fetch/handle-request.test.ts rename packages/server/src/fetch/{handle.ts => handle-request.ts} (67%) delete mode 100644 packages/server/src/fetch/handle.test.ts diff --git a/packages/server/src/fetch/handle-request.test-d.ts b/packages/server/src/fetch/handle-request.test-d.ts new file mode 100644 index 000000000..e75863f77 --- /dev/null +++ b/packages/server/src/fetch/handle-request.test-d.ts @@ -0,0 +1,71 @@ +import type { Procedure } from '../procedure' +import type { CallerOptions, WELL_CONTEXT } from '../types' +import { lazy } from '../lazy' +import { handleFetchRequest } from './handle-request' + +describe('handleFetchRequest', () => { + const ping = {} as Procedure<{ auth: boolean }, { db: string }, undefined, undefined, undefined> + const pong = {} as Procedure + + const router = { + ping: lazy(() => Promise.resolve({ default: ping })), + pong, + nested: lazy(() => Promise.resolve({ default: { + ping, + pong: lazy(() => Promise.resolve({ default: pong })), + } })), + } + + it('infer correct context', () => { + handleFetchRequest({ + request: {} as Request, + router, + handlers: [vi.fn()], + context: { auth: true }, + }) + + handleFetchRequest({ + request: {} as Request, + router, + handlers: [vi.fn()], + context: () => ({ auth: true }), + }) + + handleFetchRequest({ + request: {} as Request, + router, + handlers: [vi.fn()], + context: async () => ({ auth: true }), + }) + + handleFetchRequest({ + request: {} as Request, + router, + handlers: [vi.fn()], + // @ts-expect-error --- invalid context + context: { auth: 123 }, + }) + + // @ts-expect-error --- missing context + handleFetchRequest({ + request: {} as Request, + router, + handlers: [vi.fn()], + }) + }) + + it('hooks', () => { + handleFetchRequest({ + request: {} as Request, + router, + handlers: [vi.fn()], + context: { auth: true }, + onSuccess: ({ output, input }, context, meta) => { + expectTypeOf(output).toEqualTypeOf() + expectTypeOf(input).toEqualTypeOf() + expectTypeOf(context).toEqualTypeOf<{ auth: boolean }>() + expectTypeOf(meta).toEqualTypeOf() + }, + }) + }) +}) diff --git a/packages/server/src/fetch/handle-request.test.ts b/packages/server/src/fetch/handle-request.test.ts new file mode 100644 index 000000000..f28e7beee --- /dev/null +++ b/packages/server/src/fetch/handle-request.test.ts @@ -0,0 +1,94 @@ +import type { Procedure } from '../procedure' +import type { WELL_CONTEXT } from '../types' +import { lazy } from '../lazy' +import { handleFetchRequest } from './handle-request' + +beforeEach(() => { + vi.clearAllMocks() +}) + +describe('handleFetchRequest', () => { + const ping = {} as Procedure<{ auth: boolean }, { db: string }, undefined, undefined, undefined> + const pong = {} as Procedure + + const router = { + ping: lazy(() => Promise.resolve({ default: ping })), + pong, + nested: lazy(() => Promise.resolve({ default: { + ping, + pong: lazy(() => Promise.resolve({ default: pong })), + } })), + } + + const handler1 = vi.fn() + + it('forward request to handlers', async () => { + const options = { + request: {} as Request, + router, + handlers: [handler1], + context: { auth: true }, + } as const + + const mockedResponse = new Response('__mocked__') + handler1.mockReturnValueOnce(mockedResponse) + + const response = await handleFetchRequest(options) + + expect(response).toBe(mockedResponse) + + expect(handler1).toBeCalledTimes(1) + expect(handler1).toBeCalledWith(options) + }) + + it('try all handlers utils return response', async () => { + const handler2 = vi.fn() + const handler3 = vi.fn() + + const options = { + request: {} as Request, + router, + handlers: [handler1, handler2, handler3], + context: { auth: true }, + } as const + + const mockedResponse = new Response('__mocked__') + handler2.mockReturnValueOnce(mockedResponse) + + const response = await handleFetchRequest(options) + + expect(response).toBe(mockedResponse) + + expect(handler1).toBeCalledTimes(1) + expect(handler1).toBeCalledWith(options) + expect(handler2).toBeCalledTimes(1) + expect(handler2).toBeCalledWith(options) + expect(handler3).toBeCalledTimes(0) + }) + + it('fallback 404 if no handler return response', async () => { + const handler2 = vi.fn() + const handler3 = vi.fn() + + const options = { + request: {} as Request, + router, + handlers: [handler1, handler2, handler3], + context: { auth: true }, + } as const + + const response = await handleFetchRequest(options) + + expect(handler1).toBeCalledTimes(1) + expect(handler2).toBeCalledTimes(1) + expect(handler3).toBeCalledTimes(1) + + expect(response).toBeInstanceOf(Response) + expect(response.status).toBe(404) + expect(await response.json()).toEqual({ + code: 'NOT_FOUND', + message: 'Not found', + status: 404, + }) + }) +}) diff --git a/packages/server/src/fetch/handle.ts b/packages/server/src/fetch/handle-request.ts similarity index 67% rename from packages/server/src/fetch/handle.ts rename to packages/server/src/fetch/handle-request.ts index 8f6ea70b6..238b7918c 100644 --- a/packages/server/src/fetch/handle.ts +++ b/packages/server/src/fetch/handle-request.ts @@ -1,13 +1,13 @@ -import type { Router } from '../router' +import type { Context } from '../types' import type { FetchHandler, FetchHandlerOptions } from './types' import { ORPCError } from '@orpc/shared/error' -export type HandleFetchRequestOptions> = FetchHandlerOptions & { +export type HandleFetchRequestOptions = FetchHandlerOptions & { handlers: readonly [FetchHandler, ...FetchHandler[]] } -export async function handleFetchRequest< TRouter extends Router>( - options: HandleFetchRequestOptions, +export async function handleFetchRequest( + options: HandleFetchRequestOptions, ) { for (const handler of options.handlers) { const response = await handler(options) diff --git a/packages/server/src/fetch/handle.test.ts b/packages/server/src/fetch/handle.test.ts deleted file mode 100644 index a2e3814a0..000000000 --- a/packages/server/src/fetch/handle.test.ts +++ /dev/null @@ -1,670 +0,0 @@ -import { createOpenAPIServerHandler, createOpenAPIServerlessHandler } from '@orpc/openapi/fetch' -import { ORPC_PROTOCOL_HEADER, ORPC_PROTOCOL_VALUE } from '@orpc/shared' -import { oz } from '@orpc/zod' -import { describe, expect, it } from 'vitest' -import { z } from 'zod' -import { ORPCError, os } from '..' -import { handleFetchRequest } from './handle' -import { createORPCHandler } from './handler' - -const router = os.router({ - throw: os.func(() => { - throw new Error('test') - }), - ping: os.func(() => { - return 'ping' - }), - ping2: os.route({ method: 'GET', path: '/ping2' }).func(() => { - return 'ping2' - }), -}) - -describe('simple', () => { - const osw = os.context<{ auth?: boolean }>() - const router = osw.router({ - ping: osw.func(async () => 'pong'), - ping2: osw - .route({ method: 'GET', path: '/ping2' }) - .func(async () => 'pong2'), - }) - - it('200: public url', async () => { - const response = await handleFetchRequest({ - router, - handlers: [createORPCHandler(), createOpenAPIServerlessHandler()], - prefix: '/orpc', - request: new Request('http://localhost/orpc/ping', { - method: 'POST', - }), - context: () => ({ auth: true }), - }) - - expect(response.status).toBe(200) - expect(await response.json()).toEqual('pong') - - const response2 = await handleFetchRequest({ - router, - handlers: [createORPCHandler(), createOpenAPIServerlessHandler()], - prefix: '/orpc', - request: new Request('http://localhost/orpc/ping2', { - method: 'GET', - }), - context: { auth: true }, - }) - - expect(response2.status).toBe(200) - expect(await response2.json()).toEqual('pong2') - }) - - it('200: internal url', async () => { - const response = await handleFetchRequest({ - router, - handlers: [createORPCHandler(), createOpenAPIServerlessHandler()], - request: new Request('http://localhost/ping', { - method: 'POST', - }), - context: { auth: true }, - }) - - expect(response.status).toBe(200) - expect(await response.json()).toEqual('pong') - - const response2 = await handleFetchRequest({ - router, - handlers: [createORPCHandler(), createOpenAPIServerlessHandler()], - prefix: '/orpc', - request: new Request('http://localhost/orpc/ping2'), - context: { auth: true }, - }) - - expect(response2.status).toBe(200) - expect(await response2.json()).toEqual('pong2') - }) - - it('404', async () => { - const response = await handleFetchRequest({ - router, - handlers: [createORPCHandler(), createOpenAPIServerHandler()], - prefix: '/orpc', - request: new Request('http://localhost/orpc/pingp', { - method: 'POST', - }), - context: { auth: true }, - }) - - expect(response.status).toBe(404) - }) -}) - -describe('procedure throw error', () => { - it('unknown error', async () => { - const response = await handleFetchRequest({ - router, - handlers: [createORPCHandler(), createOpenAPIServerHandler()], - request: new Request('https://local.com/throw', { method: 'POST' }), - }) - - expect(response.status).toBe(500) - expect(await response.json()).toEqual({ - code: 'INTERNAL_SERVER_ERROR', - status: 500, - message: 'Internal server error', - }) - }) - - it('orpc error', async () => { - const router = os.router({ - ping: os.func(() => { - throw new ORPCError({ code: 'TIMEOUT' }) - }), - }) - - const response = await handleFetchRequest({ - router, - handlers: [createORPCHandler(), createOpenAPIServerHandler()], - request: new Request('https://local.com/ping', { method: 'POST' }), - }) - - expect(response.status).toBe(408) - expect(await response.json()).toEqual({ - code: 'TIMEOUT', - status: 408, - message: '', - }) - }) - - it('orpc error with data', async () => { - const router = os.router({ - ping: os.func(() => { - throw new ORPCError({ - code: 'PAYLOAD_TOO_LARGE', - message: 'test', - data: { max: '10mb' }, - }) - }), - }) - - const response = await handleFetchRequest({ - router, - handlers: [createORPCHandler(), createOpenAPIServerHandler()], - request: new Request('https://local.com/ping', { method: 'POST' }), - }) - - expect(response.status).toBe(413) - expect(await response.json()).toEqual({ - code: 'PAYLOAD_TOO_LARGE', - status: 413, - message: 'test', - data: { max: '10mb' }, - }) - }) - - it('orpc error with custom status', async () => { - const router = os.router({ - ping: os.func(() => { - throw new ORPCError({ - code: 'PAYLOAD_TOO_LARGE', - status: 100, - }) - }), - - ping2: os.func(() => { - throw new ORPCError({ - code: 'PAYLOAD_TOO_LARGE', - status: 488, - }) - }), - }) - - const response = await handleFetchRequest({ - router, - handlers: [createORPCHandler(), createOpenAPIServerHandler()], - request: new Request('https://local.com/ping', { method: 'POST' }), - }) - - expect(response.status).toBe(500) - expect(await response.json()).toEqual({ - code: 'INTERNAL_SERVER_ERROR', - status: 500, - message: 'Internal server error', - }) - - const response2 = await handleFetchRequest({ - router, - handlers: [createORPCHandler(), createOpenAPIServerHandler()], - request: new Request('https://local.com/ping2', { method: 'POST' }), - }) - - expect(response2.status).toBe(488) - expect(await response2.json()).toEqual({ - code: 'PAYLOAD_TOO_LARGE', - status: 488, - message: '', - }) - }) - - it('input validation error', async () => { - const router = os.router({ - ping: os - .input(z.object({})) - .output(z.string()) - .func(() => { - return 'unnoq' - }), - }) - - const response = await handleFetchRequest({ - router, - handlers: [createORPCHandler(), createOpenAPIServerHandler()], - request: new Request('https://local.com/ping', { method: 'POST' }), - }) - - expect(response.status).toBe(400) - expect(await response.json()).toEqual({ - code: 'BAD_REQUEST', - status: 400, - message: 'Input validation failed', - issues: [ - { - code: 'invalid_type', - expected: 'object', - message: 'Required', - path: [], - received: 'undefined', - }, - ], - }) - }) - - it('output validation error', async () => { - const router = os.router({ - ping: os - .input(z.string()) - .output(z.string()) - .func(() => { - return 12344 as any - }), - }) - - const response = await handleFetchRequest({ - router, - handlers: [createORPCHandler(), createOpenAPIServerHandler()], - request: new Request('https://local.com/ping', { - method: 'POST', - body: '"hi"', - }), - }) - - expect(response.status).toBe(500) - expect(await response.json()).toEqual({ - code: 'INTERNAL_SERVER_ERROR', - status: 500, - message: 'Output validation failed', - }) - }) -}) - -describe('file upload', () => { - const router = os.router({ - signal: os.input(z.instanceof(Blob)).func((input) => { - return input - }), - multiple: os - .input( - z.object({ first: z.instanceof(Blob), second: z.instanceof(Blob) }), - ) - .func((input) => { - return input - }), - }) - - const blob1 = new Blob(['hello'], { type: 'text/plain;charset=utf-8' }) - const blob2 = new Blob(['"world"'], { type: 'image/png' }) - const blob3 = new Blob(['unnoq'], { type: 'application/octet-stream' }) - - it('single file', async () => { - const rForm = new FormData() - rForm.set('meta', JSON.stringify([])) - rForm.set('maps', JSON.stringify([[]])) - rForm.set('0', blob3) - - const response = await handleFetchRequest({ - router, - handlers: [createORPCHandler(), createOpenAPIServerHandler()], - prefix: '/orpc', - request: new Request('http://localhost/orpc/signal', { - method: 'POST', - body: rForm, - headers: { - [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE, - }, - }), - }) - - expect(response.status).toBe(200) - const form = await response.formData() - - const file0 = form.get('0') as File - expect(file0).toBeInstanceOf(File) - expect(file0.name).toBe('blob') - expect(file0.type).toBe('application/octet-stream') - expect(await file0.text()).toBe('unnoq') - }) - - it('multiple file', async () => { - const form = new FormData() - form.set('data', JSON.stringify({ first: blob1, second: blob2 })) - form.set('meta', JSON.stringify([])) - form.set('maps', JSON.stringify([['first'], ['second']])) - form.set('0', blob1) - form.set('1', blob2) - - const response = await handleFetchRequest({ - router, - handlers: [createORPCHandler(), createOpenAPIServerHandler()], - prefix: '/orpc', - request: new Request('http://localhost/orpc/multiple', { - method: 'POST', - body: form, - headers: { - [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE, - }, - }), - }) - - expect(response.status).toBe(200) - - const form_ = await response.formData() - const file0 = form_.get('0') as File - const file1 = form_.get('1') as File - - expect(file0).toBeInstanceOf(File) - expect(file0.name).toBe('blob') - expect(file0.type).toBe('text/plain;charset=utf-8') - expect(await file0.text()).toBe('hello') - - expect(file1).toBeInstanceOf(File) - expect(file1.name).toBe('blob') - expect(file1.type).toBe('image/png') - expect(await file1.text()).toBe('"world"') - }) -}) - -describe('accept header', () => { - const router = os.router({ - ping: os.func(async () => 'pong'), - }) - - it('application/json', async () => { - const response = await handleFetchRequest({ - router, - handlers: [createORPCHandler(), createOpenAPIServerHandler()], - prefix: '/orpc', - request: new Request('http://localhost/orpc/ping', { - method: 'POST', - headers: { - Accept: 'application/json', - }, - }), - }) - - expect(response.headers.get('Content-Type')).toEqual('application/json') - - expect(await response.json()).toEqual('pong') - }) - - it('multipart/form-data', async () => { - const response = await handleFetchRequest({ - router, - handlers: [createORPCHandler(), createOpenAPIServerHandler()], - prefix: '/orpc', - request: new Request('http://localhost/orpc/ping', { - method: 'POST', - headers: { - Accept: 'multipart/form-data', - }, - }), - }) - - expect(response.headers.get('Content-Type')).toContain( - 'multipart/form-data', - ) - - const form = await response.formData() - expect(form.get('')).toEqual('pong') - }) - - it('application/x-www-form-urlencoded', async () => { - const response = await handleFetchRequest({ - router, - handlers: [createORPCHandler(), createOpenAPIServerHandler()], - prefix: '/orpc', - request: new Request('http://localhost/orpc/ping', { - method: 'POST', - headers: { - Accept: 'application/x-www-form-urlencoded', - }, - }), - }) - - expect(response.headers.get('Content-Type')).toEqual( - 'application/x-www-form-urlencoded', - ) - - const params = new URLSearchParams(await response.text()) - expect(params.get('')).toEqual('pong') - }) - - it('*/*', async () => { - const response = await handleFetchRequest({ - router, - handlers: [createORPCHandler(), createOpenAPIServerHandler()], - prefix: '/orpc', - request: new Request('http://localhost/orpc/ping', { - method: 'POST', - headers: { - Accept: '*/*', - }, - }), - }) - - expect(response.headers.get('Content-Type')).toEqual('application/json') - expect(await response.json()).toEqual('pong') - }) - - it('invalid', async () => { - const response = await handleFetchRequest({ - router, - handlers: [createORPCHandler(), createOpenAPIServerHandler()], - prefix: '/orpc', - request: new Request('http://localhost/orpc/ping', { - method: 'POST', - headers: { - Accept: 'invalid', - }, - }), - }) - - expect(response.headers.get('Content-Type')).toEqual('application/json') - expect(await response.json()).toEqual({ - code: 'NOT_ACCEPTABLE', - message: 'Unsupported content-type: invalid', - status: 406, - }) - }) -}) - -describe('dynamic params', () => { - const router = os.router({ - deep: os - .route({ - method: 'POST', - path: '/{id}/{id2}', - }) - .input( - z.object({ - id: z.number(), - id2: z.string(), - file: oz.file(), - }), - ) - .func(input => input), - - find: os - .route({ - method: 'GET', - path: '/{id}', - }) - .input( - z.object({ - id: z.number(), - }), - ) - .func(input => input), - }) - - const handlers = [ - { - router, - handlers: [createORPCHandler(), createOpenAPIServerHandler()] as const, - }, - { - router, - handlers: [createORPCHandler(), createOpenAPIServerlessHandler()] as const, - }, - ] - - it.each(handlers)('should handle dynamic params', async ({ router, handlers }) => { - const response = await handleFetchRequest({ - router, - handlers, - request: new Request('http://localhost/123'), - }) - - expect(response.status).toEqual(200) - expect(response.headers.get('Content-Type')).toEqual('application/json') - expect(await response.json()).toEqual({ id: 123 }) - }) - - it.each(handlers)('should handle deep dynamic params', async ({ handlers }) => { - const form = new FormData() - form.append('file', new Blob(['hello']), 'hello.txt') - - const response = await handleFetchRequest({ - router, - handlers, - request: new Request('http://localhost/123/dfdsfds', { - method: 'POST', - body: form, - }), - }) - - expect(response.status).toEqual(200) - const rForm = await response.formData() - expect(rForm.get('id')).toEqual('123') - expect(rForm.get('id2')).toEqual('dfdsfds') - }) -}) - -describe('can control method on POST request', () => { - const router = os.router({ - update: os - .route({ - method: 'PUT', - path: '/{id}', - }) - .input( - z.object({ - id: z.number(), - file: oz.file(), - }), - ) - .func(input => input), - }) - - const handlers = [ - [createORPCHandler(), createOpenAPIServerHandler()], - [createORPCHandler(), createOpenAPIServerlessHandler()], - ] as const - - it.each(handlers)('work', async (...handlers) => { - const form = new FormData() - form.set('file', new File(['hello'], 'hello.txt')) - - const response = await handleFetchRequest({ - router, - handlers, - request: new Request('http://localhost/123', { - method: 'POST', - body: form, - }), - }) - - expect(response.status).toEqual(404) - - const response2 = await handleFetchRequest({ - router, - handlers, - request: new Request('http://localhost/123?method=PUT', { - method: 'POST', - body: form, - }), - }) - - expect(response2.status).toEqual(200) - }) -}) - -it('hooks', async () => { - const onSuccess = vi.fn() - const onError = vi.fn() - - const router = { - ping: os.input(z.object({ value: z.string() })).func(input => input.value), - } - - const handlers = [ - createORPCHandler(), - createOpenAPIServerHandler(), - ] as const - - const context = { auth: true } - - const request = new Request('http://localhost/ping', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ value: 'hello' }), - }) - - const response = await handleFetchRequest({ - router, - request, - handlers, - onSuccess, - onError, - context, - }) - - expect(response.status).toEqual(200) - expect(onSuccess).toHaveBeenCalledTimes(1) - expect(onSuccess).toBeCalledWith({ input: request, output: response, status: 'success' }, context, {}) - expect(onError).toHaveBeenCalledTimes(0) - - onSuccess.mockClear() - onError.mockClear() - - const errorRequest = new Request('http://localhost/ping', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ value: 1233 }), - }) - - const errorResponse = await handleFetchRequest({ - router, - request: errorRequest, - handlers, - onSuccess, - onError, - context, - }) - - expect(errorResponse.status).toEqual(400) - expect(onSuccess).toHaveBeenCalledTimes(0) - expect(onError).toHaveBeenCalledTimes(1) - expect(onError).toBeCalledWith({ input: errorRequest, error: expect.any(Error), status: 'error' }, context, {}) -}) - -it('abort signal', async () => { - const controller = new AbortController() - const signal = controller.signal - - const func = vi.fn() - const onSuccess = vi.fn() - - const ping = os.func(func) - - const response = await handleFetchRequest({ - router: { ping }, - request: new Request('http://localhost/ping', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ value: '123' }), - }), - signal, - onSuccess, - handlers: [createOpenAPIServerHandler()], - }) - - expect(response?.status).toEqual(200) - - expect(func).toBeCalledTimes(1) - expect(func.mock.calls[0]![2].signal).toBe(signal) - expect(onSuccess).toBeCalledTimes(1) - expect(func.mock.calls[0]![2].signal).toBe(signal) -}) diff --git a/packages/server/src/fetch/index.ts b/packages/server/src/fetch/index.ts index 969d7042e..f2899b2d4 100644 --- a/packages/server/src/fetch/index.ts +++ b/packages/server/src/fetch/index.ts @@ -1,3 +1,3 @@ -export * from './handle' +export * from './handle-request' export * from './handler' export * from './types' diff --git a/packages/server/src/fetch/types.ts b/packages/server/src/fetch/types.ts index c9bf2a6fa..f1dc8a1f8 100644 --- a/packages/server/src/fetch/types.ts +++ b/packages/server/src/fetch/types.ts @@ -1,41 +1,34 @@ /// -import type { Hooks, PartialOnUndefinedDeep, Value } from '@orpc/shared' +import type { Hooks, Value } from '@orpc/shared' import type { Router } from '../router' -import type { CallerOptions } from '../types' +import type { CallerOptions, Context } from '../types' -export type FetchHandlerOptions< - TRouter extends Router, -> = { +export type FetchHandlerOptions = + { /** * The `router` used for handling the request and routing, * */ - router: TRouter + router: Router - /** - * The request need to be handled. - */ - request: Request + /** + * The request need to be handled. + */ + request: Request - /** - * Remove the prefix from the request path. - * - * @example /orpc - * @example /api - */ - prefix?: string -} & PartialOnUndefinedDeep<{ - /** - * The context used to handle the request. - */ - context: Value< - TRouter extends Router ? UContext : never - > -}> -& CallerOptions -& Hooks ? UContext : never, CallerOptions> + /** + * Remove the prefix from the request path. + * + * @example /orpc + * @example /api + */ + prefix?: string + } + & NoInfer<(undefined extends T ? { context?: Value } : { context: Value })> + & CallerOptions + & Hooks -export type FetchHandler = >( - options: FetchHandlerOptions +export type FetchHandler = ( + options: FetchHandlerOptions ) => Promise From a04d95d9c27976b40fd1aecc212d5e9467b4bc6b Mon Sep 17 00:00:00 2001 From: unnoq Date: Thu, 19 Dec 2024 18:13:28 +0700 Subject: [PATCH 38/51] fetch, and some improvement on caller --- packages/server/package.json | 1 - packages/server/src/fetch/handler.test.ts | 242 ------------------ packages/server/src/fetch/index.ts | 2 +- .../server/src/fetch/orpc-handler.test-d.ts | 21 ++ .../server/src/fetch/orpc-handler.test.ts | 202 +++++++++++++++ .../src/fetch/{handler.ts => orpc-handler.ts} | 82 +++--- packages/server/src/fetch/types.ts | 3 +- packages/server/src/procedure-caller.test.ts | 10 - packages/server/src/procedure-caller.ts | 50 +--- packages/server/src/router-caller.test.ts | 39 ++- packages/server/src/router-caller.ts | 16 +- packages/server/src/types.ts | 4 +- pnpm-lock.yaml | 3 - 13 files changed, 335 insertions(+), 340 deletions(-) delete mode 100644 packages/server/src/fetch/handler.test.ts create mode 100644 packages/server/src/fetch/orpc-handler.test-d.ts create mode 100644 packages/server/src/fetch/orpc-handler.test.ts rename packages/server/src/fetch/{handler.ts => orpc-handler.ts} (60%) diff --git a/packages/server/package.json b/packages/server/package.json index 43761fe41..0b8fdfefd 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -56,7 +56,6 @@ "@orpc/transformer": "workspace:*" }, "devDependencies": { - "@orpc/openapi": "workspace:*", "zod": "^3.24.1" } } diff --git a/packages/server/src/fetch/handler.test.ts b/packages/server/src/fetch/handler.test.ts deleted file mode 100644 index 005bb20b9..000000000 --- a/packages/server/src/fetch/handler.test.ts +++ /dev/null @@ -1,242 +0,0 @@ -import { ORPC_PROTOCOL_HEADER, ORPC_PROTOCOL_VALUE } from '@orpc/shared' -import { z } from 'zod' -import { os } from '..' -import { createORPCHandler } from './handler' - -describe('oRPCHandler', () => { - const handler = createORPCHandler() - - const ping = os.input(z.object({ value: z.string() })).output(z.string()).func((input) => { - return input.value - }) - const pong = os.func(() => 'pong') - - const lazyRouter = os.lazy(() => Promise.resolve({ - default: { - ping: os.lazy(() => Promise.resolve({ default: ping })), - pong, - lazyRouter: os.lazy(() => Promise.resolve({ default: { ping, pong } })), - }, - })) - - const router = os.router({ - ping, - pong, - lazyRouter, - }) - - it('should handle request', async () => { - const response = await handler({ - router, - request: new Request('http://localhost/ping', { - method: 'POST', - headers: { - [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ data: { value: '123' }, meta: [] }), - }), - }) - - expect(response?.status).toEqual(200) - expect(await response?.json()).toEqual({ data: '123', meta: [] }) - }) - - it('should handle request - lazy', async () => { - const response = await handler({ - router, - request: new Request('http://localhost/lazyRouter/ping', { - method: 'POST', - headers: { - [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ data: { value: '123' }, meta: [] }), - }), - }) - - expect(response?.status).toEqual(200) - expect(await response?.json()).toEqual({ data: '123', meta: [] }) - }) - - it('should handle request - lazy - lazy', async () => { - const response = await handler({ - router, - request: new Request('http://localhost/lazyRouter/lazyRouter/ping', { - method: 'POST', - headers: { - [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ data: { value: '123' }, meta: [] }), - }), - }) - - expect(response?.status).toEqual(200) - expect(await response?.json()).toEqual({ data: '123', meta: [] }) - }) - - it('should throw error - not found', async () => { - const response = await handler({ - router, - request: new Request('http://localhost/pingp', { - method: 'POST', - headers: { - [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE, - }, - body: JSON.stringify({ data: { value: '123' }, meta: [] }), - }), - }) - - expect(response?.status).toEqual(404) - expect(await response?.json()).toEqual({ data: { code: 'NOT_FOUND', message: 'Not found', status: 404 }, meta: [] }) - }) - - it('should throw error - not found - lazy', async () => { - const response = await handler({ - router, - request: new Request('http://localhost/lazyRouter/not_found', { - method: 'POST', - headers: { - [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE, - }, - body: JSON.stringify({ data: { value: '123' }, meta: [] }), - }), - }) - - expect(response?.status).toEqual(404) - expect(await response?.json()).toEqual({ data: { code: 'NOT_FOUND', message: 'Not found', status: 404 }, meta: [] }) - }) - - it('should throw error - invalid input', async () => { - const response = await handler({ - router, - request: new Request('http://localhost/ping', { - method: 'POST', - headers: { - [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE, - }, - body: JSON.stringify({ data: { value: 123 }, meta: [] }), - }), - }) - - expect(response?.status).toEqual(400) - expect(await response?.json()).toEqual({ - data: { - code: 'BAD_REQUEST', - status: 400, - message: 'Input validation failed', - issues: [ - { - code: 'invalid_type', - expected: 'string', - received: 'number', - path: [ - 'value', - ], - message: 'Expected string, received number', - }, - ], - }, - meta: [], - }) - }) - - it('should throw error - invalid input - lazy', async () => { - const response = await handler({ - router, - request: new Request('http://localhost/lazyRouter/ping', { - method: 'POST', - headers: { - [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE, - }, - body: JSON.stringify({ data: { value: 123 }, meta: [] }), - }), - }) - - expect(response?.status).toEqual(400) - expect(await response?.json()).toEqual({ - data: { - code: 'BAD_REQUEST', - status: 400, - message: 'Input validation failed', - issues: [ - { - code: 'invalid_type', - expected: 'string', - received: 'number', - path: [ - 'value', - ], - message: 'Expected string, received number', - }, - ], - }, - meta: [], - }) - }) - - it('should throw error - invalid input - lazy - lazy', async () => { - const response = await handler({ - router, - request: new Request('http://localhost/lazyRouter/lazyRouter/ping', { - method: 'POST', - headers: { - [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE, - }, - body: JSON.stringify({ data: { value: 123 }, meta: [] }), - }), - }) - - expect(response?.status).toEqual(400) - expect(await response?.json()).toEqual({ - data: { - code: 'BAD_REQUEST', - status: 400, - message: 'Input validation failed', - issues: [ - { - code: 'invalid_type', - expected: 'string', - received: 'number', - path: [ - 'value', - ], - message: 'Expected string, received number', - }, - ], - }, - meta: [], - }) - }) - - it('abort signal', async () => { - const controller = new AbortController() - const signal = controller.signal - - const func = vi.fn() - const onSuccess = vi.fn() - - const ping = os.func(func) - - const response = await handler({ - router: { ping }, - request: new Request('http://localhost/ping', { - method: 'POST', - headers: { - [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ data: { value: '123' }, meta: [] }), - }), - signal, - onSuccess, - }) - - expect(response?.status).toEqual(200) - expect(func).toBeCalledTimes(1) - expect(func.mock.calls[0]![2].signal).toBe(signal) - expect(onSuccess).toBeCalledTimes(1) - expect(func.mock.calls[0]![2].signal).toBe(signal) - }) -}) diff --git a/packages/server/src/fetch/index.ts b/packages/server/src/fetch/index.ts index f2899b2d4..11433a67d 100644 --- a/packages/server/src/fetch/index.ts +++ b/packages/server/src/fetch/index.ts @@ -1,3 +1,3 @@ export * from './handle-request' -export * from './handler' +export * from './orpc-handler' export * from './types' diff --git a/packages/server/src/fetch/orpc-handler.test-d.ts b/packages/server/src/fetch/orpc-handler.test-d.ts new file mode 100644 index 000000000..e9544444a --- /dev/null +++ b/packages/server/src/fetch/orpc-handler.test-d.ts @@ -0,0 +1,21 @@ +import { handleFetchRequest } from './handle-request' +import { createORPCHandler } from './orpc-handler' + +it('assignable to handlers', () => { + handleFetchRequest({ + request: new Request('https://example.com', {}), + router: {}, + handlers: [ + createORPCHandler(), + ], + }) + + handleFetchRequest({ + request: new Request('https://example.com', {}), + router: {}, + handlers: [ + // @ts-expect-error - invalid handler + createORPCHandler, + ], + }) +}) diff --git a/packages/server/src/fetch/orpc-handler.test.ts b/packages/server/src/fetch/orpc-handler.test.ts new file mode 100644 index 000000000..9034e2899 --- /dev/null +++ b/packages/server/src/fetch/orpc-handler.test.ts @@ -0,0 +1,202 @@ +import { ContractProcedure } from '@orpc/contract' +import { ORPC_PROTOCOL_HEADER, ORPC_PROTOCOL_VALUE } from '@orpc/shared' +import { describe, expect, it, vi } from 'vitest' +import { lazy } from '../lazy' +import { Procedure } from '../procedure' +import { createProcedureCaller } from '../procedure-caller' +import { createORPCHandler } from './orpc-handler' + +vi.mock('../procedure-caller', () => ({ + createProcedureCaller: vi.fn(() => vi.fn()), +})) + +describe('createORPCHandler', () => { + const ping = new Procedure({ + contract: new ContractProcedure({ + InputSchema: undefined, + OutputSchema: undefined, + }), + func: vi.fn(), + }) + const pong = new Procedure({ + contract: new ContractProcedure({ + InputSchema: undefined, + OutputSchema: undefined, + }), + func: vi.fn(), + }) + + const router = { + ping: lazy(() => Promise.resolve({ default: ping })), + pong, + nested: lazy(() => Promise.resolve({ default: { + ping, + pong: lazy(() => Promise.resolve({ default: pong })), + } })), + } + + it('should return undefined if the protocol header is missing or incorrect', async () => { + const handler = createORPCHandler() + + const response = await handler({ + request: new Request('https://example.com', { + headers: new Headers({}), + }), + router, + context: undefined, + signal: undefined, + }) + + expect(response).toBeUndefined() + }) + + it('should return a 404 response if no matching procedure is found', async () => { + const handler = createORPCHandler() + + const mockRequest = new Request('https://example.com/not_found', { + headers: new Headers({ [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE }), + }) + + const response = await handler({ + request: mockRequest, + router, + context: undefined, + signal: undefined, + }) + + expect(response?.status).toBe(404) + + const body = await response?.text() + expect(body).toContain('Not found') + }) + + it('should return a 200 response with serialized output if procedure is resolved successfully', async () => { + const handler = createORPCHandler() + + const caller = vi.fn().mockReturnValueOnce('__mocked__') + vi.mocked(createProcedureCaller).mockReturnValue(caller) + + const mockRequest = new Request('https://example.com/ping', { + headers: new Headers({ [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE }), + method: 'POST', + body: JSON.stringify({ data: { value: '123' }, meta: [] }), + }) + + const response = await handler({ + request: mockRequest, + router, + }) + + expect(response?.status).toBe(200) + + const body = await response?.json() + expect(body).toEqual({ data: '__mocked__', meta: [] }) + + expect(caller).toBeCalledTimes(1) + expect(caller).toBeCalledWith({ value: '123' }, { signal: undefined }) + }) + + it('should handle deserialization errors and return a 400 response', async () => { + const handler = createORPCHandler() + + const mockRequest = new Request('https://example.com/ping', { + method: 'POST', + headers: new Headers({ [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE, 'Content-Type': 'application/json' }), + body: '{ invalid json', + }) + + const response = await handler({ + request: mockRequest, + router, + }) + + expect(response?.status).toBe(400) + + const body = await response?.text() + expect(body).toContain('Cannot parse request') + }) + + it('should handle unexpected errors and return a 500 response', async () => { + const handler = createORPCHandler() + + vi.mocked(createProcedureCaller).mockImplementationOnce(() => { + throw new Error('Unexpected error') + }) + + const mockRequest = new Request('https://example.com/ping', { + headers: new Headers({ [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE }), + method: 'POST', + body: JSON.stringify({ data: { value: '123' }, meta: [] }), + }) + + const response = await handler({ + request: mockRequest, + router, + context: undefined, + signal: undefined, + }) + + expect(response?.status).toBe(500) + + const body = await response?.text() + expect(body).toContain('Internal server error') + }) + + it('support signal', async () => { + const handler = createORPCHandler() + + const caller = vi.fn().mockReturnValueOnce('__mocked__') + vi.mocked(createProcedureCaller).mockReturnValue(caller) + + const mockRequest = new Request('https://example.com/ping', { + headers: new Headers({ [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE }), + method: 'POST', + body: JSON.stringify({ data: { value: '123' }, meta: [] }), + }) + + const controller = new AbortController() + const signal = controller.signal + + const response = await handler({ + request: mockRequest, + router, + signal, + }) + + expect(response?.status).toBe(200) + + const body = await response?.json() + expect(body).toEqual({ data: '__mocked__', meta: [] }) + + expect(caller).toBeCalledTimes(1) + expect(caller).toBeCalledWith({ value: '123' }, { signal }) + }) + + it('hooks', async () => { + const handler = createORPCHandler() + + const mockRequest = new Request('https://example.com/not_found', { + headers: new Headers({ [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE }), + method: 'POST', + body: JSON.stringify({ data: { value: '123' }, meta: [] }), + }) + + const onStart = vi.fn() + const onSuccess = vi.fn() + const onError = vi.fn() + + const response = await handler({ + request: mockRequest, + router, + onStart, + onSuccess, + onError, + }) + + expect(response?.status).toBe(404) + + expect(onStart).toBeCalledTimes(1) + expect(onSuccess).toBeCalledTimes(0) + expect(onError).toBeCalledTimes(1) + }) +}) diff --git a/packages/server/src/fetch/handler.ts b/packages/server/src/fetch/orpc-handler.ts similarity index 60% rename from packages/server/src/fetch/handler.ts rename to packages/server/src/fetch/orpc-handler.ts index 7a564ad64..e88d784d9 100644 --- a/packages/server/src/fetch/handler.ts +++ b/packages/server/src/fetch/orpc-handler.ts @@ -1,12 +1,12 @@ import type { ANY_LAZY_PROCEDURE, ANY_PROCEDURE } from '../procedure' -import type { Router } from '../router' import type { FetchHandler } from './types' import { executeWithHooks, ORPC_PROTOCOL_HEADER, ORPC_PROTOCOL_VALUE, trim, value } from '@orpc/shared' import { ORPCError } from '@orpc/shared/error' import { ORPCDeserializer, ORPCSerializer } from '@orpc/transformer' -import { isLazy } from '../lazy' +import { unlazy } from '../lazy' import { isProcedure } from '../procedure' import { createProcedureCaller } from '../procedure-caller' +import { type ANY_ROUTER, getRouterChild } from '../router' const serializer = new ORPCSerializer() const deserializer = new ORPCDeserializer() @@ -23,17 +23,17 @@ export function createORPCHandler(): FetchHandler { const url = new URL(options.request.url) const pathname = `/${trim(url.pathname.replace(options.prefix ?? '', ''), '/')}` - const match = resolveORPCRouter(options.router, pathname) + const match = await resolveRouterMatch(options.router, pathname) if (!match) { throw new ORPCError({ code: 'NOT_FOUND', message: 'Not found' }) } - const input = await deserializeRequest(options.request) + const input = await parseRequestInput(options.request) const caller = createProcedureCaller({ context, - procedure: match.procedure as any, + procedure: match.procedure, path: match.path, }) @@ -58,58 +58,60 @@ export function createORPCHandler(): FetchHandler { }, }) } - catch (e) { - const error = e instanceof ORPCError - ? e - : new ORPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: 'Internal server error', - cause: e, - }) - - const { body, headers } = serializer.serialize(error.toJSON()) - - return new Response(body, { - status: error.status, - headers, - }) + catch (error) { + return handleErrorResponse(error) } } } -function resolveORPCRouter(router: Router, pathname: string): { +async function resolveRouterMatch( + router: ANY_ROUTER, + pathname: string, +): Promise<{ path: string[] procedure: ANY_PROCEDURE | ANY_LAZY_PROCEDURE -} | undefined { - const path = trim(pathname, '/').split('/').map(decodeURIComponent) - - let current: Router | ANY_PROCEDURE | ANY_LAZY_PROCEDURE | undefined = router - for (const segment of path) { - if ((typeof current !== 'object' && typeof current !== 'function') || !current) { - current = undefined - break - } +} | undefined> { + const pathSegments = trim(pathname, '/').split('/').map(decodeURIComponent) + + const match = getRouterChild(router, ...pathSegments) + const { default: maybeProcedure } = await unlazy(match) - current = (current as any)[segment] + if (!isProcedure(maybeProcedure)) { + return undefined } - return isProcedure(current) || isLazy(current) - ? { - procedure: current, - path, - } - : undefined + return { + procedure: maybeProcedure, + path: pathSegments, + } } -async function deserializeRequest(request: Request): Promise { +async function parseRequestInput(request: Request): Promise { try { return await deserializer.deserialize(request) } - catch (e) { + catch (error) { throw new ORPCError({ code: 'BAD_REQUEST', message: 'Cannot parse request. Please check the request body and Content-Type header.', - cause: e, + cause: error, }) } } + +function handleErrorResponse(error: unknown): Response { + const orpcError = error instanceof ORPCError + ? error + : new ORPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: 'Internal server error', + cause: error, + }) + + const { body, headers } = serializer.serialize(orpcError.toJSON()) + + return new Response(body, { + status: orpcError.status, + headers, + }) +} diff --git a/packages/server/src/fetch/types.ts b/packages/server/src/fetch/types.ts index f1dc8a1f8..0e810ac02 100644 --- a/packages/server/src/fetch/types.ts +++ b/packages/server/src/fetch/types.ts @@ -1,5 +1,6 @@ /// +import type { HTTPPath } from '@orpc/contract' import type { Hooks, Value } from '@orpc/shared' import type { Router } from '../router' import type { CallerOptions, Context } from '../types' @@ -23,7 +24,7 @@ export type FetchHandlerOptions = * @example /orpc * @example /api */ - prefix?: string + prefix?: HTTPPath } & NoInfer<(undefined extends T ? { context?: Value } : { context: Value })> & CallerOptions diff --git a/packages/server/src/procedure-caller.test.ts b/packages/server/src/procedure-caller.test.ts index 43f5bda6f..3715f47e5 100644 --- a/packages/server/src/procedure-caller.test.ts +++ b/packages/server/src/procedure-caller.test.ts @@ -295,16 +295,6 @@ describe.each(procedureCases)('createProcedureCaller - case %s', async (_, proce }) }) -it('should throw error when invalid lazy procedure', () => { - const lazied = lazy(() => Promise.resolve({ default: 123 })) - - const caller = createProcedureCaller({ - procedure: lazied, - }) - - expect(caller()).rejects.toThrow('Not found') -}) - it('still work without middleware', async () => { const procedure = new Procedure({ contract: new ContractProcedure({ diff --git a/packages/server/src/procedure-caller.ts b/packages/server/src/procedure-caller.ts index b7d9b014d..4aa6296a2 100644 --- a/packages/server/src/procedure-caller.ts +++ b/packages/server/src/procedure-caller.ts @@ -1,19 +1,16 @@ import type { Schema, SchemaInput, SchemaOutput } from '@orpc/contract' import type { Hooks, Value } from '@orpc/shared' -import type { Lazy, Lazyable } from './lazy' +import type { Lazyable } from './lazy' import type { MiddlewareMeta } from './middleware' -import type { Caller, Context, Meta, WELL_CONTEXT } from './types' +import type { + ANY_PROCEDURE, + Procedure, +} from './procedure' -import { executeWithHooks, trim, value } from '@orpc/shared' +import type { Caller, Context, Meta, WELL_CONTEXT } from './types' +import { executeWithHooks, value } from '@orpc/shared' import { ORPCError } from '@orpc/shared/error' -import { isLazy, unlazy } from './lazy' -import { - type ANY_LAZY_PROCEDURE, - type ANY_PROCEDURE, - isProcedure, - type Procedure, - type WELL_PROCEDURE, -} from './procedure' +import { unlazy } from './lazy' import { mergeContext } from './utils' /** @@ -26,9 +23,7 @@ export type CreateProcedureCallerOptions< TFuncOutput extends SchemaInput, > = & { - procedure: - | Lazyable> - | Lazy + procedure: Lazyable> /** * This is helpful for logging and analytics. @@ -55,7 +50,7 @@ export function createProcedureCaller< ): Caller, SchemaOutput> { return async (...[input, callerOptions]) => { const path = options.path ?? [] - const procedure = await loadProcedure(options.procedure) as WELL_PROCEDURE + const { default: procedure } = await unlazy(options.procedure) const context = await value(options.context) as TContext const meta: Meta = { @@ -87,7 +82,7 @@ export function createProcedureCaller< } } -async function validateInput(procedure: WELL_PROCEDURE, input: unknown) { +async function validateInput(procedure: ANY_PROCEDURE, input: unknown) { const schema = procedure['~orpc'].contract['~orpc'].InputSchema if (!schema) return input @@ -104,7 +99,7 @@ async function validateInput(procedure: WELL_PROCEDURE, input: unknown) { return result.value } -async function validateOutput(procedure: WELL_PROCEDURE, output: unknown) { +async function validateOutput(procedure: ANY_PROCEDURE, output: unknown) { const schema = procedure['~orpc'].contract['~orpc'].OutputSchema if (!schema) return output @@ -122,7 +117,7 @@ async function validateOutput(procedure: WELL_PROCEDURE, output: unknown) { } async function executeMiddlewareChain( - procedure: WELL_PROCEDURE, + procedure: ANY_PROCEDURE, input: unknown, context: Context, meta: Meta, @@ -154,22 +149,3 @@ async function executeMiddlewareChain( return (await next({})).output } - -export async function loadProcedure(procedure: ANY_PROCEDURE | ANY_LAZY_PROCEDURE | Lazy): Promise { - const loadedProcedure = isLazy(procedure) - ? (await unlazy(procedure)).default - : procedure - - if (!isProcedure(loadedProcedure)) { - throw new ORPCError({ - code: 'NOT_FOUND', - message: 'Not found', - cause: new Error(trim(` - Attempted to call a lazy router or invalid procedure. - This should typically be caught by TypeScript compilation. - `)), - }) - } - - return loadedProcedure -} diff --git a/packages/server/src/router-caller.test.ts b/packages/server/src/router-caller.test.ts index 14d1e3bf3..67f4e9f22 100644 --- a/packages/server/src/router-caller.test.ts +++ b/packages/server/src/router-caller.test.ts @@ -1,6 +1,6 @@ import { ContractProcedure } from '@orpc/contract' import { z } from 'zod' -import { lazy, unlazy } from './lazy' +import { isLazy, lazy, unlazy } from './lazy' import { Procedure } from './procedure' import { createProcedureCaller } from './procedure-caller' import { createRouterCaller } from './router-caller' @@ -147,4 +147,41 @@ describe('createRouterCaller', () => { it('not recursive on symbol', () => { expect((caller as any)[Symbol('something')]).toBeUndefined() }) + + it('throw error if call on invalid lazy', async () => { + const caller = createRouterCaller({ + router: lazy(() => Promise.resolve({ default: undefined })), + }) + + // @ts-expect-error --- invalid lazy + caller.router.ping.pong({ val: '123' }) + + const procedure = vi.mocked(createProcedureCaller).mock.calls[0]![0].procedure + + expect(procedure).toSatisfy(isLazy) + + expect(unlazy(procedure)).rejects.toThrow('Expected a valid procedure or lazy but got unknown.') + }) + + it('return undefined if access the undefined key', async () => { + const caller = createRouterCaller({ + router: { + ping, + }, + }) + + // @ts-expect-error --- invalid access + expect(caller.router).toBeUndefined() + }) + + it('works without base path', async () => { + const caller = createRouterCaller({ + router: { + ping, + }, + }) + + expect(caller.ping({ val: '123' })).toEqual('__mocked__') + expect(vi.mocked(createProcedureCaller).mock.calls[0]![0].path).toEqual(['ping']) + }) }) diff --git a/packages/server/src/router-caller.ts b/packages/server/src/router-caller.ts index 72f470725..461cc87a2 100644 --- a/packages/server/src/router-caller.ts +++ b/packages/server/src/router-caller.ts @@ -3,7 +3,7 @@ import type { Hooks, Value } from '@orpc/shared' import type { Lazy } from './lazy' import type { Procedure } from './procedure' import type { Caller, Meta } from './types' -import { isLazy } from './lazy' +import { isLazy, lazy, unlazy } from './lazy' import { isProcedure } from './procedure' import { createProcedureCaller } from './procedure-caller' import { type ANY_ROUTER, getRouterChild, type Router } from './router' @@ -53,7 +53,19 @@ export function createRouterCaller< const procedureCaller = isLazy(options.router) ? createProcedureCaller({ ...options, - procedure: options.router, + procedure: lazy(async () => { + const { default: maybeProcedure } = await unlazy(options.router) + + if (!isProcedure(maybeProcedure)) { + throw new Error(` + Expected a valid procedure or lazy but got unknown. + This should be caught by TypeScript compilation. + Please report this issue if this makes you feel uncomfortable. + `) + } + + return { default: maybeProcedure } + }), context: options.context, path: options.path, }) diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index 95d063741..ab2936e0d 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -1,6 +1,6 @@ /// -import type { WELL_PROCEDURE } from './procedure' +import type { ANY_PROCEDURE } from './procedure' export type Context = Record | undefined export type WELL_CONTEXT = Record | undefined @@ -20,5 +20,5 @@ export interface Caller { export interface Meta extends CallerOptions { path: string[] - procedure: WELL_PROCEDURE + procedure: ANY_PROCEDURE } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 46068357a..4b9a0353e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -325,9 +325,6 @@ importers: specifier: workspace:* version: link:../zod devDependencies: - '@orpc/openapi': - specifier: workspace:* - version: link:../openapi zod: specifier: ^3.24.1 version: 3.24.1 From 4e4b8697368bd8f3541f54d785c832ac8b131c09 Mon Sep 17 00:00:00 2001 From: unnoq Date: Fri, 20 Dec 2024 10:01:49 +0700 Subject: [PATCH 39/51] rename caller to client, and some improvements --- packages/next/src/action-form.ts | 4 +- packages/next/src/action-safe.ts | 4 +- packages/openapi/src/fetch/base-handler.ts | 4 +- packages/server/src/builder.ts | 4 +- .../server/src/fetch/handle-request.test-d.ts | 4 +- .../server/src/fetch/orpc-handler.test.ts | 12 +-- packages/server/src/fetch/orpc-handler.ts | 4 +- packages/server/src/fetch/types.ts | 8 +- packages/server/src/index.ts | 4 +- packages/server/src/lazy-decorated.test-d.ts | 12 +-- packages/server/src/lazy-decorated.test.ts | 20 ++-- packages/server/src/lazy-decorated.ts | 15 +-- packages/server/src/lazy-utils.test-d.ts | 10 ++ packages/server/src/lazy-utils.test.ts | 37 ++++++++ packages/server/src/lazy-utils.ts | 21 ++++ packages/server/src/lazy.ts | 2 +- ...r.test-d.ts => procedure-client.test-d.ts} | 93 ++++++++++++++---- ...aller.test.ts => procedure-client.test.ts} | 68 ++++++------- ...rocedure-caller.ts => procedure-client.ts} | 16 ++-- packages/server/src/procedure-decorated.ts | 21 ++-- ...ller.test-d.ts => router-client.test-d.ts} | 59 ++++++------ ...r-caller.test.ts => router-client.test.ts} | 95 ++++++++----------- .../{router-caller.ts => router-client.ts} | 44 ++++----- packages/server/src/types.test-d.ts | 54 +---------- packages/server/src/types.ts | 10 +- packages/server/tsconfig.json | 1 + 26 files changed, 330 insertions(+), 296 deletions(-) create mode 100644 packages/server/src/lazy-utils.test-d.ts create mode 100644 packages/server/src/lazy-utils.test.ts create mode 100644 packages/server/src/lazy-utils.ts rename packages/server/src/{procedure-caller.test-d.ts => procedure-client.test-d.ts} (60%) rename packages/server/src/{procedure-caller.test.ts => procedure-client.test.ts} (84%) rename packages/server/src/{procedure-caller.ts => procedure-client.ts} (90%) rename packages/server/src/{router-caller.test-d.ts => router-client.test-d.ts} (60%) rename packages/server/src/{router-caller.test.ts => router-client.test.ts} (50%) rename packages/server/src/{router-caller.ts => router-client.ts} (60%) diff --git a/packages/next/src/action-form.ts b/packages/next/src/action-form.ts index 8f9988521..5243826ec 100644 --- a/packages/next/src/action-form.ts +++ b/packages/next/src/action-form.ts @@ -1,12 +1,12 @@ import type { ANY_LAZY_PROCEDURE, ANY_PROCEDURE, CreateProcedureCallerOptions } from '@orpc/server' -import { createProcedureCaller, loadProcedure, ORPCError } from '@orpc/server' +import { createProcedureClient, loadProcedure, ORPCError } from '@orpc/server' import { OpenAPIDeserializer } from '@orpc/transformer' import { forbidden, notFound, unauthorized } from 'next/navigation' export type FormAction = (input: FormData) => Promise export function createFormAction(opt: CreateProcedureCallerOptions): FormAction { - const caller = createProcedureCaller(opt) + const caller = createProcedureClient(opt) const formAction = async (input: FormData): Promise => { try { diff --git a/packages/next/src/action-safe.ts b/packages/next/src/action-safe.ts index 1210f0185..054c2aadd 100644 --- a/packages/next/src/action-safe.ts +++ b/packages/next/src/action-safe.ts @@ -1,6 +1,6 @@ import type { SchemaInput, SchemaOutput } from '@orpc/contract' import type { ANY_LAZY_PROCEDURE, ANY_PROCEDURE, CreateProcedureCallerOptions, Lazy, Procedure, WELL_ORPC_ERROR_JSON } from '@orpc/server' -import { createProcedureCaller, ORPCError } from '@orpc/server' +import { createProcedureClient, ORPCError } from '@orpc/server' export type SafeAction = T extends | Procedure @@ -16,7 +16,7 @@ export type SafeAction = T extends : never export function createSafeAction(opt: CreateProcedureCallerOptions): SafeAction { - const caller = createProcedureCaller(opt) + const caller = createProcedureClient(opt) const safeAction = async (...input: [any] | []) => { try { diff --git a/packages/openapi/src/fetch/base-handler.ts b/packages/openapi/src/fetch/base-handler.ts index c13495099..8ac99f4eb 100644 --- a/packages/openapi/src/fetch/base-handler.ts +++ b/packages/openapi/src/fetch/base-handler.ts @@ -5,7 +5,7 @@ import type { ANY_LAZY_PROCEDURE, ANY_PROCEDURE, Router } from '@orpc/server' import type { FetchHandler } from '@orpc/server/fetch' import type { Router as HonoRouter } from 'hono/router' import type { EachContractLeafResultItem, EachLeafOptions } from '../utils' -import { createProcedureCaller, isLazy, isProcedure, LAZY_LOADER_SYMBOL, LAZY_ROUTER_PREFIX_SYMBOL, ORPCError } from '@orpc/server' +import { createProcedureClient, isLazy, isProcedure, LAZY_LOADER_SYMBOL, LAZY_ROUTER_PREFIX_SYMBOL, ORPCError } from '@orpc/server' import { executeWithHooks, isPlainObject, mapValues, ORPC_PROTOCOL_HEADER, ORPC_PROTOCOL_VALUE, trim, value } from '@orpc/shared' import { OpenAPIDeserializer, OpenAPISerializer, zodCoerce } from '@orpc/transformer' import { eachContractProcedureLeaf, standardizeHTTPPath } from '../utils' @@ -65,7 +65,7 @@ export function createOpenAPIHandler(createHonoRouter: () => Routing): FetchHand const input = await deserializeInput(options.request, procedure) const mergedInput = mergeParamsAndInput(params, input) - const caller = createProcedureCaller({ + const caller = createProcedureClient({ context, procedure, path, diff --git a/packages/server/src/builder.ts b/packages/server/src/builder.ts index e095f193a..4a09e396b 100644 --- a/packages/server/src/builder.ts +++ b/packages/server/src/builder.ts @@ -1,5 +1,5 @@ import type { ANY_CONTRACT_PROCEDURE, ContractRouter, HTTPPath, RouteOptions, Schema, SchemaInput, SchemaOutput } from '@orpc/contract' -import type { DecoratedLazy } from './lazy-decorated' +import type { FlattenLazy } from './lazy' import type { Middleware } from './middleware' import type { DecoratedMiddleware } from './middleware-decorated' import type { Router } from './router' @@ -137,7 +137,7 @@ export class Builder { lazy, any>>( loader: () => Promise<{ default: U }>, - ): DecoratedLazy> { + ): AdaptedRouter> { return new RouterBuilder(this['~orpc']).lazy(loader) } diff --git a/packages/server/src/fetch/handle-request.test-d.ts b/packages/server/src/fetch/handle-request.test-d.ts index e75863f77..cff1d0c29 100644 --- a/packages/server/src/fetch/handle-request.test-d.ts +++ b/packages/server/src/fetch/handle-request.test-d.ts @@ -1,5 +1,5 @@ import type { Procedure } from '../procedure' -import type { CallerOptions, WELL_CONTEXT } from '../types' +import type { WELL_CONTEXT, WithSignal } from '../types' import { lazy } from '../lazy' import { handleFetchRequest } from './handle-request' @@ -64,7 +64,7 @@ describe('handleFetchRequest', () => { expectTypeOf(output).toEqualTypeOf() expectTypeOf(input).toEqualTypeOf() expectTypeOf(context).toEqualTypeOf<{ auth: boolean }>() - expectTypeOf(meta).toEqualTypeOf() + expectTypeOf(meta).toEqualTypeOf() }, }) }) diff --git a/packages/server/src/fetch/orpc-handler.test.ts b/packages/server/src/fetch/orpc-handler.test.ts index 9034e2899..6916e571f 100644 --- a/packages/server/src/fetch/orpc-handler.test.ts +++ b/packages/server/src/fetch/orpc-handler.test.ts @@ -3,11 +3,11 @@ import { ORPC_PROTOCOL_HEADER, ORPC_PROTOCOL_VALUE } from '@orpc/shared' import { describe, expect, it, vi } from 'vitest' import { lazy } from '../lazy' import { Procedure } from '../procedure' -import { createProcedureCaller } from '../procedure-caller' +import { createProcedureClient } from '../procedure-client' import { createORPCHandler } from './orpc-handler' -vi.mock('../procedure-caller', () => ({ - createProcedureCaller: vi.fn(() => vi.fn()), +vi.mock('../procedure-client', () => ({ + createProcedureClient: vi.fn(() => vi.fn()), })) describe('createORPCHandler', () => { @@ -74,7 +74,7 @@ describe('createORPCHandler', () => { const handler = createORPCHandler() const caller = vi.fn().mockReturnValueOnce('__mocked__') - vi.mocked(createProcedureCaller).mockReturnValue(caller) + vi.mocked(createProcedureClient).mockReturnValue(caller) const mockRequest = new Request('https://example.com/ping', { headers: new Headers({ [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE }), @@ -119,7 +119,7 @@ describe('createORPCHandler', () => { it('should handle unexpected errors and return a 500 response', async () => { const handler = createORPCHandler() - vi.mocked(createProcedureCaller).mockImplementationOnce(() => { + vi.mocked(createProcedureClient).mockImplementationOnce(() => { throw new Error('Unexpected error') }) @@ -146,7 +146,7 @@ describe('createORPCHandler', () => { const handler = createORPCHandler() const caller = vi.fn().mockReturnValueOnce('__mocked__') - vi.mocked(createProcedureCaller).mockReturnValue(caller) + vi.mocked(createProcedureClient).mockReturnValue(caller) const mockRequest = new Request('https://example.com/ping', { headers: new Headers({ [ORPC_PROTOCOL_HEADER]: ORPC_PROTOCOL_VALUE }), diff --git a/packages/server/src/fetch/orpc-handler.ts b/packages/server/src/fetch/orpc-handler.ts index e88d784d9..281c507e4 100644 --- a/packages/server/src/fetch/orpc-handler.ts +++ b/packages/server/src/fetch/orpc-handler.ts @@ -5,7 +5,7 @@ import { ORPCError } from '@orpc/shared/error' import { ORPCDeserializer, ORPCSerializer } from '@orpc/transformer' import { unlazy } from '../lazy' import { isProcedure } from '../procedure' -import { createProcedureCaller } from '../procedure-caller' +import { createProcedureClient } from '../procedure-client' import { type ANY_ROUTER, getRouterChild } from '../router' const serializer = new ORPCSerializer() @@ -31,7 +31,7 @@ export function createORPCHandler(): FetchHandler { const input = await parseRequestInput(options.request) - const caller = createProcedureCaller({ + const caller = createProcedureClient({ context, procedure: match.procedure, path: match.path, diff --git a/packages/server/src/fetch/types.ts b/packages/server/src/fetch/types.ts index 0e810ac02..4b58f7851 100644 --- a/packages/server/src/fetch/types.ts +++ b/packages/server/src/fetch/types.ts @@ -1,9 +1,7 @@ -/// - import type { HTTPPath } from '@orpc/contract' import type { Hooks, Value } from '@orpc/shared' import type { Router } from '../router' -import type { CallerOptions, Context } from '../types' +import type { Context, WithSignal } from '../types' export type FetchHandlerOptions = { @@ -27,8 +25,8 @@ export type FetchHandlerOptions = prefix?: HTTPPath } & NoInfer<(undefined extends T ? { context?: Value } : { context: Value })> - & CallerOptions - & Hooks + & WithSignal + & Hooks export type FetchHandler = ( options: FetchHandlerOptions diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 92c41230d..4ffbfb393 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -10,12 +10,12 @@ export * from './middleware' export * from './middleware-decorated' export * from './procedure' export * from './procedure-builder' -export * from './procedure-caller' +export * from './procedure-client' export * from './procedure-decorated' export * from './procedure-implementer' export * from './router' export * from './router-builder' -export * from './router-caller' +export * from './router-client' export * from './router-implementer' export * from './types' export * from './utils' diff --git a/packages/server/src/lazy-decorated.test-d.ts b/packages/server/src/lazy-decorated.test-d.ts index 03c58e5dc..6e30a7ea4 100644 --- a/packages/server/src/lazy-decorated.test-d.ts +++ b/packages/server/src/lazy-decorated.test-d.ts @@ -1,4 +1,4 @@ -import type { ANY_PROCEDURE, ANY_ROUTER, Caller, DecoratedProcedure, Procedure, WELL_CONTEXT } from '.' +import type { ANY_PROCEDURE, ANY_ROUTER, DecoratedProcedure, Procedure, ProcedureClient, WELL_CONTEXT } from '.' import type { Lazy } from './lazy' import type { DecoratedLazy } from './lazy-decorated' import { z } from 'zod' @@ -42,7 +42,7 @@ describe('DecoratedLazy', () => { expectTypeOf(decorated).toMatchTypeOf>() expectTypeOf(decorated).toMatchTypeOf< - Caller + ProcedureClient >() }) @@ -53,19 +53,19 @@ describe('DecoratedLazy', () => { expectTypeOf({ router: decorated }).toMatchTypeOf() expectTypeOf(decorated.ping).toMatchTypeOf>() - expectTypeOf(decorated.ping).toMatchTypeOf >() + expectTypeOf(decorated.ping).toMatchTypeOf >() expectTypeOf(decorated.pong).toMatchTypeOf>() - expectTypeOf(decorated.pong).toMatchTypeOf>() + expectTypeOf(decorated.pong).toMatchTypeOf>() expectTypeOf(decorated.nested).toMatchTypeOf>() expectTypeOf({ router: decorated.nested }).toMatchTypeOf() expectTypeOf(decorated.nested.ping).toMatchTypeOf>() - expectTypeOf(decorated.nested.ping).toMatchTypeOf>() + expectTypeOf(decorated.nested.ping).toMatchTypeOf>() expectTypeOf(decorated.nested.pong).toMatchTypeOf>() - expectTypeOf(decorated.nested.pong).toMatchTypeOf>() + expectTypeOf(decorated.nested.pong).toMatchTypeOf>() }) it('flat lazy', () => { diff --git a/packages/server/src/lazy-decorated.test.ts b/packages/server/src/lazy-decorated.test.ts index 00dd6d99b..97794c742 100644 --- a/packages/server/src/lazy-decorated.test.ts +++ b/packages/server/src/lazy-decorated.test.ts @@ -3,7 +3,7 @@ import { z } from 'zod' import { isLazy, lazy, unlazy } from './lazy' import { decorateLazy } from './lazy-decorated' import { Procedure } from './procedure' -import { createProcedureCaller } from './procedure-caller' +import { createProcedureClient } from './procedure-client' vi.mock('./procedure-caller', () => ({ createProcedureCaller: vi.fn(() => vi.fn()), @@ -62,19 +62,19 @@ describe('decorated lazy', () => { const signal = controller.signal const caller = vi.fn(() => '__mocked__') - vi.mocked(createProcedureCaller).mockReturnValue(caller as any) + vi.mocked(createProcedureClient).mockReturnValue(caller as any) it('on root', async () => { const decorated = decorateLazy(lazied) as any expect(decorated).toBeInstanceOf(Function) - expect(createProcedureCaller).toHaveBeenCalledTimes(1) - expect(createProcedureCaller).toHaveBeenCalledWith({ + expect(createProcedureClient).toHaveBeenCalledTimes(1) + expect(createProcedureClient).toHaveBeenCalledWith({ procedure: expect.any(Object), context: undefined, }) - expect(vi.mocked(createProcedureCaller).mock.calls[0]![0].procedure).toSatisfy(isLazy) - const unwrapped = await unlazy(vi.mocked(createProcedureCaller).mock.calls[0]![0].procedure as any) + expect(vi.mocked(createProcedureClient).mock.calls[0]![0].procedure).toSatisfy(isLazy) + const unwrapped = await unlazy(vi.mocked(createProcedureClient).mock.calls[0]![0].procedure as any) expect(unwrapped.default).toBe(router) expect(await decorated({ val: '1' }, { signal })).toBe('__mocked__') @@ -86,13 +86,13 @@ describe('decorated lazy', () => { const decorated = decorateLazy(lazied).nested.ping as any expect(decorated).toBeInstanceOf(Function) - expect(createProcedureCaller).toHaveBeenCalledTimes(3) - expect(createProcedureCaller).toHaveBeenNthCalledWith(3, { + expect(createProcedureClient).toHaveBeenCalledTimes(3) + expect(createProcedureClient).toHaveBeenNthCalledWith(3, { procedure: expect.any(Object), context: undefined, }) - expect(vi.mocked(createProcedureCaller).mock.calls[2]![0].procedure).toSatisfy(isLazy) - const unwrapped = await unlazy(vi.mocked(createProcedureCaller).mock.calls[2]![0].procedure as any) + expect(vi.mocked(createProcedureClient).mock.calls[2]![0].procedure).toSatisfy(isLazy) + const unwrapped = await unlazy(vi.mocked(createProcedureClient).mock.calls[2]![0].procedure as any) expect(unwrapped.default).toBe(ping) expect(await decorated({ val: '1' }, { signal })).toBe('__mocked__') diff --git a/packages/server/src/lazy-decorated.ts b/packages/server/src/lazy-decorated.ts index 1d4cbf236..ebb2683d5 100644 --- a/packages/server/src/lazy-decorated.ts +++ b/packages/server/src/lazy-decorated.ts @@ -1,9 +1,10 @@ import type { SchemaInput, SchemaOutput } from '@orpc/contract' import type { Lazy } from './lazy' import type { Procedure } from './procedure' -import type { Caller } from './types' +import type { ProcedureClient } from './procedure-client' import { flatLazy } from './lazy' -import { createProcedureCaller } from './procedure-caller' +import { createLazyProcedureFormAnyLazy } from './lazy-utils' +import { createProcedureClient } from './procedure-client' import { type ANY_ROUTER, getRouterChild } from './router' export type DecoratedLazy = T extends Lazy @@ -13,7 +14,7 @@ export type DecoratedLazy = T extends Lazy & ( T extends Procedure ? undefined extends UContext - ? Caller, SchemaOutput> + ? ProcedureClient, SchemaOutput> : unknown : { [K in keyof T]: T[K] extends object ? DecoratedLazy : never @@ -23,14 +24,14 @@ export type DecoratedLazy = T extends Lazy export function decorateLazy>(lazied: T): DecoratedLazy { const flattenLazy = flatLazy(lazied) - const procedureCaller = createProcedureCaller({ - procedure: flattenLazy, + const procedureProcedureClient = createProcedureClient({ + procedure: createLazyProcedureFormAnyLazy(flattenLazy), context: undefined, }) - Object.assign(procedureCaller, flattenLazy) + Object.assign(procedureProcedureClient, flattenLazy) - const recursive = new Proxy(procedureCaller, { + const recursive = new Proxy(procedureProcedureClient, { get(target, key) { if (typeof key !== 'string') { return Reflect.get(target, key) diff --git a/packages/server/src/lazy-utils.test-d.ts b/packages/server/src/lazy-utils.test-d.ts new file mode 100644 index 000000000..13fe13e9c --- /dev/null +++ b/packages/server/src/lazy-utils.test-d.ts @@ -0,0 +1,10 @@ +import type { Lazy } from './lazy' +import type { ANY_PROCEDURE } from './procedure' +import { lazy } from './lazy' +import { createLazyProcedureFormAnyLazy } from './lazy-utils' + +it('createLazyProcedureFormAnyLazy return a Lazy', async () => { + const lazyPing = lazy(() => Promise.resolve({ default: {} as unknown })) + const lazyProcedure = createLazyProcedureFormAnyLazy(lazyPing) + expectTypeOf(lazyProcedure).toEqualTypeOf>() +}) diff --git a/packages/server/src/lazy-utils.test.ts b/packages/server/src/lazy-utils.test.ts new file mode 100644 index 000000000..7bd1c4247 --- /dev/null +++ b/packages/server/src/lazy-utils.test.ts @@ -0,0 +1,37 @@ +import { ContractProcedure } from '@orpc/contract' +import { isLazy, lazy, unlazy } from './lazy' +import { createLazyProcedureFormAnyLazy } from './lazy-utils' +import { Procedure } from './procedure' + +describe('createLazyProcedureFormAnyLazy', () => { + const ping = new Procedure({ + contract: new ContractProcedure({ + InputSchema: undefined, + OutputSchema: undefined, + }), + func: vi.fn(), + }) + + it('return a Lazy', async () => { + const lazyPing = lazy(() => Promise.resolve({ default: ping })) + + const lazyProcedure = createLazyProcedureFormAnyLazy(lazyPing) + + expect(lazyProcedure).toSatisfy(isLazy) + expect(unlazy(lazyProcedure)).resolves.toEqual({ default: ping }) + }) + + it('throw un unlazy non-procedure', () => { + const lazyPing = lazy(() => Promise.resolve({ default: {} as unknown })) + const lazyProcedure = createLazyProcedureFormAnyLazy(lazyPing) + + expect(unlazy(lazyProcedure)).rejects.toThrow('Expected a lazy but got lazy') + }) + + it('flat lazy', () => { + const lazyPing = lazy(() => Promise.resolve({ default: lazy(() => Promise.resolve({ default: ping })) })) + const lazyProcedure = createLazyProcedureFormAnyLazy(lazyPing) + + expect(unlazy(lazyProcedure)).resolves.toEqual({ default: ping }) + }) +}) diff --git a/packages/server/src/lazy-utils.ts b/packages/server/src/lazy-utils.ts new file mode 100644 index 000000000..881f97846 --- /dev/null +++ b/packages/server/src/lazy-utils.ts @@ -0,0 +1,21 @@ +import type { Lazy } from './lazy' +import { flatLazy, lazy, unlazy } from './lazy' +import { type ANY_PROCEDURE, isProcedure } from './procedure' + +export function createLazyProcedureFormAnyLazy(lazied: Lazy): Lazy { + const lazyProcedure = lazy(async () => { + const { default: maybeProcedure } = await unlazy(flatLazy(lazied)) + + if (!isProcedure(maybeProcedure)) { + throw new Error(` + Expected a lazy but got lazy. + This should be caught by TypeScript compilation. + Please report this issue if this makes you feel uncomfortable. + `) + } + + return { default: maybeProcedure } + }) + + return lazyProcedure +} diff --git a/packages/server/src/lazy.ts b/packages/server/src/lazy.ts index 253079da1..05dee30ff 100644 --- a/packages/server/src/lazy.ts +++ b/packages/server/src/lazy.ts @@ -23,7 +23,7 @@ export function isLazy(item: unknown): item is ANY_LAZY { ) } -export function unlazy(lazied: Lazyable): Promise<{ default: T }> { +export function unlazy>(lazied: T): Promise<{ default: T extends Lazy ? U : T }> { return isLazy(lazied) ? lazied[LAZY_LOADER_SYMBOL]() : Promise.resolve({ default: lazied }) } diff --git a/packages/server/src/procedure-caller.test-d.ts b/packages/server/src/procedure-client.test-d.ts similarity index 60% rename from packages/server/src/procedure-caller.test-d.ts rename to packages/server/src/procedure-client.test-d.ts index 9f8428c77..befc42a59 100644 --- a/packages/server/src/procedure-caller.test-d.ts +++ b/packages/server/src/procedure-client.test-d.ts @@ -1,69 +1,122 @@ import type { Procedure } from './procedure' -import type { Caller, Meta, WELL_CONTEXT } from './types' +import type { ProcedureClient } from './procedure-client' +import type { Meta, WELL_CONTEXT, WithSignal } from './types' import { z } from 'zod' import { lazy } from './lazy' -import { createProcedureCaller } from './procedure-caller' +import { createProcedureClient } from './procedure-client' beforeEach(() => { vi.resetAllMocks() }) -describe('createProcedureCaller', () => { +describe('ProcedureClient', () => { + const fn: ProcedureClient = async (input, options) => { + expectTypeOf(input).toEqualTypeOf() + expectTypeOf(options).toEqualTypeOf() + return 123 + } + + const fnWithOptionalInput: ProcedureClient = async (...args) => { + const [input, options] = args + + expectTypeOf(input).toEqualTypeOf() + expectTypeOf(options).toEqualTypeOf() + return 123 + } + + it('just a function', () => { + expectTypeOf(fn).toEqualTypeOf<(input: string, options?: WithSignal) => Promise>() + expectTypeOf(fnWithOptionalInput).toMatchTypeOf<(input: string | undefined, options?: WithSignal) => Promise>() + }) + + it('infer correct input', () => { + fn('123') + fnWithOptionalInput('123') + + // @ts-expect-error - invalid input + fn(123) + // @ts-expect-error - invalid input + fnWithOptionalInput(123) + + // @ts-expect-error - invalid input + fn({}) + // @ts-expect-error - invalid input + fnWithOptionalInput({}) + }) + + it('accept signal', () => { + fn('123', { signal: new AbortSignal() }) + fnWithOptionalInput('123', { signal: new AbortSignal() }) + + // @ts-expect-error - invalid signal + fn('123', { signal: 1234 }) + // @ts-expect-error - invalid signal + fnWithOptionalInput('123', { signal: 1234 }) + }) + + it('can accept call without args', () => { + expectTypeOf(fnWithOptionalInput()).toEqualTypeOf>() + // @ts-expect-error - input is required + expectTypeOf(fn()).toEqualTypeOf>() + }) +}) + +describe('createProcedureClient', () => { const schema = z.object({ val: z.string().transform(v => Number(v)) }) const procedure = {} as Procedure const procedureWithContext = {} as Procedure<{ userId?: string }, { db: string }, typeof schema, typeof schema, { val: string }> - it('just a caller', () => { - const caller = createProcedureCaller({ + it('just a client', () => { + const client = createProcedureClient({ procedure, }) - expectTypeOf(caller).toEqualTypeOf>() + expectTypeOf(client).toEqualTypeOf>() }) it('context can be optional and can be a sync or async function', () => { - createProcedureCaller({ + createProcedureClient({ procedure, }) - createProcedureCaller({ + createProcedureClient({ procedure, context: undefined, }) // @ts-expect-error - missing context - createProcedureCaller({ + createProcedureClient({ procedure: procedureWithContext, }) - createProcedureCaller({ + createProcedureClient({ procedure: procedureWithContext, context: { userId: '123' }, }) - createProcedureCaller({ + createProcedureClient({ procedure: procedureWithContext, // @ts-expect-error invalid context context: { userId: 123 }, }) - createProcedureCaller({ + createProcedureClient({ procedure: procedureWithContext, context: () => ({ userId: '123' }), }) - createProcedureCaller({ + createProcedureClient({ procedure: procedureWithContext, // @ts-expect-error invalid context context: () => ({ userId: 123 }), }) - createProcedureCaller({ + createProcedureClient({ procedure: procedureWithContext, context: async () => ({ userId: '123' }), }) - createProcedureCaller({ + createProcedureClient({ procedure: procedureWithContext, // @ts-expect-error invalid context context: async () => ({ userId: 123 }), @@ -71,7 +124,7 @@ describe('createProcedureCaller', () => { }) it('accept hooks', () => { - createProcedureCaller({ + createProcedureClient({ procedure, async execute(input, context, meta) { @@ -109,12 +162,12 @@ describe('createProcedureCaller', () => { }) it('accept paths', () => { - createProcedureCaller({ + createProcedureClient({ procedure, path: ['users'], }) - createProcedureCaller({ + createProcedureClient({ procedure, // @ts-expect-error - invalid path path: [123], @@ -127,7 +180,7 @@ it('support lazy procedure', () => { const procedure = {} as Procedure<{ userId?: string }, undefined, typeof schema, typeof schema, { val: string }> const lazied = lazy(() => Promise.resolve({ default: procedure })) - const caller = createProcedureCaller({ + const client = createProcedureClient({ procedure: lazied, context: async () => ({ userId: 'string' }), path: ['users'], @@ -139,5 +192,5 @@ it('support lazy procedure', () => { }, }) - expectTypeOf(caller).toEqualTypeOf>() + expectTypeOf(client).toEqualTypeOf>() }) diff --git a/packages/server/src/procedure-caller.test.ts b/packages/server/src/procedure-client.test.ts similarity index 84% rename from packages/server/src/procedure-caller.test.ts rename to packages/server/src/procedure-client.test.ts index 3715f47e5..cb592fb6f 100644 --- a/packages/server/src/procedure-caller.test.ts +++ b/packages/server/src/procedure-client.test.ts @@ -3,7 +3,7 @@ import { ContractProcedure } from '@orpc/contract' import { z } from 'zod' import { isLazy, lazy, unlazy } from './lazy' import { Procedure } from './procedure' -import { createProcedureCaller } from './procedure-caller' +import { createProcedureClient } from './procedure-client' const schema = z.object({ val: z.string().transform(v => Number(v)) }) @@ -29,15 +29,15 @@ beforeEach(() => { vi.clearAllMocks() }) -describe.each(procedureCases)('createProcedureCaller - case %s', async (_, procedure) => { +describe.each(procedureCases)('createProcedureClient - case %s', async (_, procedure) => { const unwrappedProcedure = isLazy(procedure) ? (await unlazy(procedure)).default : procedure - it('just a caller', async () => { - const caller = createProcedureCaller({ + it('just a client', async () => { + const client = createProcedureClient({ procedure, }) - await expect(caller({ val: '123' })).resolves.toEqual({ val: 123 }) + await expect(client({ val: '123' })).resolves.toEqual({ val: 123 }) expect(func).toBeCalledTimes(1) expect(func).toBeCalledWith({ val: 123 }, undefined, { path: [], procedure: unwrappedProcedure }) @@ -50,26 +50,26 @@ describe.each(procedureCases)('createProcedureCaller - case %s', async (_, proce }) it('validate input and output', () => { - const caller = createProcedureCaller({ + const client = createProcedureClient({ procedure, }) // @ts-expect-error - invalid input - expect(caller({ val: 123 })).rejects.toThrow('Input validation failed') + expect(client({ val: 123 })).rejects.toThrow('Input validation failed') // @ts-expect-error - invalid output func.mockReturnValueOnce({ val: 1234 }) - expect(caller({ val: '1234' })).rejects.toThrow('Output validation failed') + expect(client({ val: '1234' })).rejects.toThrow('Output validation failed') }) it('middleware can return output directly', async () => { - const caller = createProcedureCaller({ + const client = createProcedureClient({ procedure, }) mid1.mockReturnValueOnce({ output: { val: '990' } }) - await expect(caller({ val: '123' })).resolves.toEqual({ val: 990 }) + await expect(client({ val: '123' })).resolves.toEqual({ val: 990 }) expect(mid1).toBeCalledTimes(1) expect(mid2).toBeCalledTimes(0) @@ -79,7 +79,7 @@ describe.each(procedureCases)('createProcedureCaller - case %s', async (_, proce mid2.mockReturnValueOnce({ output: { val: '9900' } }) - await expect(caller({ val: '123' })).resolves.toEqual({ val: 9900 }) + await expect(client({ val: '123' })).resolves.toEqual({ val: 9900 }) expect(mid1).toBeCalledTimes(1) expect(mid2).toBeCalledTimes(1) @@ -89,22 +89,22 @@ describe.each(procedureCases)('createProcedureCaller - case %s', async (_, proce }) it('output from middleware still be validated', async () => { - const caller = createProcedureCaller({ + const client = createProcedureClient({ procedure, context: { userId: '123' }, }) mid1.mockReturnValueOnce({ output: { val: 990 } }) - await expect(caller({ val: '1234' })).rejects.toThrow('Output validation failed') + await expect(client({ val: '1234' })).rejects.toThrow('Output validation failed') vi.clearAllMocks() mid2.mockReturnValueOnce({ output: { val: 9900 } }) - await expect(caller({ val: '1234' })).rejects.toThrow('Output validation failed') + await expect(client({ val: '1234' })).rejects.toThrow('Output validation failed') }) it('middleware can add extra context - single', async () => { - const caller = createProcedureCaller({ + const client = createProcedureClient({ procedure, }) @@ -124,7 +124,7 @@ describe.each(procedureCases)('createProcedureCaller - case %s', async (_, proce }) }) - await expect(caller({ val: '123' })).resolves.toEqual({ val: 123 }) + await expect(client({ val: '123' })).resolves.toEqual({ val: 123 }) expect(mid1).toBeCalledTimes(1) expect(mid1).toHaveBeenCalledWith(expect.any(Object), undefined, expect.any(Object)) @@ -137,7 +137,7 @@ describe.each(procedureCases)('createProcedureCaller - case %s', async (_, proce }) it('middleware can override context', async () => { - const caller = createProcedureCaller({ + const client = createProcedureClient({ procedure, context: { userId: '123' }, }) @@ -158,7 +158,7 @@ describe.each(procedureCases)('createProcedureCaller - case %s', async (_, proce }) }) - await expect(caller({ val: '123' })).resolves.toEqual({ val: 123 }) + await expect(client({ val: '123' })).resolves.toEqual({ val: 123 }) expect(mid1).toBeCalledTimes(1) expect(mid1).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({ userId: '123' }), expect.any(Object)) @@ -177,12 +177,12 @@ describe.each(procedureCases)('createProcedureCaller - case %s', async (_, proce ] as const it.each(contextCases)('can accept context: %s', async (_, context) => { - const caller = createProcedureCaller({ + const client = createProcedureClient({ procedure, context, }) - await caller({ val: '123' }) + await client({ val: '123' }) expect(mid1).toBeCalledTimes(1) expect(mid1).toBeCalledWith(expect.any(Object), { val: '__val__' }, expect.any(Object)) @@ -201,7 +201,7 @@ describe.each(procedureCases)('createProcedureCaller - case %s', async (_, proce const onError = vi.fn() const onFinish = vi.fn() - const caller = createProcedureCaller({ + const client = createProcedureClient({ procedure, context, path: ['users'], @@ -212,7 +212,7 @@ describe.each(procedureCases)('createProcedureCaller - case %s', async (_, proce onFinish, }) - await caller({ val: '123' }) + await client({ val: '123' }) const meta = { path: ['users'], @@ -246,13 +246,13 @@ describe.each(procedureCases)('createProcedureCaller - case %s', async (_, proce it('accept paths', async () => { const onSuccess = vi.fn() - const caller = createProcedureCaller({ + const client = createProcedureClient({ procedure, path: ['users'], onSuccess, }) - await caller({ val: '123' }) + await client({ val: '123' }) expect(mid1).toBeCalledTimes(1) expect(mid1).toHaveBeenCalledWith(expect.any(Object), undefined, expect.objectContaining({ path: ['users'] })) @@ -273,13 +273,13 @@ describe.each(procedureCases)('createProcedureCaller - case %s', async (_, proce const onSuccess = vi.fn() - const caller = createProcedureCaller({ + const client = createProcedureClient({ procedure, onSuccess, context: { userId: '123' }, }) - await caller({ val: '123' }, { signal }) + await client({ val: '123' }, { signal }) expect(mid1).toBeCalledTimes(1) expect(mid1).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), expect.objectContaining({ signal })) @@ -304,11 +304,11 @@ it('still work without middleware', async () => { func, }) - const caller = createProcedureCaller({ + const client = createProcedureClient({ procedure, }) - await expect(caller({ val: '123' })).resolves.toEqual({ val: 123 }) + await expect(client({ val: '123' })).resolves.toEqual({ val: 123 }) expect(func).toBeCalledTimes(1) expect(func).toHaveBeenCalledWith({ val: 123 }, undefined, { path: [], procedure }) @@ -323,11 +323,11 @@ it('still work without InputSchema', async () => { func, }) - const caller = createProcedureCaller({ + const client = createProcedureClient({ procedure, }) - await expect(caller('anything')).resolves.toEqual({ val: 123 }) + await expect(client('anything')).resolves.toEqual({ val: 123 }) expect(func).toBeCalledTimes(1) expect(func).toHaveBeenCalledWith('anything', undefined, { path: [], procedure }) @@ -342,21 +342,21 @@ it('still work without OutputSchema', async () => { func, }) - const caller = createProcedureCaller({ + const client = createProcedureClient({ procedure, }) // @ts-expect-error - without output schema func.mockReturnValueOnce('anything') - await expect(caller({ val: '123' })).resolves.toEqual('anything') + await expect(client({ val: '123' })).resolves.toEqual('anything') expect(func).toBeCalledTimes(1) expect(func).toHaveBeenCalledWith({ val: 123 }, undefined, { path: [], procedure }) }) it('has helper `output` in meta', async () => { - const caller = createProcedureCaller({ + const client = createProcedureClient({ procedure, }) @@ -364,7 +364,7 @@ it('has helper `output` in meta', async () => { return meta.output({ val: '99990' }) }) - await expect(caller({ val: '123' })).resolves.toEqual({ val: 99990 }) + await expect(client({ val: '123' })).resolves.toEqual({ val: 99990 }) expect(mid1).toBeCalledTimes(1) expect(mid2).toBeCalledTimes(1) diff --git a/packages/server/src/procedure-caller.ts b/packages/server/src/procedure-client.ts similarity index 90% rename from packages/server/src/procedure-caller.ts rename to packages/server/src/procedure-client.ts index 4aa6296a2..b9af07e1f 100644 --- a/packages/server/src/procedure-caller.ts +++ b/packages/server/src/procedure-client.ts @@ -2,17 +2,17 @@ import type { Schema, SchemaInput, SchemaOutput } from '@orpc/contract' import type { Hooks, Value } from '@orpc/shared' import type { Lazyable } from './lazy' import type { MiddlewareMeta } from './middleware' -import type { - ANY_PROCEDURE, - Procedure, -} from './procedure' - -import type { Caller, Context, Meta, WELL_CONTEXT } from './types' +import type { ANY_PROCEDURE, Procedure } from './procedure' +import type { Context, Meta, WELL_CONTEXT, WithSignal } from './types' import { executeWithHooks, value } from '@orpc/shared' import { ORPCError } from '@orpc/shared/error' import { unlazy } from './lazy' import { mergeContext } from './utils' +export interface ProcedureClient { + (...opts: [input: TInput, options?: WithSignal] | (undefined extends TInput ? [] : never)): Promise +} + /** * Options for creating a procedure caller with comprehensive type safety */ @@ -40,14 +40,14 @@ export type CreateProcedureCallerOptions< } | (undefined extends TContext ? { context?: undefined } : never)) & Hooks, TContext, Meta> -export function createProcedureCaller< +export function createProcedureClient< TContext extends Context = WELL_CONTEXT, TInputSchema extends Schema = undefined, TOutputSchema extends Schema = undefined, TFuncOutput extends SchemaInput = SchemaInput, >( options: CreateProcedureCallerOptions, -): Caller, SchemaOutput> { +): ProcedureClient, SchemaOutput> { return async (...[input, callerOptions]) => { const path = options.path ?? [] const { default: procedure } = await unlazy(options.procedure) diff --git a/packages/server/src/procedure-decorated.ts b/packages/server/src/procedure-decorated.ts index b0a267747..48cc97c79 100644 --- a/packages/server/src/procedure-decorated.ts +++ b/packages/server/src/procedure-decorated.ts @@ -1,10 +1,11 @@ import type { HTTPPath, RouteOptions, Schema, SchemaInput, SchemaOutput } from '@orpc/contract' import type { ANY_MIDDLEWARE, MapInputMiddleware, Middleware } from './middleware' -import type { Caller, Context, MergeContext } from './types' +import type { ProcedureClient } from './procedure-client' +import type { Context, MergeContext } from './types' import { DecoratedContractProcedure } from '@orpc/contract' import { decorateMiddleware } from './middleware-decorated' import { Procedure } from './procedure' -import { createProcedureCaller } from './procedure-caller' +import { createProcedureClient } from './procedure-client' export type DecoratedProcedure< TContext extends Context, @@ -71,7 +72,7 @@ export type DecoratedProcedure< ) => DecoratedProcedure } - & (undefined extends TContext ? Caller, SchemaOutput> : unknown) + & (undefined extends TContext ? ProcedureClient, SchemaOutput> : unknown) export function decorateProcedure< TContext extends Context, @@ -80,16 +81,10 @@ export function decorateProcedure< TOutputSchema extends Schema, TFuncOutput extends SchemaInput, >( - procedure: Procedure< - TContext, - TExtraContext, - TInputSchema, - TOutputSchema, - TFuncOutput - >, -): DecoratedProcedure { - const caller = createProcedureCaller({ - procedure: procedure as any, + procedure: Procedure, +): DecoratedProcedure { + const caller = createProcedureClient({ + procedure, context: undefined as any, }) diff --git a/packages/server/src/router-caller.test-d.ts b/packages/server/src/router-client.test-d.ts similarity index 60% rename from packages/server/src/router-caller.test-d.ts rename to packages/server/src/router-client.test-d.ts index 5a9f8b506..056f5da09 100644 --- a/packages/server/src/router-caller.test-d.ts +++ b/packages/server/src/router-client.test-d.ts @@ -1,8 +1,9 @@ import type { Procedure } from './procedure' -import type { Caller, Meta, WELL_CONTEXT } from './types' +import type { ProcedureClient } from './procedure-client' +import type { Meta, WELL_CONTEXT } from './types' import { z } from 'zod' import { lazy } from './lazy' -import { createRouterCaller, type RouterCaller } from './router-caller' +import { createRouterClient, type RouterClient } from './router-client' const schema = z.object({ val: z.string().transform(val => Number(val)) }) const ping = {} as Procedure @@ -26,84 +27,84 @@ const routerWithLazy = { } })), } -describe('RouterCaller', () => { +describe('RouterClient', () => { it('router without lazy', () => { - const caller = {} as RouterCaller + const client = {} as RouterClient - expectTypeOf(caller.ping).toEqualTypeOf< - Caller<{ val: string }, { val: number }> + expectTypeOf(client.ping).toEqualTypeOf< + ProcedureClient<{ val: string }, { val: number }> >() - expectTypeOf(caller.pong).toEqualTypeOf< - Caller + expectTypeOf(client.pong).toEqualTypeOf< + ProcedureClient >() - expectTypeOf(caller.nested.ping).toEqualTypeOf< - Caller<{ val: string }, { val: number }> + expectTypeOf(client.nested.ping).toEqualTypeOf< + ProcedureClient<{ val: string }, { val: number }> >() - expectTypeOf(caller.nested.pong).toEqualTypeOf< - Caller + expectTypeOf(client.nested.pong).toEqualTypeOf< + ProcedureClient >() }) it('support lazy', () => { - expectTypeOf>().toEqualTypeOf>() + expectTypeOf>().toEqualTypeOf>() }) it('support procedure as router', () => { - expectTypeOf>().toEqualTypeOf>() + expectTypeOf>().toEqualTypeOf>() }) }) -describe('createRouterCaller', () => { - it('return RouterCaller', () => { - const caller = createRouterCaller({ +describe('createRouterClient', () => { + it('return RouterClient', () => { + const client = createRouterClient({ router, context: { auth: true }, }) - expectTypeOf(caller).toMatchTypeOf>() + expectTypeOf(client).toMatchTypeOf>() - const caller2 = createRouterCaller({ + const client2 = createRouterClient({ router: routerWithLazy, context: { auth: true }, }) - expectTypeOf(caller2).toMatchTypeOf>() + expectTypeOf(client2).toMatchTypeOf>() }) it('required context when needed', () => { - createRouterCaller({ + createRouterClient({ router: { ping }, }) - createRouterCaller({ + createRouterClient({ router: { pong }, context: { auth: true }, }) - createRouterCaller({ + createRouterClient({ router: { pong }, context: () => ({ auth: true }), }) - createRouterCaller({ + createRouterClient({ router: { pong }, context: async () => ({ auth: true }), }) - createRouterCaller({ + createRouterClient({ router: { pong }, // @ts-expect-error --- invalid context context: { auth: 'invalid' }, }) // @ts-expect-error --- missing context - createRouterCaller({ + createRouterClient({ router: { pong }, }) }) it('support hooks', () => { - createRouterCaller({ + createRouterClient({ router, context: { auth: true }, onSuccess: async ({ output }, context, meta) => { @@ -117,13 +118,13 @@ describe('createRouterCaller', () => { }) it('support base path', () => { - createRouterCaller({ + createRouterClient({ router: { ping }, context: { auth: true }, path: ['users'], }) - createRouterCaller({ + createRouterClient({ router: { ping }, context: { auth: true }, // @ts-expect-error --- invalid path diff --git a/packages/server/src/router-caller.test.ts b/packages/server/src/router-client.test.ts similarity index 50% rename from packages/server/src/router-caller.test.ts rename to packages/server/src/router-client.test.ts index 67f4e9f22..5eab7437e 100644 --- a/packages/server/src/router-caller.test.ts +++ b/packages/server/src/router-client.test.ts @@ -1,19 +1,19 @@ import { ContractProcedure } from '@orpc/contract' import { z } from 'zod' -import { isLazy, lazy, unlazy } from './lazy' +import { lazy, unlazy } from './lazy' import { Procedure } from './procedure' -import { createProcedureCaller } from './procedure-caller' -import { createRouterCaller } from './router-caller' +import { createProcedureClient } from './procedure-client' +import { createRouterClient } from './router-client' -vi.mock('./procedure-caller', () => ({ - createProcedureCaller: vi.fn(() => vi.fn(() => '__mocked__')), +vi.mock('./procedure-client', () => ({ + createProcedureClient: vi.fn(() => vi.fn(() => '__mocked__')), })) beforeEach(() => { vi.clearAllMocks() }) -describe('createRouterCaller', () => { +describe('createRouterClient', () => { const schema = z.object({ val: z.string().transform(v => Number(v)) }) const ping = new Procedure({ contract: new ContractProcedure({ @@ -39,77 +39,77 @@ describe('createRouterCaller', () => { } })), } - const caller = createRouterCaller({ + const client = createRouterClient({ router, context: { auth: true }, path: ['users'], }) it('works', () => { - expect(caller.pong({ val: '123' })).toEqual('__mocked__') + expect(client.pong({ val: '123' })).toEqual('__mocked__') - expect(createProcedureCaller).toBeCalledTimes(1) - expect(createProcedureCaller).toBeCalledWith(expect.objectContaining({ + expect(createProcedureClient).toBeCalledTimes(1) + expect(createProcedureClient).toBeCalledWith(expect.objectContaining({ procedure: pong, context: { auth: true }, path: ['users', 'pong'], })) - expect(vi.mocked(createProcedureCaller).mock.results[0]?.value).toBeCalledTimes(1) - expect(vi.mocked(createProcedureCaller).mock.results[0]?.value).toBeCalledWith({ val: '123' }) + expect(vi.mocked(createProcedureClient).mock.results[0]?.value).toBeCalledTimes(1) + expect(vi.mocked(createProcedureClient).mock.results[0]?.value).toBeCalledWith({ val: '123' }) }) it('work with lazy', async () => { - expect(caller.ping({ val: '123' })).toEqual('__mocked__') + expect(client.ping({ val: '123' })).toEqual('__mocked__') - expect(createProcedureCaller).toBeCalledTimes(1) - expect(createProcedureCaller).toHaveBeenNthCalledWith(1, expect.objectContaining({ + expect(createProcedureClient).toBeCalledTimes(1) + expect(createProcedureClient).toHaveBeenNthCalledWith(1, expect.objectContaining({ procedure: expect.any(Object), context: { auth: true }, path: ['users', 'ping'], })) - expect((await unlazy(vi.mocked(createProcedureCaller as any).mock.calls[0]![0].procedure)).default).toBe(ping) + expect((await unlazy(vi.mocked(createProcedureClient as any).mock.calls[0]![0].procedure)).default).toBe(ping) - expect(vi.mocked(createProcedureCaller).mock.results[0]?.value).toBeCalledTimes(1) - expect(vi.mocked(createProcedureCaller).mock.results[0]?.value).toBeCalledWith({ val: '123' }) + expect(vi.mocked(createProcedureClient).mock.results[0]?.value).toBeCalledTimes(1) + expect(vi.mocked(createProcedureClient).mock.results[0]?.value).toBeCalledWith({ val: '123' }) }) it('work with nested lazy', async () => { - expect(caller.nested.ping({ val: '123' })).toEqual('__mocked__') + expect(client.nested.ping({ val: '123' })).toEqual('__mocked__') - expect(createProcedureCaller).toBeCalledTimes(2) - expect(createProcedureCaller).toHaveBeenNthCalledWith(2, expect.objectContaining({ + expect(createProcedureClient).toBeCalledTimes(2) + expect(createProcedureClient).toHaveBeenNthCalledWith(2, expect.objectContaining({ procedure: expect.any(Object), context: { auth: true }, path: ['users', 'nested', 'ping'], })) - const lazied = vi.mocked(createProcedureCaller as any).mock.calls[1]![0].procedure + const lazied = vi.mocked(createProcedureClient as any).mock.calls[1]![0].procedure expect(await unlazy(lazied)).toEqual({ default: ping }) - expect(vi.mocked(createProcedureCaller).mock.results[1]?.value).toBeCalledTimes(1) - expect(vi.mocked(createProcedureCaller).mock.results[1]?.value).toBeCalledWith({ val: '123' }) + expect(vi.mocked(createProcedureClient).mock.results[1]?.value).toBeCalledTimes(1) + expect(vi.mocked(createProcedureClient).mock.results[1]?.value).toBeCalledWith({ val: '123' }) }) it('work with procedure as router', () => { - const caller = createRouterCaller({ + const client = createRouterClient({ router: ping, context: { auth: true }, path: ['users'], }) - expect(caller({ val: '123' })).toEqual('__mocked__') + expect(client({ val: '123' })).toEqual('__mocked__') - expect(createProcedureCaller).toBeCalledTimes(1) - expect(createProcedureCaller).toHaveBeenCalledWith(expect.objectContaining({ + expect(createProcedureClient).toBeCalledTimes(1) + expect(createProcedureClient).toHaveBeenCalledWith(expect.objectContaining({ procedure: ping, context: { auth: true }, path: ['users'], })) - expect(vi.mocked(createProcedureCaller).mock.results[0]?.value).toBeCalledTimes(1) - expect(vi.mocked(createProcedureCaller).mock.results[0]?.value).toBeCalledWith({ val: '123' }) + expect(vi.mocked(createProcedureClient).mock.results[0]?.value).toBeCalledTimes(1) + expect(vi.mocked(createProcedureClient).mock.results[0]?.value).toBeCalledWith({ val: '123' }) }) it('hooks', async () => { @@ -119,7 +119,7 @@ describe('createRouterCaller', () => { const onFinish = vi.fn() const execute = vi.fn() - const caller = createRouterCaller({ + const client = createRouterClient({ router, context: { auth: true }, onStart, @@ -129,10 +129,10 @@ describe('createRouterCaller', () => { execute, }) - expect(caller.pong({ val: '123' })).toEqual('__mocked__') + expect(client.pong({ val: '123' })).toEqual('__mocked__') - expect(createProcedureCaller).toBeCalledTimes(1) - expect(createProcedureCaller).toHaveBeenCalledWith(expect.objectContaining({ + expect(createProcedureClient).toBeCalledTimes(1) + expect(createProcedureClient).toHaveBeenCalledWith(expect.objectContaining({ procedure: pong, context: { auth: true }, path: ['pong'], @@ -145,43 +145,28 @@ describe('createRouterCaller', () => { }) it('not recursive on symbol', () => { - expect((caller as any)[Symbol('something')]).toBeUndefined() - }) - - it('throw error if call on invalid lazy', async () => { - const caller = createRouterCaller({ - router: lazy(() => Promise.resolve({ default: undefined })), - }) - - // @ts-expect-error --- invalid lazy - caller.router.ping.pong({ val: '123' }) - - const procedure = vi.mocked(createProcedureCaller).mock.calls[0]![0].procedure - - expect(procedure).toSatisfy(isLazy) - - expect(unlazy(procedure)).rejects.toThrow('Expected a valid procedure or lazy but got unknown.') + expect((client as any)[Symbol('something')]).toBeUndefined() }) it('return undefined if access the undefined key', async () => { - const caller = createRouterCaller({ + const client = createRouterClient({ router: { ping, }, }) // @ts-expect-error --- invalid access - expect(caller.router).toBeUndefined() + expect(client.router).toBeUndefined() }) it('works without base path', async () => { - const caller = createRouterCaller({ + const client = createRouterClient({ router: { ping, }, }) - expect(caller.ping({ val: '123' })).toEqual('__mocked__') - expect(vi.mocked(createProcedureCaller).mock.calls[0]![0].path).toEqual(['ping']) + expect(client.ping({ val: '123' })).toEqual('__mocked__') + expect(vi.mocked(createProcedureClient).mock.calls[0]![0].path).toEqual(['ping']) }) }) diff --git a/packages/server/src/router-caller.ts b/packages/server/src/router-client.ts similarity index 60% rename from packages/server/src/router-caller.ts rename to packages/server/src/router-client.ts index 461cc87a2..0653a9652 100644 --- a/packages/server/src/router-caller.ts +++ b/packages/server/src/router-client.ts @@ -2,21 +2,23 @@ import type { SchemaInput, SchemaOutput } from '@orpc/contract' import type { Hooks, Value } from '@orpc/shared' import type { Lazy } from './lazy' import type { Procedure } from './procedure' -import type { Caller, Meta } from './types' -import { isLazy, lazy, unlazy } from './lazy' +import type { ProcedureClient } from './procedure-client' +import type { Meta } from './types' +import { isLazy } from './lazy' +import { createLazyProcedureFormAnyLazy } from './lazy-utils' import { isProcedure } from './procedure' -import { createProcedureCaller } from './procedure-caller' +import { createProcedureClient } from './procedure-client' import { type ANY_ROUTER, getRouterChild, type Router } from './router' -export type RouterCaller = T extends Lazy - ? RouterCaller +export type RouterClient = T extends Lazy + ? RouterClient : T extends Procedure - ? Caller, SchemaOutput> + ? ProcedureClient, SchemaOutput> : { - [K in keyof T]: T[K] extends ANY_ROUTER ? RouterCaller : never + [K in keyof T]: T[K] extends ANY_ROUTER ? RouterClient : never } -export type CreateRouterCallerOptions< +export type CreateRouterClientOptions< TRouter extends ANY_ROUTER, > = & { @@ -34,13 +36,13 @@ export type CreateRouterCallerOptions< : never) & Hooks ? UContext : never, Meta> -export function createRouterCaller< +export function createRouterClient< TRouter extends ANY_ROUTER, >( - options: CreateRouterCallerOptions, -): RouterCaller { + options: CreateRouterClientOptions, +): RouterClient { if (isProcedure(options.router)) { - const caller = createProcedureCaller({ + const caller = createProcedureClient({ ...options, procedure: options.router, context: options.context, @@ -51,21 +53,9 @@ export function createRouterCaller< } const procedureCaller = isLazy(options.router) - ? createProcedureCaller({ + ? createProcedureClient({ ...options, - procedure: lazy(async () => { - const { default: maybeProcedure } = await unlazy(options.router) - - if (!isProcedure(maybeProcedure)) { - throw new Error(` - Expected a valid procedure or lazy but got unknown. - This should be caught by TypeScript compilation. - Please report this issue if this makes you feel uncomfortable. - `) - } - - return { default: maybeProcedure } - }), + procedure: createLazyProcedureFormAnyLazy(options.router), context: options.context, path: options.path, }) @@ -83,7 +73,7 @@ export function createRouterCaller< return Reflect.get(target, key) } - return createRouterCaller({ + return createRouterClient({ ...options, router: next, path: [...(options.path ?? []), key], diff --git a/packages/server/src/types.test-d.ts b/packages/server/src/types.test-d.ts index a7f5aab0c..d14c1d0ec 100644 --- a/packages/server/src/types.test-d.ts +++ b/packages/server/src/types.test-d.ts @@ -1,4 +1,4 @@ -import type { Caller, CallerOptions, MergeContext } from './types' +import type { MergeContext } from './types' it('mergeContext', () => { expectTypeOf>().toEqualTypeOf() @@ -16,55 +16,3 @@ it('mergeContext', () => { bar: string }>() }) - -describe('Caller', () => { - const fn: Caller = async (input, options) => { - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(options).toEqualTypeOf() - return 123 - } - - const fnWithOptionalInput: Caller = async (...args) => { - const [input, options] = args - - expectTypeOf(input).toEqualTypeOf() - expectTypeOf(options).toEqualTypeOf() - return 123 - } - - it('just a function', () => { - expectTypeOf(fn).toEqualTypeOf<(input: string, options?: CallerOptions) => Promise>() - expectTypeOf(fnWithOptionalInput).toMatchTypeOf<(input: string | undefined, options?: CallerOptions) => Promise>() - }) - - it('infer correct input', () => { - fn('123') - fnWithOptionalInput('123') - - // @ts-expect-error - invalid input - fn(123) - // @ts-expect-error - invalid input - fnWithOptionalInput(123) - - // @ts-expect-error - invalid input - fn({}) - // @ts-expect-error - invalid input - fnWithOptionalInput({}) - }) - - it('accept signal', () => { - fn('123', { signal: new AbortSignal() }) - fnWithOptionalInput('123', { signal: new AbortSignal() }) - - // @ts-expect-error - invalid signal - fn('123', { signal: 1234 }) - // @ts-expect-error - invalid signal - fnWithOptionalInput('123', { signal: 1234 }) - }) - - it('can accept call without args', () => { - expectTypeOf(fnWithOptionalInput()).toEqualTypeOf>() - // @ts-expect-error - input is required - expectTypeOf(fn()).toEqualTypeOf>() - }) -}) diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index ab2936e0d..471b9621f 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -1,5 +1,3 @@ -/// - import type { ANY_PROCEDURE } from './procedure' export type Context = Record | undefined @@ -10,15 +8,11 @@ export type MergeContext< TB extends Context, > = TA extends undefined ? TB : TB extends undefined ? TA : TA & TB -export interface CallerOptions { +export interface WithSignal { signal?: AbortSignal } -export interface Caller { - (...opts: [input: TInput, options?: CallerOptions] | (undefined extends TInput ? [] : never)): Promise -} - -export interface Meta extends CallerOptions { +export interface Meta extends WithSignal { path: string[] procedure: ANY_PROCEDURE } diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json index 99061213a..e6322353d 100644 --- a/packages/server/tsconfig.json +++ b/packages/server/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.lib.json", "compilerOptions": { + "lib": ["ES2022", "DOM"], "types": [] }, "references": [ From 880223723b0f27a4ceccc18a703d67d1d5d7841e Mon Sep 17 00:00:00 2001 From: unnoq Date: Fri, 20 Dec 2024 10:15:01 +0700 Subject: [PATCH 40/51] hidden tests --- packages/server/src/hidden.test.ts | 97 ++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 packages/server/src/hidden.test.ts diff --git a/packages/server/src/hidden.test.ts b/packages/server/src/hidden.test.ts new file mode 100644 index 000000000..729204e83 --- /dev/null +++ b/packages/server/src/hidden.test.ts @@ -0,0 +1,97 @@ +import { oc } from '@orpc/contract' +import { deepSetLazyRouterPrefix, getLazyRouterPrefix, getRouterContract, setRouterContract } from './hidden' + +describe('setRouterContract', () => { + const ping = oc.route({}) + const baseContract = { ping } + const nestedContract = { ping, nested: { ping } } + + it('sets contract on empty object', () => { + const obj = {} + const router = setRouterContract(obj, baseContract) + expect(getRouterContract(router)).toBe(baseContract) + }) + + it('preserves original object properties', () => { + const obj = { existingProp: 'value' } + const router = setRouterContract(obj, baseContract) + expect(router.existingProp).toBe('value') + expect(getRouterContract(router)).toBe(baseContract) + }) + + it('handles nested contracts', () => { + const obj = { nested: { value: 42 } } + const router = setRouterContract(obj, nestedContract) + expect(getRouterContract(router)).toBe(nestedContract) + expect(router.nested.value).toBe(42) + expect(getRouterContract(router.nested)).toBeUndefined() + }) + + it('allows contract overwriting', () => { + const obj = {} + const router1 = setRouterContract(obj, baseContract) + const router2 = setRouterContract(router1, nestedContract) + expect(getRouterContract(router2)).toBe(nestedContract) + }) +}) + +describe('deepSetLazyRouterPrefix', () => { + it('sets prefix on root object', () => { + const obj = { value: 1 } + const prefixed = deepSetLazyRouterPrefix(obj, '/api') + expect(getLazyRouterPrefix(prefixed)).toBe('/api') + expect(prefixed.value).toBe(1) + }) + + it('sets prefix on all nested objects', () => { + const obj = { + l1: { + l2: { + l3: { value: 42 }, + }, + }, + } + const prefixed = deepSetLazyRouterPrefix(obj, '/api') + expect(getLazyRouterPrefix(prefixed)).toBe('/api') + expect(getLazyRouterPrefix(prefixed.l1)).toBe('/api') + expect(getLazyRouterPrefix(prefixed.l1.l2)).toBe('/api') + expect(getLazyRouterPrefix(prefixed.l1.l2.l3)).toBe('/api') + expect(prefixed.l1.l2.l3.value).toBe(42) + }) + + it('handles functions in objects', () => { + const obj = { + fn: () => 42, + nested: { fn: () => 43 }, + } + const prefixed = deepSetLazyRouterPrefix(obj, '/api') + expect(getLazyRouterPrefix(prefixed.fn)).toBe('/api') + expect(getLazyRouterPrefix(prefixed.nested.fn)).toBe('/api') + expect(prefixed.fn()).toBe(42) + expect(prefixed.nested.fn()).toBe(43) + }) + + it('allows prefix override', () => { + const obj = { value: 1 } + const prefixed1 = deepSetLazyRouterPrefix(obj, '/api') + const prefixed2 = deepSetLazyRouterPrefix(prefixed1, '/v2') + + expect(getLazyRouterPrefix(prefixed1)).toBe('/api') + expect(getLazyRouterPrefix(prefixed2)).toBe('/v2') + expect(prefixed2.value).toBe(1) + }) + + it('handles nested prefix override', () => { + const obj = { + l1: { value: 1 }, + l2: { value: 2 }, + } + const prefixed1 = deepSetLazyRouterPrefix(obj, '/api') + const prefixed2 = deepSetLazyRouterPrefix(prefixed1.l1, '/v2') + + expect(getLazyRouterPrefix(prefixed1)).toBe('/api') + expect(getLazyRouterPrefix(prefixed1.l2)).toBe('/api') + expect(getLazyRouterPrefix(prefixed2)).toBe('/v2') + expect(prefixed2.value).toBe(1) + }) +}) From 345d8d02d761f4aaa9d274e814bd6a88b7a177c2 Mon Sep 17 00:00:00 2001 From: unnoq Date: Fri, 20 Dec 2024 10:37:29 +0700 Subject: [PATCH 41/51] rename client --- packages/client/src/index.ts | 8 +-- .../src/procedure-fetch-client.test-d.ts | 13 +++++ ...test.ts => procedure-fetch-client.test.ts} | 10 ++-- ...procedure.ts => procedure-fetch-client.ts} | 17 +++---- packages/client/src/procedure.test-d.ts | 13 ----- ...est-d.ts => router-fetch-client.test-d.ts} | 18 +++---- ...er.test.ts => router-fetch-client.test.ts} | 28 +++++------ packages/client/src/router-fetch-client.ts | 33 ++++++++++++ packages/client/src/router.ts | 50 ------------------- packages/client/src/types.ts | 12 +++++ packages/client/tests/helpers.ts | 4 +- packages/client/tsconfig.json | 1 + packages/react-query/tests/helpers.tsx | 4 +- packages/react/tests/orpc.tsx | 4 +- packages/vue-query/tests/helpers.ts | 4 +- 15 files changed, 105 insertions(+), 114 deletions(-) create mode 100644 packages/client/src/procedure-fetch-client.test-d.ts rename packages/client/src/{procedure.test.ts => procedure-fetch-client.test.ts} (89%) rename packages/client/src/{procedure.ts => procedure-fetch-client.ts} (84%) delete mode 100644 packages/client/src/procedure.test-d.ts rename packages/client/src/{router.test-d.ts => router-fetch-client.test-d.ts} (59%) rename packages/client/src/{router.test.ts => router-fetch-client.test.ts} (56%) create mode 100644 packages/client/src/router-fetch-client.ts delete mode 100644 packages/client/src/router.ts create mode 100644 packages/client/src/types.ts diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 01fcd4e44..90be18871 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -1,9 +1,9 @@ /** unnoq */ -import { createRouterClient } from './router' +import { createRouterFetchClient } from './router-fetch-client' -export * from './procedure' -export * from './router' +export * from './procedure-fetch-client' +export * from './router-fetch-client' export * from '@orpc/shared/error' -export const createORPCClient = createRouterClient +export const createORPCFetchClient = createRouterFetchClient diff --git a/packages/client/src/procedure-fetch-client.test-d.ts b/packages/client/src/procedure-fetch-client.test-d.ts new file mode 100644 index 000000000..f8959887d --- /dev/null +++ b/packages/client/src/procedure-fetch-client.test-d.ts @@ -0,0 +1,13 @@ +import type { ProcedureClient } from '@orpc/server' +import { createProcedureFetchClient } from './procedure-fetch-client' + +describe('procedure fetch client', () => { + it('just a caller', () => { + const client = createProcedureFetchClient({ + baseURL: 'http://localhost:3000/orpc', + path: ['ping'], + }) + + expectTypeOf(client).toEqualTypeOf>() + }) +}) diff --git a/packages/client/src/procedure.test.ts b/packages/client/src/procedure-fetch-client.test.ts similarity index 89% rename from packages/client/src/procedure.test.ts rename to packages/client/src/procedure-fetch-client.test.ts index 3b4647705..46e32fe1e 100644 --- a/packages/client/src/procedure.test.ts +++ b/packages/client/src/procedure-fetch-client.test.ts @@ -1,5 +1,5 @@ import { ORPCDeserializer, ORPCSerializer } from '@orpc/transformer' -import { createProcedureClient } from './procedure' +import { createProcedureFetchClient } from './procedure-fetch-client' vi.mock('@orpc/transformer', () => ({ ORPCSerializer: vi.fn().mockReturnValue({ serialize: vi.fn() }), @@ -10,7 +10,7 @@ beforeEach(() => { vi.clearAllMocks() }) -describe('procedure client', () => { +describe('procedure fetch client', () => { const serialize = (ORPCSerializer as any)().serialize const deserialize = (ORPCDeserializer as any)().deserialize const response = new Response('output') @@ -22,7 +22,7 @@ describe('procedure client', () => { deserialize.mockReturnValue('transformed_output') it('works', async () => { - const client = createProcedureClient({ + const client = createProcedureFetchClient({ baseURL: 'http://localhost:3000/orpc', path: ['ping'], fetch: fakeFetch, @@ -50,7 +50,7 @@ describe('procedure client', () => { async () => new Headers({ 'x-test': 'hello' }), async () => ({ 'x-test': 'hello' }), ])('works with headers', async (headers) => { - const client = createProcedureClient({ + const client = createProcedureFetchClient({ path: ['ping'], baseURL: 'http://localhost:3000/orpc', fetch: fakeFetch, @@ -72,7 +72,7 @@ describe('procedure client', () => { const controller = new AbortController() const signal = controller.signal - const client = createProcedureClient({ + const client = createProcedureFetchClient({ path: ['ping'], baseURL: 'http://localhost:3000/orpc', fetch: fakeFetch, diff --git a/packages/client/src/procedure.ts b/packages/client/src/procedure-fetch-client.ts similarity index 84% rename from packages/client/src/procedure.ts rename to packages/client/src/procedure-fetch-client.ts index 010aebb60..a8f6e01ad 100644 --- a/packages/client/src/procedure.ts +++ b/packages/client/src/procedure-fetch-client.ts @@ -1,7 +1,4 @@ -/// -/// - -import type { Caller } from '@orpc/server' +import type { ProcedureClient } from '@orpc/server' import type { Promisable } from '@orpc/shared' import { ORPC_PROTOCOL_HEADER, ORPC_PROTOCOL_VALUE, trim } from '@orpc/shared' import { ORPCError } from '@orpc/shared/error' @@ -34,13 +31,11 @@ export interface CreateProcedureClientOptions { const serializer = new ORPCSerializer() const deserializer = new ORPCDeserializer() -export function createProcedureClient( +export function createProcedureFetchClient( options: CreateProcedureClientOptions, -): Caller { - const client: Caller = async (...args) => { - const [input, callerOptions] = args - - const fetch_ = options.fetch ?? fetch +): ProcedureClient { + const client: ProcedureClient = async (...[input, callerOptions]) => { + const fetchClient = options.fetch ?? fetch const url = `${trim(options.baseURL, '/')}/${options.path.map(encodeURIComponent).join('/')}` const headers = new Headers({ @@ -59,7 +54,7 @@ export function createProcedureClient( headers.append(key, value) } - const response = await fetch_(url, { + const response = await fetchClient(url, { method: 'POST', headers, body: serialized.body, diff --git a/packages/client/src/procedure.test-d.ts b/packages/client/src/procedure.test-d.ts deleted file mode 100644 index f520fb340..000000000 --- a/packages/client/src/procedure.test-d.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { Caller } from '@orpc/server' -import { createProcedureClient } from './procedure' - -describe('procedure client', () => { - it('just a caller', () => { - const client = createProcedureClient({ - baseURL: 'http://localhost:3000/orpc', - path: ['ping'], - }) - - expectTypeOf(client).toEqualTypeOf>() - }) -}) diff --git a/packages/client/src/router.test-d.ts b/packages/client/src/router-fetch-client.test-d.ts similarity index 59% rename from packages/client/src/router.test-d.ts rename to packages/client/src/router-fetch-client.test-d.ts index 37cac7c20..1ffa7c682 100644 --- a/packages/client/src/router.test-d.ts +++ b/packages/client/src/router-fetch-client.test-d.ts @@ -1,10 +1,10 @@ -import type { Caller } from '@orpc/server' +import type { ProcedureClient } from '@orpc/server' import { oc } from '@orpc/contract' import { os } from '@orpc/server' import { z } from 'zod' -import { createRouterClient } from './router' +import { createRouterFetchClient } from './router-fetch-client' -describe('router client', () => { +describe('router fetch client', () => { const pingContract = oc .input(z.object({ in: z.string() }).transform(i => i.in)) .output(z.string().transform(out => ({ out }))) @@ -28,20 +28,20 @@ describe('router client', () => { }) it('build correct types with contract router', () => { - const client = createRouterClient({ + const client = createRouterFetchClient({ baseURL: 'http://localhost:3000/orpc', }) - expectTypeOf(client.ping).toMatchTypeOf>() - expectTypeOf(client.nested.pong).toMatchTypeOf>() + expectTypeOf(client.ping).toMatchTypeOf>() + expectTypeOf(client.nested.pong).toMatchTypeOf>() }) it('build correct types with router', () => { - const client = createRouterClient({ + const client = createRouterFetchClient({ baseURL: 'http://localhost:3000/orpc', }) - expectTypeOf(client.ping).toMatchTypeOf>() - expectTypeOf(client.nested.pong).toMatchTypeOf>() + expectTypeOf(client.ping).toMatchTypeOf>() + expectTypeOf(client.nested.pong).toMatchTypeOf>() }) }) diff --git a/packages/client/src/router.test.ts b/packages/client/src/router-fetch-client.test.ts similarity index 56% rename from packages/client/src/router.test.ts rename to packages/client/src/router-fetch-client.test.ts index d81d431a0..9b87e41d6 100644 --- a/packages/client/src/router.test.ts +++ b/packages/client/src/router-fetch-client.test.ts @@ -1,20 +1,20 @@ -import { createProcedureClient } from './procedure' -import { createRouterClient } from './router' +import { createProcedureFetchClient } from './procedure-fetch-client' +import { createRouterFetchClient } from './router-fetch-client' -vi.mock('./procedure', () => ({ - createProcedureClient: vi.fn(), +vi.mock('./procedure-fetch-client', () => ({ + createProcedureFetchClient: vi.fn(), })) beforeEach(() => { vi.clearAllMocks() }) -describe('router client', () => { +describe('router fetch client', () => { const procedureClient = vi.fn().mockReturnValue('__mocked__') - vi.mocked(createProcedureClient).mockReturnValue(procedureClient) + vi.mocked(createProcedureFetchClient).mockReturnValue(procedureClient) it('works', async () => { - const client = createRouterClient({ + const client = createRouterFetchClient({ baseURL: 'http://localhost:3000/orpc', }) as any @@ -22,8 +22,8 @@ describe('router client', () => { const o1 = await client.ping({ value: 'hello' }) expect(o1).toEqual('__mocked__') - expect(createProcedureClient).toBeCalledTimes(1) - expect(createProcedureClient).toBeCalledWith({ + expect(createProcedureFetchClient).toBeCalledTimes(1) + expect(createProcedureFetchClient).toBeCalledWith({ baseURL: 'http://localhost:3000/orpc', path: ['ping'], }) @@ -32,8 +32,8 @@ describe('router client', () => { const o2 = await client.nested.pong({ value: 'hello' }) expect(o2).toEqual('__mocked__') - expect(createProcedureClient).toBeCalledTimes(2) - expect(createProcedureClient).toBeCalledWith({ + expect(createProcedureFetchClient).toBeCalledTimes(2) + expect(createProcedureFetchClient).toBeCalledWith({ baseURL: 'http://localhost:3000/orpc', path: ['nested', 'pong'], }) @@ -42,7 +42,7 @@ describe('router client', () => { it('works with options', async () => { const headers = vi.fn() const fetch = vi.fn() - const client = createRouterClient({ + const client = createRouterFetchClient({ baseURL: 'http://localhost:3000/orpc', path: ['base'], headers, @@ -52,8 +52,8 @@ describe('router client', () => { vi.clearAllMocks() await client.ping({ value: 'hello' }) - expect(createProcedureClient).toBeCalledTimes(1) - expect(createProcedureClient).toBeCalledWith({ + expect(createProcedureFetchClient).toBeCalledTimes(1) + expect(createProcedureFetchClient).toBeCalledWith({ baseURL: 'http://localhost:3000/orpc', path: ['base', 'ping'], headers, diff --git a/packages/client/src/router-fetch-client.ts b/packages/client/src/router-fetch-client.ts new file mode 100644 index 000000000..64d59335c --- /dev/null +++ b/packages/client/src/router-fetch-client.ts @@ -0,0 +1,33 @@ +import type { ContractRouter } from '@orpc/contract' +import type { ANY_ROUTER } from '@orpc/server' +import type { SetOptional } from '@orpc/shared' +import type { CreateProcedureClientOptions } from './procedure-fetch-client' +import type { RemoteRouterClient } from './types' +import { createProcedureFetchClient } from './procedure-fetch-client' + +export function createRouterFetchClient( + options: SetOptional, +): RemoteRouterClient { + const path = options?.path ?? [] + + const client = new Proxy( + createProcedureFetchClient({ + ...options, + path, + }), + { + get(target, key) { + if (typeof key !== 'string') { + return Reflect.get(target, key) + } + + return createRouterFetchClient({ + ...options, + path: [...path, key], + }) + }, + }, + ) + + return client as any +} diff --git a/packages/client/src/router.ts b/packages/client/src/router.ts deleted file mode 100644 index 859e34883..000000000 --- a/packages/client/src/router.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { - ContractProcedure, - ContractRouter, - SchemaInput, - SchemaOutput, -} from '@orpc/contract' -import type { Caller, Lazy, Procedure, Router } from '@orpc/server' -import type { SetOptional } from '@orpc/shared' -import type { CreateProcedureClientOptions } from './procedure' -import { createProcedureClient } from './procedure' - -export type RouterClient | ContractRouter> = { - [K in keyof T]: T[K] extends - | ContractProcedure - | Procedure - | Lazy> - ? Caller, SchemaOutput> - : T[K] extends Router | ContractRouter - ? RouterClient - : never -} - -export function createRouterClient | ContractRouter>( - options: SetOptional, -): RouterClient { - const path = options?.path ?? [] - - const client = new Proxy( - createProcedureClient({ - baseURL: options.baseURL, - fetch: options.fetch, - headers: options.headers, - path, - }), - { - get(target, key) { - if (typeof key !== 'string') { - return Reflect.get(target, key) - } - - return createRouterClient({ - ...options, - path: [...path, key], - }) - }, - }, - ) - - return client as any -} diff --git a/packages/client/src/types.ts b/packages/client/src/types.ts new file mode 100644 index 000000000..3121caa2a --- /dev/null +++ b/packages/client/src/types.ts @@ -0,0 +1,12 @@ +import type { ContractProcedure, ContractRouter, SchemaInput, SchemaOutput } from '@orpc/contract' +import type { ANY_ROUTER, Lazyable, Procedure, ProcedureClient } from '@orpc/server' + +export type RemoteRouterClient = T extends + | ContractProcedure + | Lazyable> + ? ProcedureClient, SchemaOutput> + : { + [K in keyof T]: T[K] extends ANY_ROUTER | ContractRouter + ? RemoteRouterClient + : never + } diff --git a/packages/client/tests/helpers.ts b/packages/client/tests/helpers.ts index df8e8bfb6..0759ca4ed 100644 --- a/packages/client/tests/helpers.ts +++ b/packages/client/tests/helpers.ts @@ -1,7 +1,7 @@ import { os } from '@orpc/server' import { createORPCHandler, handleFetchRequest } from '@orpc/server/fetch' import { z } from 'zod' -import { createORPCClient } from '../src' +import { createORPCFetchClient } from '../src' export const orpcServer = os @@ -96,7 +96,7 @@ export const appRouter = orpcServer.router({ }, }) -export const client = createORPCClient({ +export const client = createORPCFetchClient({ baseURL: 'http://localhost:3000', async fetch(...args) { diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json index 8abbc732f..9dd9f5f90 100644 --- a/packages/client/tsconfig.json +++ b/packages/client/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.lib.json", "compilerOptions": { + "lib": ["ES2020", "DOM"], "types": [] }, "references": [ diff --git a/packages/react-query/tests/helpers.tsx b/packages/react-query/tests/helpers.tsx index e310b8c06..fa2329659 100644 --- a/packages/react-query/tests/helpers.tsx +++ b/packages/react-query/tests/helpers.tsx @@ -1,4 +1,4 @@ -import { createORPCClient } from '@orpc/client' +import { createORPCFetchClient } from '@orpc/client' import { os } from '@orpc/server' import { createORPCHandler, handleFetchRequest } from '@orpc/server/fetch' import { QueryClient } from '@tanstack/react-query' @@ -91,7 +91,7 @@ export const appRouter = orpcServer.router({ }, }) -export const orpcClient = createORPCClient({ +export const orpcClient = createORPCFetchClient({ baseURL: 'http://localhost:3000', async fetch(...args) { diff --git a/packages/react/tests/orpc.tsx b/packages/react/tests/orpc.tsx index 3dc6cdb6a..3547426e9 100644 --- a/packages/react/tests/orpc.tsx +++ b/packages/react/tests/orpc.tsx @@ -1,4 +1,4 @@ -import { createORPCClient } from '@orpc/client' +import { createORPCFetchClient } from '@orpc/client' import { os } from '@orpc/server' import { createORPCHandler, handleFetchRequest } from '@orpc/server/fetch' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' @@ -92,7 +92,7 @@ export const appRouter = orpcServer.router({ }, }) -export const orpcClient = createORPCClient({ +export const orpcClient = createORPCFetchClient({ baseURL: 'http://localhost:3000', async fetch(...args) { diff --git a/packages/vue-query/tests/helpers.ts b/packages/vue-query/tests/helpers.ts index 4aab98505..8ad44cc1a 100644 --- a/packages/vue-query/tests/helpers.ts +++ b/packages/vue-query/tests/helpers.ts @@ -1,4 +1,4 @@ -import { createORPCClient } from '@orpc/client' +import { createORPCFetchClient } from '@orpc/client' import { os } from '@orpc/server' import { createORPCHandler, handleFetchRequest } from '@orpc/server/fetch' import { QueryClient } from '@tanstack/vue-query' @@ -85,7 +85,7 @@ export const appRouter = orpcServer.router({ }, }) -export const orpcClient = createORPCClient({ +export const orpcClient = createORPCFetchClient({ baseURL: 'http://localhost:3000', async fetch(...args) { From 393e2a82e308d28b7bd619ce2db697726d376553 Mon Sep 17 00:00:00 2001 From: unnoq Date: Fri, 20 Dec 2024 13:58:29 +0700 Subject: [PATCH 42/51] merge types on client --- packages/client/src/procedure-fetch-client.ts | 6 +++--- packages/client/src/router-fetch-client.test.ts | 8 ++++++++ packages/client/src/router-fetch-client.ts | 5 ++--- packages/client/src/types.ts | 12 ------------ packages/server/src/router-client.ts | 11 +++++++---- 5 files changed, 20 insertions(+), 22 deletions(-) delete mode 100644 packages/client/src/types.ts diff --git a/packages/client/src/procedure-fetch-client.ts b/packages/client/src/procedure-fetch-client.ts index a8f6e01ad..073b386b7 100644 --- a/packages/client/src/procedure-fetch-client.ts +++ b/packages/client/src/procedure-fetch-client.ts @@ -34,7 +34,7 @@ const deserializer = new ORPCDeserializer() export function createProcedureFetchClient( options: CreateProcedureClientOptions, ): ProcedureClient { - const client: ProcedureClient = async (...[input, callerOptions]) => { + const client: ProcedureClient = async (...[input, callerOptions]) => { const fetchClient = options.fetch ?? fetch const url = `${trim(options.baseURL, '/')}/${options.path.map(encodeURIComponent).join('/')}` @@ -85,8 +85,8 @@ export function createProcedureFetchClient( ) } - return json + return json as any } - return client as any + return client } diff --git a/packages/client/src/router-fetch-client.test.ts b/packages/client/src/router-fetch-client.test.ts index 9b87e41d6..780fe416d 100644 --- a/packages/client/src/router-fetch-client.test.ts +++ b/packages/client/src/router-fetch-client.test.ts @@ -60,4 +60,12 @@ describe('router fetch client', () => { fetch, }) }) + + it('not recursive on symbol', async () => { + const client = createRouterFetchClient({ + baseURL: 'http://localhost:3000/orpc', + }) as any + + expect(client[Symbol('test')]).toBeUndefined() + }) }) diff --git a/packages/client/src/router-fetch-client.ts b/packages/client/src/router-fetch-client.ts index 64d59335c..9cb92be8e 100644 --- a/packages/client/src/router-fetch-client.ts +++ b/packages/client/src/router-fetch-client.ts @@ -1,13 +1,12 @@ import type { ContractRouter } from '@orpc/contract' -import type { ANY_ROUTER } from '@orpc/server' +import type { ANY_ROUTER, RouterClient } from '@orpc/server' import type { SetOptional } from '@orpc/shared' import type { CreateProcedureClientOptions } from './procedure-fetch-client' -import type { RemoteRouterClient } from './types' import { createProcedureFetchClient } from './procedure-fetch-client' export function createRouterFetchClient( options: SetOptional, -): RemoteRouterClient { +): RouterClient { const path = options?.path ?? [] const client = new Proxy( diff --git a/packages/client/src/types.ts b/packages/client/src/types.ts deleted file mode 100644 index 3121caa2a..000000000 --- a/packages/client/src/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { ContractProcedure, ContractRouter, SchemaInput, SchemaOutput } from '@orpc/contract' -import type { ANY_ROUTER, Lazyable, Procedure, ProcedureClient } from '@orpc/server' - -export type RemoteRouterClient = T extends - | ContractProcedure - | Lazyable> - ? ProcedureClient, SchemaOutput> - : { - [K in keyof T]: T[K] extends ANY_ROUTER | ContractRouter - ? RemoteRouterClient - : never - } diff --git a/packages/server/src/router-client.ts b/packages/server/src/router-client.ts index 0653a9652..0df11cb78 100644 --- a/packages/server/src/router-client.ts +++ b/packages/server/src/router-client.ts @@ -1,4 +1,4 @@ -import type { SchemaInput, SchemaOutput } from '@orpc/contract' +import type { ContractProcedure, ContractRouter, SchemaInput, SchemaOutput } from '@orpc/contract' import type { Hooks, Value } from '@orpc/shared' import type { Lazy } from './lazy' import type { Procedure } from './procedure' @@ -10,12 +10,15 @@ import { isProcedure } from './procedure' import { createProcedureClient } from './procedure-client' import { type ANY_ROUTER, getRouterChild, type Router } from './router' -export type RouterClient = T extends Lazy +export type RouterClient = +T extends Lazy ? RouterClient - : T extends Procedure + : T extends + | ContractProcedure + | Procedure ? ProcedureClient, SchemaOutput> : { - [K in keyof T]: T[K] extends ANY_ROUTER ? RouterClient : never + [K in keyof T]: T[K] extends ANY_ROUTER | ContractRouter ? RouterClient : never } export type CreateRouterClientOptions< From cad921c0efb2169890dfbdee2f0e84121100ed67 Mon Sep 17 00:00:00 2001 From: unnoq Date: Fri, 20 Dec 2024 14:05:53 +0700 Subject: [PATCH 43/51] react-query sync --- .../react-query/src/utils-procedure.test-d.ts | 6 ++--- .../react-query/src/utils-procedure.test.ts | 6 ++--- packages/react-query/src/utils-procedure.ts | 4 +-- .../react-query/src/utils-router.test-d.ts | 5 ++-- packages/react-query/src/utils-router.ts | 26 +++++++------------ 5 files changed, 20 insertions(+), 27 deletions(-) diff --git a/packages/react-query/src/utils-procedure.test-d.ts b/packages/react-query/src/utils-procedure.test-d.ts index c42d3ce0a..dea2bbf05 100644 --- a/packages/react-query/src/utils-procedure.test-d.ts +++ b/packages/react-query/src/utils-procedure.test-d.ts @@ -1,10 +1,10 @@ -import type { Caller } from '@orpc/server' +import type { ProcedureClient } from '@orpc/server' import type { InfiniteData, QueryKey } from '@tanstack/react-query' import { useInfiniteQuery, useQuery } from '@tanstack/react-query' import { createProcedureUtils } from './utils-procedure' describe('queryOptions', () => { - const client = vi.fn>( + const client = vi.fn>( (...[input]) => Promise.resolve(input?.toString()), ) const utils = createProcedureUtils(client, []) @@ -151,7 +151,7 @@ describe('infiniteOptions', () => { }) it('input can be optional', () => { - const utils = createProcedureUtils({} as Caller<{ limit?: number, cursor?: number } | undefined, string>, []) + const utils = createProcedureUtils({} as ProcedureClient<{ limit?: number, cursor?: number } | undefined, string>, []) utils.infiniteOptions({ getNextPageParam, diff --git a/packages/react-query/src/utils-procedure.test.ts b/packages/react-query/src/utils-procedure.test.ts index bafdcbb51..23ab5fd94 100644 --- a/packages/react-query/src/utils-procedure.test.ts +++ b/packages/react-query/src/utils-procedure.test.ts @@ -1,4 +1,4 @@ -import type { Caller } from '@orpc/server' +import type { ProcedureClient } from '@orpc/server' import * as keyModule from './key' import { createProcedureUtils } from './utils-procedure' @@ -12,7 +12,7 @@ beforeEach(() => { }) describe('queryOptions', () => { - const client = vi.fn>((...[input]) => Promise.resolve(input?.toString())) + const client = vi.fn>((...[input]) => Promise.resolve(input?.toString())) const utils = createProcedureUtils(client, ['ping']) it('works', async () => { @@ -75,7 +75,7 @@ describe('infiniteOptions', () => { describe('mutationOptions', () => { it('works', async () => { - const client = vi.fn>( + const client = vi.fn>( (...[input]) => Promise.resolve(input?.toString()), ) const utils = createProcedureUtils(client, ['ping']) diff --git a/packages/react-query/src/utils-procedure.ts b/packages/react-query/src/utils-procedure.ts index 590207e16..6bf69a222 100644 --- a/packages/react-query/src/utils-procedure.ts +++ b/packages/react-query/src/utils-procedure.ts @@ -1,4 +1,4 @@ -import type { Caller } from '@orpc/server' +import type { ProcedureClient } from '@orpc/server' import type { IsEqual } from '@orpc/shared' import type { QueryKey } from '@tanstack/react-query' import type { InfiniteOptions, MutationOptions, QueryOptions } from './types' @@ -26,7 +26,7 @@ export interface ProcedureUtils { } export function createProcedureUtils( - client: Caller, + client: ProcedureClient, path: string[], ): ProcedureUtils { return { diff --git a/packages/react-query/src/utils-router.test-d.ts b/packages/react-query/src/utils-router.test-d.ts index 41d89c735..488ec9b75 100644 --- a/packages/react-query/src/utils-router.test-d.ts +++ b/packages/react-query/src/utils-router.test-d.ts @@ -1,3 +1,4 @@ +import type { RouterClient } from '@orpc/server' import { oc } from '@orpc/contract' import { os } from '@orpc/server' import { z } from 'zod' @@ -22,7 +23,7 @@ const router = os.contract(contractRouter).router({ describe('with contract router', () => { it('build correct types', () => { - const utils = createRouterUtils({} as any) + const utils = createRouterUtils({} as RouterClient) const generalUtils = createGeneralUtils([]) const pingUtils = createProcedureUtils(ping, []) @@ -40,7 +41,7 @@ describe('with contract router', () => { describe('with router', () => { it('build correct types', () => { - const utils = createRouterUtils({} as any) + const utils = createRouterUtils({} as RouterClient) const generalUtils = createGeneralUtils([]) const pingUtils = createProcedureUtils(ping, []) diff --git a/packages/react-query/src/utils-router.ts b/packages/react-query/src/utils-router.ts index 1a34a8151..b352410bc 100644 --- a/packages/react-query/src/utils-router.ts +++ b/packages/react-query/src/utils-router.ts @@ -1,28 +1,20 @@ -import type { RouterClient } from '@orpc/client' -import type { ContractProcedure, ContractRouter, SchemaInput, SchemaOutput } from '@orpc/contract' -import type { Lazy, Procedure, Router } from '@orpc/server' +import type { ProcedureClient, RouterClient } from '@orpc/server' import { createGeneralUtils, type GeneralUtils } from './utils-general' import { createProcedureUtils, type ProcedureUtils } from './utils-procedure' -export type RouterUtils | ContractRouter> = { - [K in keyof T]: T[K] extends - | ContractProcedure - | Procedure - | Lazy> - ? - & ProcedureUtils, SchemaOutput> - & GeneralUtils> - : T[K] extends Router | ContractRouter - ? RouterUtils - : never -} & GeneralUtils +export type RouterUtils> = +T extends ProcedureClient + ? ProcedureUtils & GeneralUtils + : { + [K in keyof T]: T[K] extends RouterClient ? RouterUtils : never + } & GeneralUtils /** * @param client - The client create form `@orpc/client` * @param path - The base path for query key */ -export function createRouterUtils | ContractRouter>( - client: RouterClient, +export function createRouterUtils>( + client: T, path: string[] = [], ): RouterUtils { const generalUtils = createGeneralUtils(path) From 4d8bb06fb22927c7d65176d7230ab5e76fc78dea Mon Sep 17 00:00:00 2001 From: unnoq Date: Fri, 20 Dec 2024 14:11:10 +0700 Subject: [PATCH 44/51] vue-query sync --- .../vue-query/src/utils-procedure.test-d.ts | 6 ++--- .../vue-query/src/utils-procedure.test.ts | 6 ++--- packages/vue-query/src/utils-procedure.ts | 4 +-- packages/vue-query/src/utils-router.test-d.ts | 5 ++-- packages/vue-query/src/utils-router.ts | 26 +++++++------------ 5 files changed, 20 insertions(+), 27 deletions(-) diff --git a/packages/vue-query/src/utils-procedure.test-d.ts b/packages/vue-query/src/utils-procedure.test-d.ts index 0a0fff057..7e84f5494 100644 --- a/packages/vue-query/src/utils-procedure.test-d.ts +++ b/packages/vue-query/src/utils-procedure.test-d.ts @@ -1,11 +1,11 @@ -import type { Caller } from '@orpc/server' +import type { ProcedureClient } from '@orpc/server' import type { InfiniteData, QueryKey } from '@tanstack/vue-query' import { useInfiniteQuery, useQuery } from '@tanstack/vue-query' import { ref } from 'vue' import { createProcedureUtils } from './utils-procedure' describe('queryOptions', () => { - const client = vi.fn >( + const client = vi.fn >( (...[input]) => Promise.resolve(input?.toString()), ) const utils = createProcedureUtils(client, []) @@ -179,7 +179,7 @@ describe('infiniteOptions', () => { }) it('input can be optional', () => { - const utils = createProcedureUtils({} as Caller<{ limit?: number, cursor?: number } | undefined, string>, []) + const utils = createProcedureUtils({} as ProcedureClient<{ limit?: number, cursor?: number } | undefined, string>, []) utils.infiniteOptions({ getNextPageParam, diff --git a/packages/vue-query/src/utils-procedure.test.ts b/packages/vue-query/src/utils-procedure.test.ts index 962a21257..f316a2294 100644 --- a/packages/vue-query/src/utils-procedure.test.ts +++ b/packages/vue-query/src/utils-procedure.test.ts @@ -1,4 +1,4 @@ -import type { Caller } from '@orpc/server' +import type { ProcedureClient } from '@orpc/server' import { ref } from 'vue' import * as keyModule from './key' import { createProcedureUtils } from './utils-procedure' @@ -13,7 +13,7 @@ beforeEach(() => { }) describe('queryOptions', () => { - const client = vi.fn>( + const client = vi.fn>( (...[input]) => Promise.resolve(input?.toString()), ) const utils = createProcedureUtils(client, ['ping']) @@ -117,7 +117,7 @@ describe('infiniteOptions', () => { }) describe('mutationOptions', () => { - const client = vi.fn>( + const client = vi.fn>( (...[input]) => Promise.resolve(input?.toString()), ) const utils = createProcedureUtils(client, ['ping']) diff --git a/packages/vue-query/src/utils-procedure.ts b/packages/vue-query/src/utils-procedure.ts index 4d897884f..381c951f2 100644 --- a/packages/vue-query/src/utils-procedure.ts +++ b/packages/vue-query/src/utils-procedure.ts @@ -1,4 +1,4 @@ -import type { Caller } from '@orpc/server' +import type { ProcedureClient } from '@orpc/server' import type { IsEqual } from '@orpc/shared' import type { QueryKey } from '@tanstack/vue-query' import type { ComputedRef } from 'vue' @@ -29,7 +29,7 @@ export interface ProcedureUtils { } export function createProcedureUtils( - client: Caller, + client: ProcedureClient, path: string[], ): ProcedureUtils { return { diff --git a/packages/vue-query/src/utils-router.test-d.ts b/packages/vue-query/src/utils-router.test-d.ts index 41d89c735..488ec9b75 100644 --- a/packages/vue-query/src/utils-router.test-d.ts +++ b/packages/vue-query/src/utils-router.test-d.ts @@ -1,3 +1,4 @@ +import type { RouterClient } from '@orpc/server' import { oc } from '@orpc/contract' import { os } from '@orpc/server' import { z } from 'zod' @@ -22,7 +23,7 @@ const router = os.contract(contractRouter).router({ describe('with contract router', () => { it('build correct types', () => { - const utils = createRouterUtils({} as any) + const utils = createRouterUtils({} as RouterClient) const generalUtils = createGeneralUtils([]) const pingUtils = createProcedureUtils(ping, []) @@ -40,7 +41,7 @@ describe('with contract router', () => { describe('with router', () => { it('build correct types', () => { - const utils = createRouterUtils({} as any) + const utils = createRouterUtils({} as RouterClient) const generalUtils = createGeneralUtils([]) const pingUtils = createProcedureUtils(ping, []) diff --git a/packages/vue-query/src/utils-router.ts b/packages/vue-query/src/utils-router.ts index 1a34a8151..1df8d268c 100644 --- a/packages/vue-query/src/utils-router.ts +++ b/packages/vue-query/src/utils-router.ts @@ -1,28 +1,20 @@ -import type { RouterClient } from '@orpc/client' -import type { ContractProcedure, ContractRouter, SchemaInput, SchemaOutput } from '@orpc/contract' -import type { Lazy, Procedure, Router } from '@orpc/server' +import type { ProcedureClient, RouterClient } from '@orpc/server' import { createGeneralUtils, type GeneralUtils } from './utils-general' import { createProcedureUtils, type ProcedureUtils } from './utils-procedure' -export type RouterUtils | ContractRouter> = { - [K in keyof T]: T[K] extends - | ContractProcedure - | Procedure - | Lazy> - ? - & ProcedureUtils, SchemaOutput> - & GeneralUtils> - : T[K] extends Router | ContractRouter - ? RouterUtils - : never -} & GeneralUtils +export type RouterUtils> = + T extends ProcedureClient + ? ProcedureUtils & GeneralUtils + : { + [K in keyof T]: T[K] extends RouterClient ? RouterUtils : never + } & GeneralUtils /** * @param client - The client create form `@orpc/client` * @param path - The base path for query key */ -export function createRouterUtils | ContractRouter>( - client: RouterClient, +export function createRouterUtils>( + client: T, path: string[] = [], ): RouterUtils { const generalUtils = createGeneralUtils(path) From 0f429bd875043112bd39fc71628938367c356814 Mon Sep 17 00:00:00 2001 From: unnoq Date: Fri, 20 Dec 2024 14:25:52 +0700 Subject: [PATCH 45/51] sync openapi --- packages/openapi/src/fetch/base-handler.ts | 58 +++++++++------------- packages/openapi/src/generator.ts | 8 +-- packages/openapi/src/utils.ts | 20 ++++---- 3 files changed, 38 insertions(+), 48 deletions(-) diff --git a/packages/openapi/src/fetch/base-handler.ts b/packages/openapi/src/fetch/base-handler.ts index 8ac99f4eb..1792d0484 100644 --- a/packages/openapi/src/fetch/base-handler.ts +++ b/packages/openapi/src/fetch/base-handler.ts @@ -1,18 +1,18 @@ /// import type { HTTPPath } from '@orpc/contract' -import type { ANY_LAZY_PROCEDURE, ANY_PROCEDURE, Router } from '@orpc/server' +import type { ANY_PROCEDURE, ANY_ROUTER } from '@orpc/server' import type { FetchHandler } from '@orpc/server/fetch' import type { Router as HonoRouter } from 'hono/router' import type { EachContractLeafResultItem, EachLeafOptions } from '../utils' -import { createProcedureClient, isLazy, isProcedure, LAZY_LOADER_SYMBOL, LAZY_ROUTER_PREFIX_SYMBOL, ORPCError } from '@orpc/server' +import { createProcedureClient, getLazyRouterPrefix, getRouterChild, isProcedure, LAZY_LOADER_SYMBOL, ORPCError, unlazy } from '@orpc/server' import { executeWithHooks, isPlainObject, mapValues, ORPC_PROTOCOL_HEADER, ORPC_PROTOCOL_VALUE, trim, value } from '@orpc/shared' import { OpenAPIDeserializer, OpenAPISerializer, zodCoerce } from '@orpc/transformer' import { eachContractProcedureLeaf, standardizeHTTPPath } from '../utils' -export type ResolveRouter = (router: Router, method: string, pathname: string) => Promise<{ +export type ResolveRouter = (router: ANY_ROUTER, method: string, pathname: string) => Promise<{ path: string[] - procedure: ANY_PROCEDURE | ANY_LAZY_PROCEDURE + procedure: ANY_PROCEDURE params: Record } | undefined> @@ -44,19 +44,12 @@ export function createOpenAPIHandler(createHonoRouter: () => Routing): FetchHand if (!match) { throw new ORPCError({ code: 'NOT_FOUND', message: 'Not found' }) } - const procedure = isLazy(match.procedure) ? (await match.procedure[LAZY_LOADER_SYMBOL]()).default : match.procedure - const path = match.path - if (!isProcedure(procedure)) { - throw new ORPCError({ - code: 'NOT_FOUND', - message: 'Not found', - }) - } + const { path, procedure } = match - const params = procedure.zz$p.contract['~orpc'].InputSchema + const params = procedure['~orpc'].contract['~orpc'].InputSchema ? zodCoerce( - procedure.zz$p.contract['~orpc'].InputSchema, + procedure['~orpc'].contract['~orpc'].InputSchema, match.params, { bracketNotation: true }, ) as Record @@ -120,8 +113,8 @@ export function createOpenAPIHandler(createHonoRouter: () => Routing): FetchHand } } -const routingCache = new Map, Routing>() -const pendingCache = new Map, { ref: EachContractLeafResultItem[] }> () +const routingCache = new Map() +const pendingCache = new Map () export function createResolveRouter(createHonoRouter: () => Routing): ResolveRouter { const addRoutes = (routing: Routing, pending: { ref: EachContractLeafResultItem[] }, options: EachLeafOptions) => { @@ -137,7 +130,7 @@ export function createResolveRouter(createHonoRouter: () => Routing): ResolveRou pending.ref.push(...lazies) } - return async (router: Router, method: string, pathname: string) => { + return async (router: ANY_ROUTER, method: string, pathname: string) => { const pending = (() => { let pending = pendingCache.get(router) if (!pending) { @@ -163,10 +156,11 @@ export function createResolveRouter(createHonoRouter: () => Routing): ResolveRou const newPending = [] for (const item of pending.ref) { + const lazyPrefix = getLazyRouterPrefix(item.lazy) + if ( - (LAZY_ROUTER_PREFIX_SYMBOL in item.lazy) - && item.lazy[LAZY_ROUTER_PREFIX_SYMBOL] - && !pathname.startsWith(item.lazy[LAZY_ROUTER_PREFIX_SYMBOL] as HTTPPath) + lazyPrefix + && !pathname.startsWith(lazyPrefix) && !pathname.startsWith(`/${item.path.map(encodeURIComponent).join('/')}`) ) { newPending.push(item) @@ -198,23 +192,17 @@ export function createResolveRouter(createHonoRouter: () => Routing): ResolveRou ) : match[1] as Record - let current: Router | ANY_PROCEDURE | ANY_LAZY_PROCEDURE | undefined = router - for (const segment of path) { - if ((typeof current !== 'object' && typeof current !== 'function') || !current) { - current = undefined - break - } + const { default: maybeProcedure } = await unlazy(getRouterChild(router, ...path)) - current = (current as any)[segment] + if (!isProcedure(maybeProcedure)) { + return undefined } - return isProcedure(current) || isLazy(current) - ? { - path, - procedure: current, - params: { ...params }, // params from hono not a normal object, so we need spread here - } - : undefined + return { + path, + procedure: maybeProcedure, + params: { ...params }, // params from hono not a normal object, so we need spread here + } } } @@ -235,7 +223,7 @@ function mergeParamsAndInput(coercedParams: Record, input: unkn async function deserializeInput(request: Request, procedure: ANY_PROCEDURE): Promise { const deserializer = new OpenAPIDeserializer({ - schema: procedure.zz$p.contract['~orpc'].InputSchema, + schema: procedure['~orpc'].contract['~orpc'].InputSchema, }) try { diff --git a/packages/openapi/src/generator.ts b/packages/openapi/src/generator.ts index dafacaca6..b7e80694d 100644 --- a/packages/openapi/src/generator.ts +++ b/packages/openapi/src/generator.ts @@ -1,7 +1,7 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12' import type { EachLeafOptions } from './utils' import { type ContractRouter, isContractProcedure } from '@orpc/contract' -import { LAZY_LOADER_SYMBOL, type Router } from '@orpc/server' +import { type ANY_ROUTER, unlazy } from '@orpc/server' import { findDeepMatches, isPlainObject, omit } from '@orpc/shared' import { preSerialize } from '@orpc/transformer' import { @@ -43,7 +43,7 @@ export interface GenerateOpenAPIOptions { export async function generateOpenAPI( opts: { - router: ContractRouter | Router + router: ContractRouter | ANY_ROUTER } & Omit, options?: GenerateOpenAPIOptions, ): Promise { @@ -329,7 +329,7 @@ export async function generateOpenAPI( summary: internal.route?.summary, description: internal.route?.description, deprecated: internal.route?.deprecated, - tags: internal.route?.tags, + tags: internal.route?.tags ? [...internal.route.tags] : undefined, operationId: path.join('.'), parameters: parameters.length ? parameters : undefined, requestBody, @@ -344,7 +344,7 @@ export async function generateOpenAPI( }) for (const lazy of lazies) { - const router = (await lazy.lazy[LAZY_LOADER_SYMBOL]()).default + const { default: router } = await unlazy(lazy.lazy) pending.push({ path: lazy.path, diff --git a/packages/openapi/src/utils.ts b/packages/openapi/src/utils.ts index a8d852e80..ec9167292 100644 --- a/packages/openapi/src/utils.ts +++ b/packages/openapi/src/utils.ts @@ -1,10 +1,10 @@ -import type { ANY_CONTRACT_PROCEDURE, ContractRouter, HTTPPath, WELL_CONTRACT_PROCEDURE } from '@orpc/contract' -import type { ANY_LAZY_PROCEDURE, ANY_PROCEDURE, Lazy, Router } from '@orpc/server' +import type { ContractRouter, HTTPPath, WELL_CONTRACT_PROCEDURE } from '@orpc/contract' +import type { ANY_PROCEDURE, ANY_ROUTER, Lazy } from '@orpc/server' import { isContractProcedure } from '@orpc/contract' -import { isLazy, isProcedure, ROUTER_CONTRACT_SYMBOL } from '@orpc/server' +import { flatLazy, getRouterContract, isLazy, isProcedure } from '@orpc/server' export interface EachLeafOptions { - router: ANY_PROCEDURE | Router | ContractRouter | ANY_CONTRACT_PROCEDURE + router: ContractRouter | ANY_ROUTER path: string[] } @@ -14,7 +14,7 @@ export interface EachLeafCallbackOptions { } export interface EachContractLeafResultItem { - lazy: ANY_LAZY_PROCEDURE | Lazy> + lazy: Lazy | Lazy> path: string[] } @@ -24,11 +24,13 @@ export function eachContractProcedureLeaf( result: EachContractLeafResultItem[] = [], isCurrentRouterContract = false, ): EachContractLeafResultItem[] { - if (!isCurrentRouterContract && ROUTER_CONTRACT_SYMBOL in options.router && options.router[ROUTER_CONTRACT_SYMBOL]) { + const hiddenContract = getRouterContract(options.router) + + if (!isCurrentRouterContract && hiddenContract) { return eachContractProcedureLeaf( { path: options.path, - router: options.router[ROUTER_CONTRACT_SYMBOL] as any, + router: hiddenContract, }, callback, result, @@ -38,7 +40,7 @@ export function eachContractProcedureLeaf( if (isLazy(options.router)) { result.push({ - lazy: options.router, + lazy: flatLazy(options.router), path: options.path, }) } @@ -46,7 +48,7 @@ export function eachContractProcedureLeaf( // else if (isProcedure(options.router)) { callback({ - contract: options.router.zz$p.contract, + contract: options.router['~orpc'].contract, path: options.path, }) } From 61ce8497d2e161cdc9a51bb8c3ad65a5d63e7dcc Mon Sep 17 00:00:00 2001 From: unnoq Date: Fri, 20 Dec 2024 14:41:50 +0700 Subject: [PATCH 46/51] sync next --- packages/next/src/action-form.ts | 20 +++++++---- packages/next/src/action-safe.ts | 35 ++++++++++--------- packages/next/src/client/action-hooks.ts | 3 +- packages/next/src/client/action-safe-hooks.ts | 10 +++--- 4 files changed, 40 insertions(+), 28 deletions(-) diff --git a/packages/next/src/action-form.ts b/packages/next/src/action-form.ts index 5243826ec..9d38f125c 100644 --- a/packages/next/src/action-form.ts +++ b/packages/next/src/action-form.ts @@ -1,24 +1,32 @@ -import type { ANY_LAZY_PROCEDURE, ANY_PROCEDURE, CreateProcedureCallerOptions } from '@orpc/server' -import { createProcedureClient, loadProcedure, ORPCError } from '@orpc/server' +import type { Schema, SchemaInput } from '@orpc/contract' +import type { Context, CreateProcedureCallerOptions } from '@orpc/server' +import { createProcedureClient, ORPCError, unlazy } from '@orpc/server' import { OpenAPIDeserializer } from '@orpc/transformer' import { forbidden, notFound, unauthorized } from 'next/navigation' export type FormAction = (input: FormData) => Promise -export function createFormAction(opt: CreateProcedureCallerOptions): FormAction { +export function createFormAction< + TContext extends Context, + TInputSchema extends Schema, + TOutputSchema extends Schema, + TFuncOutput extends SchemaInput, +>(opt: CreateProcedureCallerOptions): FormAction { const caller = createProcedureClient(opt) const formAction = async (input: FormData): Promise => { try { - const procedure = await loadProcedure(opt.procedure) + const { default: procedure } = await unlazy(opt.procedure) + + const inputSchema = procedure['~orpc'].contract['~orpc'].InputSchema const deserializer = new OpenAPIDeserializer({ - schema: procedure.zz$p.contract['~orpc'].InputSchema, + schema: inputSchema?.['~standard'].vendor === 'zod' ? inputSchema as any : undefined, }) const deserializedInput = deserializer.deserializeAsFormData(input) - await caller(deserializedInput) + await caller(deserializedInput as any) } catch (e) { if (e instanceof ORPCError) { diff --git a/packages/next/src/action-safe.ts b/packages/next/src/action-safe.ts index 054c2aadd..8334f7ea6 100644 --- a/packages/next/src/action-safe.ts +++ b/packages/next/src/action-safe.ts @@ -1,26 +1,27 @@ -import type { SchemaInput, SchemaOutput } from '@orpc/contract' -import type { ANY_LAZY_PROCEDURE, ANY_PROCEDURE, CreateProcedureCallerOptions, Lazy, Procedure, WELL_ORPC_ERROR_JSON } from '@orpc/server' +import type { Schema, SchemaInput, SchemaOutput } from '@orpc/contract' +import type { Context, CreateProcedureCallerOptions, ProcedureClient, WELL_ORPC_ERROR_JSON } from '@orpc/server' import { createProcedureClient, ORPCError } from '@orpc/server' -export type SafeAction = T extends - | Procedure - | Lazy> - ? ( - ...options: - | [input: SchemaInput] - | (undefined extends SchemaInput ? [] : never) - ) => Promise< - | [SchemaOutput, undefined, 'success'] - | [undefined, WELL_ORPC_ERROR_JSON, 'error'] - > - : never +export type SafeAction = ProcedureClient< + TInput, + | [TOutput, undefined, 'success'] + | [undefined, WELL_ORPC_ERROR_JSON, 'error'] +> -export function createSafeAction(opt: CreateProcedureCallerOptions): SafeAction { +export function createSafeAction< + TContext extends Context, + TInputSchema extends Schema, + TOutputSchema extends Schema, + TFuncOutput extends SchemaInput, +>( + opt: CreateProcedureCallerOptions, +): SafeAction, SchemaOutput> { const caller = createProcedureClient(opt) - const safeAction = async (...input: [any] | []) => { + const safeAction: SafeAction, SchemaOutput> = async (...[input, option]) => { try { - const output = await caller(...input) + const output = await caller(input as any, option) return [output as any, undefined, 'success'] } catch (e) { diff --git a/packages/next/src/client/action-hooks.ts b/packages/next/src/client/action-hooks.ts index ec452cee1..b20f9c351 100644 --- a/packages/next/src/client/action-hooks.ts +++ b/packages/next/src/client/action-hooks.ts @@ -1,3 +1,4 @@ +import type { ProcedureClient } from '@orpc/server' import type { Hooks } from '@orpc/shared' import { convertToStandardError } from '@orpc/server' import { convertToArray, executeWithHooks } from '@orpc/shared' @@ -22,7 +23,7 @@ export type UseActionState = { const idleState = { status: 'idle', isPending: false, isError: false, input: undefined, output: undefined, error: undefined } as const export function useAction( - action: (input: TInput) => Promise, + action: ProcedureClient, hooks?: Hooks, ): UseActionState { const [state, setState] = useState, 'execute' | 'reset'>>(idleState) diff --git a/packages/next/src/client/action-safe-hooks.ts b/packages/next/src/client/action-safe-hooks.ts index 2613342da..45aa2b8da 100644 --- a/packages/next/src/client/action-safe-hooks.ts +++ b/packages/next/src/client/action-safe-hooks.ts @@ -1,14 +1,16 @@ +import type { ProcedureClient } from '@orpc/server' import type { Hooks } from '@orpc/shared' -import { type ANY_ORPC_ERROR_JSON, ORPCError } from '@orpc/server' +import type { SafeAction } from '../action-safe' +import { ORPCError } from '@orpc/server' import { useCallback } from 'react' import { useAction, type UseActionState } from './action-hooks' export function useSafeAction( - action: (input: TInput) => Promise<[TOutput, undefined, 'success'] | [undefined, ANY_ORPC_ERROR_JSON, 'error']>, + action: SafeAction, hooks?: Hooks, ): UseActionState { - const normal = useCallback(async (input: TInput) => { - const [output, errorJson, status] = await action(input) + const normal: ProcedureClient = useCallback(async (...args) => { + const [output, errorJson, status] = await action(...args) if (status === 'error') { throw ORPCError.fromJSON(errorJson) From f7049318223a0ee89b04d592a43af77f358783d3 Mon Sep 17 00:00:00 2001 From: unnoq Date: Fri, 20 Dec 2024 15:45:53 +0700 Subject: [PATCH 47/51] sync react --- packages/client/tsconfig.json | 2 +- packages/react/src/general-hooks.test-d.ts | 35 +++-- packages/react/src/general-hooks.test.tsx | 46 ++----- packages/react/src/general-hooks.ts | 26 ++-- packages/react/src/general-utils.test-d.ts | 24 ++-- packages/react/src/general-utils.ts | 96 +++++++------- packages/react/src/orpc-path.ts | 10 +- packages/react/src/procedure-hooks.test-d.ts | 43 ++++--- packages/react/src/procedure-hooks.test.tsx | 50 ++------ packages/react/src/procedure-hooks.ts | 72 +++++------ packages/react/src/procedure-utils.test-d.ts | 66 +++++----- packages/react/src/procedure-utils.test.tsx | 32 +++-- packages/react/src/procedure-utils.ts | 121 ++++++++---------- packages/react/src/react-context.ts | 24 ++-- packages/react/src/react-hooks.ts | 60 +++------ packages/react/src/react-utils.ts | 46 ++----- packages/react/src/react.tsx | 65 +++------- packages/react/src/tanstack-key.ts | 20 +-- packages/react/src/types.ts | 10 +- .../react/src/use-queries/builder.test-d.ts | 4 +- .../react/src/use-queries/builder.test.ts | 4 +- packages/react/src/use-queries/builder.ts | 38 ++---- .../react/src/use-queries/builders.test.tsx | 8 +- packages/react/src/use-queries/builders.ts | 62 ++------- packages/react/src/use-queries/hook.ts | 62 ++++----- packages/react/tests/orpc.tsx | 3 +- 26 files changed, 377 insertions(+), 652 deletions(-) diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json index 9dd9f5f90..ed55cc038 100644 --- a/packages/client/tsconfig.json +++ b/packages/client/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.lib.json", "compilerOptions": { - "lib": ["ES2020", "DOM"], + "lib": ["ES2020", "DOM", "DOM.Iterable"], "types": [] }, "references": [ diff --git a/packages/react/src/general-hooks.test-d.ts b/packages/react/src/general-hooks.test-d.ts index 8b6663797..4c2b850c3 100644 --- a/packages/react/src/general-hooks.test-d.ts +++ b/packages/react/src/general-hooks.test-d.ts @@ -1,4 +1,4 @@ -import type { SchemaInput, SchemaOutput } from '@orpc/contract' +import type { SchemaOutput } from '@orpc/contract' import type { DefaultError, Mutation, @@ -6,22 +6,21 @@ import type { } from '@tanstack/react-query' import { ORPCContext, - type UserCreateInputSchema, - type UserFindInputSchema, type UserSchema, } from '../tests/orpc' import { createGeneralHooks } from './general-hooks' +type User = SchemaOutput + describe('useIsFetching', () => { - const hooks = createGeneralHooks({ + const hooks = createGeneralHooks({ context: ORPCContext, path: ['user'], }) const procedureHooks = createGeneralHooks< - typeof UserFindInputSchema, - typeof UserSchema, - SchemaOutput + { id: string }, + User >({ context: ORPCContext, path: ['user', 'find'], @@ -55,15 +54,14 @@ describe('useIsFetching', () => { }) describe('useIsMutating', () => { - const hooks = createGeneralHooks({ + const hooks = createGeneralHooks({ context: ORPCContext, path: ['user'], }) const procedureHooks = createGeneralHooks< - typeof UserCreateInputSchema, - typeof UserSchema, - SchemaOutput + { id: string }, + User >({ context: ORPCContext, path: ['user', 'create'], @@ -83,15 +81,14 @@ describe('useIsMutating', () => { }) describe('useMutationState', () => { - const hooks = createGeneralHooks({ + const hooks = createGeneralHooks({ context: ORPCContext, path: ['user'], }) const procedureHooks = createGeneralHooks< - typeof UserCreateInputSchema, - typeof UserSchema, - SchemaOutput + { id: string }, + User >({ context: ORPCContext, path: ['user', 'create'], @@ -106,9 +103,9 @@ describe('useMutationState', () => { const result2 = procedureHooks.useMutationState() expectTypeOf(result2).toEqualTypeOf< MutationState< - SchemaOutput, + { id: string, name: string }, DefaultError, - SchemaInput + { id: string } >[] >() }) @@ -137,9 +134,9 @@ describe('useMutationState', () => { select: (data) => { expectTypeOf(data).toEqualTypeOf< Mutation< - SchemaOutput, + { id: string, name: string }, DefaultError, - SchemaInput + { id: string } > >() diff --git a/packages/react/src/general-hooks.test.tsx b/packages/react/src/general-hooks.test.tsx index e0fa14083..f93255ce2 100644 --- a/packages/react/src/general-hooks.test.tsx +++ b/packages/react/src/general-hooks.test.tsx @@ -1,12 +1,8 @@ -import type { SchemaOutput } from '@orpc/contract' import { useMutation, useQuery } from '@tanstack/react-query' import { renderHook } from '@testing-library/react' import { ORPCContext, queryClient, - type UserCreateInputSchema, - type UserFindInputSchema, - type UserSchema, wrapper, } from '../tests/orpc' import { createGeneralHooks } from './general-hooks' @@ -16,25 +12,17 @@ beforeEach(() => { }) describe('useIsFetching', () => { - const user_hooks = createGeneralHooks({ + const user_hooks = createGeneralHooks({ context: ORPCContext, path: ['user'], }) - const user_find_Hooks = createGeneralHooks< - typeof UserFindInputSchema, - typeof UserSchema, - SchemaOutput - >({ + const user_find_Hooks = createGeneralHooks({ context: ORPCContext, path: ['user', 'find'], }) - const user_create_Hooks = createGeneralHooks< - typeof UserCreateInputSchema, - typeof UserSchema, - SchemaOutput - >({ + const user_create_Hooks = createGeneralHooks({ context: ORPCContext, path: ['user', 'create'], }) @@ -104,25 +92,17 @@ describe('useIsFetching', () => { }) describe('useIsMutating', () => { - const user_hooks = createGeneralHooks({ + const user_hooks = createGeneralHooks({ context: ORPCContext, path: ['user'], }) - const user_find_Hooks = createGeneralHooks< - typeof UserFindInputSchema, - typeof UserSchema, - SchemaOutput - >({ + const user_find_Hooks = createGeneralHooks({ context: ORPCContext, path: ['user', 'find'], }) - const user_create_Hooks = createGeneralHooks< - typeof UserCreateInputSchema, - typeof UserSchema, - SchemaOutput - >({ + const user_create_Hooks = createGeneralHooks({ context: ORPCContext, path: ['user', 'create'], }) @@ -165,25 +145,17 @@ describe('useIsMutating', () => { }) describe('useMutationState', () => { - const user_hooks = createGeneralHooks({ + const user_hooks = createGeneralHooks({ context: ORPCContext, path: ['user'], }) - const user_find_Hooks = createGeneralHooks< - typeof UserFindInputSchema, - typeof UserSchema, - SchemaOutput - >({ + const user_find_Hooks = createGeneralHooks({ context: ORPCContext, path: ['user', 'find'], }) - const user_create_Hooks = createGeneralHooks< - typeof UserCreateInputSchema, - typeof UserSchema, - SchemaOutput - >({ + const user_create_Hooks = createGeneralHooks({ context: ORPCContext, path: ['user', 'create'], }) diff --git a/packages/react/src/general-hooks.ts b/packages/react/src/general-hooks.ts index 3a10a913a..62ea6c3a1 100644 --- a/packages/react/src/general-hooks.ts +++ b/packages/react/src/general-hooks.ts @@ -1,4 +1,3 @@ -import type { Schema, SchemaInput, SchemaOutput } from '@orpc/contract' import type { PartialDeep, SetOptional } from '@orpc/shared' import type { ORPCQueryFilters } from './tanstack-query' import { @@ -13,29 +12,25 @@ import { import { type ORPCContext, useORPCContext } from './react-context' import { getMutationKeyFromPath, getQueryKeyFromPath } from './tanstack-key' -export interface GeneralHooks< - TInputSchema extends Schema, - TOutputSchema extends Schema, - TFuncOutput extends SchemaOutput, -> { +export interface GeneralHooks { useIsFetching: ( - filers?: ORPCQueryFilters>>, + filers?: ORPCQueryFilters>, ) => number useIsMutating: (filters?: SetOptional) => number useMutationState: < UResult = MutationState< - SchemaOutput, + TOutput, DefaultError, - SchemaInput + TInput >, >(options?: { filters?: SetOptional select?: ( mutation: Mutation< - SchemaOutput, + TOutput, DefaultError, - SchemaInput + TInput >, ) => UResult } @@ -53,14 +48,9 @@ export interface CreateGeneralHooksOptions { path: string[] } -export function createGeneralHooks< - TInputSchema extends Schema = undefined, - TOutputSchema extends Schema = undefined, - TFuncOutput extends - SchemaOutput = SchemaOutput, ->( +export function createGeneralHooks( options: CreateGeneralHooksOptions, -): GeneralHooks { +): GeneralHooks { return { useIsFetching(filters) { const { queryType, input, ...rest } = filters ?? {} diff --git a/packages/react/src/general-utils.test-d.ts b/packages/react/src/general-utils.test-d.ts index 03297793d..09dbc9102 100644 --- a/packages/react/src/general-utils.test-d.ts +++ b/packages/react/src/general-utils.test-d.ts @@ -1,9 +1,8 @@ -import type { SchemaOutput } from '@orpc/contract' +import type { SchemaInput, SchemaOutput } from '@orpc/contract' import type { Promisable } from '@orpc/shared' import type { InfiniteData } from '@tanstack/react-query' import { queryClient, - type UserCreateInputSchema, type UserFindInputSchema, type UserListInputSchema, type UserListOutputSchema, @@ -11,33 +10,36 @@ import { } from '../tests/orpc' import { createGeneralUtils } from './general-utils' +type UserFindInput = SchemaInput +type User = SchemaOutput + +type UserListInput = SchemaInput +type UserListOutput = SchemaOutput + const user_utils = createGeneralUtils({ queryClient, path: ['user'], }) const user_find_utils = createGeneralUtils< - typeof UserFindInputSchema, - typeof UserSchema, - SchemaOutput + UserFindInput, + User >({ queryClient, path: ['user', 'find'], }) const user_list_utils = createGeneralUtils< - typeof UserListInputSchema, - typeof UserListOutputSchema, - SchemaOutput + UserListInput, + UserListOutput >({ queryClient, path: ['user', 'list'], }) const user_create_utils = createGeneralUtils< - typeof UserCreateInputSchema, - typeof UserSchema, - SchemaOutput + UserFindInput, + User >({ queryClient, path: ['user', 'create'], diff --git a/packages/react/src/general-utils.ts b/packages/react/src/general-utils.ts index 54b7ff216..a64b1e033 100644 --- a/packages/react/src/general-utils.ts +++ b/packages/react/src/general-utils.ts @@ -1,4 +1,3 @@ -import type { Schema, SchemaInput, SchemaOutput } from '@orpc/contract' import type { PartialDeep, SetOptional } from '@orpc/shared' import type { CancelOptions, @@ -23,56 +22,52 @@ import type { import type { InferCursor, SchemaInputForInfiniteQuery } from './types' import { getMutationKeyFromPath, getQueryKeyFromPath } from './tanstack-key' -export interface GeneralUtils< - TInputSchema extends Schema, - TOutputSchema extends Schema, - TFuncOutput extends SchemaOutput, -> { +export interface GeneralUtils { getQueriesData: ( filters?: OmitKeyof< - ORPCQueryFilters>>, + ORPCQueryFilters>, 'queryType' >, - ) => [QueryKey, SchemaOutput | undefined][] + ) => [QueryKey, TOutput | undefined][] getInfiniteQueriesData: ( filters?: OmitKeyof< - ORPCQueryFilters>>, + ORPCQueryFilters>>, 'queryType' >, ) => [ QueryKey, | undefined | InfiniteData< - SchemaOutput, - InferCursor + TOutput, + InferCursor >, ][] setQueriesData: ( filters: OmitKeyof< - ORPCQueryFilters>>, + ORPCQueryFilters>, 'queryType' >, updater: Updater< - SchemaOutput | undefined, - SchemaOutput | undefined + TOutput | undefined, + TOutput | undefined >, options?: SetDataOptions, - ) => [QueryKey, SchemaOutput | undefined][] + ) => [QueryKey, TOutput | undefined][] setInfiniteQueriesData: ( filters: OmitKeyof< - ORPCQueryFilters>>, + ORPCQueryFilters>>, 'queryType' >, updater: Updater< | InfiniteData< - SchemaOutput, - InferCursor + TOutput, + InferCursor > | undefined, | InfiniteData< - SchemaOutput, - InferCursor + TOutput, + InferCursor > | undefined >, @@ -81,60 +76,60 @@ export interface GeneralUtils< QueryKey, | undefined | InfiniteData< - SchemaOutput, - InferCursor + TOutput, + InferCursor >, ][] invalidate: ( filters?: ORPCInvalidateQueryFilters< - PartialDeep> + PartialDeep >, options?: InvalidateOptions, ) => Promise refetch: ( - filters?: ORPCQueryFilters>>, + filters?: ORPCQueryFilters>, options?: RefetchOptions, ) => Promise cancel: ( - filters?: ORPCQueryFilters>>, + filters?: ORPCQueryFilters>, options?: CancelOptions, ) => Promise remove: ( - filters?: ORPCQueryFilters>>, + filters?: ORPCQueryFilters>, ) => void reset: ( - filters?: ORPCQueryFilters>>, + filters?: ORPCQueryFilters>, options?: ResetOptions, ) => Promise isFetching: ( - filters?: ORPCQueryFilters>>, + filters?: ORPCQueryFilters>, ) => number isMutating: (filters?: SetOptional) => number getQueryDefaults: ( filters?: Pick< - ORPCQueryFilters>>, + ORPCQueryFilters>, 'input' | 'queryKey' >, ) => OmitKeyof< - QueryObserverOptions>, + QueryObserverOptions, 'queryKey' > getInfiniteQueryDefaults: ( filters?: Pick< - ORPCQueryFilters>>, + ORPCQueryFilters>>, 'input' | 'queryKey' >, ) => OmitKeyof< QueryObserverOptions< - SchemaOutput, + TOutput, DefaultError, - SchemaOutput, - InfiniteData>, + TOutput, + InfiniteData, QueryKey, - InferCursor + InferCursor >, 'queryKey' > @@ -142,12 +137,12 @@ export interface GeneralUtils< setQueryDefaults: ( options: Partial< OmitKeyof< - QueryObserverOptions>, + QueryObserverOptions, 'queryKey' > >, filters?: Pick< - ORPCQueryFilters>>, + ORPCQueryFilters>, 'input' | 'queryKey' >, ) => void @@ -155,18 +150,18 @@ export interface GeneralUtils< options: Partial< OmitKeyof< QueryObserverOptions< - SchemaOutput, + TOutput, DefaultError, - SchemaOutput, - InfiniteData>, + TOutput, + InfiniteData, QueryKey, - InferCursor + InferCursor >, 'queryKey' > >, filters?: Pick< - ORPCQueryFilters>>, + ORPCQueryFilters>, 'input' | 'queryKey' >, ) => void @@ -174,16 +169,16 @@ export interface GeneralUtils< getMutationDefaults: ( filters?: Pick, ) => MutationObserverOptions< - SchemaOutput, + TOutput, DefaultError, - SchemaInput + TInput > setMutationDefaults: ( options: OmitKeyof< MutationObserverOptions< - SchemaOutput, + TOutput, DefaultError, - SchemaInput + TInput >, 'mutationKey' >, @@ -202,14 +197,9 @@ export interface CreateGeneralUtilsOptions { path: string[] } -export function createGeneralUtils< - TInputSchema extends Schema = undefined, - TOutputSchema extends Schema = undefined, - TFuncOutput extends - SchemaOutput = SchemaOutput, ->( +export function createGeneralUtils( options: CreateGeneralUtilsOptions, -): GeneralUtils { +): GeneralUtils { return { getQueriesData(filters) { const { input, ...rest } = filters ?? {} diff --git a/packages/react/src/orpc-path.ts b/packages/react/src/orpc-path.ts index 2f554d181..c5043f971 100644 --- a/packages/react/src/orpc-path.ts +++ b/packages/react/src/orpc-path.ts @@ -1,16 +1,12 @@ import type { ProcedureHooks } from './procedure-hooks' -import type { - ORPCHooksWithContractRouter, - ORPCHooksWithRouter, -} from './react-hooks' +import type { ORPCHooks } from './react-hooks' export const orpcPathSymbol = Symbol('orpcPathSymbol') export function getORPCPath( orpc: - | ORPCHooksWithContractRouter - | ORPCHooksWithRouter - | ProcedureHooks, + | ORPCHooks + | ProcedureHooks, ): string[] { const val = Reflect.get(orpc, orpcPathSymbol) diff --git a/packages/react/src/procedure-hooks.test-d.ts b/packages/react/src/procedure-hooks.test-d.ts index 1f86c47f2..ecbd45c8f 100644 --- a/packages/react/src/procedure-hooks.test-d.ts +++ b/packages/react/src/procedure-hooks.test-d.ts @@ -11,11 +11,16 @@ import { } from '../tests/orpc' import { createProcedureHooks } from './procedure-hooks' +type UserFindInput = SchemaInput +type User = SchemaOutput + +type UserListInput = SchemaInput +type UserListOutput = SchemaOutput + describe('useQuery', () => { const hooks = createProcedureHooks< - typeof UserFindInputSchema, - typeof UserSchema, - SchemaOutput + UserFindInput, + User >({ context: ORPCContext, path: ['user', 'find'], @@ -55,9 +60,8 @@ describe('useQuery', () => { describe('useInfiniteQuery', () => { const hooks = createProcedureHooks< - typeof UserListInputSchema, - typeof UserListOutputSchema, - SchemaOutput + UserListInput, + UserListOutput >({ context: ORPCContext, path: ['user', 'list'], @@ -122,9 +126,8 @@ describe('useInfiniteQuery', () => { describe('useSuspenseQuery', () => { const hooks = createProcedureHooks< - typeof UserFindInputSchema, - typeof UserSchema, - SchemaOutput + UserFindInput, + User >({ context: ORPCContext, path: ['user', 'find'], @@ -162,9 +165,8 @@ describe('useSuspenseQuery', () => { describe('useSuspenseInfiniteQuery', () => { const hooks = createProcedureHooks< - typeof UserListInputSchema, - typeof UserListOutputSchema, - SchemaOutput + UserListInput, + UserListOutput >({ context: ORPCContext, path: ['user', 'list'], @@ -225,9 +227,8 @@ describe('useSuspenseInfiniteQuery', () => { describe('usePrefetchQuery', () => { const hooks = createProcedureHooks< - typeof UserFindInputSchema, - typeof UserSchema, - SchemaOutput + UserFindInput, + User >({ context: ORPCContext, path: ['user', 'find'], @@ -246,9 +247,8 @@ describe('usePrefetchQuery', () => { describe('usePrefetchInfiniteQuery', () => { const hooks = createProcedureHooks< - typeof UserListInputSchema, - typeof UserListOutputSchema, - SchemaOutput + UserListInput, + UserListOutput >({ context: ORPCContext, path: ['user', 'list'], @@ -286,11 +286,12 @@ describe('usePrefetchInfiniteQuery', () => { }) }) +type UserCreateInput = SchemaInput + describe('useMutation', () => { const hooks = createProcedureHooks< - typeof UserCreateInputSchema, - typeof UserSchema, - SchemaOutput + UserCreateInput, + User >({ context: ORPCContext, path: ['user', 'create'], diff --git a/packages/react/src/procedure-hooks.test.tsx b/packages/react/src/procedure-hooks.test.tsx index e9117c380..178930726 100644 --- a/packages/react/src/procedure-hooks.test.tsx +++ b/packages/react/src/procedure-hooks.test.tsx @@ -1,13 +1,10 @@ -import type { SchemaOutput } from '@orpc/contract' +import type { SchemaInput, SchemaOutput } from '@orpc/contract' import { renderHook, screen, waitFor } from '@testing-library/react' import { ORPCContext, queryClient, - type UserCreateInputSchema, - type UserFindInputSchema, type UserListInputSchema, type UserListOutputSchema, - type UserSchema, wrapper, } from '../tests/orpc' import { createProcedureHooks } from './procedure-hooks' @@ -17,11 +14,7 @@ beforeEach(() => { }) describe('useQuery', () => { - const hooks = createProcedureHooks< - typeof UserFindInputSchema, - typeof UserSchema, - SchemaOutput - >({ + const hooks = createProcedureHooks({ context: ORPCContext, path: ['user', 'find'], }) @@ -47,7 +40,6 @@ describe('useQuery', () => { }) it('on error', async () => { - // @ts-expect-error invalid input const { result } = renderHook(() => hooks.useQuery({ id: {} }), { wrapper, }) @@ -60,11 +52,13 @@ describe('useQuery', () => { }) }) +type UserListInput = SchemaInput +type UserListOutput = SchemaOutput + describe('useInfiniteQuery', () => { const hooks = createProcedureHooks< - typeof UserListInputSchema, - typeof UserListOutputSchema, - SchemaOutput + UserListInput, + UserListOutput >({ context: ORPCContext, path: ['user', 'list'], @@ -149,11 +143,7 @@ describe('useInfiniteQuery', () => { }) describe('useSuspenseQuery', () => { - const hooks = createProcedureHooks< - typeof UserFindInputSchema, - typeof UserSchema, - SchemaOutput - >({ + const hooks = createProcedureHooks({ context: ORPCContext, path: ['user', 'find'], }) @@ -179,7 +169,6 @@ describe('useSuspenseQuery', () => { }) it('on error', async () => { - // @ts-expect-error invalid input const { result } = renderHook(() => hooks.useSuspenseQuery({ id: {} }), { wrapper, }) @@ -194,9 +183,8 @@ describe('useSuspenseQuery', () => { describe('useSuspenseInfiniteQuery', () => { const hooks = createProcedureHooks< - typeof UserListInputSchema, - typeof UserListOutputSchema, - SchemaOutput + UserListInput, + UserListOutput >({ context: ORPCContext, path: ['user', 'list'], @@ -281,11 +269,7 @@ describe('useSuspenseInfiniteQuery', () => { }) describe('usePrefetchQuery', () => { - const hooks = createProcedureHooks< - typeof UserFindInputSchema, - typeof UserSchema, - SchemaOutput - >({ + const hooks = createProcedureHooks({ context: ORPCContext, path: ['user', 'find'], }) @@ -311,9 +295,8 @@ describe('usePrefetchQuery', () => { describe('usePrefetchInfiniteQuery', () => { const hooks = createProcedureHooks< - typeof UserListInputSchema, - typeof UserListOutputSchema, - SchemaOutput + UserListInput, + UserListOutput >({ context: ORPCContext, path: ['user', 'list'], @@ -350,11 +333,7 @@ describe('usePrefetchInfiniteQuery', () => { }) describe('useMutation', () => { - const hooks = createProcedureHooks< - typeof UserCreateInputSchema, - typeof UserSchema, - SchemaOutput - >({ + const hooks = createProcedureHooks({ context: ORPCContext, path: ['user', 'create'], }) @@ -376,7 +355,6 @@ describe('useMutation', () => { wrapper, }) - // @ts-expect-error invalid input result.current.mutate({ name: {} }) await waitFor(() => diff --git a/packages/react/src/procedure-hooks.ts b/packages/react/src/procedure-hooks.ts index 681327a37..b97220f87 100644 --- a/packages/react/src/procedure-hooks.ts +++ b/packages/react/src/procedure-hooks.ts @@ -1,4 +1,3 @@ -import type { Schema, SchemaInput, SchemaOutput } from '@orpc/contract' import type { InferCursor, SchemaInputForInfiniteQuery } from './types' import { get, @@ -33,16 +32,12 @@ import { orpcPathSymbol } from './orpc-path' import { type ORPCContext, useORPCContext } from './react-context' import { getMutationKeyFromPath, getQueryKeyFromPath } from './tanstack-key' -export interface ProcedureHooks< - TInputSchema extends Schema, - TOutputSchema extends Schema, - TFuncOutput extends SchemaOutput, -> { - useQuery: >( - input: SchemaInput, +export interface ProcedureHooks { + useQuery: ( + input: TInput, options?: SetOptional< UseQueryOptions< - SchemaOutput, + TOutput, DefaultError, USelectData >, @@ -51,32 +46,32 @@ export interface ProcedureHooks< ) => UseQueryResult useInfiniteQuery: < USelectData = InfiniteData< - SchemaOutput, - InferCursor + TOutput, + InferCursor >, >( options: PartialOnUndefinedDeep< SetOptional< UseInfiniteQueryOptions< - SchemaOutput, + TOutput, DefaultError, USelectData, - SchemaOutput, + TOutput, QueryKey, - InferCursor + InferCursor >, 'queryFn' | 'queryKey' > & { - input: SchemaInputForInfiniteQuery + input: SchemaInputForInfiniteQuery } >, ) => UseInfiniteQueryResult - useSuspenseQuery: >( - input: SchemaInput, + useSuspenseQuery: ( + input: TInput, options?: SetOptional< UseSuspenseQueryOptions< - SchemaOutput, + TOutput, DefaultError, USelectData >, @@ -85,44 +80,44 @@ export interface ProcedureHooks< ) => UseSuspenseQueryResult useSuspenseInfiniteQuery: < USelectData = InfiniteData< - SchemaOutput, - InferCursor + TOutput, + InferCursor >, >( options: PartialOnUndefinedDeep< SetOptional< UseSuspenseInfiniteQueryOptions< - SchemaOutput, + TOutput, DefaultError, USelectData, - SchemaOutput, + TOutput, QueryKey, - InferCursor + InferCursor >, 'queryFn' | 'queryKey' > & { - input: SchemaInputForInfiniteQuery + input: SchemaInputForInfiniteQuery } >, ) => UseSuspenseInfiniteQueryResult usePrefetchQuery: ( - input: SchemaInput, - options?: FetchQueryOptions>, + input: TInput, + options?: FetchQueryOptions, ) => void usePrefetchInfiniteQuery: ( options: PartialOnUndefinedDeep< SetOptional< FetchInfiniteQueryOptions< - SchemaOutput, + TOutput, DefaultError, - SchemaOutput, + TOutput, QueryKey, - InferCursor + InferCursor >, 'queryKey' | 'queryFn' > & { - input: SchemaInputForInfiniteQuery + input: SchemaInputForInfiniteQuery } >, ) => void @@ -130,16 +125,16 @@ export interface ProcedureHooks< useMutation: ( options?: SetOptional< UseMutationOptions< - SchemaOutput, + TOutput, DefaultError, - SchemaInput + TInput >, 'mutationFn' | 'mutationKey' >, ) => UseMutationResult< - SchemaOutput, + TOutput, DefaultError, - SchemaInput + TInput > } @@ -154,14 +149,9 @@ export interface CreateProcedureHooksOptions { path: string[] } -export function createProcedureHooks< - TInputSchema extends Schema = undefined, - TOutputSchema extends Schema = undefined, - TFuncOutput extends - SchemaOutput = SchemaOutput, ->( +export function createProcedureHooks( options: CreateProcedureHooksOptions, -): ProcedureHooks { +): ProcedureHooks { return { [orpcPathSymbol as any]: options.path, diff --git a/packages/react/src/procedure-utils.test-d.ts b/packages/react/src/procedure-utils.test-d.ts index ce43233a4..f3d2c9592 100644 --- a/packages/react/src/procedure-utils.test-d.ts +++ b/packages/react/src/procedure-utils.test-d.ts @@ -8,11 +8,16 @@ import type { } from '../tests/orpc' import { createProcedureUtils } from './procedure-utils' +type UserFindInput = SchemaInput +type User = SchemaOutput + +type UserListInput = SchemaInput +type UserListOutput = SchemaOutput + describe('fetchQuery', () => { const utils = createProcedureUtils< - typeof UserFindInputSchema, - typeof UserSchema, - SchemaOutput + UserFindInput, + User >({} as any) it('simple', async () => { @@ -45,9 +50,8 @@ describe('fetchQuery', () => { describe('fetchInfiniteQuery', () => { const utils = createProcedureUtils< - typeof UserListInputSchema, - typeof UserListOutputSchema, - SchemaOutput + UserListInput, + UserListOutput >({} as any) it('simple', async () => { @@ -105,9 +109,8 @@ describe('fetchInfiniteQuery', () => { describe('prefetchQuery', () => { const utils = createProcedureUtils< - typeof UserFindInputSchema, - typeof UserSchema, - SchemaOutput + UserFindInput, + User >({} as any) it('simple', async () => { @@ -140,9 +143,8 @@ describe('prefetchQuery', () => { describe('prefetchInfiniteQuery', () => { const utils = createProcedureUtils< - typeof UserListInputSchema, - typeof UserListOutputSchema, - SchemaOutput + UserListInput, + UserListOutput >({} as any) it('simple', () => { @@ -195,9 +197,8 @@ describe('prefetchInfiniteQuery', () => { describe('getQueryData', () => { const utils = createProcedureUtils< - typeof UserFindInputSchema, - typeof UserSchema, - SchemaOutput + UserFindInput, + User >({} as any) it('simple', () => { @@ -215,9 +216,8 @@ describe('getQueryData', () => { describe('getInfiniteQueryData', () => { const utils = createProcedureUtils< - typeof UserListInputSchema, - typeof UserListOutputSchema, - SchemaOutput + UserListInput, + UserListOutput >({} as any) it('simple', () => { @@ -242,9 +242,8 @@ describe('getInfiniteQueryData', () => { describe('ensureQueryData', () => { const utils = createProcedureUtils< - typeof UserFindInputSchema, - typeof UserSchema, - SchemaOutput + UserFindInput, + User >({} as any) it('simple', async () => { @@ -277,9 +276,8 @@ describe('ensureQueryData', () => { describe('ensureInfiniteQuery', () => { const utils = createProcedureUtils< - typeof UserListInputSchema, - typeof UserListOutputSchema, - SchemaOutput + UserListInput, + UserListOutput >({} as any) it('simple', async () => { @@ -339,9 +337,8 @@ describe('ensureInfiniteQuery', () => { describe('getQueryState', () => { const utils = createProcedureUtils< - typeof UserFindInputSchema, - typeof UserSchema, - SchemaOutput + UserFindInput, + User >({} as any) it('simple', () => { @@ -359,9 +356,8 @@ describe('getQueryState', () => { describe('getInfiniteQueryState', () => { const utils = createProcedureUtils< - typeof UserListInputSchema, - typeof UserListOutputSchema, - SchemaOutput + UserListInput, + UserListOutput >({} as any) it('simple', () => { @@ -388,9 +384,8 @@ describe('getInfiniteQueryState', () => { describe('setQueryData', () => { const utils = createProcedureUtils< - typeof UserFindInputSchema, - typeof UserSchema, - SchemaOutput + UserFindInput, + User >({} as any) it('simple', () => { @@ -427,9 +422,8 @@ describe('setQueryData', () => { describe('setInfiniteQueryData', () => { const utils = createProcedureUtils< - typeof UserListInputSchema, - typeof UserListOutputSchema, - SchemaOutput + UserListInput, + UserListOutput >({} as any) it('simple', () => { diff --git a/packages/react/src/procedure-utils.test.tsx b/packages/react/src/procedure-utils.test.tsx index 799ec7c7a..0f8ac7218 100644 --- a/packages/react/src/procedure-utils.test.tsx +++ b/packages/react/src/procedure-utils.test.tsx @@ -1,14 +1,20 @@ -import type { SchemaOutput } from '@orpc/contract' +import type { SchemaInput, SchemaOutput } from '@orpc/contract' import type { UserFindInputSchema, UserListInputSchema, UserListOutputSchema, UserSchema } from '../tests/orpc' import { orpcClient, queryClient } from '../tests/orpc' import { createProcedureUtils } from './procedure-utils' +type UserFindInput = SchemaInput +type User = SchemaOutput + +type UserListInput = SchemaInput +type UserListOutput = SchemaOutput + beforeEach(() => { queryClient.clear() }) describe('fetchQuery', () => { - const utils = createProcedureUtils>({ + const utils = createProcedureUtils({ client: orpcClient.user.find, path: ['user', 'find'], queryClient, @@ -39,7 +45,7 @@ describe('fetchQuery', () => { }) describe('fetchInfiniteQuery', () => { - const utils = createProcedureUtils>({ + const utils = createProcedureUtils({ client: orpcClient.user.list, path: ['user', 'list'], queryClient, @@ -75,7 +81,7 @@ describe('fetchInfiniteQuery', () => { }) describe('prefetchQuery', () => { - const utils = createProcedureUtils>({ + const utils = createProcedureUtils({ client: orpcClient.user.find, path: ['user', 'find'], queryClient, @@ -108,7 +114,7 @@ describe('prefetchQuery', () => { }) describe('prefetchInfiniteQuery', () => { - const utils = createProcedureUtils>({ + const utils = createProcedureUtils({ client: orpcClient.user.list, path: ['user', 'list'], queryClient, @@ -143,7 +149,7 @@ describe('prefetchInfiniteQuery', () => { }) describe('ensureQueryData', () => { - const utils = createProcedureUtils>({ + const utils = createProcedureUtils({ client: orpcClient.user.find, path: ['user', 'find'], queryClient, @@ -174,7 +180,7 @@ describe('ensureQueryData', () => { }) describe('ensureInfiniteQuery', () => { - const utils = createProcedureUtils>({ + const utils = createProcedureUtils({ client: orpcClient.user.list, path: ['user', 'list'], queryClient, @@ -210,7 +216,7 @@ describe('ensureInfiniteQuery', () => { }) describe('getQueryData', () => { - const utils = createProcedureUtils>({ + const utils = createProcedureUtils({ client: orpcClient.user.find, path: ['user', 'find'], queryClient, @@ -224,7 +230,7 @@ describe('getQueryData', () => { }) describe('getInfiniteQueryData', () => { - const utils = createProcedureUtils>({ + const utils = createProcedureUtils({ client: orpcClient.user.list, path: ['user', 'list'], queryClient, @@ -238,7 +244,7 @@ describe('getInfiniteQueryData', () => { }) describe('getQueryState', () => { - const utils = createProcedureUtils>({ + const utils = createProcedureUtils({ client: orpcClient.user.find, path: ['user', 'find'], queryClient, @@ -255,7 +261,7 @@ describe('getQueryState', () => { }) describe('getInfiniteQueryState', () => { - const utils = createProcedureUtils>({ + const utils = createProcedureUtils({ client: orpcClient.user.list, path: ['user', 'list'], queryClient, @@ -272,7 +278,7 @@ describe('getInfiniteQueryState', () => { }) describe('setQueryData', () => { - const utils = createProcedureUtils>({ + const utils = createProcedureUtils({ client: orpcClient.user.find, path: ['user', 'find'], queryClient, @@ -302,7 +308,7 @@ describe('setQueryData', () => { }) describe('getInfiniteQueryData 2', () => { - const utils = createProcedureUtils>({ + const utils = createProcedureUtils({ client: orpcClient.user.list, path: ['user', 'list'], queryClient, diff --git a/packages/react/src/procedure-utils.ts b/packages/react/src/procedure-utils.ts index df65341eb..e27c7114b 100644 --- a/packages/react/src/procedure-utils.ts +++ b/packages/react/src/procedure-utils.ts @@ -1,5 +1,4 @@ -import type { Schema, SchemaInput, SchemaOutput } from '@orpc/contract' -import type { Caller } from '@orpc/server' +import type { ProcedureClient } from '@orpc/server' import type { PartialOnUndefinedDeep, SetOptional } from '@orpc/shared' import type { DefaultError, @@ -17,44 +16,40 @@ import type { import type { InferCursor, SchemaInputForInfiniteQuery } from './types' import { getQueryKeyFromPath } from './tanstack-key' -export interface ProcedureUtils< - TInputSchema extends Schema, - TOutputSchema extends Schema, - TFuncOutput extends SchemaOutput, -> { +export interface ProcedureUtils { fetchQuery: ( - input: SchemaInput, + input: TInput, options?: SetOptional< - FetchQueryOptions>, + FetchQueryOptions, 'queryKey' | 'queryFn' >, - ) => Promise> + ) => Promise fetchInfiniteQuery: ( options: PartialOnUndefinedDeep< SetOptional< FetchInfiniteQueryOptions< - SchemaOutput, + TOutput, DefaultError, - SchemaOutput, + TOutput, QueryKey, - InferCursor + InferCursor >, 'queryKey' | 'queryFn' > & { - input: SchemaInputForInfiniteQuery + input: SchemaInputForInfiniteQuery } >, ) => Promise< InfiniteData< - SchemaOutput, - InferCursor + TOutput, + InferCursor > > prefetchQuery: ( - input: SchemaInput, + input: TInput, options?: SetOptional< - FetchQueryOptions>, + FetchQueryOptions, 'queryKey' | 'queryFn' >, ) => Promise @@ -62,108 +57,104 @@ export interface ProcedureUtils< options: PartialOnUndefinedDeep< SetOptional< FetchInfiniteQueryOptions< - SchemaOutput, + TOutput, DefaultError, - SchemaOutput, + TOutput, QueryKey, - InferCursor + InferCursor >, 'queryKey' | 'queryFn' > & { - input: SchemaInputForInfiniteQuery + input: SchemaInputForInfiniteQuery } >, ) => Promise getQueryData: ( - input: SchemaInput, - ) => SchemaOutput | undefined + input: TInput, + ) => TOutput | undefined getInfiniteQueryData: ( - input: SchemaInputForInfiniteQuery, + input: SchemaInputForInfiniteQuery, ) => | InfiniteData< - SchemaOutput, - InferCursor + TOutput, + InferCursor > | undefined ensureQueryData: ( - input: SchemaInput, + input: TInput, options?: SetOptional< - EnsureQueryDataOptions>, + EnsureQueryDataOptions, 'queryFn' | 'queryKey' >, - ) => Promise> + ) => Promise ensureInfiniteQueryData: ( options: PartialOnUndefinedDeep< SetOptional< EnsureInfiniteQueryDataOptions< - SchemaOutput, + TOutput, DefaultError, - SchemaOutput, + TOutput, QueryKey, - InferCursor + InferCursor >, 'queryKey' | 'queryFn' > & { - input: SchemaInputForInfiniteQuery + input: SchemaInputForInfiniteQuery } >, ) => Promise< InfiniteData< - SchemaOutput, - InferCursor + TOutput, + InferCursor > > getQueryState: ( - input: SchemaInput, - ) => QueryState> | undefined + input: TInput, + ) => QueryState | undefined getInfiniteQueryState: ( - input: SchemaInputForInfiniteQuery, + input: SchemaInputForInfiniteQuery, ) => | QueryState< InfiniteData< - SchemaOutput, - InferCursor + TOutput, + InferCursor > > | undefined setQueryData: ( - input: SchemaInput, + input: TInput, updater: Updater< - SchemaOutput | undefined, - SchemaOutput | undefined + TOutput | undefined, + TOutput | undefined >, options?: SetDataOptions, - ) => SchemaOutput | undefined + ) => TOutput | undefined setInfiniteQueryData: ( - input: SchemaInputForInfiniteQuery, + input: SchemaInputForInfiniteQuery, updater: Updater< | InfiniteData< - SchemaOutput, - InferCursor + TOutput, + InferCursor > | undefined, | InfiniteData< - SchemaOutput, - InferCursor + TOutput, + InferCursor > | undefined >, options?: SetDataOptions, ) => | InfiniteData< - SchemaOutput, - InferCursor + TOutput, + InferCursor > | undefined } -export interface CreateProcedureUtilsOptions< - TInputSchema extends Schema, - TOutputSchema extends Schema, - TFuncOutput , -> { - client: Caller, SchemaOutput> +export interface CreateProcedureUtilsOptions { + client: ProcedureClient queryClient: QueryClient /** @@ -172,17 +163,9 @@ export interface CreateProcedureUtilsOptions< path: string[] } -export function createProcedureUtils< - TInputSchema extends Schema, - TOutputSchema extends Schema, - TFuncOutput extends SchemaOutput, ->( - options: CreateProcedureUtilsOptions< - TInputSchema, - TOutputSchema, - TFuncOutput - >, -): ProcedureUtils { +export function createProcedureUtils( + options: CreateProcedureUtilsOptions, +): ProcedureUtils { return { fetchQuery(input, options_) { return options.queryClient.fetchQuery({ diff --git a/packages/react/src/react-context.ts b/packages/react/src/react-context.ts index 18939605b..b895fab6a 100644 --- a/packages/react/src/react-context.ts +++ b/packages/react/src/react-context.ts @@ -1,29 +1,23 @@ -import type { RouterClient } from '@orpc/client' -import type { ContractRouter } from '@orpc/contract' -import type { Router } from '@orpc/server' +import type { RouterClient } from '@orpc/server' import type { QueryClient } from '@tanstack/react-query' import { type Context, createContext, useContext } from 'react' -export interface ORPCContextValue< - TRouter extends ContractRouter | Router, -> { - client: RouterClient +export interface ORPCContextValue> { + client: T queryClient: QueryClient } -export type ORPCContext> = Context< - ORPCContextValue | undefined +export type ORPCContext> = Context< + ORPCContextValue | undefined > -export function createORPCContext< - TRouter extends ContractRouter | Router, ->(): ORPCContext { +export function createORPCContext>(): ORPCContext { return createContext(undefined as any) } -export function useORPCContext>( - context: ORPCContext, -): ORPCContextValue { +export function useORPCContext>( + context: ORPCContext, +): ORPCContextValue { const value = useContext(context) if (!value) { diff --git a/packages/react/src/react-hooks.ts b/packages/react/src/react-hooks.ts index 1be8c05be..630ddbd64 100644 --- a/packages/react/src/react-hooks.ts +++ b/packages/react/src/react-hooks.ts @@ -1,39 +1,18 @@ -import type { - ContractProcedure, - ContractRouter, - SchemaOutput, -} from '@orpc/contract' -import type { Lazy, Procedure, Router } from '@orpc/server' +import type { ProcedureClient, RouterClient } from '@orpc/server' import type { ORPCContext } from './react-context' import { createGeneralHooks, type GeneralHooks } from './general-hooks' import { orpcPathSymbol } from './orpc-path' import { createProcedureHooks, type ProcedureHooks } from './procedure-hooks' -export type ORPCHooksWithContractRouter = { - [K in keyof TRouter]: TRouter[K] extends ContractProcedure< - infer UInputSchema, - infer UOutputSchema - > - ? ProcedureHooks> & GeneralHooks> - : TRouter[K] extends ContractRouter - ? ORPCHooksWithContractRouter - : never -} & GeneralHooks +export type ORPCHooks> = + T extends ProcedureClient + ? ProcedureHooks & GeneralHooks + : { + [K in keyof T]: T[K] extends RouterClient ? ORPCHooks : never + } & GeneralHooks -export type ORPCHooksWithRouter> = { - [K in keyof TRouter]: TRouter[K] extends - | Procedure - | Lazy> - ? ProcedureHooks & GeneralHooks - : TRouter[K] extends Router - ? ORPCHooksWithRouter - : never -} & GeneralHooks - -export interface CreateORPCHooksOptions< - TRouter extends ContractRouter | Router, -> { - context: ORPCContext +export interface CreateORPCHooksOptions> { + context: ORPCContext /** * The path of the router. @@ -43,23 +22,16 @@ export interface CreateORPCHooksOptions< path?: string[] } -export function createORPCHooks>( - options: CreateORPCHooksOptions, -): TRouter extends Router - ? ORPCHooksWithRouter - : TRouter extends ContractRouter - ? ORPCHooksWithContractRouter - : never { +export function createORPCHooks>( + options: CreateORPCHooksOptions, +): ORPCHooks { const path = options.path ?? [] const generalHooks = createGeneralHooks({ context: options.context, path }) - // for sure root is not procedure, so do not it procedure hooks on root - const procedureHooks = path.length - ? createProcedureHooks({ - context: options.context, - path, - }) - : {} + const procedureHooks = createProcedureHooks({ + context: options.context, + path, + }) return new Proxy( { diff --git a/packages/react/src/react-utils.ts b/packages/react/src/react-utils.ts index c2ba056e9..d43720571 100644 --- a/packages/react/src/react-utils.ts +++ b/packages/react/src/react-utils.ts @@ -1,35 +1,17 @@ -import type { - ContractProcedure, - ContractRouter, - SchemaOutput, -} from '@orpc/contract' -import type { Lazy, Procedure, Router } from '@orpc/server' +import type { ProcedureClient, RouterClient } from '@orpc/server' import type { ORPCContextValue } from './react-context' import { createGeneralUtils, type GeneralUtils } from './general-utils' import { createProcedureUtils, type ProcedureUtils } from './procedure-utils' -export type ORPCUtilsWithContractRouter = { - [K in keyof TRouter]: TRouter[K] extends ContractProcedure - ? ProcedureUtils> & GeneralUtils> - : TRouter[K] extends ContractRouter - ? ORPCUtilsWithContractRouter - : never -} & GeneralUtils +export type ORPCUtils> = + T extends ProcedureClient + ? ProcedureUtils & GeneralUtils + : { + [K in keyof T]: T[K] extends RouterClient ? ORPCUtils : never + } & GeneralUtils -export type ORPCUtilsWithRouter> = { - [K in keyof TRouter]: TRouter[K] extends - | Procedure - | Lazy> - ? ProcedureUtils & GeneralUtils - : TRouter[K] extends Router - ? ORPCUtilsWithRouter - : never -} & GeneralUtils - -export interface CreateORPCUtilsOptions< - TRouter extends ContractRouter | Router, -> { - contextValue: ORPCContextValue +export interface CreateORPCUtilsOptions> { + contextValue: ORPCContextValue /** * The path of the router. @@ -39,13 +21,9 @@ export interface CreateORPCUtilsOptions< path?: string[] } -export function createORPCUtils>( - options: CreateORPCUtilsOptions, -): TRouter extends Router - ? ORPCUtilsWithRouter - : TRouter extends ContractRouter - ? ORPCUtilsWithContractRouter - : never { +export function createORPCUtils>( + options: CreateORPCUtilsOptions, +): ORPCUtils { const path = options.path ?? [] const client = options.contextValue.client as any diff --git a/packages/react/src/react.tsx b/packages/react/src/react.tsx index b865de4fd..8efdc08d6 100644 --- a/packages/react/src/react.tsx +++ b/packages/react/src/react.tsx @@ -1,52 +1,25 @@ -import type { ContractRouter } from '@orpc/contract' -import type { Router } from '@orpc/server' -import { - createORPCContext, - type ORPCContext, - type ORPCContextValue, - useORPCContext, -} from './react-context' -import { - createORPCHooks, - type ORPCHooksWithContractRouter, - type ORPCHooksWithRouter, -} from './react-hooks' -import { - createORPCUtils, - type ORPCUtilsWithContractRouter, - type ORPCUtilsWithRouter, -} from './react-utils' -import { - useQueriesFactory, - type UseQueriesWithContractRouter, - type UseQueriesWithRouter, -} from './use-queries/hook' +import type { RouterClient } from '@orpc/server' +import type { ORPCContext, ORPCContextValue } from './react-context' +import type { ORPCHooks } from './react-hooks' +import type { ORPCUtils } from './react-utils' +import type { UseQueries } from './use-queries/hook' +import { createORPCContext, useORPCContext } from './react-context' +import { createORPCHooks } from './react-hooks' +import { createORPCUtils } from './react-utils' +import { useQueriesFactory } from './use-queries/hook' -export type ORPCReactWithContractRouter = - ORPCHooksWithContractRouter & { - useContext: () => ORPCContextValue - useUtils: () => ORPCUtilsWithContractRouter - useQueries: UseQueriesWithContractRouter +export type ORPCReact> = + ORPCHooks & { + useContext: () => ORPCContextValue + useUtils: () => ORPCUtils + useQueries: UseQueries } -export type ORPCReactWithRouter> = - ORPCHooksWithRouter & { - useContext: () => ORPCContextValue - useUtils: () => ORPCUtilsWithRouter - useQueries: UseQueriesWithRouter - } - -export function createORPCReact< - TRouter extends ContractRouter | Router, ->(): { - orpc: TRouter extends Router - ? ORPCReactWithRouter - : TRouter extends ContractRouter - ? ORPCReactWithContractRouter - : never - ORPCContext: ORPCContext +export function createORPCReact>(): { + orpc: ORPCReact + ORPCContext: ORPCContext } { - const Context = createORPCContext() + const Context = createORPCContext() const useContext = () => useORPCContext(Context) const useUtils = () => createORPCUtils({ contextValue: useContext() }) // eslint-disable-next-line react-hooks/rules-of-hooks @@ -70,7 +43,7 @@ export function createORPCReact< return new Proxy(value, { get(_, key) { - return Reflect.get(nextHooks, key) + return Reflect.get(nextHooks as any, key) }, }) }, diff --git a/packages/react/src/tanstack-key.ts b/packages/react/src/tanstack-key.ts index 723b49d85..14038ca49 100644 --- a/packages/react/src/tanstack-key.ts +++ b/packages/react/src/tanstack-key.ts @@ -1,11 +1,7 @@ -import type { SchemaInput } from '@orpc/contract' import type { PartialDeep } from '@orpc/shared' import type { MutationKey, QueryKey } from '@tanstack/react-query' import type { ProcedureHooks } from './procedure-hooks' -import type { - ORPCHooksWithContractRouter, - ORPCHooksWithRouter, -} from './react-hooks' +import type { ORPCHooks } from './react-hooks' import { getORPCPath } from './orpc-path' export type QueryType = 'query' | 'infinite' | undefined @@ -17,14 +13,13 @@ export interface GetQueryKeyOptions { export function getQueryKey< T extends - | ORPCHooksWithContractRouter - | ORPCHooksWithRouter - | ProcedureHooks, + | ORPCHooks + | ProcedureHooks, >( orpc: T, options?: GetQueryKeyOptions< - T extends ProcedureHooks - ? PartialDeep> + T extends ProcedureHooks + ? PartialDeep : unknown >, ): QueryKey { @@ -51,9 +46,8 @@ export function getQueryKeyFromPath( export function getMutationKey< T extends - | ORPCHooksWithContractRouter - | ORPCHooksWithRouter - | ProcedureHooks, + | ORPCHooks + | ProcedureHooks, >(orpc: T): MutationKey { const path = getORPCPath(orpc) return getMutationKeyFromPath(path) diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index 754b2874c..aee47a5bd 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -1,9 +1,3 @@ -import type { Schema, SchemaInput } from '@orpc/contract' +export type SchemaInputForInfiniteQuery = Omit -export type SchemaInputForInfiniteQuery = Omit< - SchemaInput, - 'cursor' -> - -export type InferCursor = - SchemaInput extends { cursor?: any } ? SchemaInput['cursor'] : never +export type InferCursor = TInput extends { cursor?: any } ? TInput['cursor'] : never diff --git a/packages/react/src/use-queries/builder.test-d.ts b/packages/react/src/use-queries/builder.test-d.ts index 28195617f..3998456b0 100644 --- a/packages/react/src/use-queries/builder.test-d.ts +++ b/packages/react/src/use-queries/builder.test-d.ts @@ -1,11 +1,9 @@ -import type { SchemaOutput } from '@orpc/contract' import type { Promisable } from '@orpc/shared' -import type { UserFindInputSchema, UserSchema } from '../../tests/orpc' import { orpcClient } from '../../tests/orpc' import { createUseQueriesBuilder } from './builder' it('createUseQueriesBuilder', () => { - const builder = createUseQueriesBuilder>({ + const builder = createUseQueriesBuilder({ client: orpcClient.user.find, path: ['user', 'find'], }) diff --git a/packages/react/src/use-queries/builder.test.ts b/packages/react/src/use-queries/builder.test.ts index abb40ae02..769d41da4 100644 --- a/packages/react/src/use-queries/builder.test.ts +++ b/packages/react/src/use-queries/builder.test.ts @@ -1,10 +1,8 @@ -import type { SchemaOutput } from '@orpc/contract' -import type { UserFindInputSchema, UserSchema } from '../../tests/orpc' import { orpcClient } from '../../tests/orpc' import { createUseQueriesBuilder } from './builder' it('createUseQueriesBuilder', async () => { - const builder = createUseQueriesBuilder>({ + const builder = createUseQueriesBuilder({ client: orpcClient.user.find, path: ['user', 'find'], }) diff --git a/packages/react/src/use-queries/builder.ts b/packages/react/src/use-queries/builder.ts index 73e9a2bd6..46b59c283 100644 --- a/packages/react/src/use-queries/builder.ts +++ b/packages/react/src/use-queries/builder.ts @@ -1,5 +1,4 @@ -import type { Schema, SchemaInput, SchemaOutput } from '@orpc/contract' -import type { Caller } from '@orpc/server' +import type { ProcedureClient } from '@orpc/server' import type { SetOptional } from '@orpc/shared' import type { DefaultError, @@ -22,26 +21,18 @@ type UseQueryOptionsForUseQueries< placeholderData?: TQueryFnData | QueriesPlaceholderDataFunction } -export interface UseQueriesBuilder< - TInputSchema extends Schema, - TOutputSchema extends Schema, - TFuncOutput extends SchemaOutput, -> { +export interface UseQueriesBuilder { ( - input: SchemaInput, + input: TInput, options?: SetOptional< - UseQueryOptionsForUseQueries>, + UseQueryOptionsForUseQueries, 'queryFn' | 'queryKey' >, - ): UseQueryOptionsForUseQueries> + ): UseQueryOptionsForUseQueries } -export interface CreateUseQueriesBuilderOptions< - TInputSchema extends Schema, - TOutputSchema extends Schema, - TFuncOutput extends SchemaOutput, -> { - client: Caller, SchemaOutput> +export interface CreateUseQueriesBuilderOptions { + client: ProcedureClient /** * The path of procedure on server @@ -49,18 +40,9 @@ export interface CreateUseQueriesBuilderOptions< path: string[] } -export function createUseQueriesBuilder< - TInputSchema extends Schema = undefined, - TOutputSchema extends Schema = undefined, - TFuncOutput extends - SchemaOutput = SchemaOutput, ->( - options: CreateUseQueriesBuilderOptions< - TInputSchema, - TOutputSchema, - TFuncOutput - >, -): UseQueriesBuilder { +export function createUseQueriesBuilder( + options: CreateUseQueriesBuilderOptions, +): UseQueriesBuilder { return (input, options_) => { return { queryKey: getQueryKeyFromPath(options.path, { input, type: 'query' }), diff --git a/packages/react/src/use-queries/builders.test.tsx b/packages/react/src/use-queries/builders.test.tsx index 8bc22be2d..d0ee0d18f 100644 --- a/packages/react/src/use-queries/builders.test.tsx +++ b/packages/react/src/use-queries/builders.test.tsx @@ -1,16 +1,14 @@ -import type { SchemaOutput } from '@orpc/contract' -import type { appRouter, UserFindInputSchema, UserListInputSchema, UserListOutputSchema, UserSchema } from '../../tests/orpc' import { orpcClient } from '../../tests/orpc' import { createUseQueriesBuilder } from './builder' import { createUseQueriesBuilders } from './builders' it('createUseQueriesBuilders', async () => { - const builder = createUseQueriesBuilders({ + const builder = createUseQueriesBuilders({ client: orpcClient, }) const e1 = builder.user.find({ id: '123' }) - const a1 = createUseQueriesBuilder>({ + const a1 = createUseQueriesBuilder({ client: orpcClient.user.find, path: ['user', 'find'], })({ id: '123' }) @@ -20,7 +18,7 @@ it('createUseQueriesBuilders', async () => { expect(await (e1 as any).queryFn({})).toEqual(await (a1 as any).queryFn({})) const e2 = builder.user.list({}) - const a2 = createUseQueriesBuilder>({ + const a2 = createUseQueriesBuilder({ client: orpcClient.user.list, path: ['user', 'list'], })({}) diff --git a/packages/react/src/use-queries/builders.ts b/packages/react/src/use-queries/builders.ts index 3ded05c5e..afed01a7c 100644 --- a/packages/react/src/use-queries/builders.ts +++ b/packages/react/src/use-queries/builders.ts @@ -1,48 +1,16 @@ -import type { RouterClient } from '@orpc/client' -import type { - ContractProcedure, - ContractRouter, - SchemaOutput, -} from '@orpc/contract' -import type { Procedure, Router } from '@orpc/server' +import type { ProcedureClient, RouterClient } from '@orpc/server' import type {} from '@tanstack/react-query' import { createUseQueriesBuilder, type UseQueriesBuilder } from './builder' -export type UseQueriesBuildersWithContractRouter< - TRouter extends ContractRouter, -> = { - [K in keyof TRouter]: TRouter[K] extends ContractProcedure< - infer UInputSchema, - infer UOutputSchema - > - ? UseQueriesBuilder< - UInputSchema, - UOutputSchema, - SchemaOutput - > - : TRouter[K] extends ContractRouter - ? UseQueriesBuildersWithContractRouter - : never -} - -export type UseQueriesBuildersWithRouter> = { - [K in keyof TRouter]: TRouter[K] extends Procedure< - any, - any, - infer UInputSchema, - infer UOutputSchema, - infer UFuncOutput - > - ? UseQueriesBuilder - : TRouter[K] extends Router - ? UseQueriesBuildersWithRouter - : never -} +export type UseQueriesBuilders> = + T extends ProcedureClient + ? UseQueriesBuilder + : { + [K in keyof T]: T[K] extends RouterClient ? UseQueriesBuilders : never + } -export interface CreateUseQueriesBuildersOptions< - TRouter extends Router | ContractRouter, -> { - client: RouterClient +export interface CreateUseQueriesBuildersOptions> { + client: T /** * The path of router on server @@ -50,15 +18,9 @@ export interface CreateUseQueriesBuildersOptions< path?: string[] } -export function createUseQueriesBuilders< - TRouter extends Router | ContractRouter, ->( - options: CreateUseQueriesBuildersOptions, -): TRouter extends Router - ? UseQueriesBuildersWithRouter - : TRouter extends ContractRouter - ? UseQueriesBuildersWithContractRouter - : never { +export function createUseQueriesBuilders>( + options: CreateUseQueriesBuildersOptions, +): UseQueriesBuilders { const path = options.path ?? [] const client = options.client as any diff --git a/packages/react/src/use-queries/hook.ts b/packages/react/src/use-queries/hook.ts index 3ebb1b50e..07899e125 100644 --- a/packages/react/src/use-queries/hook.ts +++ b/packages/react/src/use-queries/hook.ts @@ -1,51 +1,33 @@ -import type { ContractRouter } from '@orpc/contract' -import type { Router } from '@orpc/server' -import { - type QueriesOptions, - type QueriesResults, - useQueries, +import type { RouterClient } from '@orpc/server' +import type { + QueriesOptions, + QueriesResults, } from '@tanstack/react-query' -import { type ORPCContext, useORPCContext } from '../react-context' -import { - createUseQueriesBuilders, - type UseQueriesBuildersWithContractRouter, - type UseQueriesBuildersWithRouter, -} from './builders' +import type { ORPCContext } from '../react-context' +import type { UseQueriesBuilders } from './builders' +import { useQueries } from '@tanstack/react-query' +import { useORPCContext } from '../react-context' +import { createUseQueriesBuilders } from './builders' -export interface UseQueriesWithContractRouter { - = [], TCombinedResult = QueriesResults>( +export interface UseQueries> { + = [], UCombinedResult = QueriesResults>( build: ( - builders: UseQueriesBuildersWithContractRouter, - ) => [...QueriesOptions], - combine?: (result: QueriesResults) => TCombinedResult, - ): TCombinedResult + builders: UseQueriesBuilders, + ) => [...QueriesOptions], + combine?: (result: QueriesResults) => UCombinedResult, + ): UCombinedResult } -export interface UseQueriesWithRouter> { - = [], TCombinedResult = QueriesResults>( - build: ( - builders: UseQueriesBuildersWithRouter, - ) => [...QueriesOptions], - combine?: (result: QueriesResults) => TCombinedResult, - ): TCombinedResult -} - -export interface UseQueriesFactoryOptions< - TRouter extends Router | ContractRouter, -> { - context: ORPCContext +export interface UseQueriesFactoryOptions> { + context: ORPCContext } -export function useQueriesFactory | ContractRouter>( - options: UseQueriesFactoryOptions, -): TRouter extends Router - ? UseQueriesWithRouter - : TRouter extends ContractRouter - ? UseQueriesWithContractRouter - : never { +export function useQueriesFactory>( + options: UseQueriesFactoryOptions, +): UseQueries { const Hook = (build: any, combine?: any): any => { const orpc = useORPCContext(options.context) - const builders = createUseQueriesBuilders({ client: orpc.client as any }) + const builders = createUseQueriesBuilders({ client: orpc.client }) return useQueries({ queries: build(builders), @@ -53,5 +35,5 @@ export function useQueriesFactory | ContractRouter>( }) } - return Hook as any + return Hook } diff --git a/packages/react/tests/orpc.tsx b/packages/react/tests/orpc.tsx index 3547426e9..e45b9a60f 100644 --- a/packages/react/tests/orpc.tsx +++ b/packages/react/tests/orpc.tsx @@ -1,3 +1,4 @@ +import type { RouterClient } from '@orpc/server' import { createORPCFetchClient } from '@orpc/client' import { os } from '@orpc/server' import { createORPCHandler, handleFetchRequest } from '@orpc/server/fetch' @@ -107,7 +108,7 @@ export const orpcClient = createORPCFetchClient({ }, }) -export const { orpc, ORPCContext } = createORPCReact() +export const { orpc, ORPCContext } = createORPCReact>() export const queryClient = new QueryClient({ defaultOptions: { From ea1333ebcdf35255263feda6921c08536e0069d4 Mon Sep 17 00:00:00 2001 From: unnoq Date: Fri, 20 Dec 2024 15:54:57 +0700 Subject: [PATCH 48/51] sync rest --- apps/content/examples/react-query.ts | 3 +- apps/content/examples/react.ts | 5 +-- apps/content/examples/vue-query.ts | 3 +- packages/react/src/general-utils.test.tsx | 41 +++++++++---------- packages/react/src/react.test-d.ts | 33 ++++++++------- .../react/src/use-queries/builders.test-d.ts | 6 +-- packages/shared/src/value.ts | 2 +- 7 files changed, 45 insertions(+), 48 deletions(-) diff --git a/apps/content/examples/react-query.ts b/apps/content/examples/react-query.ts index d3b20b7ca..74ac0a88c 100644 --- a/apps/content/examples/react-query.ts +++ b/apps/content/examples/react-query.ts @@ -1,4 +1,5 @@ +import type { RouterClient } from '@orpc/server' import type { router } from 'examples/server' import { createORPCReactQueryUtils } from '@orpc/react-query' -export const orpc = createORPCReactQueryUtils('fake-client' as any) +export const orpc = createORPCReactQueryUtils({} as RouterClient) diff --git a/apps/content/examples/react.ts b/apps/content/examples/react.ts index bde0496b1..516262c77 100644 --- a/apps/content/examples/react.ts +++ b/apps/content/examples/react.ts @@ -1,6 +1,5 @@ +import type { RouterClient } from '@orpc/server' import type { router } from 'examples/server' import { createORPCReact } from '@orpc/react' -// biome-ignore lint/correctness/noUnusedImports: -export const { orpc, ORPCContext } - = createORPCReact() +export const { orpc, ORPCContext } = createORPCReact>() diff --git a/apps/content/examples/vue-query.ts b/apps/content/examples/vue-query.ts index 71dee1231..29a0fcb4c 100644 --- a/apps/content/examples/vue-query.ts +++ b/apps/content/examples/vue-query.ts @@ -1,4 +1,5 @@ +import type { RouterClient } from '@orpc/server' import type { router } from 'examples/server' import { createORPCVueQueryUtils } from '@orpc/vue-query' -export const orpc = createORPCVueQueryUtils('fake-client' as any) +export const orpc = createORPCVueQueryUtils({} as RouterClient) diff --git a/packages/react/src/general-utils.test.tsx b/packages/react/src/general-utils.test.tsx index 3786d32ef..469e47d9a 100644 --- a/packages/react/src/general-utils.test.tsx +++ b/packages/react/src/general-utils.test.tsx @@ -1,4 +1,4 @@ -import type { SchemaOutput } from '@orpc/contract' +import type { SchemaInput, SchemaOutput } from '@orpc/contract' import type { UserCreateInputSchema, UserFindInputSchema, @@ -15,6 +15,14 @@ import { import { renderHook } from '@testing-library/react' import { createGeneralUtils } from './general-utils' +type UserFindInput = SchemaInput +type User = SchemaOutput + +type UserListInput = SchemaInput +type UserListOutput = SchemaOutput + +type UserCreateInput = SchemaInput + let qc = new QueryClient() let user_utils = createGeneralUtils({ @@ -22,28 +30,22 @@ let user_utils = createGeneralUtils({ path: ['user'], }) -let user_find_utils = createGeneralUtils< - typeof UserFindInputSchema, - typeof UserSchema, - SchemaOutput ->({ +let user_find_utils = createGeneralUtils({ queryClient: qc, path: ['user', 'find'], }) let user_list_utils = createGeneralUtils< - typeof UserListInputSchema, - typeof UserListOutputSchema, - SchemaOutput + UserListInput, + UserListOutput >({ queryClient: qc, path: ['user', 'list'], }) let user_create_utils = createGeneralUtils< - typeof UserCreateInputSchema, - typeof UserSchema, - SchemaOutput + UserCreateInput, + User >({ queryClient: qc, path: ['user', 'create'], @@ -57,27 +59,24 @@ beforeEach(() => { }) user_find_utils = createGeneralUtils< - typeof UserFindInputSchema, - typeof UserSchema, - SchemaOutput + UserFindInput, + User >({ queryClient: qc, path: ['user', 'find'], }) user_list_utils = createGeneralUtils< - typeof UserListInputSchema, - typeof UserListOutputSchema, - SchemaOutput + UserListInput, + UserListOutput >({ queryClient: qc, path: ['user', 'list'], }) user_create_utils = createGeneralUtils< - typeof UserCreateInputSchema, - typeof UserSchema, - SchemaOutput + UserCreateInput, + User >({ queryClient: qc, path: ['user', 'create'], diff --git a/packages/react/src/react.test-d.ts b/packages/react/src/react.test-d.ts index efbc95d5a..caa5ba92e 100644 --- a/packages/react/src/react.test-d.ts +++ b/packages/react/src/react.test-d.ts @@ -1,4 +1,4 @@ -import type { SchemaOutput } from '@orpc/contract' +import type { SchemaInput, SchemaOutput } from '@orpc/contract' import type { QueryClient } from '@tanstack/react-query' import type { GeneralHooks } from './general-hooks' import type { GeneralUtils } from './general-utils' @@ -13,33 +13,34 @@ import { } from '../tests/orpc' import { useQueriesFactory } from './use-queries/hook' +type UserFindInput = SchemaInput +type User = SchemaOutput + describe('useUtils', () => { const utils = orpc.useUtils() it('router level', () => { expectTypeOf(utils).toMatchTypeOf< - GeneralUtils + GeneralUtils >() expectTypeOf(utils.user).toMatchTypeOf< - GeneralUtils + GeneralUtils >() }) it('procedure level', () => { expectTypeOf(utils.user.find).toMatchTypeOf< GeneralUtils< - typeof UserFindInputSchema, - typeof UserSchema, - SchemaOutput + UserFindInput, + User > >() expectTypeOf(utils.user.find).toMatchTypeOf< ProcedureUtils< - typeof UserFindInputSchema, - typeof UserSchema, - SchemaOutput + UserFindInput, + User > >() }) @@ -61,28 +62,26 @@ it('useQueries', () => { describe('hooks', () => { it('router level', () => { expectTypeOf(orpc).toMatchTypeOf< - GeneralHooks + GeneralHooks >() expectTypeOf(orpc.user).toMatchTypeOf< - GeneralHooks + GeneralHooks >() }) it('procedure level', () => { expectTypeOf(orpc.user.find).toMatchTypeOf< GeneralHooks< - typeof UserFindInputSchema, - typeof UserSchema, - SchemaOutput + UserFindInput, + User > >() expectTypeOf(orpc.user.find).toMatchTypeOf< ProcedureHooks< - typeof UserFindInputSchema, - typeof UserSchema, - SchemaOutput + UserFindInput, + User > >() }) diff --git a/packages/react/src/use-queries/builders.test-d.ts b/packages/react/src/use-queries/builders.test-d.ts index 62cccc85f..5becb2803 100644 --- a/packages/react/src/use-queries/builders.test-d.ts +++ b/packages/react/src/use-queries/builders.test-d.ts @@ -1,5 +1,3 @@ -import type { SchemaOutput } from '@orpc/contract' -import type { UserFindInputSchema, UserListInputSchema, UserListOutputSchema, UserSchema } from '../../tests/orpc' import { orpcClient } from '../../tests/orpc' import { createUseQueriesBuilder } from './builder' import { createUseQueriesBuilders } from './builders' @@ -10,14 +8,14 @@ it('createUseQueriesBuilders', () => { }) expectTypeOf(builder.user.find).toEqualTypeOf( - createUseQueriesBuilder>({ + createUseQueriesBuilder({ client: orpcClient.user.find, path: ['user', 'find'], }), ) expectTypeOf(builder.user.list).toEqualTypeOf( - createUseQueriesBuilder>({ + createUseQueriesBuilder({ client: orpcClient.user.list, path: ['user', 'list'], }), diff --git a/packages/shared/src/value.ts b/packages/shared/src/value.ts index d259f077c..bfd096f64 100644 --- a/packages/shared/src/value.ts +++ b/packages/shared/src/value.ts @@ -2,7 +2,7 @@ import type { Promisable } from 'type-fest' export type Value = T | (() => Promisable) -export function value(value: Value): Promise { +export function value>(value: T): Promise ? U : never> { if (typeof value === 'function') { return (value as any)() } From 1be1bc6a990a2d62f62aa47fbd1ec05cb9abfa53 Mon Sep 17 00:00:00 2001 From: unnoq Date: Fri, 20 Dec 2024 16:07:19 +0700 Subject: [PATCH 49/51] sync playground --- packages/server/src/builder.test-d.ts | 4 ++-- packages/server/src/builder.test.ts | 4 ++-- packages/server/src/builder.ts | 2 +- playgrounds/contract-openapi/src/contract/index.ts | 4 ++-- playgrounds/contract-openapi/src/orpc.ts | 5 ++++- .../contract-openapi/src/playground-client.ts | 4 ++-- .../contract-openapi/src/playground-react.ts | 3 ++- playgrounds/expressjs/src/orpc.ts | 5 ++++- playgrounds/expressjs/src/playground-client.ts | 4 ++-- playgrounds/expressjs/src/playground-react.ts | 3 ++- playgrounds/expressjs/src/router/index.ts | 4 ++-- playgrounds/nextjs/src/app/api/[...rest]/router.ts | 4 ++-- playgrounds/nextjs/src/lib/orpc.ts | 7 +++---- playgrounds/nextjs/src/orpc.ts | 14 +++++++------- playgrounds/nuxt/lib/orpc.ts | 4 ++-- playgrounds/nuxt/server/orpc.ts | 5 ++++- playgrounds/nuxt/server/router/index.ts | 4 ++-- playgrounds/openapi/src/orpc.ts | 5 ++++- playgrounds/openapi/src/playground-react.ts | 3 ++- playgrounds/openapi/src/router/index.ts | 4 ++-- 20 files changed, 53 insertions(+), 39 deletions(-) diff --git a/packages/server/src/builder.test-d.ts b/packages/server/src/builder.test-d.ts index f4c376393..54d825ea5 100644 --- a/packages/server/src/builder.test-d.ts +++ b/packages/server/src/builder.test-d.ts @@ -140,12 +140,12 @@ describe('to RouterBuilder', () => { }) it('tags', () => { - expectTypeOf(builder.tags('test', 'test2')).toEqualTypeOf< + expectTypeOf(builder.tag('test', 'test2')).toEqualTypeOf< RouterBuilder<{ auth: boolean }, { db: string }> >() // @ts-expect-error invalid tags - builder.tags(123) + builder.tag(123) }) }) diff --git a/packages/server/src/builder.test.ts b/packages/server/src/builder.test.ts index d14fb50aa..e00e9d79b 100644 --- a/packages/server/src/builder.test.ts +++ b/packages/server/src/builder.test.ts @@ -120,9 +120,9 @@ describe('to RouterBuilder', () => { })) }) - it('tags', () => { + it('tag', () => { vi.mocked(RouterBuilder).mockReturnValueOnce({ mocked: true } as any) - expect(builder.tags('tag1', 'tag2')).toEqual({ mocked: true }) + expect(builder.tag('tag1', 'tag2')).toEqual({ mocked: true }) expect(RouterBuilder).toBeCalledTimes(1) expect(RouterBuilder).toBeCalledWith(expect.objectContaining({ diff --git a/packages/server/src/builder.ts b/packages/server/src/builder.ts index 4a09e396b..2b4d1f4b0 100644 --- a/packages/server/src/builder.ts +++ b/packages/server/src/builder.ts @@ -122,7 +122,7 @@ export class Builder { }) } - tags(...tags: string[]): RouterBuilder { + tag(...tags: string[]): RouterBuilder { return new RouterBuilder({ middlewares: this['~orpc'].middlewares, tags, diff --git a/playgrounds/contract-openapi/src/contract/index.ts b/playgrounds/contract-openapi/src/contract/index.ts index a6a11d03c..fc4e6f4e7 100644 --- a/playgrounds/contract-openapi/src/contract/index.ts +++ b/playgrounds/contract-openapi/src/contract/index.ts @@ -10,7 +10,7 @@ import { } from './planet' export const contract = oc.router({ - auth: oc.tags('Authentication').prefix('/auth').router({ + auth: oc.tag('Authentication').prefix('/auth').router({ signup, signin, refresh, @@ -18,7 +18,7 @@ export const contract = oc.router({ me, }), - planet: oc.tags('Planets').prefix('/planets').router({ + planet: oc.tag('Planets').prefix('/planets').router({ list: listPlanets, create: createPlanet, find: findPlanet, diff --git a/playgrounds/contract-openapi/src/orpc.ts b/playgrounds/contract-openapi/src/orpc.ts index 11a2d2fef..26b802015 100644 --- a/playgrounds/contract-openapi/src/orpc.ts +++ b/playgrounds/contract-openapi/src/orpc.ts @@ -3,7 +3,10 @@ import type { UserSchema } from './schemas/user' import { ORPCError, os } from '@orpc/server' import { contract } from './contract' -export type ORPCContext = { user?: z.infer, db: any } +export interface ORPCContext { + user?: z.infer + db?: any +} const base = os.context().use(async (input, context, meta) => { const start = Date.now() diff --git a/playgrounds/contract-openapi/src/playground-client.ts b/playgrounds/contract-openapi/src/playground-client.ts index 89f64da41..783b94bfe 100644 --- a/playgrounds/contract-openapi/src/playground-client.ts +++ b/playgrounds/contract-openapi/src/playground-client.ts @@ -3,9 +3,9 @@ */ import type { contract } from './contract' -import { createORPCClient } from '@orpc/client' +import { createORPCFetchClient } from '@orpc/client' -const orpc = createORPCClient({ +const orpc = createORPCFetchClient({ baseURL: 'http://localhost:3000/api', }) diff --git a/playgrounds/contract-openapi/src/playground-react.ts b/playgrounds/contract-openapi/src/playground-react.ts index 28f7ee0d0..695bb07a8 100644 --- a/playgrounds/contract-openapi/src/playground-react.ts +++ b/playgrounds/contract-openapi/src/playground-react.ts @@ -2,10 +2,11 @@ * This file is where you can play with type of oRPC React. */ +import type { RouterClient } from '@orpc/server' import type { contract } from './contract' import { createORPCReact } from '@orpc/react' -const { orpc } = createORPCReact() +const { orpc } = createORPCReact>() const listQuery = orpc.planet.list.useInfiniteQuery({ input: {}, diff --git a/playgrounds/expressjs/src/orpc.ts b/playgrounds/expressjs/src/orpc.ts index d9631dcc7..6f03d3a64 100644 --- a/playgrounds/expressjs/src/orpc.ts +++ b/playgrounds/expressjs/src/orpc.ts @@ -2,7 +2,10 @@ import type { z } from 'zod' import type { UserSchema } from './schemas/user' import { ORPCError, os } from '@orpc/server' -export type ORPCContext = { user?: z.infer, db: any } +export interface ORPCContext { + user?: z.infer + db?: any +} export const pub = os.context().use(async (input, context, meta) => { const start = Date.now() diff --git a/playgrounds/expressjs/src/playground-client.ts b/playgrounds/expressjs/src/playground-client.ts index fe7af2ba5..b53f16cee 100644 --- a/playgrounds/expressjs/src/playground-client.ts +++ b/playgrounds/expressjs/src/playground-client.ts @@ -3,9 +3,9 @@ */ import type { router } from './router' -import { createORPCClient } from '@orpc/client' +import { createORPCFetchClient } from '@orpc/client' -const orpc = createORPCClient({ +const orpc = createORPCFetchClient({ baseURL: 'http://localhost:3000/api', }) diff --git a/playgrounds/expressjs/src/playground-react.ts b/playgrounds/expressjs/src/playground-react.ts index 33e0cb7b5..708f65b83 100644 --- a/playgrounds/expressjs/src/playground-react.ts +++ b/playgrounds/expressjs/src/playground-react.ts @@ -2,10 +2,11 @@ * This file is where you can play with type of oRPC React. */ +import type { RouterClient } from '@orpc/server' import type { router } from './router' import { createORPCReact } from '@orpc/react' -const { orpc } = createORPCReact() +const { orpc } = createORPCReact >() const listQuery = orpc.planet.list.useInfiniteQuery({ input: {}, diff --git a/playgrounds/expressjs/src/router/index.ts b/playgrounds/expressjs/src/router/index.ts index 48dd6d0c2..802e2bd20 100644 --- a/playgrounds/expressjs/src/router/index.ts +++ b/playgrounds/expressjs/src/router/index.ts @@ -10,7 +10,7 @@ import { } from './planet' export const router = pub.router({ - auth: pub.tags('Authentication').prefix('/auth').router({ + auth: pub.tag('Authentication').prefix('/auth').router({ signup, signin, refresh, @@ -18,7 +18,7 @@ export const router = pub.router({ me, }), - planet: pub.tags('Planets').prefix('/planets').router({ + planet: pub.tag('Planets').prefix('/planets').router({ list: listPlanets, create: createPlanet, find: findPlanet, diff --git a/playgrounds/nextjs/src/app/api/[...rest]/router.ts b/playgrounds/nextjs/src/app/api/[...rest]/router.ts index b7aa350af..dc4f9dac6 100644 --- a/playgrounds/nextjs/src/app/api/[...rest]/router.ts +++ b/playgrounds/nextjs/src/app/api/[...rest]/router.ts @@ -10,7 +10,7 @@ import { import { pub } from '@/orpc' export const router = pub.router({ - auth: pub.tags('Authentication').router({ + auth: pub.tag('Authentication').router({ signup, signin, refresh, @@ -18,7 +18,7 @@ export const router = pub.router({ me, }), - planet: pub.tags('Planets').router({ + planet: pub.tag('Planets').router({ list: listPlanets, create: createPlanet, find: findPlanet, diff --git a/playgrounds/nextjs/src/lib/orpc.ts b/playgrounds/nextjs/src/lib/orpc.ts index 4f24f06c4..52a058efd 100644 --- a/playgrounds/nextjs/src/lib/orpc.ts +++ b/playgrounds/nextjs/src/lib/orpc.ts @@ -1,13 +1,12 @@ import type { router } from '@/app/api/[...rest]/router' -import { createORPCClient } from '@orpc/client' - +import { createORPCFetchClient } from '@orpc/client' import { createORPCReact } from '@orpc/react' -export const orpcClient = createORPCClient({ +export const orpcClient = createORPCFetchClient({ baseURL: 'http://localhost:3000/api', headers: () => ({ Authorization: 'Bearer default-token', }), }) -export const { orpc, ORPCContext } = createORPCReact() +export const { orpc, ORPCContext } = createORPCReact() diff --git a/playgrounds/nextjs/src/orpc.ts b/playgrounds/nextjs/src/orpc.ts index efccd81ee..8a855608b 100644 --- a/playgrounds/nextjs/src/orpc.ts +++ b/playgrounds/nextjs/src/orpc.ts @@ -20,13 +20,13 @@ const base = os } }) .use(async (input, context, meta) => { - // You can use headers or cookies here to create the user object: - // import { cookies, headers } from 'next/headers' - // const headerList = await headers(); - // const cookieList = await cookies(); - // - // These lines are commented out because Stackblitz has issues with Next.js headers and cookies. - // However, this works fine in a local environment. + // You can use headers or cookies here to create the user object: + // import { cookies, headers } from 'next/headers' + // const headerList = await headers(); + // const cookieList = await cookies(); + // + // These lines are commented out because Stackblitz has issues with Next.js headers and cookies. + // However, this works fine in a local environment. const user = { id: 'test', name: 'John Doe', email: 'john@doe.com' } satisfies z.infer diff --git a/playgrounds/nuxt/lib/orpc.ts b/playgrounds/nuxt/lib/orpc.ts index 6641907fc..274dd6993 100644 --- a/playgrounds/nuxt/lib/orpc.ts +++ b/playgrounds/nuxt/lib/orpc.ts @@ -1,8 +1,8 @@ -import { createORPCClient } from '@orpc/client' +import { createORPCFetchClient } from '@orpc/client' import { createORPCVueQueryUtils } from '@orpc/vue-query' import type { router } from '~/server/router' -export const client = createORPCClient({ +export const client = createORPCFetchClient({ baseURL: 'http://localhost:3000/api', headers: () => ({ Authorization: 'Bearer default-token', diff --git a/playgrounds/nuxt/server/orpc.ts b/playgrounds/nuxt/server/orpc.ts index d9631dcc7..6f03d3a64 100644 --- a/playgrounds/nuxt/server/orpc.ts +++ b/playgrounds/nuxt/server/orpc.ts @@ -2,7 +2,10 @@ import type { z } from 'zod' import type { UserSchema } from './schemas/user' import { ORPCError, os } from '@orpc/server' -export type ORPCContext = { user?: z.infer, db: any } +export interface ORPCContext { + user?: z.infer + db?: any +} export const pub = os.context().use(async (input, context, meta) => { const start = Date.now() diff --git a/playgrounds/nuxt/server/router/index.ts b/playgrounds/nuxt/server/router/index.ts index 831594ffb..46198268b 100644 --- a/playgrounds/nuxt/server/router/index.ts +++ b/playgrounds/nuxt/server/router/index.ts @@ -10,7 +10,7 @@ import { } from './planet' export const router = { - auth: pub.tags('Authentication').prefix('/auth').router({ + auth: pub.tag('Authentication').prefix('/auth').router({ signup, signin, refresh, @@ -18,7 +18,7 @@ export const router = { me, }), - planet: pub.tags('Planets').prefix('/planets').router({ + planet: pub.tag('Planets').prefix('/planets').router({ list: listPlanets, create: createPlanet, find: findPlanet, diff --git a/playgrounds/openapi/src/orpc.ts b/playgrounds/openapi/src/orpc.ts index d9631dcc7..6f03d3a64 100644 --- a/playgrounds/openapi/src/orpc.ts +++ b/playgrounds/openapi/src/orpc.ts @@ -2,7 +2,10 @@ import type { z } from 'zod' import type { UserSchema } from './schemas/user' import { ORPCError, os } from '@orpc/server' -export type ORPCContext = { user?: z.infer, db: any } +export interface ORPCContext { + user?: z.infer + db?: any +} export const pub = os.context().use(async (input, context, meta) => { const start = Date.now() diff --git a/playgrounds/openapi/src/playground-react.ts b/playgrounds/openapi/src/playground-react.ts index 33e0cb7b5..708f65b83 100644 --- a/playgrounds/openapi/src/playground-react.ts +++ b/playgrounds/openapi/src/playground-react.ts @@ -2,10 +2,11 @@ * This file is where you can play with type of oRPC React. */ +import type { RouterClient } from '@orpc/server' import type { router } from './router' import { createORPCReact } from '@orpc/react' -const { orpc } = createORPCReact() +const { orpc } = createORPCReact >() const listQuery = orpc.planet.list.useInfiniteQuery({ input: {}, diff --git a/playgrounds/openapi/src/router/index.ts b/playgrounds/openapi/src/router/index.ts index 48dd6d0c2..802e2bd20 100644 --- a/playgrounds/openapi/src/router/index.ts +++ b/playgrounds/openapi/src/router/index.ts @@ -10,7 +10,7 @@ import { } from './planet' export const router = pub.router({ - auth: pub.tags('Authentication').prefix('/auth').router({ + auth: pub.tag('Authentication').prefix('/auth').router({ signup, signin, refresh, @@ -18,7 +18,7 @@ export const router = pub.router({ me, }), - planet: pub.tags('Planets').prefix('/planets').router({ + planet: pub.tag('Planets').prefix('/planets').router({ list: listPlanets, create: createPlanet, find: findPlanet, From 72c8579179467d213850d40d12247e4b3d7b9fae Mon Sep 17 00:00:00 2001 From: unnoq Date: Fri, 20 Dec 2024 16:10:18 +0700 Subject: [PATCH 50/51] fix lazy-decorated tests --- packages/server/src/lazy-decorated.test.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/server/src/lazy-decorated.test.ts b/packages/server/src/lazy-decorated.test.ts index 97794c742..83aae92c5 100644 --- a/packages/server/src/lazy-decorated.test.ts +++ b/packages/server/src/lazy-decorated.test.ts @@ -5,8 +5,8 @@ import { decorateLazy } from './lazy-decorated' import { Procedure } from './procedure' import { createProcedureClient } from './procedure-client' -vi.mock('./procedure-caller', () => ({ - createProcedureCaller: vi.fn(() => vi.fn()), +vi.mock('./procedure-client', () => ({ + createProcedureClient: vi.fn(() => vi.fn()), })) beforeEach(() => { @@ -74,8 +74,7 @@ describe('decorated lazy', () => { context: undefined, }) expect(vi.mocked(createProcedureClient).mock.calls[0]![0].procedure).toSatisfy(isLazy) - const unwrapped = await unlazy(vi.mocked(createProcedureClient).mock.calls[0]![0].procedure as any) - expect(unwrapped.default).toBe(router) + expect(unlazy(vi.mocked(createProcedureClient).mock.calls[0]![0].procedure as any)).rejects.toThrow('Expected a lazy but got lazy') expect(await decorated({ val: '1' }, { signal })).toBe('__mocked__') expect(caller).toHaveBeenCalledTimes(1) From 3fc0b29a5ce6c6bc29592c5481d7e23f62a6d022 Mon Sep 17 00:00:00 2001 From: unnoq Date: Fri, 20 Dec 2024 18:06:38 +0700 Subject: [PATCH 51/51] sync docs --- .../content/docs/client/react-query.mdx | 11 ++++---- apps/content/content/docs/client/react.mdx | 7 +++-- apps/content/content/docs/client/vanilla.mdx | 4 +-- .../content/content/docs/client/vue-query.mdx | 4 +-- apps/content/content/docs/contract-first.mdx | 4 +-- apps/content/content/docs/index.mdx | 4 +-- .../docs/server/{caller.mdx => client.mdx} | 28 +++++++++---------- apps/content/content/docs/server/context.mdx | 10 +++---- .../content/docs/server/file-upload.mdx | 4 +-- apps/content/content/docs/server/lazy.mdx | 2 +- apps/content/content/docs/server/meta.json | 2 +- .../content/docs/server/server-action.mdx | 16 +++++------ apps/content/content/home/client.mdx | 11 ++++---- apps/content/content/home/landing.mdx | 4 +-- apps/content/examples/server.ts | 4 +-- .../src/procedure-fetch-client.test-d.ts | 2 +- packages/next/src/action-form.ts | 4 +-- packages/next/src/action-safe.ts | 4 +-- packages/server/src/lazy-decorated.ts | 3 +- packages/server/src/procedure-client.ts | 4 +-- playgrounds/openapi/src/playground-client.ts | 4 +-- 21 files changed, 69 insertions(+), 67 deletions(-) rename apps/content/content/docs/server/{caller.mdx => client.mdx} (75%) diff --git a/apps/content/content/docs/client/react-query.mdx b/apps/content/content/docs/client/react-query.mdx index 7aad76bfb..354b4c48e 100644 --- a/apps/content/content/docs/client/react-query.mdx +++ b/apps/content/content/docs/client/react-query.mdx @@ -15,11 +15,11 @@ description: Simplify React Query usage with minimal integration using ORPC and ```ts twoslash import { createORPCReactQueryUtils } from '@orpc/react-query'; -import { createORPCClient } from '@orpc/client'; +import { createORPCFetchClient } from '@orpc/client'; import type { router } from 'examples/server'; // Create an ORPC client -export const client = createORPCClient({ +export const client = createORPCFetchClient({ baseURL: 'http://localhost:3000/api', }); @@ -35,12 +35,13 @@ orpc.getting. ```tsx twoslash import { createORPCReactQueryUtils, RouterUtils } from '@orpc/react-query'; -import { createORPCClient } from '@orpc/client'; +import { createORPCFetchClient } from '@orpc/client'; +import { RouterClient } from '@orpc/server'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import type { router } from 'examples/server'; import * as React from 'react'; -const ORPCContext = React.createContext | undefined>(undefined); +const ORPCContext = React.createContext> | undefined>(undefined); export function useORPC() { const orpc = React.useContext(ORPCContext); @@ -54,7 +55,7 @@ export function useORPC() { export function ORPCProvider({ children }: { children: React.ReactNode }) { const [client] = React.useState(() => - createORPCClient({ + createORPCFetchClient({ baseURL: 'http://localhost:3000/api', }) ); diff --git a/apps/content/content/docs/client/react.mdx b/apps/content/content/docs/client/react.mdx index 8f1d039e4..ba0eea679 100644 --- a/apps/content/content/docs/client/react.mdx +++ b/apps/content/content/docs/client/react.mdx @@ -13,16 +13,17 @@ npm i @orpc/client @orpc/react @tanstack/react-query ```tsx twoslash import { createORPCReact } from '@orpc/react' -import { createORPCClient } from '@orpc/client' +import { createORPCFetchClient } from '@orpc/client' +import { RouterClient } from '@orpc/server' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { useState } from 'react' import type { router } from 'examples/server' import * as React from 'react' -export const { orpc, ORPCContext } = createORPCReact() +export const { orpc, ORPCContext } = createORPCReact>() export function ORPCProvider({ children }: { children: React.ReactNode }) { - const [client] = useState(() => createORPCClient({ + const [client] = useState(() => createORPCFetchClient({ baseURL: 'http://localhost:3000/api', })) const [queryClient] = useState(() => new QueryClient()) diff --git a/apps/content/content/docs/client/vanilla.mdx b/apps/content/content/docs/client/vanilla.mdx index 24d829d10..68feac97a 100644 --- a/apps/content/content/docs/client/vanilla.mdx +++ b/apps/content/content/docs/client/vanilla.mdx @@ -14,10 +14,10 @@ npm i @orpc/client To create a fully typed client, you need either the type of the [router](/docs/server/router) you intend to use or the [contract](/docs/contract/builder). ```ts twoslash -import { createORPCClient, ORPCError } from '@orpc/client' +import { createORPCFetchClient, ORPCError } from '@orpc/client' import type { router } from 'examples/server' -const client = createORPCClient({ +const client = createORPCFetchClient({ baseURL: 'http://localhost:3000/api', // fetch: optional override for the default fetch function // headers: provide additional headers diff --git a/apps/content/content/docs/client/vue-query.mdx b/apps/content/content/docs/client/vue-query.mdx index 1fc476b8c..88f555352 100644 --- a/apps/content/content/docs/client/vue-query.mdx +++ b/apps/content/content/docs/client/vue-query.mdx @@ -13,11 +13,11 @@ description: Simplify Vue Query usage with minimal integration using ORPC and Ta ```ts twoslash import { createORPCVueQueryUtils } from '@orpc/vue-query'; -import { createORPCClient } from '@orpc/client'; +import { createORPCFetchClient } from '@orpc/client'; import type { router } from 'examples/server'; // Create an ORPC client -export const client = createORPCClient({ +export const client = createORPCFetchClient({ baseURL: 'http://localhost:3000/api', }); diff --git a/apps/content/content/docs/contract-first.mdx b/apps/content/content/docs/contract-first.mdx index 9e31d8372..adbd5b411 100644 --- a/apps/content/content/docs/contract-first.mdx +++ b/apps/content/content/docs/contract-first.mdx @@ -129,10 +129,10 @@ That's it! The contract definition and implementation are now completely separat Create a fully typed client using just the contract definition: ```ts twoslash -import { createORPCClient, ORPCError } from '@orpc/client' +import { createORPCFetchClient, ORPCError } from '@orpc/client' import type { contract } from 'examples/contract' -const client = createORPCClient({ +const client = createORPCFetchClient({ baseURL: 'http://localhost:3000/prefix', // fetch: optional override for the default fetch function // headers: provide additional headers diff --git a/apps/content/content/docs/index.mdx b/apps/content/content/docs/index.mdx index 6668df92c..30a136e27 100644 --- a/apps/content/content/docs/index.mdx +++ b/apps/content/content/docs/index.mdx @@ -184,10 +184,10 @@ Start the server and visit http://localhost:3000/api/getting?name=yourname to se Use the fully typed client in any environment: ```ts twoslash -import { createORPCClient, ORPCError } from '@orpc/client' +import { createORPCFetchClient, ORPCError } from '@orpc/client' import type { router } from 'examples/server' -const client = createORPCClient({ +const client = createORPCFetchClient({ baseURL: 'http://localhost:3000/api', // fetch: optional override for the default fetch function // headers: provide additional headers diff --git a/apps/content/content/docs/server/caller.mdx b/apps/content/content/docs/server/client.mdx similarity index 75% rename from apps/content/content/docs/server/caller.mdx rename to apps/content/content/docs/server/client.mdx index 3331b9808..38b4e6c9c 100644 --- a/apps/content/content/docs/server/caller.mdx +++ b/apps/content/content/docs/server/client.mdx @@ -1,5 +1,5 @@ --- -title: Caller +title: Caller/Client description: Make your procedures callable in oRPC. --- @@ -9,7 +9,7 @@ You can directly call a procedure if its [Global Context](/docs/server/global-co For security reasons, context cannot be passed when invoking such procedures directly. ```ts twoslash -import { os, createProcedureCaller } from '@orpc/server' +import { os, createProcedureClient } from '@orpc/server' import { z } from 'zod' // ❌ Cannot call this procedure directly because undefined is not assignable to 'Context' @@ -36,17 +36,17 @@ const output_ = await router.getting({ name: 'World' }) // output is 'Hello, Wor ## Calling Procedures with Context -For context-sensitive calls, use a Procedure Caller. -A Procedure Caller securely provides the required context during invocation. +For context-sensitive calls, use a Procedure Client. +A Procedure Client securely provides the required context during invocation. ```ts twoslash -import { os, createProcedureCaller } from '@orpc/server' +import { os, createProcedureClient } from '@orpc/server' type Context = { user?: { id: string } } const getting = os.context().func(() => 'pong') -const gettingCaller = createProcedureCaller({ +const gettingClient = createProcedureClient({ procedure: getting, context: async () => { // you can access headers, cookies, etc. here to create context @@ -54,35 +54,35 @@ const gettingCaller = createProcedureCaller({ }, }) -const output = await gettingCaller() // output is 'pong' +const output = await gettingClient() // output is 'pong' ``` Now, you can provide context when invoking a procedure. -Additionally, you can use `gettingCaller` as a [Server Action](/docs/server/server-action). +Additionally, you can use `gettingClient` as a [Server Action](/docs/server/server-action). ## Calling Routers with Shared Context -To call multiple procedures with shared context, use a `Router Caller`. +To call multiple procedures with shared context, use a `Router Client`. ```ts twoslash -import { os, createRouterCaller } from '@orpc/server' +import { os, createRouterClient } from '@orpc/server' const router = os.router({ ping: os.func(() => 'pong') }) -const caller = createRouterCaller({ +const client = createRouterClient({ router: router, context: {}, }) -const result = await caller.ping() // result is 'pong' +const result = await client.ping() // result is 'pong' ``` ## Summary - **Direct Calls:** Use when no context is required, or the context accepts `undefined`. -- **Procedure Caller:** Use for securely calling a single procedure with a specific context. -- **Router Caller:** Use for securely calling multiple procedures with shared context. +- **Procedure Client:** Use for securely calling a single procedure with a specific context. +- **Router Client:** Use for securely calling multiple procedures with shared context. oRPC provides flexible and secure ways to invoke procedures tailored to your application needs. \ No newline at end of file diff --git a/apps/content/content/docs/server/context.mdx b/apps/content/content/docs/server/context.mdx index bb468f613..13c9c4f60 100644 --- a/apps/content/content/docs/server/context.mdx +++ b/apps/content/content/docs/server/context.mdx @@ -42,7 +42,7 @@ export const router = pub.router({ Middleware context is the context that is created or modified by middleware. If your procedure only depends on `Middleware Context`, you can -[call it](/docs/server/caller) or use it as a [Server Action](/docs/server/server-action) directly. +[call it](/docs/server/client) or use it as a [Server Action](/docs/server/server-action) directly. ```ts twoslash import { os, ORPCError } from '@orpc/server' @@ -105,7 +105,7 @@ This pattern is useful for server-side applications where dependencies can be in rather than relying on global mechanisms like `headers` or `cookies` in Next.js. ```ts twoslash -import { os, ORPCError, createProcedureCaller } from '@orpc/server' +import { os, ORPCError, createProcedureClient } from '@orpc/server' import { handleFetchRequest, createORPCHandler } from '@orpc/server/fetch' import { createOpenAPIServerlessHandler, createOpenAPIServerHandler } from '@orpc/openapi/fetch' @@ -147,8 +147,8 @@ export function fetch(request: Request) { } // If you want to call this procedure or use as server action -// you must create another caller with context by using `createProcedureCaller` or `createRouterCaller` -const caller = createProcedureCaller({ +// you must create another client with context by using `createProcedureClient` or `createRouterClient` +const client = createProcedureClient({ procedure: router.getting, context: async () => { // some logic to create context @@ -159,7 +159,7 @@ const caller = createProcedureCaller({ }, }) -const output = await caller() +const output = await client() ``` ## Summary diff --git a/apps/content/content/docs/server/file-upload.mdx b/apps/content/content/docs/server/file-upload.mdx index d447aaca6..31b62b6e2 100644 --- a/apps/content/content/docs/server/file-upload.mdx +++ b/apps/content/content/docs/server/file-upload.mdx @@ -63,9 +63,9 @@ To upload files with oRPC from the client, set up an oRPC client and pass a `File` object directly to the upload endpoint. ```typescript -import { createORPCClient } from '@orpc/client' +import { createORPCFetchClient } from '@orpc/client' -const client = createORPCClient({ +const client = createORPCFetchClient({ baseURL: 'http://localhost:3000', }) diff --git a/apps/content/content/docs/server/lazy.mdx b/apps/content/content/docs/server/lazy.mdx index bc4fba770..f3b49d986 100644 --- a/apps/content/content/docs/server/lazy.mdx +++ b/apps/content/content/docs/server/lazy.mdx @@ -16,7 +16,7 @@ Here's how you can set up and use them: ```typescript twoslash import { os } from '@orpc/server' -const pub = os.context<{ user?: { id: string } }>() +const pub = os.context<{ user?: { id: string } } | undefined>() // Define a router with lazy loading const router = pub.router({ diff --git a/apps/content/content/docs/server/meta.json b/apps/content/content/docs/server/meta.json index 91511bec5..287bdb8b1 100644 --- a/apps/content/content/docs/server/meta.json +++ b/apps/content/content/docs/server/meta.json @@ -8,7 +8,7 @@ "file-upload", "lazy", "server-action", - "caller", + "client", "error-handling", "data-types", "integrations", diff --git a/apps/content/content/docs/server/server-action.mdx b/apps/content/content/docs/server/server-action.mdx index 0ba3fe214..2cf987fd0 100644 --- a/apps/content/content/docs/server/server-action.mdx +++ b/apps/content/content/docs/server/server-action.mdx @@ -9,7 +9,7 @@ oRPC makes it simple to implement server actions, offering a robust and type-saf Server actions are supported out of the box and are powered by several key features: - [Middleware](/docs/server/middleware), -- [Procedure Caller](/docs/server/caller) +- [Procedure Client](/docs/server/client) - [Smart Conversion](/docs/openapi/smart-conversion) - [Bracket Notation](/docs/openapi/bracket-notation), @@ -17,8 +17,8 @@ Server actions are supported out of the box and are powered by several key featu To use a procedure as a server action, the procedure must either: -1. Be [directly callable](/docs/server/caller#direct-procedure-calls), or -2. Use [Calling Procedures with Context](/docs/server/caller#calling-procedures-with-context) to create a callable procedure with context. +1. Be [directly callable](/docs/server/client#direct-procedure-calls), or +2. Use [Calling Procedures with Context](/docs/server/client#calling-procedures-with-context) to create a callable procedure with context. ## Usage @@ -172,13 +172,13 @@ automatically convert `1992` into a `bigint` and seamlessly parse objects like ` Some procedures cannot be used as server actions directly. This is typically because they require additional context, such as user information or other runtime data. -In such cases, you can use [createProcedureCaller](/docs/server/caller#calling-procedures-with-context) -or `createSafeAction` and `createFormAction` (built on top of `createProcedureCaller`) +In such cases, you can use [createProcedureClient](/docs/server/client#calling-procedures-with-context) +or `createSafeAction` and `createFormAction` (built on top of `createProcedureClient`) to provide the required context dynamically, making the procedure callable and usable as a server action. ```ts twoslash import { createSafeAction, createFormAction } from '@orpc/next' -import { createProcedureCaller, os } from '@orpc/server' +import { createProcedureClient, os } from '@orpc/server' import { z } from 'zod' type Context = { user?: { id: string } } @@ -191,7 +191,7 @@ const getting = os // @errors: 2349 getting({ name: 'Unnoq' }) // ❌ cannot call this procedure directly, and cannot be used as a server action -export const caller = createProcedureCaller({ // or createSafeAction or createFormAction +export const client = createProcedureClient({ // or createSafeAction or createFormAction procedure: getting, context: async () => { // you can access headers, cookies, etc. here to create context @@ -199,7 +199,7 @@ export const caller = createProcedureCaller({ // or createSafeAction or createFo }, }) -caller({ name: 'Unnoq' }) // ✅ can call this procedure directly, and can be used as a server action +client({ name: 'Unnoq' }) // ✅ can call this procedure directly, and can be used as a server action ``` This flexibility ensures you can adapt server actions to scenarios requiring runtime information, enhancing usability across diverse use cases. \ No newline at end of file diff --git a/apps/content/content/home/client.mdx b/apps/content/content/home/client.mdx index e7f7ed61f..462714709 100644 --- a/apps/content/content/home/client.mdx +++ b/apps/content/content/home/client.mdx @@ -1,10 +1,10 @@ ```ts twoslash -import { createORPCClient, ORPCError } from '@orpc/client' +import { createORPCFetchClient, ORPCError } from '@orpc/client' import type { router } from 'examples/server' -const client = createORPCClient({ +const client = createORPCFetchClient({ baseURL: 'http://localhost:3000/api', // fetch: optional override for the default fetch function // headers: provide additional headers @@ -44,13 +44,14 @@ try { ```tsx twoslash import { createORPCReact } from '@orpc/react' -import { createORPCClient } from '@orpc/client' +import { createORPCFetchClient } from '@orpc/client' +import { RouterClient } from '@orpc/server' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { useState } from 'react' import type { router } from 'examples/server' import * as React from 'react' -export const { orpc, ORPCContext } = createORPCReact() +export const { orpc, ORPCContext } = createORPCReact>() // ------------------ Example ------------------ @@ -117,7 +118,7 @@ const queries = orpc.useQueries(o => [ // ------------------ Provider ------------------ export function ORPCProvider({ children }: { children: React.ReactNode }) { - const [client] = useState(() => createORPCClient({ + const [client] = useState(() => createORPCFetchClient({ baseURL: 'http://localhost:3000/api', })) const [queryClient] = useState(() => new QueryClient()) diff --git a/apps/content/content/home/landing.mdx b/apps/content/content/home/landing.mdx index 638fc89cd..098d4bfa1 100644 --- a/apps/content/content/home/landing.mdx +++ b/apps/content/content/home/landing.mdx @@ -4,7 +4,6 @@ export const getting = os .use(authMiddleware) // require auth .use(cache('5m')) // cache the output - .use(canMiddleware, (i) => i.id) // permission check by id .route({ path: '/getting/{id}' // dynamic params support method: 'POST' // custom OpenAPI method @@ -16,6 +15,7 @@ export const getting = os avatar: oz.file().type('image/*') }) })) + .use(canMiddleware, (i) => i.id) // permission check by id .output(z.string()) // validate output .func(async (input) => 'Name and Avatar has been updated') ``` @@ -38,7 +38,7 @@ const text = await getting({ }) ``` -The [Procedure Caller](/docs/server/caller) feature lets your procedures behave like regular TypeScript functions. +The [Procedure Client](/docs/server/client) feature lets your procedures behave like regular TypeScript functions. ## Expose It Online with a Fully Typed Client diff --git a/apps/content/examples/server.ts b/apps/content/examples/server.ts index 720ce74bd..33094fe8c 100644 --- a/apps/content/examples/server.ts +++ b/apps/content/examples/server.ts @@ -3,7 +3,7 @@ import { ORPCError, os } from '@orpc/server' import { oz } from '@orpc/zod' import { z } from 'zod' -export type Context = { user?: { id: string } } +export type Context = { user?: { id: string } } | undefined // global pub, authed completely optional export const pub /** public access */ = os.context() @@ -41,7 +41,7 @@ export const router = pub.router({ }), ) .use(async (input, context, meta) => { - if (!context.user) { + if (!context?.user) { throw new ORPCError({ code: 'UNAUTHORIZED', }) diff --git a/packages/client/src/procedure-fetch-client.test-d.ts b/packages/client/src/procedure-fetch-client.test-d.ts index f8959887d..54598e131 100644 --- a/packages/client/src/procedure-fetch-client.test-d.ts +++ b/packages/client/src/procedure-fetch-client.test-d.ts @@ -2,7 +2,7 @@ import type { ProcedureClient } from '@orpc/server' import { createProcedureFetchClient } from './procedure-fetch-client' describe('procedure fetch client', () => { - it('just a caller', () => { + it('just a client', () => { const client = createProcedureFetchClient({ baseURL: 'http://localhost:3000/orpc', path: ['ping'], diff --git a/packages/next/src/action-form.ts b/packages/next/src/action-form.ts index 9d38f125c..401892ecd 100644 --- a/packages/next/src/action-form.ts +++ b/packages/next/src/action-form.ts @@ -1,5 +1,5 @@ import type { Schema, SchemaInput } from '@orpc/contract' -import type { Context, CreateProcedureCallerOptions } from '@orpc/server' +import type { Context, CreateProcedureClientOptions } from '@orpc/server' import { createProcedureClient, ORPCError, unlazy } from '@orpc/server' import { OpenAPIDeserializer } from '@orpc/transformer' import { forbidden, notFound, unauthorized } from 'next/navigation' @@ -11,7 +11,7 @@ export function createFormAction< TInputSchema extends Schema, TOutputSchema extends Schema, TFuncOutput extends SchemaInput, ->(opt: CreateProcedureCallerOptions): FormAction { +>(opt: CreateProcedureClientOptions): FormAction { const caller = createProcedureClient(opt) const formAction = async (input: FormData): Promise => { diff --git a/packages/next/src/action-safe.ts b/packages/next/src/action-safe.ts index 8334f7ea6..52c988090 100644 --- a/packages/next/src/action-safe.ts +++ b/packages/next/src/action-safe.ts @@ -1,5 +1,5 @@ import type { Schema, SchemaInput, SchemaOutput } from '@orpc/contract' -import type { Context, CreateProcedureCallerOptions, ProcedureClient, WELL_ORPC_ERROR_JSON } from '@orpc/server' +import type { Context, CreateProcedureClientOptions, ProcedureClient, WELL_ORPC_ERROR_JSON } from '@orpc/server' import { createProcedureClient, ORPCError } from '@orpc/server' export type SafeAction, >( - opt: CreateProcedureCallerOptions, + opt: CreateProcedureClientOptions, ): SafeAction, SchemaOutput> { const caller = createProcedureClient(opt) diff --git a/packages/server/src/lazy-decorated.ts b/packages/server/src/lazy-decorated.ts index ebb2683d5..a258de516 100644 --- a/packages/server/src/lazy-decorated.ts +++ b/packages/server/src/lazy-decorated.ts @@ -9,8 +9,7 @@ import { type ANY_ROUTER, getRouterChild } from './router' export type DecoratedLazy = T extends Lazy ? DecoratedLazy - : - & Lazy + : Lazy & ( T extends Procedure ? undefined extends UContext diff --git a/packages/server/src/procedure-client.ts b/packages/server/src/procedure-client.ts index b9af07e1f..c1abef5c1 100644 --- a/packages/server/src/procedure-client.ts +++ b/packages/server/src/procedure-client.ts @@ -16,7 +16,7 @@ export interface ProcedureClient { /** * Options for creating a procedure caller with comprehensive type safety */ -export type CreateProcedureCallerOptions< +export type CreateProcedureClientOptions< TContext extends Context, TInputSchema extends Schema, TOutputSchema extends Schema, @@ -46,7 +46,7 @@ export function createProcedureClient< TOutputSchema extends Schema = undefined, TFuncOutput extends SchemaInput = SchemaInput, >( - options: CreateProcedureCallerOptions, + options: CreateProcedureClientOptions, ): ProcedureClient, SchemaOutput> { return async (...[input, callerOptions]) => { const path = options.path ?? [] diff --git a/playgrounds/openapi/src/playground-client.ts b/playgrounds/openapi/src/playground-client.ts index fe7af2ba5..b53f16cee 100644 --- a/playgrounds/openapi/src/playground-client.ts +++ b/playgrounds/openapi/src/playground-client.ts @@ -3,9 +3,9 @@ */ import type { router } from './router' -import { createORPCClient } from '@orpc/client' +import { createORPCFetchClient } from '@orpc/client' -const orpc = createORPCClient({ +const orpc = createORPCFetchClient({ baseURL: 'http://localhost:3000/api', })