Skip to content

Commit 43c0c87

Browse files
authored
feat(server): strict builders & allow use middleware between .input/output (#93)
* server: strict builder * server: strict procedure builder wip * server: strict procedure builder tests * server: strict procedure builder tests * implement context guard * fix types * contract * reflect middleware between .input/.output inside builders * reflect middleware between .input/.output inside procedure client * fix
1 parent c4a591c commit 43c0c87

67 files changed

Lines changed: 3747 additions & 1559 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/content/content/docs/server/context.mdx

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -45,53 +45,53 @@ If your procedure only depends on `Middleware Context`, you can
4545
[call it](/docs/server/client) or use it as a [Server Action](/docs/server/server-action) directly.
4646

4747
```ts twoslash
48-
import { os, ORPCError, call } from '@orpc/server'
48+
import { call, ORPCError, os } from '@orpc/server'
49+
import { RPCHandler } from '@orpc/server/fetch'
4950
import { headers } from 'next/headers'
5051

5152
const base = os.use(async ({ context, path, next }, input) => {
52-
return next({
53-
context: {
54-
db: 'fake-db',
55-
}
56-
})
53+
return next({
54+
context: {
55+
db: 'fake-db',
56+
},
57+
})
5758
})
5859

59-
const authMid = base.middleware(async ({ context, next, path }, input) => {
60+
const authMid = os
61+
.context<{ db: string }>() // this middleware depends on some context
62+
.middleware(async ({ context, next, path }, input) => {
6063
const headersList = await headers()
6164
const user = headersList.get('Authorization') ? { id: 'example' } : undefined
6265

6366
if (!user) {
64-
throw new ORPCError({ code: 'UNAUTHORIZED' })
67+
throw new ORPCError({ code: 'UNAUTHORIZED' })
6568
}
6669

6770
return next({
68-
context: {
69-
user,
70-
}
71+
context: {
72+
user,
73+
},
7174
})
72-
})
75+
})
7376

7477
export const router = base.router({
75-
getUser: base
76-
.use(authMid)
77-
.handler(({ input, context }) => {
78-
// ^ context is fully typed
79-
}),
78+
getUser: base
79+
.use(authMid)
80+
.handler(({ input, context }) => {
81+
// ^ context is fully typed
82+
}),
8083
})
8184

8285
// You can call this procedure directly without manually providing context
8386
const output = await call(router.getUser, null)
8487

85-
import { RPCHandler } from '@orpc/server/fetch'
86-
import { OpenAPIServerlessHandler, OpenAPIServerHandler } from '@orpc/openapi/fetch'
87-
8888
const rpcHandler = new RPCHandler(router)
8989

9090
export async function fetch(request: Request) {
91-
// No need to pass context; middleware handles it
92-
const { response } = await rpcHandler.handle(request)
91+
// No need to pass context; middleware handles it
92+
const { response } = await rpcHandler.handle(request)
9393

94-
return response ?? new Response('Not found', { status: 404 })
94+
return response ?? new Response('Not found', { status: 404 })
9595
}
9696
```
9797

apps/content/examples/middleware.ts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,7 @@ import { z } from 'zod'
55

66
export type Context = { user?: { id: string } }
77

8-
export const pub = os
9-
.context<Context>()
10-
.use(async ({ context, path, next }, input) => {
11-
// This middleware will apply to everything create from pub
12-
const start = Date.now()
13-
14-
try {
15-
return await next({})
16-
}
17-
finally {
18-
// eslint-disable-next-line no-console
19-
console.log(`middleware cost ${Date.now() - start}ms`)
20-
}
21-
})
8+
export const pub = os.context<Context>()
229

2310
export const authMiddleware = pub.middleware(async ({ context, next, path, procedure }, input) => {
2411
if (!context.user) {
Lines changed: 47 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1-
import type { DecoratedContractProcedure } from './procedure-decorated'
1+
import type { ContractProcedure } from './procedure'
2+
import type { ContractProcedureBuilder } from './procedure-builder'
3+
import type { ContractProcedureBuilderWithInput } from './procedure-builder-with-input'
4+
import type { ContractProcedureBuilderWithOutput } from './procedure-builder-with-output'
25
import type { AdaptedContractRouter, ContractRouterBuilder } from './router-builder'
36
import { z } from 'zod'
47
import { ContractBuilder } from './builder'
5-
import { ContractProcedure } from './procedure'
68

7-
const schema = z.object({
8-
value: z.string(),
9-
})
9+
const schema = z.object({ value: z.string() })
1010

1111
const baseErrorMap = {
1212
BASE: {
13-
status: 500,
1413
data: z.object({
1514
message: z.string(),
1615
}),
@@ -19,142 +18,72 @@ const baseErrorMap = {
1918

2019
const builder = new ContractBuilder({ errorMap: baseErrorMap, OutputSchema: undefined, InputSchema: undefined })
2120

22-
it('also is a contract procedure', () => {
23-
expectTypeOf(builder).toMatchTypeOf<ContractProcedure<undefined, undefined, typeof baseErrorMap>>()
24-
})
25-
26-
describe('self chainable', () => {
27-
describe('errors', () => {
28-
const errors = {
29-
BAD: {
30-
status: 500,
31-
data: schema,
32-
},
33-
ERROR2: {
34-
status: 401,
35-
data: schema,
36-
},
37-
} as const
38-
39-
it('should merge and strict with old one', () => {
40-
expectTypeOf(builder.errors(errors)).toEqualTypeOf<
41-
ContractBuilder<typeof errors & typeof baseErrorMap>
42-
>()
43-
})
44-
45-
it('should prevent redefine errorMap', () => {
46-
// @ts-expect-error - not allow redefine errorMap
47-
builder.errors({ BASE: baseErrorMap.BASE })
48-
// @ts-expect-error - not allow redefine errorMap - even with undefined
49-
builder.errors({ BASE: undefined })
50-
})
21+
describe('ContractBuilder', () => {
22+
it('is a contract procedure', () => {
23+
expectTypeOf(builder).toMatchTypeOf<ContractProcedure<undefined, undefined, typeof baseErrorMap>>()
5124
})
52-
})
53-
54-
describe('to ContractRouterBuilder', () => {
55-
it('prefix', () => {
56-
expectTypeOf(builder.prefix('/prefix')).toEqualTypeOf<
57-
ContractRouterBuilder<typeof baseErrorMap>
58-
>()
5925

60-
// @ts-expect-error - invalid prefix
61-
builder.prefix(1)
62-
// @ts-expect-error - invalid prefix
63-
builder.prefix('')
64-
})
26+
it('.errors', () => {
27+
const errors = { BAD_GATEWAY: { data: schema } } as const
6528

66-
it('tags', () => {
67-
expectTypeOf(builder.tag('tag1', 'tag2')).toEqualTypeOf<
68-
ContractRouterBuilder<typeof baseErrorMap>
69-
>()
29+
expectTypeOf(builder.errors(errors))
30+
.toEqualTypeOf<ContractBuilder<typeof baseErrorMap & typeof errors>>()
7031

71-
// @ts-expect-error - invalid tag
72-
builder.tag(1)
73-
// @ts-expect-error - invalid tag
74-
builder.tag({})
32+
// @ts-expect-error - not allow redefine error map
33+
builder.errors({ BASE: baseErrorMap.BASE })
7534
})
76-
})
7735

78-
describe('to DecoratedContractProcedure', () => {
79-
it('route', () => {
80-
expectTypeOf(builder.route({ method: 'GET', path: '/path' })).toEqualTypeOf<
81-
DecoratedContractProcedure<undefined, undefined, typeof baseErrorMap>
82-
>()
83-
84-
expectTypeOf(builder.route({ })).toEqualTypeOf<
85-
DecoratedContractProcedure<undefined, undefined, typeof baseErrorMap>
86-
>()
36+
it('.route', () => {
37+
expectTypeOf(builder.route({ method: 'GET' })).toEqualTypeOf<ContractProcedureBuilder<typeof baseErrorMap>>()
8738

8839
// @ts-expect-error - invalid method
8940
builder.route({ method: 'HE' })
90-
// @ts-expect-error - invalid path
91-
builder.route({ method: 'GET', path: '' })
9241
})
9342

94-
it('input', () => {
43+
it('.input', () => {
9544
expectTypeOf(builder.input(schema)).toEqualTypeOf<
96-
DecoratedContractProcedure<typeof schema, undefined, typeof baseErrorMap>
97-
>()
98-
99-
expectTypeOf(builder.input(schema, { value: 'example' })).toEqualTypeOf<
100-
DecoratedContractProcedure<typeof schema, undefined, typeof baseErrorMap>
45+
ContractProcedureBuilderWithInput<typeof schema, typeof baseErrorMap >
10146
>()
102-
103-
// @ts-expect-error - invalid schema
104-
builder.input({})
105-
106-
// @ts-expect-error - invalid example
107-
builder.input(schema, { })
10847
})
10948

110-
it('output', () => {
49+
it('.output', () => {
11150
expectTypeOf(builder.output(schema)).toEqualTypeOf<
112-
DecoratedContractProcedure<undefined, typeof schema, typeof baseErrorMap>
113-
>()
114-
115-
expectTypeOf(builder.output(schema, { value: 'example' })).toEqualTypeOf<
116-
DecoratedContractProcedure<undefined, typeof schema, typeof baseErrorMap>
51+
ContractProcedureBuilderWithOutput<typeof schema, typeof baseErrorMap >
11752
>()
53+
})
11854

119-
// @ts-expect-error - invalid schema
120-
builder.output({})
55+
it('.prefix', () => {
56+
expectTypeOf(builder.prefix('/api')).toEqualTypeOf<ContractRouterBuilder<typeof baseErrorMap>>()
12157

122-
// @ts-expect-error - invalid example
123-
builder.output(schema, {})
58+
// @ts-expect-error - invalid prefix
59+
builder.prefix(1)
12460
})
125-
})
12661

127-
describe('to router', () => {
128-
const errors = {
129-
CONFLICT: {
130-
status: 400,
131-
data: z.object({
132-
message: z.string(),
133-
}),
134-
},
135-
}
136-
137-
const router = { a: { b: {
138-
c: new ContractProcedure({ InputSchema: undefined, OutputSchema: undefined, errorMap: errors }),
139-
} } }
140-
141-
it('adapt all procedures', () => {
142-
expectTypeOf(builder.router(router)).toEqualTypeOf<AdaptedContractRouter<typeof router, typeof baseErrorMap>>()
143-
expectTypeOf(builder.router({})).toEqualTypeOf<Record<never, never>>()
62+
it('.tag', () => {
63+
expectTypeOf(builder.tag('tag1', 'tag2')).toEqualTypeOf<ContractRouterBuilder<typeof baseErrorMap>>()
14464

145-
// @ts-expect-error - invalid router
146-
builder.router({ a: 1 })
65+
// @ts-expect-error - invalid tag
66+
builder.tag(1)
14767
})
14868

149-
it('throw on conflict error map', () => {
150-
builder.router({ ping: {} as ContractProcedure<any, any, { BASE: typeof baseErrorMap['BASE'] }> })
151-
// @ts-expect-error conflict
152-
builder.router({ ping: {} as ContractProcedure<any, any, { BASE: { message: string } }> })
153-
})
69+
it('.router', () => {
70+
const router = {
71+
ping: {} as ContractProcedure<undefined, typeof schema, typeof baseErrorMap>,
72+
pong: {} as ContractProcedure<typeof schema, undefined, Record<never, never>>,
73+
}
15474

155-
it('only required partial match error map', () => {
156-
expectTypeOf(builder.router({ ping: {} as ContractProcedure<any, any, { OTHER: { status: number } }> })).toEqualTypeOf<{
157-
ping: DecoratedContractProcedure<any, any, { OTHER: { status: number } } & typeof baseErrorMap>
158-
}>()
75+
expectTypeOf(builder.router(router)).toEqualTypeOf<AdaptedContractRouter<typeof router, typeof baseErrorMap>>()
76+
77+
const invalidErrorMap = {
78+
BASE: {
79+
...baseErrorMap.BASE,
80+
status: 400,
81+
},
82+
}
83+
84+
builder.router({
85+
// @ts-expect-error - error map is not match
86+
ping: {} as ContractProcedure<undefined, typeof schema, typeof invalidErrorMap>,
87+
})
15988
})
16089
})

0 commit comments

Comments
 (0)