Skip to content

Commit 2d41af5

Browse files
authored
feat(server): allow .concat middlewares with narrowed input types (#420)
For example, if middleware A accepts unknown as input, B can now specify a more specific input type (e.g., number, { id: string }, etc.). Previously, both A and B were required to use unknown for the input type when concatenated. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Documentation** - Improved middleware documentation with new notes and examples on using input mapping and aligning input types when composing middleware. - **Bug Fixes** - Enhanced type safety and flexibility for middleware input types, ensuring better compatibility when chaining or transforming middleware. - **Tests** - Updated and expanded tests to verify correct handling and rejection of new input shapes in middleware. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent ff41b3a commit 2d41af5

File tree

3 files changed

+37
-16
lines changed

3 files changed

+37
-16
lines changed

apps/content/docs/middleware.md

+18
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,20 @@ const pong = os
121121
})
122122
```
123123

124+
::: info
125+
You can adapt a middleware to accept a different input shape by using `.mapInput`.
126+
127+
```ts
128+
const canUpdate = os.middleware(async ({ context, next }, input: number) => {
129+
return next()
130+
})
131+
132+
// Transform middleware to accept a new input shape
133+
const mappedCanUpdate = canUpdate.mapInput((input: { id: number }) => input.id)
134+
```
135+
136+
:::
137+
124138
## Middleware Output
125139

126140
Middleware can also modify the output of a handler, such as implementing caching mechanisms.
@@ -151,6 +165,10 @@ const concatMiddleware = aMiddleware
151165
.concat(anotherMiddleware)
152166
```
153167

168+
::: info
169+
If you want to concatenate two middlewares with different input types, you can use `.mapInput` to align their input types before concatenation.
170+
:::
171+
154172
## Built-in Middlewares
155173

156174
oRPC provides some built-in middlewares that can be used to simplify common use cases.

packages/server/src/middleware-decorated.test-d.ts

