Skip to content

Commit d452413

Browse files
authored
fix(server): reflect middleware initial context in procedure when use (#234)
**Bug Description:** The procedure ignores the middleware's initial context type. It only checks that the current context meets the expected type when calling `.use`. As a result, you can accidentally pass extra or invalid context values that the middleware doesn't expect, potentially causing it to break. **Example:** ```ts const mid = os.$context<{ retryable?: boolean }>().middleware(({ context, next }) => { // Use context.retryable here return next(); }); const procedure = os.use(mid).handler(() => 'Hello world'); ``` In this example, `procedure` does not reflect the initial context type `{ retryable?: boolean }`. It only verifies that the current context satisfies the `{ retryable?: boolean }`. ```ts const handler = new RPCHandler(procedure); handler.handle(request, { context: { retryable: 'any-value-can-provide' // Invalid value that may break the middleware } }); ``` Now, the middleware `mid` can break when it encounters unexpected context values. --- This PR also removes the unnecessary current context check after `.use`, as we had already removed the `dedupe-middleware` logic earlier. Therefore, it is safe to remove this check. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Refactor** - Enhanced middleware and context management for more flexible and robust API behavior. - Updated builder, procedure, and router interfaces to offer clearer configuration and improved type safety. - Simplified type definitions for middleware functions, focusing on output types and removing unnecessary constraints. - **Tests** - Expanded test coverage to rigorously validate context merging and middleware configurations, ensuring accurate error handling and robust type checking. - Added new tests for invalid context types and refined existing tests for better clarity and type safety. - Restructured tests for better organization and readability, focusing on specific context handling scenarios. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent a5c2886 commit d452413

22 files changed

Lines changed: 817 additions & 514 deletions

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

Lines changed: 267 additions & 165 deletions
Large diffs are not rendered by default.

packages/server/src/builder-variants.ts

Lines changed: 56 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { AnySchema, ContractRouter, ErrorMap, HTTPPath, InferSchemaInput, InferSchemaOutput, MergedErrorMap, Meta, Route, Schema } from '@orpc/contract'
22
import type { BuilderDef } from './builder'
3-
import type { ConflictContextGuard, Context, MergedContext } from './context'
3+
import type { Context, ContextExtendsGuard, MergedCurrentContext, MergedInitialContext } from './context'
44
import type { ORPCErrorConstructorMap } from './error'
55
import type { Lazy } from './lazy'
66
import type { MapInputMiddleware, Middleware } from './middleware'
@@ -30,19 +30,19 @@ export interface BuilderWithMiddlewares<
3030
TMeta
3131
>
3232

33-
'use'<UOutContext extends Context>(
33+
'use'<UOutContext extends Context, UInContext extends Context = TCurrentContext>(
3434
middleware: Middleware<
35-
TCurrentContext,
35+
UInContext,
3636
UOutContext,
3737
unknown,
3838
unknown,
3939
ORPCErrorConstructorMap<TErrorMap>,
4040
TMeta
4141
>,
42-
): ConflictContextGuard<MergedContext<TCurrentContext, UOutContext>> &
43-
BuilderWithMiddlewares<
44-
TInitialContext,
45-
MergedContext<TCurrentContext, UOutContext>,
42+
): ContextExtendsGuard<TCurrentContext, UInContext>
43+
& BuilderWithMiddlewares<
44+
MergedInitialContext<TInitialContext, UInContext, TCurrentContext>,
45+
MergedCurrentContext<TCurrentContext, UOutContext>,
4646
TInputSchema,
4747
TOutputSchema,
4848
TErrorMap,
@@ -75,11 +75,11 @@ export interface BuilderWithMiddlewares<
7575

7676
'router'<U extends Router<ContractRouter<TMeta>, TCurrentContext>>(
7777
router: U
78-
): EnhancedRouter<U, TInitialContext, TErrorMap>
78+
): EnhancedRouter<U, TInitialContext, TCurrentContext, TErrorMap>
7979

8080
'lazy'<U extends Router<ContractRouter<TMeta>, TCurrentContext>>(
8181
loader: () => Promise<{ default: U }>,
82-
): EnhancedRouter<Lazy<U>, TInitialContext, TErrorMap>
82+
): EnhancedRouter<Lazy<U>, TInitialContext, TCurrentContext, TErrorMap>
8383
}
8484

8585
export interface ProcedureBuilder<
@@ -103,19 +103,19 @@ export interface ProcedureBuilder<
103103
TMeta
104104
>
105105

