Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions packages/server/src/implementer-procedure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,17 @@ export interface ImplementedProcedure<
/**
* Make this procedure callable (works like a function while still being a procedure).
*/
callable<TClientContext>(...rest: CreateProcedureClientRest<TInitialContext, TOutputSchema, THandlerOutput, TClientContext>): & Procedure<TInitialContext, TCurrentContext, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta>
callable<TClientContext>(
...rest: CreateProcedureClientRest<TInitialContext, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta, TClientContext>
): Procedure<TInitialContext, TCurrentContext, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta>
& ProcedureClient < TClientContext, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap >

/**
* Make this procedure compatible with server action (the same as .callable, but the type is compatible with server action).
*/
actionable<TClientContext>(...rest: CreateProcedureClientRest<TInitialContext, TOutputSchema, THandlerOutput, TClientContext>): & Procedure<TInitialContext, TCurrentContext, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta>
actionable<TClientContext>(
...rest: CreateProcedureClientRest<TInitialContext, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta, TClientContext>
): Procedure<TInitialContext, TCurrentContext, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta>
& ((...rest: ClientRest<TClientContext, SchemaInput<TInputSchema>>) => Promise<SchemaOutput<TOutputSchema, THandlerOutput>>)
}

Expand Down
42 changes: 40 additions & 2 deletions packages/server/src/procedure-client.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { baseErrorMap, inputSchema, outputSchema } from '../../contract/tests/shared'
import { type Client, type ORPCError, safe } from '@orpc/contract'
import type { Client, ErrorMap, ORPCError, ORPCErrorConstructorMap, Schema } from '@orpc/contract'
import type { baseErrorMap, BaseMeta, inputSchema, outputSchema } from '../../contract/tests/shared'
import type { Context } from './context'
import type { Procedure } from './procedure'
import { safe } from '@orpc/contract'
import { ping, pong } from '../tests/shared'
import { createProcedureClient, type ProcedureClient } from './procedure-client'

Expand Down Expand Up @@ -77,4 +80,39 @@ describe('createProcedureClient', () => {
>
>()
})

it('optional context when all fields are optional', () => {
createProcedureClient(pong)
createProcedureClient(pong, {})

// @ts-expect-error - context is required
createProcedureClient(ping)
// @ts-expect-error - context is required
createProcedureClient(ping, {})
createProcedureClient(ping, { context: { db: 'postgres' } })
})

it('well type interceptor', () => {
createProcedureClient(ping, {
context: { db: 'postgres' },
interceptors: [
async ({ next, signal, procedure, path, errors, context, input }) => {
expectTypeOf(signal).toEqualTypeOf<undefined | InstanceType<typeof AbortSignal>>()
expectTypeOf(procedure).toEqualTypeOf<
Procedure<Context, Context, Schema, Schema, unknown, ErrorMap, BaseMeta>
>()
expectTypeOf(path).toEqualTypeOf<string[]>()
expectTypeOf(errors).toEqualTypeOf<ORPCErrorConstructorMap<typeof baseErrorMap>>()
expectTypeOf(context).toEqualTypeOf<{ db: string }>()
expectTypeOf(input).toEqualTypeOf<{ input: number }>()

const output = await next()

expectTypeOf(output).toEqualTypeOf<{ output: string }>()

return output
},
],
})
})
})
63 changes: 15 additions & 48 deletions packages/server/src/procedure-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,60 +344,36 @@ describe.each(procedureCases)('createProcedureClient - case %s', async (_, proce
expect(handler).toBeCalledWith(expect.objectContaining({ context: { val: '__val__' } }))
})