+13-12
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { Procedure } from './procedure'
1010
const decorated = {} as DecoratedMiddleware<
1111
CurrentContext,
1212
{ extra: boolean },
13-
{ input: string },
13+
{ input1: string, input2: string },
1414
{ output: number },
1515
ORPCErrorConstructorMap<typeof baseErrorMap>,
1616
BaseMeta
@@ -22,7 +22,7 @@ describe('DecoratedMiddleware', () => {
2222
Middleware<
2323
CurrentContext,
2424
{ extra: boolean },
25-
{ input: string },
25+
{ input1: string, input2: string },
2626
{ output: number },
2727
ORPCErrorConstructorMap<typeof baseErrorMap>,
2828
BaseMeta
@@ -31,7 +31,7 @@ describe('DecoratedMiddleware', () => {
3131
})
3232

3333
it('.mapInput', () => {
34-
const mapped = decorated.mapInput((input: 'input') => ({ input }))
34+
const mapped = decorated.mapInput((input: 'input') => ({ input1: input, input2: input }))
3535

3636
expectTypeOf(mapped).toEqualTypeOf<
3737
DecoratedMiddleware<
@@ -50,8 +50,7 @@ describe('DecoratedMiddleware', () => {
5050

5151
describe('.concat', () => {
5252
it('without map input', () => {
53-
const applied = decorated.concat(({ context, next, path, procedure, errors, signal }, input, output) => {
54-
expectTypeOf(input).toEqualTypeOf<{ input: string }>()
53+
const applied = decorated.concat(({ context, next, path, procedure, errors, signal }, input: { input1: string }, output) => {
5554
expectTypeOf(context).toEqualTypeOf<Omit<CurrentContext, 'extra'> & { extra: boolean }>()
5655
expectTypeOf(path).toEqualTypeOf<readonly string[]>()
5756
expectTypeOf(procedure).toEqualTypeOf<
@@ -72,7 +71,7 @@ describe('DecoratedMiddleware', () => {
7271
DecoratedMiddleware<
7372
CurrentContext & Record<never, never>,
7473
Omit<{ extra: boolean }, never> & { extra2: boolean },
75-
{ input: string },
74+
{ input1: string, input2: string },
7675
{ output: number },
7776
ORPCErrorConstructorMap<typeof baseErrorMap>,
7877
BaseMeta
@@ -81,6 +80,8 @@ describe('DecoratedMiddleware', () => {
8180

8281
// @ts-expect-error --- invalid TInContext
8382
decorated.concat({} as Middleware<{ auth: 'invalid' }, any, any, any, any, any>)
83+
// @ts-expect-error --- input is not match
84+
decorated.concat(({ next }, input: { invalid: string }) => next({}))
8485
// @ts-expect-error --- output is not match
8586
decorated.concat(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({}))
8687
// @ts-expect-error --- conflict context
@@ -104,17 +105,15 @@ describe('DecoratedMiddleware', () => {
104105
extra2: true,
105106
},
106107
})
107-
}, (input) => {
108-
expectTypeOf(input).toEqualTypeOf<{ input: string }>()
109-
108+
}, (input: { input1: string }) => {
110109
return { mapped: true }
111110
})
112111

113112
expectTypeOf(applied).toEqualTypeOf<
114113
DecoratedMiddleware<
115114
CurrentContext & Record<never, never>,
116115
Omit<{ extra: boolean }, never> & { extra2: boolean },
117-
{ input: string },
116+
{ input1: string, input2: string },
118117
{ output: number },
119118
ORPCErrorConstructorMap<typeof baseErrorMap>,
120119
BaseMeta
@@ -129,6 +128,8 @@ describe('DecoratedMiddleware', () => {
129128

130129
// @ts-expect-error --- invalid TInContext
131130
decorated.concat({} as Middleware<{ auth: 'invalid' }, any, any, any, any, any>, () => { })
131+
// @ts-expect-error --- input is not match
132+
decorated.concat(({ next }, input: { mapped: boolean }) => next({}), (input: { invalid: string }) => ({ mapped: true }))
132133
// @ts-expect-error --- output is not match
133134
decorated.concat(({ next }, input, output: MiddlewareOutputFn<'invalid'>) => next({}), input => ({ mapped: true }))
134135
// @ts-expect-error --- conflict context
@@ -142,7 +143,7 @@ describe('DecoratedMiddleware', () => {
142143
DecoratedMiddleware<
143144
CurrentContext & Omit<{ cacheable?: boolean } & Record<never, never>, 'db' | 'auth' | 'extra'>,
144145
Omit<{ extra: boolean }, never> & Record<never, never>,
145-
{ input: string },
146+
{ input1: string, input2: string },
146147
{ output: number },
147148
ORPCErrorConstructorMap<typeof baseErrorMap>,
148149
BaseMeta
@@ -153,7 +154,7 @@ describe('DecoratedMiddleware', () => {
153154
DecoratedMiddleware<
154155
CurrentContext & Omit<{ cacheable?: boolean } & Record<never, never>, 'db' | 'auth' | 'extra'>,
155156
Omit<{ extra: boolean }, never> & Record<never, never>,
156-
{ input: string },
157+
{ input1: string, input2: string },
157158
{ output: number },
158159
ORPCErrorConstructorMap<typeof baseErrorMap>,
159160
BaseMeta

packages/server/src/middleware-decorated.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,21 @@ export interface DecoratedMiddleware<
2727
*/
2828
concat<
2929
UOutContext extends IntersectPick<MergedCurrentContext<TInContext, TOutContext>, UOutContext>,
30+
UInput extends TInput,
3031
UInContext extends Context = MergedCurrentContext<TInContext, TOutContext>,
3132
>(
3233
middleware: Middleware<
3334
UInContext | MergedCurrentContext<TInContext, TOutContext>,
3435
UOutContext,
35-
TInput,
36+
UInput,
3637
TOutput,
3738
TErrorConstructorMap,
3839
TMeta
3940
>,
4041
): DecoratedMiddleware<
4142
MergedInitialContext<TInContext, UInContext, MergedCurrentContext<TInContext, TOutContext>>,
4243
MergedCurrentContext<TOutContext, UOutContext>,
43-
TInput,
44+
UInput,
4445
TOutput,
4546
TErrorConstructorMap,
4647
TMeta
@@ -54,6 +55,7 @@ export interface DecoratedMiddleware<
5455
*/
5556
concat<
5657
UOutContext extends IntersectPick<MergedCurrentContext<TInContext, TOutContext>, UOutContext>,
58+
UInput extends TInput,
5759
UMappedInput,
5860
UInContext extends Context = MergedCurrentContext<TInContext, TOutContext>,
5961
>(
@@ -65,11 +67,11 @@ export interface DecoratedMiddleware<
6567
TErrorConstructorMap,
6668
TMeta
6769
>,
68-
mapInput: MapInputMiddleware<TInput, UMappedInput>,
70+
mapInput: MapInputMiddleware<UInput, UMappedInput>,
6971
): DecoratedMiddleware<
7072
MergedInitialContext<TInContext, UInContext, MergedCurrentContext<TInContext, TOutContext>>,
7173
MergedCurrentContext<TOutContext, UOutContext>,
72-
TInput,
74+
UInput,
7375
TOutput,
7476
TErrorConstructorMap,
7577
TMeta

0 commit comments

Comments
 (0)