106-
'use'<UOutContext extends Context>(
106+
'use'<UOutContext extends Context, UInContext extends Context = TCurrentContext>(
107107
middleware: Middleware<
108-
TCurrentContext,
108+
UInContext,
109109
UOutContext,
110110
unknown,
111111
unknown,
112112
ORPCErrorConstructorMap<TErrorMap>,
113113
TMeta
114114
>,
115-
): ConflictContextGuard<MergedContext<TCurrentContext, UOutContext>> &
116-
ProcedureBuilder<
117-
TInitialContext,
118-
MergedContext<TCurrentContext, UOutContext>,
115+
): ContextExtendsGuard<TCurrentContext, UInContext>
116+
& ProcedureBuilder<
117+
MergedInitialContext<TInitialContext, UInContext, TCurrentContext>,
118+
MergedCurrentContext<TCurrentContext, UOutContext>,
119119
TInputSchema,
120120
TOutputSchema,
121121
TErrorMap,
@@ -157,39 +157,39 @@ export interface ProcedureBuilderWithInput<
157157
errors: U,
158158
): ProcedureBuilderWithInput<TInitialContext, TCurrentContext, TInputSchema, TOutputSchema, MergedErrorMap<TErrorMap, U>, TMeta>
159159

160-
'use'<UOutContext extends Context>(
160+
'use'<UOutContext extends Context, UInContext extends Context = TCurrentContext>(
161161
middleware: Middleware<
162-
TCurrentContext,
162+
UInContext,
163163
UOutContext,
164164
InferSchemaOutput<TInputSchema>,
165165
unknown,
166166
ORPCErrorConstructorMap<TErrorMap>,
167167
TMeta
168168
>,
169-
): ConflictContextGuard<MergedContext<TCurrentContext, UOutContext>> &
170-
ProcedureBuilderWithInput<
171-
TInitialContext,
172-
MergedContext<TCurrentContext, UOutContext>,
169+
): ContextExtendsGuard<TCurrentContext, UInContext>
170+
& ProcedureBuilderWithInput<
171+
MergedInitialContext<TInitialContext, UInContext, TCurrentContext>,
172+
MergedCurrentContext<TCurrentContext, UOutContext>,
173173
TInputSchema,
174174
TOutputSchema,
175175
TErrorMap,
176176
TMeta
177177
>
178178

179-
'use'<UOutContext extends Context, UInput>(
179+
'use'<UOutContext extends Context, UInput, UInContext extends Context = TCurrentContext>(
180180
middleware: Middleware<
181-
TCurrentContext,
181+
UInContext,
182182
UOutContext,
183183
UInput,
184184
unknown,
185185
ORPCErrorConstructorMap<TErrorMap>,
186186
TMeta
187187
>,
188188
mapInput: MapInputMiddleware<InferSchemaOutput<TInputSchema>, UInput>,
189-
): ConflictContextGuard<MergedContext<TCurrentContext, UOutContext>> &
190-
ProcedureBuilderWithInput<
191-
TInitialContext,
192-
MergedContext<TCurrentContext, UOutContext>,
189+
): ContextExtendsGuard<TCurrentContext, UInContext>
190+
& ProcedureBuilderWithInput<
191+
MergedInitialContext<TInitialContext, UInContext, TCurrentContext>,
192+
MergedCurrentContext<TCurrentContext, UOutContext>,
193193
TInputSchema,
194194
TOutputSchema,
195195
TErrorMap,
@@ -234,19 +234,19 @@ export interface ProcedureBuilderWithOutput<
234234
TMeta
235235
>
236236

237-
'use'<UOutContext extends Context>(
237+
'use'<UOutContext extends Context, UInContext extends Context = TCurrentContext>(
238238
middleware: Middleware<
239-
TCurrentContext,
239+
UInContext,
240240
UOutContext,
241241
unknown,
242242
InferSchemaInput<TOutputSchema>,
243243
ORPCErrorConstructorMap<TErrorMap>,
244244
TMeta
245245
>,
246-
): ConflictContextGuard<MergedContext<TCurrentContext, UOutContext>> &
247-
ProcedureBuilderWithOutput<
248-
TInitialContext,
249-
MergedContext<TCurrentContext, UOutContext>,
246+
): ContextExtendsGuard<TCurrentContext, UInContext>
247+
& ProcedureBuilderWithOutput<
248+
MergedInitialContext<TInitialContext, UInContext, TCurrentContext>,
249+
MergedCurrentContext<TCurrentContext, UOutContext>,
250250
TInputSchema,
251251
TOutputSchema,
252252
TErrorMap,
@@ -284,39 +284,39 @@ export interface ProcedureBuilderWithInputOutput<
284284
errors: U,
285285
): ProcedureBuilderWithInputOutput<TInitialContext, TCurrentContext, TInputSchema, TOutputSchema, MergedErrorMap<TErrorMap, U>, TMeta>
286286