it.each(contextCases)('can accept hooks - context: %s', async (_, context) => {
const execute = vi.fn((input, context, meta) => meta.next())
const onStart = vi.fn()
const onSuccess = vi.fn()
const onError = vi.fn()
const onFinish = vi.fn()
it.each(contextCases)('can intercept - context: %s', async (_, context) => {
const interceptor = vi.fn(({ next }) => next())

const client = createProcedureClient(procedure, {
context,
path: ['users'],
interceptor: execute,
onStart,
onSuccess,
onError,
onFinish,
interceptors: [interceptor],
})

await client({ val: '123' })
vi.mocked(createORPCErrorConstructorMap).mockReturnValueOnce('__constructors__' as any)

const meta = {
path: ['users'],
procedure: unwrappedProcedure,
}
const signal = new AbortController().signal

const contextValue = { val: '__val__' }
await client({ val: '123' }, { signal })

expect(execute).toBeCalledTimes(1)
expect(execute).toHaveBeenCalledWith({ val: '123' }, contextValue, expect.objectContaining({
...meta,
expect(interceptor).toBeCalledTimes(1)
expect(interceptor).toHaveBeenCalledWith({
input: { val: '123' },
context: { val: '__val__' },
signal,
path: ['users'],
errors: '__constructors__',
procedure: unwrappedProcedure,
next: expect.any(Function),
}))

expect(onStart).toBeCalledTimes(1)
expect(onStart).toHaveBeenCalledWith(
{ status: 'pending', input: { val: '123' }, output: undefined, error: undefined },
contextValue,
expect.objectContaining(meta),
)

expect(onSuccess).toBeCalledTimes(1)
expect(onSuccess).toHaveBeenCalledWith(
{ status: 'success', input: { val: '123' }, output: { val: 123 }, error: undefined },
contextValue,
expect.objectContaining(meta),
)

expect(onError).toBeCalledTimes(0)
})
})

it('accept paths', async () => {
const onSuccess = vi.fn()
const client = createProcedureClient(procedure, {
path: ['users'],
onSuccess,
})

await client({ val: '123' })
Expand All @@ -410,19 +386,13 @@ describe.each(procedureCases)('createProcedureClient - case %s', async (_, proce

expect(handler).toBeCalledTimes(1)
expect(handler).toHaveBeenCalledWith(expect.objectContaining({ path: ['users'] }))

expect(onSuccess).toBeCalledTimes(1)
expect(onSuccess).toHaveBeenCalledWith(expect.any(Object), {}, expect.objectContaining({ path: ['users'] }))
})

it('support signal', async () => {
const controller = new AbortController()
const signal = controller.signal

const onSuccess = vi.fn()

const client = createProcedureClient(procedure, {
onSuccess,
context: { userId: '123' },
})

Expand All @@ -442,9 +412,6 @@ describe.each(procedureCases)('createProcedureClient - case %s', async (_, proce

expect(handler).toBeCalledTimes(1)
expect(handler).toHaveBeenCalledWith(expect.objectContaining({ signal }))

expect(onSuccess).toBeCalledTimes(1)
expect(onSuccess).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), expect.objectContaining({ signal }))
})

describe('error validation', () => {
Expand Down
75 changes: 53 additions & 22 deletions packages/server/src/procedure-client.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { Client, ErrorFromErrorMap, ErrorMap, Schema, SchemaInput, SchemaOutput } from '@orpc/contract'
import type { Hooks, Value } from '@orpc/shared'
import type { Client, ErrorFromErrorMap, ErrorMap, Meta, ORPCErrorConstructorMap, Schema, SchemaInput, SchemaOutput } from '@orpc/contract'
import type { Interceptor, Value } from '@orpc/shared'
import type { Context } from './context'
import type { Lazyable } from './lazy'
import type { MiddlewareNextFn } from './middleware'
import type { AnyProcedure, Procedure, ProcedureHandlerOptions } from './procedure'
import { createORPCErrorConstructorMap, ORPCError, validateORPCError, ValidationError } from '@orpc/contract'
import { executeWithHooks, toError, value } from '@orpc/shared'
import { intercept, toError, value } from '@orpc/shared'
import { unlazy } from './lazy'
import { middlewareOutputFn } from './middleware'

Expand All @@ -17,34 +17,60 @@ export type ProcedureClient<
TErrorMap extends ErrorMap,
> = Client<TClientContext, SchemaInput<TInputSchema>, SchemaOutput<TOutputSchema, THandlerOutput>, ErrorFromErrorMap<TErrorMap>>

export interface ProcedureClientInterceptorOptions<
TInitialContext extends Context,
TInputSchema extends Schema,
TErrorMap extends ErrorMap,
TMeta extends Meta,
> {
context: TInitialContext
input: SchemaInput<TInputSchema>
errors: ORPCErrorConstructorMap<TErrorMap>
path: string[]
procedure: Procedure<Context, Context, Schema, Schema, unknown, ErrorMap, TMeta>
signal?: AbortSignal
}

/**
* Options for creating a procedure caller with comprehensive type safety
*/
export type CreateProcedureClientOptions<
TInitialContext extends Context,
TCurrentContext extends Schema,
THandlerOutput extends SchemaInput<TCurrentContext>,
TInputSchema extends Schema,
TOutputSchema extends Schema,
THandlerOutput extends SchemaInput<TOutputSchema>,
TErrorMap extends ErrorMap,
TMeta extends Meta,
TClientContext,
> =
& {
/**
* This is helpful for logging and analytics.
*/
path?: string[]

interceptors?: Interceptor<
ProcedureClientInterceptorOptions<TInitialContext, TInputSchema, TErrorMap, TMeta>,
SchemaOutput<TOutputSchema, THandlerOutput>,
ErrorFromErrorMap<TErrorMap>
>[]
}
& (
| { context: Value<TInitialContext, [clientContext: TClientContext]> }
| (Record<never, never> extends TInitialContext ? Record<never, never> : never)
Record<never, never> extends TInitialContext
? { context?: Value<TInitialContext, [clientContext: TClientContext]> }
: { context: Value<TInitialContext, [clientContext: TClientContext]> }
)
& Hooks<unknown, SchemaOutput<TCurrentContext, THandlerOutput>, TInitialContext, any>

export type CreateProcedureClientRest<
TInitialContext extends Context,
TInputSchema extends Schema,
TOutputSchema extends Schema,
THandlerOutput extends SchemaInput<TOutputSchema>,
TErrorMap extends ErrorMap,
TMeta extends Meta,
TClientContext,
> =
| [options: CreateProcedureClientOptions<TInitialContext, TOutputSchema, THandlerOutput, TClientContext>]
| [options: CreateProcedureClientOptions<TInitialContext, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta, TClientContext>]
| (Record<never, never> extends TInitialContext ? [] : never)

export function createProcedureClient<
Expand All @@ -53,10 +79,19 @@ export function createProcedureClient<
TOutputSchema extends Schema,
THandlerOutput extends SchemaInput<TOutputSchema>,
TErrorMap extends ErrorMap,
TMeta extends Meta,
TClientContext,
>(
lazyableProcedure: Lazyable<Procedure<TInitialContext, any, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, any>>,
...[options]: CreateProcedureClientRest<TInitialContext, TOutputSchema, THandlerOutput, TClientContext>
lazyableProcedure: Lazyable<Procedure<TInitialContext, any, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta>>,
...[options]: CreateProcedureClientRest<
TInitialContext,
TInputSchema,
TOutputSchema,
THandlerOutput,
TErrorMap,
TMeta,
TClientContext
>
): ProcedureClient<TClientContext, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap> {
return async (...[input, callerOptions]) => {
const path = options?.path ?? []
Expand All @@ -65,25 +100,21 @@ export function createProcedureClient<
const context = await value(options?.context ?? {}, callerOptions?.context) as TInitialContext
const errors = createORPCErrorConstructorMap(procedure['~orpc'].errorMap)

const executeOptions = {
input,
const interceptorOptions: ProcedureClientInterceptorOptions<TInitialContext, TInputSchema, TErrorMap, TMeta> = {
context,
input: input as SchemaInput<TInputSchema>, // input only optional when it undefinable so we can safely cast it
errors,
path,
procedure: procedure as AnyProcedure,
signal: callerOptions?.signal,
}

try {
const output = await executeWithHooks({
hooks: options,
input,
context,
meta: executeOptions,
execute: () => executeProcedureInternal(procedure, executeOptions),
})

return output
return await intercept(
options?.interceptors ?? [],
interceptorOptions,
interceptorOptions => executeProcedureInternal(interceptorOptions.procedure, interceptorOptions),
)
}
catch (e) {
if (!(e instanceof ORPCError)) {
Expand Down
9 changes: 6 additions & 3 deletions packages/server/src/procedure-decorated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,9 @@ export class DecoratedProcedure<
/**
* Make this procedure callable (works like a function while still being a procedure).
*/
callable<TClientContext>(...rest: CreateProcedureClientRest<TInitialContext, TOutputSchema, THandlerOutput, TClientContext>):
& Procedure<TInitialContext, TCurrentContext, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta>
callable<TClientContext>(
...rest: CreateProcedureClientRest<TInitialContext, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta, TClientContext>
): Procedure<TInitialContext, TCurrentContext, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta>
& ProcedureClient<TClientContext, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap> {
return Object.assign(createProcedureClient(this, ...rest), {
'~type': 'Procedure' as const,
Expand All @@ -103,7 +104,9 @@ export class DecoratedProcedure<
/**
* Make this procedure compatible with server action (the same as .callable, but the type is compatible with server action).
*/
actionable<TClientContext>(...rest: CreateProcedureClientRest<TInitialContext, TOutputSchema, THandlerOutput, TClientContext>):
actionable<TClientContext>(
...rest: CreateProcedureClientRest<TInitialContext, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta, TClientContext>
):
& Procedure<TInitialContext, TCurrentContext, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta>
& ((...rest: ClientRest<TClientContext, SchemaInput<TInputSchema>>) => Promise<SchemaOutput<TOutputSchema, THandlerOutput>>) {
return this.callable(...rest)
Expand Down
7 changes: 4 additions & 3 deletions packages/server/src/procedure-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ClientPromiseResult, ErrorFromErrorMap, ErrorMap, Schema, SchemaInput, SchemaOutput } from '@orpc/contract'
import type { ClientPromiseResult, ErrorFromErrorMap, ErrorMap, Meta, Schema, SchemaInput, SchemaOutput } from '@orpc/contract'
import type { Context } from './context'
import type { Lazyable } from './lazy'
import type { Procedure } from './procedure'
Expand All @@ -20,10 +20,11 @@ export function call<
TOutputSchema extends Schema,
THandlerOutput extends SchemaInput<TOutputSchema>,
TErrorMap extends ErrorMap,
TMeta extends Meta,
>(
procedure: Lazyable<Procedure<TInitialContext, any, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, any>>,
procedure: Lazyable<Procedure<TInitialContext, any, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta>>,
input: SchemaInput<TInputSchema>,
...rest: CreateProcedureClientRest<TInitialContext, TOutputSchema, THandlerOutput, unknown>
...rest: CreateProcedureClientRest<TInitialContext, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta, unknown>
): ClientPromiseResult<SchemaOutput<TOutputSchema, THandlerOutput>, ErrorFromErrorMap<TErrorMap>> {
return createProcedureClient(procedure, ...rest)(input)
}
16 changes: 2 additions & 14 deletions packages/server/src/router-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,11 @@ describe('createRouterClient', () => {
})

it('hooks', async () => {
const onStart = vi.fn()
const onSuccess = vi.fn()
const onError = vi.fn()
const onFinish = vi.fn()
const interceptor = vi.fn()

const client = createRouterClient(router, {
context: { db: 'postgres' },
onStart,
onSuccess,
onError,
onFinish,
interceptor,
interceptors: [interceptor],
})

expect(client.pong({ val: '123' })).toEqual('__mocked__')
Expand All @@ -67,11 +59,7 @@ describe('createRouterClient', () => {
expect(createProcedureClient).toHaveBeenCalledWith(pong, expect.objectContaining({
context: { db: 'postgres' },
path: ['pong'],
onStart,
onSuccess,
onError,
onFinish,
interceptor,
interceptors: [interceptor],
}))
})

Expand Down
Loading