Skip to content

Commit 85e5dff

Browse files
authored
feat(server)!: prevent middleware context conflicts (#313)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Documentation** - Enhanced clarity in documentation with visually distinct informational blocks and direct links for additional details. - **Refactor** - Improved middleware and procedure API type constraints for increased consistency and robustness. - **Tests** - Expanded test coverage to validate context handling and error expectations, reinforcing overall system reliability. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 10ace4f commit 85e5dff

13 files changed

Lines changed: 65 additions & 21 deletions

apps/content/docs/procedure.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ const example = os
3131
.actionable() // Server Action compatibility
3232
```
3333

34-
> The `.handler` method is the only required step. All other chains are optional.
34+
:::info
35+
The `.handler` method is the only required step. All other chains are optional.
36+
:::
3537

3638
## Input/Output Validation
3739

@@ -56,7 +58,7 @@ const example = os
5658

5759
## Using Middleware
5860

59-
The `.use` method allows you to pass middleware, which must call `next` to continue execution.
61+
The `.use` method allows you to pass [middleware](/docs/middleware), which must call `next` to continue execution.
6062

6163
```ts
6264
const aMiddleware = os.middleware(async ({ context, next }) => next())
@@ -67,7 +69,9 @@ const example = os
6769
.handler(async ({ context }) => { /* logic */ })
6870
```
6971

70-
To learn more, see the [Middleware](/docs/middleware) documentation.
72+
::: info
73+
[Middleware](/docs/middleware) can be applied if the [current context](/docs/context#combining-initial-and-execution-context) meets the [middleware dependent context](/docs/middleware#dependent-context) requirements and does not conflict with the [current context](/docs/context#combining-initial-and-execution-context).
74+
:::
7175

7276
## Initial Configuration
7377

packages/server/src/builder-variants.test-d.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ describe('BuilderWithMiddlewares', () => {
105105
builder.use(({ next }, input: 'invalid') => next({}))
106106
// @ts-expect-error --- output is not match
107107
builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({}))
108+
// @ts-expect-error --- conflict context
109+
builder.use(({ next }) => next({ context: { db: undefined } }))
108110
})
109111

110112
it('with TInContext', () => {
@@ -364,6 +366,8 @@ describe('ProcedureBuilder', () => {
364366
builder.use(({ next }, input: 'invalid') => next({}))
365367
// @ts-expect-error --- output is not match
366368
builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({}))
369+
// @ts-expect-error --- conflict context
370+
builder.use(({ next }) => next({ context: { db: undefined } }))
367371
})
368372

369373
it('with TInContext', () => {
@@ -558,6 +562,8 @@ describe('ProcedureBuilderWithInput', () => {
558562
builder.use(({ next }, input: 'invalid') => next({}))
559563
// @ts-expect-error --- output is not match
560564
builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({}))
565+
// @ts-expect-error --- conflict context
566+
builder.use(({ next }) => next({ context: { db: undefined } }))
561567
})
562568

563569
it('with map input', () => {
@@ -606,6 +612,8 @@ describe('ProcedureBuilderWithInput', () => {
606612
builder.use(({ next }, input: 'invalid') => next({}), input => ({ mapped: true }))
607613
// @ts-expect-error --- output is not match
608614
builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({}), input => ({ mapped: true }))
615+
// @ts-expect-error --- conflict context
616+
builder.use(({ next }) => next({ context: { db: undefined } }), () => { })
609617
})
610618

611619
it('with TInContext', () => {
@@ -795,6 +803,8 @@ describe('ProcedureBuilderWithOutput', () => {
795803
builder.use(({ next }, input: 'invalid') => next({}))
796804
// @ts-expect-error --- output is not match
797805
builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({}))
806+
// @ts-expect-error --- conflict context
807+
builder.use(({ next }) => next({ context: { db: undefined } }))
798808
})
799809

800810
it('with TInContext', () => {
@@ -976,6 +986,8 @@ describe('ProcedureBuilderWithInputOutput', () => {
976986
builder.use(({ next }, input: 'invalid') => next({}))
977987
// @ts-expect-error --- output is not match
978988
builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({}))
989+
// @ts-expect-error --- conflict context
990+
builder.use(({ next }) => next({ context: { db: undefined } }))
979991
})
980992

981993
it('with map input', () => {
@@ -1024,6 +1036,8 @@ describe('ProcedureBuilderWithInputOutput', () => {
10241036
builder.use(({ next }, input: 'invalid') => next({}), input => ({ mapped: true }))
10251037
// @ts-expect-error --- output is not match
10261038
builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({}), input => ({ mapped: true }))
1039+
// @ts-expect-error --- conflict context
1040+
builder.use(({ next }) => next({ context: { db: undefined } }), input => ({ mapped: true }))
10271041
})
10281042

10291043
it('with TInContext', () => {
@@ -1169,6 +1183,8 @@ describe('RouterBuilder', () => {
11691183
builder.use(({ next }, input: 'invalid') => next({}))
11701184
// @ts-expect-error --- output is not match
11711185
builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({}))
1186+
// @ts-expect-error --- conflict context
1187+
builder.use(({ next }) => next({ context: { db: undefined } }))
11721188
})
11731189

11741190
it('with TInContext', () => {

packages/server/src/builder-variants.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { HTTPPath } from '@orpc/client'
22
import type { AnySchema, ContractRouter, ErrorMap, InferSchemaInput, InferSchemaOutput, MergedErrorMap, Meta, Route, Schema } from '@orpc/contract'
3+
import type { IntersectPick } from '@orpc/shared'
34
import type { BuilderDef } from './builder'
45
import type { Context, MergedCurrentContext, MergedInitialContext } from './context'
56
import type { ORPCErrorConstructorMap } from './error'
@@ -31,7 +32,7 @@ export interface BuilderWithMiddlewares<
3132
TMeta
3233
>
3334

34-
'use'<UOutContext extends Context, UInContext extends Context = TCurrentContext>(
35+
'use'<UOutContext extends IntersectPick<TCurrentContext, UOutContext>, UInContext extends Context = TCurrentContext>(
3536
middleware: Middleware<
3637
UInContext | TCurrentContext,
3738
UOutContext,
@@ -103,7 +104,7 @@ export interface ProcedureBuilder<
103104
TMeta
104105
>
105106

106-
'use'<UOutContext extends Context, UInContext extends Context = TCurrentContext>(
107+
'use'<UOutContext extends IntersectPick<TCurrentContext, UOutContext>, UInContext extends Context = TCurrentContext>(
107108
middleware: Middleware<
108109
UInContext | TCurrentContext,
109110
UOutContext,
@@ -156,7 +157,7 @@ export interface ProcedureBuilderWithInput<
156157
errors: U,
157158
): ProcedureBuilderWithInput<TInitialContext, TCurrentContext, TInputSchema, TOutputSchema, MergedErrorMap<TErrorMap, U>, TMeta>
158159

159-
'use'<UOutContext extends Context, UInContext extends Context = TCurrentContext>(
160+
'use'<UOutContext extends IntersectPick<TCurrentContext, UOutContext>, UInContext extends Context = TCurrentContext>(
160161
middleware: Middleware<
161162
UInContext | TCurrentContext,
162163
UOutContext,
@@ -174,7 +175,7 @@ export interface ProcedureBuilderWithInput<
174175
TMeta
175176
>
176177

177-
'use'<UOutContext extends Context, UInput, UInContext extends Context = TCurrentContext>(
178+
'use'<UOutContext extends IntersectPick<TCurrentContext, UOutContext>, UInput, UInContext extends Context = TCurrentContext>(
178179
middleware: Middleware<
179180
UInContext | TCurrentContext,
180181
UOutContext,
@@ -231,7 +232,7 @@ export interface ProcedureBuilderWithOutput<
231232
TMeta
232233
>
233234

234-
'use'<UOutContext extends Context, UInContext extends Context = TCurrentContext>(
235+
'use'<UOutContext extends IntersectPick<TCurrentContext, UOutContext>, UInContext extends Context = TCurrentContext>(
235236
middleware: Middleware<
236237
UInContext | TCurrentContext,
237238
UOutContext,
@@ -280,7 +281,7 @@ export interface ProcedureBuilderWithInputOutput<
280281
errors: U,
281282
): ProcedureBuilderWithInputOutput<TInitialContext, TCurrentContext, TInputSchema, TOutputSchema, MergedErrorMap<TErrorMap, U>, TMeta>
282283

283-
'use'<UOutContext extends Context, UInContext extends Context = TCurrentContext>(
284+
'use'<UOutContext extends IntersectPick<TCurrentContext, UOutContext>, UInContext extends Context = TCurrentContext>(
284285
middleware: Middleware<
285286
UInContext | TCurrentContext,
286287
UOutContext,
@@ -298,7 +299,7 @@ export interface ProcedureBuilderWithInputOutput<
298299
TMeta
299300
>
300301

301-
'use'<UOutContext extends Context, UInput, UInContext extends Context = TCurrentContext>(
302+
'use'<UOutContext extends IntersectPick<TCurrentContext, UOutContext>, UInput, UInContext extends Context = TCurrentContext>(
302303
middleware: Middleware<
303304
UInContext | TCurrentContext,
304305
UOutContext,
@@ -342,7 +343,7 @@ export interface RouterBuilder<
342343
errors: U,
343344
): RouterBuilder<TInitialContext, TCurrentContext, MergedErrorMap<TErrorMap, U>, TMeta>
344345

345-
'use'<UOutContext extends Context, UInContext extends Context = TCurrentContext>(
346+
'use'<UOutContext extends IntersectPick<TCurrentContext, UOutContext>, UInContext extends Context = TCurrentContext>(
346347
middleware: Middleware<
347348
UInContext | TCurrentContext,
348349
UOutContext,

packages/server/src/builder.test-d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,9 @@ describe('Builder', () => {
175175
).toEqualTypeOf<
176176
DecoratedMiddleware<InitialContext, { extra: boolean }, unknown, any, any, BaseMeta>
177177
>()
178+
179+
// @ts-expect-error --- conflict context
180+
builder.middleware(({ next }) => next({ context: { db: undefined } }))
178181
})
179182

180183
it('can type input and output', () => {
@@ -244,6 +247,8 @@ describe('Builder', () => {
244247
builder.use(({ next }, input: 'invalid') => next({}))
245248
// @ts-expect-error --- output is not match
246249
builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({}))
250+
// @ts-expect-error --- conflict context
251+
builder.use(({ next }) => next({ context: { db: undefined } }))
247252
})
248253

249254
it('with TInContext', () => {

packages/server/src/builder.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { HTTPPath } from '@orpc/client'
22
import type { AnySchema, ContractProcedureDef, ContractRouter, ErrorMap, MergedErrorMap, Meta, Route, Schema } from '@orpc/contract'
3+
import type { IntersectPick } from '@orpc/shared'
34
import type { BuilderWithMiddlewares, ProcedureBuilder, ProcedureBuilderWithInput, ProcedureBuilderWithOutput, RouterBuilder } from './builder-variants'
45
import type { Context, MergedCurrentContext, MergedInitialContext } from './context'
56
import type { ORPCErrorConstructorMap } from './error'
@@ -118,7 +119,7 @@ export class Builder<
118119
})
119120
}
120121

121-
middleware<UOutContext extends Context, TInput, TOutput = any>( // = any here is important to make middleware can be used in any output by default
122+
middleware<UOutContext extends IntersectPick<TCurrentContext, UOutContext>, TInput, TOutput = any>( // = any here is important to make middleware can be used in any output by default
122123
middleware: Middleware<TInitialContext, UOutContext, TInput, TOutput, ORPCErrorConstructorMap<TErrorMap>, TMeta>,
123124
): DecoratedMiddleware<TInitialContext, UOutContext, TInput, TOutput, any, TMeta> { // any ensures middleware can used in any procedure
124125
return decorateMiddleware(middleware)
@@ -133,7 +134,7 @@ export class Builder<
133134
})
134135
}
135136

136-
use<UOutContext extends Context, UInContext extends Context = TCurrentContext>(
137+
use<UOutContext extends IntersectPick<TCurrentContext, UOutContext>, UInContext extends Context = TCurrentContext>(
137138
middleware: Middleware<
138139
UInContext | TCurrentContext,
139140
UOutContext,

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,8 @@ describe('ProcedureImplementer', () => {
272272
builder.use(({ next }, input: 'invalid') => next({}))
273273
// @ts-expect-error --- output is not match
274274
builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({}))
275+
// @ts-expect-error --- conflict context
276+
builder.use(({ next }) => next({ context: { db: undefined } }))
275277
})
276278

277279
it('with map input', () => {
@@ -320,6 +322,8 @@ describe('ProcedureImplementer', () => {
320322
builder.use(({ next }, input: 'invalid') => next({}), input => ({ mapped: true }))
321323
// @ts-expect-error --- output is not match
322324
builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({}), input => ({ mapped: true }))
325+
// @ts-expect-error --- conflict context
326+
builder.use(({ next }) => next({ context: { db: undefined } }), input => ({ mapped: true }))
323327
})
324328

325329
it('with TInContext', () => {

packages/server/src/implementer-procedure.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export interface ProcedureImplementer<
103103
> {
104104
'~orpc': BuilderDef<TInputSchema, TOutputSchema, TErrorMap, TMeta>
105105

106-
'use'<UOutContext extends Context, UInContext extends Context = TCurrentContext>(
106+
'use'<UOutContext extends IntersectPick<TCurrentContext, UOutContext>, UInContext extends Context = TCurrentContext>(
107107
middleware: Middleware<
108108
UInContext | TCurrentContext,
109109
UOutContext,
@@ -121,7 +121,7 @@ export interface ProcedureImplementer<
121121
TMeta
122122
>
123123

124-
'use'<UOutContext extends Context, UInput, UInContext extends Context = TCurrentContext>(
124+
'use'<UOutContext extends IntersectPick<TCurrentContext, UOutContext>, UInput, UInContext extends Context = TCurrentContext>(
125125
middleware: Middleware<
126126
UInContext | TCurrentContext,
127127
UOutContext,

packages/server/src/implementer-variants.test-d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ describe('ImplementerWithMiddlewares', () => {
5353
implementer.use(({ next }, input: 'invalid') => next({}))
5454
// @ts-expect-error --- output is not match
5555
implementer.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({}))
56+
// @ts-expect-error --- conflict context
57+
implementer.use(({ next }) => next({ context: { db: undefined } }))
5658
})
5759

5860
it('with TInContext', () => {

packages/server/src/implementer-variants.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { AnyContractRouter, ContractProcedure, InferContractRouterErrorMap, InferContractRouterMeta } from '@orpc/contract'
2+
import type { IntersectPick } from '@orpc/shared'
23
import type { Context, MergedCurrentContext, MergedInitialContext } from './context'
34
import type { ORPCErrorConstructorMap } from './error'
45
import type { ProcedureImplementer } from './implementer-procedure'
@@ -12,7 +13,7 @@ export interface RouterImplementerWithMiddlewares<
1213
TInitialContext extends Context,
1314
TCurrentContext extends Context,
1415
> {
15-
use<UOutContext extends Context, UInContext extends Context = TCurrentContext>(
16+
use<UOutContext extends IntersectPick<TCurrentContext, UOutContext>, UInContext extends Context = TCurrentContext>(
1617
middleware: Middleware<
1718
UInContext | TCurrentContext,
1819
UOutContext,

packages/server/src/implementer.test-d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ describe('Implementer', () => {
6767
Meta | BaseMeta
6868
>
6969
>()
70+
71+
// @ts-expect-error --- conflict context
72+
implementer.nested.middleware(({ next }) => next({ context: { db: undefined } }))
7073
})
7174

7275
it('can type input and output', () => {
@@ -119,6 +122,8 @@ describe('Implementer', () => {
119122
implementer.use(({ next }, input: 'invalid') => next({}))
120123
// @ts-expect-error --- output is not match
121124
implementer.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({}))
125+
// @ts-expect-error --- conflict context
126+
implementer.use(({ next }) => next({ context: { db: undefined } }))
122127
})
123128

124129
it('with TInContext', () => {

0 commit comments

Comments
 (0)