287-
'use'<UOutContext extends Context>(
287+
'use'<UOutContext extends Context, UInContext extends Context = TCurrentContext>(
288288
middleware: Middleware<
289-
TCurrentContext,
289+
UInContext,
290290
UOutContext,
291291
InferSchemaOutput<TInputSchema>,
292292
InferSchemaInput<TOutputSchema>,
293293
ORPCErrorConstructorMap<TErrorMap>,
294294
TMeta
295295
>,
296-
): ConflictContextGuard<MergedContext<TCurrentContext, UOutContext>> &
297-
ProcedureBuilderWithInputOutput<
298-
TInitialContext,
299-
MergedContext<TCurrentContext, UOutContext>,
296+
): ContextExtendsGuard<TCurrentContext, UInContext>
297+
& ProcedureBuilderWithInputOutput<
298+
MergedInitialContext<TInitialContext, UInContext, TCurrentContext>,
299+
MergedCurrentContext<TCurrentContext, UOutContext>,
300300
TInputSchema,
301301
TOutputSchema,
302302
TErrorMap,
303303
TMeta
304304
>
305305

306-
'use'<UOutContext extends Context, UInput>(
306+
'use'<UOutContext extends Context, UInput, UInContext extends Context = TCurrentContext>(
307307
middleware: Middleware<
308-
TCurrentContext,
308+
UInContext,
309309
UOutContext,
310310
UInput,
311311
InferSchemaInput<TOutputSchema>,
312312
ORPCErrorConstructorMap<TErrorMap>,
313313
TMeta
314314
>,
315315
mapInput: MapInputMiddleware<InferSchemaOutput<TInputSchema>, UInput>,
316-
): ConflictContextGuard<MergedContext<TCurrentContext, UOutContext>> &
317-
ProcedureBuilderWithInputOutput<
318-
TInitialContext,
319-
MergedContext<TCurrentContext, UOutContext>,
316+
): ContextExtendsGuard<TCurrentContext, UInContext>
317+
& ProcedureBuilderWithInputOutput<
318+
MergedInitialContext<TInitialContext, UInContext, TCurrentContext>,
319+
MergedCurrentContext<TCurrentContext, UOutContext>,
320320
TInputSchema,
321321
TOutputSchema,
322322
TErrorMap,
@@ -348,27 +348,32 @@ export interface RouterBuilder<
348348
errors: U,
349349
): RouterBuilder<TInitialContext, TCurrentContext, MergedErrorMap<TErrorMap, U>, TMeta>
350350

351-
'use'<UOutContext extends Context>(
351+
'use'<UOutContext extends Context, UInContext extends Context = TCurrentContext>(
352352
middleware: Middleware<
353-
TCurrentContext,
353+
UInContext,
354354
UOutContext,
355355
unknown,
356356
unknown,
357357
ORPCErrorConstructorMap<TErrorMap>,
358358
TMeta
359359
>,
360-
): ConflictContextGuard<MergedContext<TCurrentContext, UOutContext>> &
361-
RouterBuilder<TInitialContext, MergedContext<TCurrentContext, UOutContext>, TErrorMap, TMeta>
360+
): ContextExtendsGuard<TCurrentContext, UInContext>
361+
& RouterBuilder<
362+
MergedInitialContext<TInitialContext, UInContext, TCurrentContext>,
363+
MergedCurrentContext<TCurrentContext, UOutContext>,
364+
TErrorMap,
365+
TMeta
366+
>
362367

363368
'prefix'(prefix: HTTPPath): RouterBuilder<TInitialContext, TCurrentContext, TErrorMap, TMeta>
364369

365370
'tag'(...tags: string[]): RouterBuilder<TInitialContext, TCurrentContext, TErrorMap, TMeta>
366371

367372
'router'<U extends Router<ContractRouter<TMeta>, TCurrentContext>>(
368373
router: U
369-
): EnhancedRouter<U, TInitialContext, TErrorMap>
374+
): EnhancedRouter<U, TInitialContext, TCurrentContext, TErrorMap>
370375

