Skip to content

Commit caefe3a

Browse files
authored
feat(server)!: migrate procedure hooks -> interceptors (#122)
* remove shared hooks * client interceptable * router clients * fix * type test for interceptor
1 parent e72396f commit caefe3a

10 files changed

+136
-213
lines changed

packages/server/src/implementer-procedure.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,17 @@ export interface ImplementedProcedure<
4444
/**
4545
* Make this procedure callable (works like a function while still being a procedure).
4646
*/
47-
callable<TClientContext>(...rest: CreateProcedureClientRest<TInitialContext, TOutputSchema, THandlerOutput, TClientContext>): & Procedure<TInitialContext, TCurrentContext, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta>
47+
callable<TClientContext>(
48+
...rest: CreateProcedureClientRest<TInitialContext, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta, TClientContext>
49+
): Procedure<TInitialContext, TCurrentContext, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta>
4850
& ProcedureClient < TClientContext, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap >
4951

5052
/**
5153
* Make this procedure compatible with server action (the same as .callable, but the type is compatible with server action).
5254
*/
53-
actionable<TClientContext>(...rest: CreateProcedureClientRest<TInitialContext, TOutputSchema, THandlerOutput, TClientContext>): & Procedure<TInitialContext, TCurrentContext, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta>
55+
actionable<TClientContext>(
56+
...rest: CreateProcedureClientRest<TInitialContext, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta, TClientContext>
57+
): Procedure<TInitialContext, TCurrentContext, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta>
5458
& ((...rest: ClientRest<TClientContext, SchemaInput<TInputSchema>>) => Promise<SchemaOutput<TOutputSchema, THandlerOutput>>)
5559
}
5660

packages/server/src/procedure-client.test-d.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
import type { baseErrorMap, inputSchema, outputSchema } from '../../contract/tests/shared'
2-
import { type Client, type ORPCError, safe } from '@orpc/contract'
1+
import type { Client, ErrorMap, ORPCError, ORPCErrorConstructorMap, Schema } from '@orpc/contract'
2+
import type { baseErrorMap, BaseMeta, inputSchema, outputSchema } from '../../contract/tests/shared'
3+
import type { Context } from './context'
4+
import type { Procedure } from './procedure'
5+
import { safe } from '@orpc/contract'
36
import { ping, pong } from '../tests/shared'
47
import { createProcedureClient, type ProcedureClient } from './procedure-client'
58

@@ -77,4 +80,39 @@ describe('createProcedureClient', () => {
7780
>
7881
>()
7982
})
83+
84+
it('optional context when all fields are optional', () => {
85+
createProcedureClient(pong)
86+
createProcedureClient(pong, {})
87+
88+
// @ts-expect-error - context is required
89+
createProcedureClient(ping)
90+
// @ts-expect-error - context is required
91+
createProcedureClient(ping, {})
92+
createProcedureClient(ping, { context: { db: 'postgres' } })
93+
})
94+
95+
it('well type interceptor', () => {
96+
createProcedureClient(ping, {
97+
context: { db: 'postgres' },
98+
interceptors: [
99+
async ({ next, signal, procedure, path, errors, context, input }) => {
100+
expectTypeOf(signal).toEqualTypeOf<undefined | InstanceType<typeof AbortSignal>>()
101+
expectTypeOf(procedure).toEqualTypeOf<
102+
Procedure<Context, Context, Schema, Schema, unknown, ErrorMap, BaseMeta>
103+
>()
104+
expectTypeOf(path).toEqualTypeOf<string[]>()
105+
expectTypeOf(errors).toEqualTypeOf<ORPCErrorConstructorMap<typeof baseErrorMap>>()
106+
expectTypeOf(context).toEqualTypeOf<{ db: string }>()
107+
expectTypeOf(input).toEqualTypeOf<{ input: number }>()
108+
109+
const output = await next()
110+
111+
expectTypeOf(output).toEqualTypeOf<{ output: string }>()
112+
113+
return output
114+
},
115+
],
116+
})
117+
})
80118
})

packages/server/src/procedure-client.test.ts

Lines changed: 15 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -344,60 +344,36 @@ describe.each(procedureCases)('createProcedureClient - case %s', async (_, proce
344344
expect(handler).toBeCalledWith(expect.objectContaining({ context: { val: '__val__' } }))
345345
})
346346

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