371376
'lazy'<U extends Router<ContractRouter<TMeta>, TCurrentContext>>(
372377
loader: () => Promise<{ default: U }>,
373-
): EnhancedRouter<Lazy<U>, TInitialContext, TErrorMap>
378+
): EnhancedRouter<Lazy<U>, TInitialContext, TCurrentContext, TErrorMap>
374379
}

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

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { BuilderWithMiddlewares, ProcedureBuilder, ProcedureBuilderWithInpu
66
import type { Context } from './context'
77
import type { ORPCErrorConstructorMap } from './error'
88
import type { Lazy } from './lazy'
9-
import type { MiddlewareOutputFn } from './middleware'
9+
import type { Middleware, MiddlewareOutputFn } from './middleware'
1010
import type { DecoratedMiddleware } from './middleware-decorated'
1111
import type { Procedure } from './procedure'
1212
import type { DecoratedProcedure } from './procedure-decorated'
@@ -232,19 +232,17 @@ describe('Builder', () => {
232232

233233
expectTypeOf(applied).toEqualTypeOf<
234234
BuilderWithMiddlewares<
235-
InitialContext,
236-
CurrentContext & { extra: boolean },
235+
InitialContext & Record<never, never>,
236+
Omit<CurrentContext, 'extra'> & { extra: boolean },
237237
typeof inputSchema,
238238
typeof outputSchema,
239239
typeof baseErrorMap,
240240
BaseMeta
241241
>
242242
>()
243243

244-
// @ts-expect-error --- conflict context
245-
builder.use(({ next }) => next({ context: { db: 123 } }))
246-
// conflict but not detected
247-
expectTypeOf(builder.use(({ next }) => next({ context: { db: undefined } }))).toMatchTypeOf<never>()
244+
// invalid TInContext
245+
expectTypeOf(builder.use({} as Middleware<{ auth: 'invalid' }, any, any, any, any, any>)).toEqualTypeOf<never>()
248246
// @ts-expect-error --- input is not match
249247
builder.use(({ next }, input: 'invalid') => next({}))
250248
// @ts-expect-error --- output is not match
@@ -276,8 +274,8 @@ describe('Builder', () => {
276274

277275
expectTypeOf(applied).toEqualTypeOf<
278276
BuilderWithMiddlewares<
279-
InitialContext,
280-
CurrentContext & { extra: boolean },
277+
InitialContext & Record<never, never>,
278+
Omit<CurrentContext, 'extra'> & { extra: boolean },
281279
typeof inputSchema,
282280
typeof outputSchema,
283281
typeof baseErrorMap,
@@ -291,15 +289,39 @@ describe('Builder', () => {
291289
input => ({ invalid: true }),
292290
)
293291

294-
// @ts-expect-error --- conflict context
295-
builder.use(({ next }) => next({ context: { db: 123 } }), input => ({ mapped: true }))
296-
// conflict but not detected
297-
expectTypeOf(builder.use(({ next }) => next({ context: { db: undefined } }), input => ({ mapped: true }))).toMatchTypeOf<never>()
292+
// invalid TInContext
293+
expectTypeOf(builder.use({} as Middleware<{ auth: 'invalid' }, any, any, any, any, any>, () => { })).toEqualTypeOf<never>()
298294
// @ts-expect-error --- input is not match
299295
builder.use(({ next }, input: 'invalid') => next({}), input => ({ mapped: true }))
300296
// @ts-expect-error --- output is not match
301297
builder.use(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({}), input => ({ mapped: true }))
302298
})
299+
300+
it('with TInContext', () => {
301+
const mid = {} as Middleware<{ cacheable?: boolean }, Record<never, never>, unknown, unknown, ORPCErrorConstructorMap<any>, BaseMeta>
302+
303+
expectTypeOf(builder.use(mid)).toEqualTypeOf<
304+
BuilderWithMiddlewares<
305+
InitialContext & { cacheable?: boolean },
306+
Omit<CurrentContext, never> & Record<never, never>,
307+
typeof inputSchema,
308+
typeof outputSchema,
309+
typeof baseErrorMap,
310+
BaseMeta
311+
>
312+
>()
313+
314+
expectTypeOf(builder.use(mid, () => { })).toEqualTypeOf<
315+
BuilderWithMiddlewares<
316+
InitialContext & { cacheable?: boolean },
317+
Omit<CurrentContext, never> & Record<never, never>,
318+
typeof inputSchema,
319+
typeof outputSchema,
320+
typeof baseErrorMap,
321+
BaseMeta
322+
>
323+
>()
324+
})
303325
})
304326

305327
it('.meta', () => {
@@ -410,7 +432,7 @@ describe('Builder', () => {
410432

411433
it('.router', () => {
412434
expectTypeOf(builder.router(router)).toEqualTypeOf<
413-
EnhancedRouter<typeof router, InitialContext, typeof baseErrorMap>
435+
EnhancedRouter<typeof router, InitialContext, CurrentContext, typeof baseErrorMap>
414436
>()
415437

416438
builder.router({
@@ -434,7 +456,7 @@ describe('Builder', () => {
434456

435457
it('.lazy', () => {
436458
expectTypeOf(builder.lazy(() => Promise.resolve({ default: router }))).toEqualTypeOf<
437-
EnhancedRouter<Lazy<typeof router>, InitialContext, typeof baseErrorMap>
459+
EnhancedRouter<Lazy<typeof router>, InitialContext, CurrentContext, typeof baseErrorMap>
438460
>()
439461

440462
// @ts-expect-error - initial context is not match

0 commit comments

Comments
 (0)