354350
const client = createProcedureClient(procedure, {
355351
context,
356352
path: ['users'],
357-
interceptor: execute,
358-
onStart,
359-
onSuccess,
360-
onError,
361-
onFinish,
353+
interceptors: [interceptor],
362354
})
363355

364-
await client({ val: '123' })
356+
vi.mocked(createORPCErrorConstructorMap).mockReturnValueOnce('__constructors__' as any)
365357

366-
const meta = {
367-
path: ['users'],
368-
procedure: unwrappedProcedure,
369-
}
358+
const signal = new AbortController().signal
370359

371-
const contextValue = { val: '__val__' }
360+
await client({ val: '123' }, { signal })
372361

373-
expect(execute).toBeCalledTimes(1)
374-
expect(execute).toHaveBeenCalledWith({ val: '123' }, contextValue, expect.objectContaining({
375-
...meta,
362+
expect(interceptor).toBeCalledTimes(1)
363+
expect(interceptor).toHaveBeenCalledWith({
364+
input: { val: '123' },
365+
context: { val: '__val__' },
366+
signal,
367+
path: ['users'],
368+
errors: '__constructors__',
369+
procedure: unwrappedProcedure,
376370
next: expect.any(Function),
377-
}))
378-
379-
expect(onStart).toBeCalledTimes(1)
380-
expect(onStart).toHaveBeenCalledWith(
381-
{ status: 'pending', input: { val: '123' }, output: undefined, error: undefined },
382-
contextValue,
383-
expect.objectContaining(meta),
384-
)
385-
386-
expect(onSuccess).toBeCalledTimes(1)
387-
expect(onSuccess).toHaveBeenCalledWith(
388-
{ status: 'success', input: { val: '123' }, output: { val: 123 }, error: undefined },
389-
contextValue,
390-
expect.objectContaining(meta),
391-
)
392-
393-
expect(onError).toBeCalledTimes(0)
371+
})
394372
})
395373

396374
it('accept paths', async () => {
397-
const onSuccess = vi.fn()
398375
const client = createProcedureClient(procedure, {
399376
path: ['users'],
400-
onSuccess,
401377
})
402378

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

411387
expect(handler).toBeCalledTimes(1)
412388
expect(handler).toHaveBeenCalledWith(expect.objectContaining({ path: ['users'] }))
413-
414-
expect(onSuccess).toBeCalledTimes(1)
415-
expect(onSuccess).toHaveBeenCalledWith(expect.any(Object), {}, expect.objectContaining({ path: ['users'] }))
416389
})
417390

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

422-
const onSuccess = vi.fn()
423-
424395
const client = createProcedureClient(procedure, {
425-
onSuccess,
426396
context: { userId: '123' },
427397
})
428398

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

443413
expect(handler).toBeCalledTimes(1)
444414
expect(handler).toHaveBeenCalledWith(expect.objectContaining({ signal }))
445-
446-
expect(onSuccess).toBeCalledTimes(1)
447-
expect(onSuccess).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), expect.objectContaining({ signal }))
448415
})
449416

450417
describe('error validation', () => {

packages/server/src/procedure-client.ts

Lines changed: 53 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import type { Client, ErrorFromErrorMap, ErrorMap, Schema, SchemaInput, SchemaOutput } from '@orpc/contract'
2-
import type { Hooks, Value } from '@orpc/shared'
1+
import type { Client, ErrorFromErrorMap, ErrorMap, Meta, ORPCErrorConstructorMap, Schema, SchemaInput, SchemaOutput } from '@orpc/contract'
2+
import type { Interceptor, Value } from '@orpc/shared'
33
import type { Context } from './context'
44
import type { Lazyable } from './lazy'
55
import type { MiddlewareNextFn } from './middleware'
66
import type { AnyProcedure, Procedure, ProcedureHandlerOptions } from './procedure'
77
import { createORPCErrorConstructorMap, ORPCError, validateORPCError, ValidationError } from '@orpc/contract'
8-
import { executeWithHooks, toError, value } from '@orpc/shared'
8+
import { intercept, toError, value } from '@orpc/shared'
99
import { unlazy } from './lazy'
1010
import { middlewareOutputFn } from './middleware'
1111

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

20+
export interface ProcedureClientInterceptorOptions<
21+
TInitialContext extends Context,
22+
TInputSchema extends Schema,
23+
TErrorMap extends ErrorMap,
24+
TMeta extends Meta,
25+
> {
26+
context: TInitialContext
27+
input: SchemaInput<TInputSchema>
28+
errors: ORPCErrorConstructorMap<TErrorMap>
29+
path: string[]
30+
procedure: Procedure<Context, Context, Schema, Schema, unknown, ErrorMap, TMeta>
31+
signal?: AbortSignal
32+
}
33+
2034
/**
2135
* Options for creating a procedure caller with comprehensive type safety
2236
*/
2337
export type CreateProcedureClientOptions<
2438
TInitialContext extends Context,
25-
TCurrentContext extends Schema,
26-
THandlerOutput extends SchemaInput<TCurrentContext>,
39+
TInputSchema extends Schema,
40+
TOutputSchema extends Schema,
41+
THandlerOutput extends SchemaInput<TOutputSchema>,
42+
TErrorMap extends ErrorMap,
43+
TMeta extends Meta,
2744
TClientContext,
2845
> =
2946
& {
3047
/**
3148
* This is helpful for logging and analytics.
3249
*/
3350
path?: string[]
51+
52+
interceptors?: Interceptor<
53+
ProcedureClientInterceptorOptions<TInitialContext, TInputSchema, TErrorMap, TMeta>,
54+
SchemaOutput<TOutputSchema, THandlerOutput>,
55+
ErrorFromErrorMap<TErrorMap>
56+
>[]
3457
}
3558
& (
36-
| { context: Value<TInitialContext, [clientContext: TClientContext]> }
37-
| (Record<never, never> extends TInitialContext ? Record<never, never> : never)
59+
Record<never, never> extends TInitialContext
60+
? { context?: Value<TInitialContext, [clientContext: TClientContext]> }
61+
: { context: Value<TInitialContext, [clientContext: TClientContext]> }
3862
)
39-
& Hooks<unknown, SchemaOutput<TCurrentContext, THandlerOutput>, TInitialContext, any>
4063

4164
export type CreateProcedureClientRest<
4265
TInitialContext extends Context,
66+
TInputSchema extends Schema,
4367
TOutputSchema extends Schema,
4468
THandlerOutput extends SchemaInput<TOutputSchema>,
69+
TErrorMap extends ErrorMap,
70+
TMeta extends Meta,
4571
TClientContext,
4672
> =
47-
| [options: CreateProcedureClientOptions<TInitialContext, TOutputSchema, THandlerOutput, TClientContext>]
73+
| [options: CreateProcedureClientOptions<TInitialContext, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta, TClientContext>]
4874
| (Record<never, never> extends TInitialContext ? [] : never)
4975

5076
export function createProcedureClient<
@@ -53,10 +79,19 @@ export function createProcedureClient<
5379
TOutputSchema extends Schema,
5480
THandlerOutput extends SchemaInput<TOutputSchema>,
5581
TErrorMap extends ErrorMap,
82+
TMeta extends Meta,
5683
TClientContext,
5784
>(
58-
lazyableProcedure: Lazyable<Procedure<TInitialContext, any, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, any>>,
59-
...[options]: CreateProcedureClientRest<TInitialContext, TOutputSchema, THandlerOutput, TClientContext>
85+
lazyableProcedure: Lazyable<Procedure<TInitialContext, any, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta>>,
86+
...[options]: CreateProcedureClientRest<
87+
TInitialContext,
88+
TInputSchema,
89+
TOutputSchema,
90+
THandlerOutput,
91+
TErrorMap,
92+
TMeta,
93+
TClientContext
94+
>
6095
): ProcedureClient<TClientContext, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap> {
6196
return async (...[input, callerOptions]) => {
6297
const path = options?.path ?? []
@@ -65,25 +100,21 @@ export function createProcedureClient<
65100
const context = await value(options?.context ?? {}, callerOptions?.context) as TInitialContext
66101
const errors = createORPCErrorConstructorMap(procedure['~orpc'].errorMap)
67102

68-
const executeOptions = {
69-
input,
103+
const interceptorOptions: ProcedureClientInterceptorOptions<TInitialContext, TInputSchema, TErrorMap, TMeta> = {
70104
context,
105+
input: input as SchemaInput<TInputSchema>, // input only optional when it undefinable so we can safely cast it
71106
errors,
72107
path,
73108
procedure: procedure as AnyProcedure,
74109
signal: callerOptions?.signal,
75110
}
76111

77112
try {
78-
const output = await executeWithHooks({
79-
hooks: options,
80-
input,
81-
context,
82-
meta: executeOptions,
83-
execute: () => executeProcedureInternal(procedure, executeOptions),
84-
})
85-
86-
return output
113+
return await intercept(
114+
options?.interceptors ?? [],
115+
interceptorOptions,
116+
interceptorOptions => executeProcedureInternal(interceptorOptions.procedure, interceptorOptions),
117+
)
87118
}
88119
catch (e) {
89120
if (!(e instanceof ORPCError)) {

packages/server/src/procedure-decorated.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,9 @@ export class DecoratedProcedure<
9191
/**
9292
* Make this procedure callable (works like a function while still being a procedure).
9393
*/
94-
callable<TClientContext>(...rest: CreateProcedureClientRest<TInitialContext, TOutputSchema, THandlerOutput, TClientContext>):
95-
& Procedure<TInitialContext, TCurrentContext, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta>
94+
callable<TClientContext>(
95+
...rest: CreateProcedureClientRest<TInitialContext, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta, TClientContext>
96+
): Procedure<TInitialContext, TCurrentContext, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta>
9697
& ProcedureClient<TClientContext, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap> {
9798
return Object.assign(createProcedureClient(this, ...rest), {
9899
'~type': 'Procedure' as const,
@@ -103,7 +104,9 @@ export class DecoratedProcedure<
103104
/**
104105
* Make this procedure compatible with server action (the same as .callable, but the type is compatible with server action).
105106
*/
106-
actionable<TClientContext>(...rest: CreateProcedureClientRest<TInitialContext, TOutputSchema, THandlerOutput, TClientContext>):
107+
actionable<TClientContext>(
108+
...rest: CreateProcedureClientRest<TInitialContext, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta, TClientContext>
109+
):
107110
& Procedure<TInitialContext, TCurrentContext, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta>
108111
& ((...rest: ClientRest<TClientContext, SchemaInput<TInputSchema>>) => Promise<SchemaOutput<TOutputSchema, THandlerOutput>>) {
109112
return this.callable(...rest)

packages/server/src/procedure-utils.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ClientPromiseResult, ErrorFromErrorMap, ErrorMap, Schema, SchemaInput, SchemaOutput } from '@orpc/contract'
1+
import type { ClientPromiseResult, ErrorFromErrorMap, ErrorMap, Meta, Schema, SchemaInput, SchemaOutput } from '@orpc/contract'
22
import type { Context } from './context'
33
import type { Lazyable } from './lazy'
44
import type { Procedure } from './procedure'
@@ -20,10 +20,11 @@ export function call<
2020
TOutputSchema extends Schema,
2121
THandlerOutput extends SchemaInput<TOutputSchema>,
2222
TErrorMap extends ErrorMap,
23+
TMeta extends Meta,
2324
>(
24-
procedure: Lazyable<Procedure<TInitialContext, any, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, any>>,
25+
procedure: Lazyable<Procedure<TInitialContext, any, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta>>,
2526
input: SchemaInput<TInputSchema>,
26-
...rest: CreateProcedureClientRest<TInitialContext, TOutputSchema, THandlerOutput, unknown>
27+
...rest: CreateProcedureClientRest<TInitialContext, TInputSchema, TOutputSchema, THandlerOutput, TErrorMap, TMeta, unknown>
2728
): ClientPromiseResult<SchemaOutput<TOutputSchema, THandlerOutput>, ErrorFromErrorMap<TErrorMap>> {
2829
return createProcedureClient(procedure, ...rest)(input)
2930
}

packages/server/src/router-client.test.ts

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,11 @@ describe('createRouterClient', () => {
4646
})
4747

4848
it('hooks', async () => {
49-
const onStart = vi.fn()
50-
const onSuccess = vi.fn()
51-
const onError = vi.fn()
52-
const onFinish = vi.fn()
5349
const interceptor = vi.fn()
5450

5551
const client = createRouterClient(router, {
5652
context: { db: 'postgres' },
57-
onStart,
58-
onSuccess,
59-
onError,
60-
onFinish,
61-
interceptor,
53+
interceptors: [interceptor],
6254
})
6355

6456
expect(client.pong({ val: '123' })).toEqual('__mocked__')
@@ -67,11 +59,7 @@ describe('createRouterClient', () => {
6759
expect(createProcedureClient).toHaveBeenCalledWith(pong, expect.objectContaining({
6860
context: { db: 'postgres' },
6961
path: ['pong'],
70-
onStart,
71-
onSuccess,
72-
onError,
73-
onFinish,
74-
interceptor,
62+
interceptors: [interceptor],
7563
}))
7664
})
7765

0 commit comments

Comments
 (0)