Skip to content

Commit a3c9e47

Browse files
authored
feat(contract): implement .errors in every builder (#86)
* remove undefine value from ErrorMap * errors now can define multiple time, and merged * guard error map * contract: .errors for router builder * improve error map * server: procedure allow define .errors multiple time * router builder * router implementer * fixed
1 parent 9914009 commit a3c9e47

63 files changed

Lines changed: 993 additions & 545 deletions

Some content is hidden

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

packages/client/src/client.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ export interface createORPCClientOptions {
99
path?: string[]
1010
}
1111

12-
export function createORPCClient<TRouter extends ANY_ROUTER | ContractRouter, TClientContext = unknown>(
12+
export function createORPCClient<TRouter extends ANY_ROUTER | ContractRouter<any>, TClientContext = unknown>(
1313
link: ClientLink<TClientContext>,
1414
options?: createORPCClientOptions,
15-
): TRouter extends ContractRouter
15+
): TRouter extends ContractRouter<any>
1616
? ContractRouterClient<TRouter, TClientContext>
1717
: TRouter extends ANY_ROUTER // put this in lower priority than ContractRouter, will make createORPCClient can work without @orpc/server
1818
? RouterClient<TRouter, TClientContext>
Lines changed: 75 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,56 @@
11
import type { DecoratedContractProcedure } from './procedure-decorated'
2-
import type { ContractRouterBuilder } from './router-builder'
2+
import type { AdaptedContractRouter, ContractRouterBuilder } from './router-builder'
33
import { z } from 'zod'
44
import { ContractBuilder } from './builder'
55
import { ContractProcedure } from './procedure'
66

7-
const builder = new ContractBuilder()
7+
const schema = z.object({
8+
value: z.string(),
9+
})
10+
11+
const baseErrorMap = {
12+
BASE: {
13+
status: 500,
14+
data: z.object({
15+
message: z.string(),
16+
}),
17+
},
18+
}
19+
20+
const builder = new ContractBuilder({ errorMap: baseErrorMap })
21+
22+
describe('self chainable', () => {
23+
describe('errors', () => {
24+
const errors = {
25+
BAD: {
26+
status: 500,
27+
data: schema,
28+
},
29+
ERROR2: {
30+
status: 401,
31+
data: schema,
32+
},
33+
} as const
34+
35+
it('should merge and strict with old one', () => {
36+
expectTypeOf(builder.errors(errors)).toEqualTypeOf<
37+
ContractBuilder<typeof errors & typeof baseErrorMap>
38+
>()
39+
})
40+
41+
it('should prevent redefine errorMap', () => {
42+
// @ts-expect-error - not allow redefine errorMap
43+
builder.errors({ BASE: baseErrorMap.BASE })
44+
// @ts-expect-error - not allow redefine errorMap - even with undefined
45+
builder.errors({ BASE: undefined })
46+
})
47+
})
48+
})
849

950
describe('to ContractRouterBuilder', () => {
1051
it('prefix', () => {
1152
expectTypeOf(builder.prefix('/prefix')).toEqualTypeOf<
12-
ContractRouterBuilder
53+
ContractRouterBuilder<typeof baseErrorMap>
1354
>()
1455

1556
// @ts-expect-error - invalid prefix
@@ -20,7 +61,7 @@ describe('to ContractRouterBuilder', () => {
2061

2162
it('tags', () => {
2263
expectTypeOf(builder.tag('tag1', 'tag2')).toEqualTypeOf<
23-
ContractRouterBuilder
64+
ContractRouterBuilder<typeof baseErrorMap>
2465
>()
2566

2667
// @ts-expect-error - invalid tag
@@ -33,11 +74,11 @@ describe('to ContractRouterBuilder', () => {
3374
describe('to DecoratedContractProcedure', () => {
3475
it('route', () => {
3576
expectTypeOf(builder.route({ method: 'GET', path: '/path' })).toEqualTypeOf<
36-
DecoratedContractProcedure<undefined, undefined, undefined>
77+
DecoratedContractProcedure<undefined, undefined, typeof baseErrorMap>
3778
>()
3879

3980
expectTypeOf(builder.route({ })).toEqualTypeOf<
40-
DecoratedContractProcedure<undefined, undefined, undefined>
81+
DecoratedContractProcedure<undefined, undefined, typeof baseErrorMap>
4182
>()
4283

4384
// @ts-expect-error - invalid method
@@ -46,17 +87,13 @@ describe('to DecoratedContractProcedure', () => {
4687
builder.route({ method: 'GET', path: '' })
4788
})
4889

49-
const schema = z.object({
50-
value: z.string(),
51-
})
52-
5390
it('input', () => {
5491
expectTypeOf(builder.input(schema)).toEqualTypeOf<
55-
DecoratedContractProcedure<typeof schema, undefined, undefined>
92+
DecoratedContractProcedure<typeof schema, undefined, typeof baseErrorMap>
5693
>()
5794

5895
expectTypeOf(builder.input(schema, { value: 'example' })).toEqualTypeOf<
59-
DecoratedContractProcedure<typeof schema, undefined, undefined>
96+
DecoratedContractProcedure<typeof schema, undefined, typeof baseErrorMap>
6097
>()
6198

6299
// @ts-expect-error - invalid schema
@@ -68,11 +105,11 @@ describe('to DecoratedContractProcedure', () => {
68105

69106
it('output', () => {
70107
expectTypeOf(builder.output(schema)).toEqualTypeOf<
71-
DecoratedContractProcedure<undefined, typeof schema, undefined>
108+
DecoratedContractProcedure<undefined, typeof schema, typeof baseErrorMap>
72109
>()
73110

74111
expectTypeOf(builder.output(schema, { value: 'example' })).toEqualTypeOf<
75-
DecoratedContractProcedure<undefined, typeof schema, undefined>
112+
DecoratedContractProcedure<undefined, typeof schema, typeof baseErrorMap>
76113
>()
77114

78115
// @ts-expect-error - invalid schema
@@ -81,54 +118,39 @@ describe('to DecoratedContractProcedure', () => {
81118
// @ts-expect-error - invalid example
82119
builder.output(schema, {})
83120
})
84-
85-
it('errors', () => {
86-
const errors = {
87-
BAD: {
88-
status: 500,
89-
data: schema,
90-
},
91-
ERROR2: {
92-
status: 401,
93-
data: schema,
94-
},
95-
} as const
96-
97-
expectTypeOf(builder.errors(errors)).toEqualTypeOf<
98-
DecoratedContractProcedure<undefined, undefined, typeof errors>
99-
>()
100-
101-
expectTypeOf(builder.output(schema, { value: 'example' })).toEqualTypeOf<
102-
DecoratedContractProcedure<undefined, typeof schema, undefined>
103-
>()
104-
105-
// @ts-expect-error - invalid schema
106-
builder.errors({ UNAUTHORIZED: { data: {} } })
107-
})
108121
})
109122

110123
describe('to router', () => {
111-
const router = {
112-
a: {
113-
b: {
114-
c: new ContractProcedure({ InputSchema: undefined, OutputSchema: undefined, errorMap: undefined }),
115-
},
124+
const errors = {
125+
CONFLICT: {
126+
status: 400,
127+
data: z.object({
128+
message: z.string(),
129+
}),
116130
},
117131
}
118132

119-
const emptyRouter = {
133+
const router = { a: { b: {
134+
c: new ContractProcedure({ InputSchema: undefined, OutputSchema: undefined, errorMap: errors }),
135+
} } }
120136

121-
}
137+
it('adapt all procedures', () => {
138+
expectTypeOf(builder.router(router)).toEqualTypeOf<AdaptedContractRouter<typeof router, typeof baseErrorMap>>()
139+
expectTypeOf(builder.router({})).toEqualTypeOf<Record<never, never>>()
122140

123-
const invalidRouter = {
124-
a: 1,
125-
}
141+
// @ts-expect-error - invalid router
142+
builder.router({ a: 1 })
143+
})
126144

127-
it('router', () => {
128-
expectTypeOf(builder.router(router)).toEqualTypeOf<typeof router>()
129-
expectTypeOf(builder.router(emptyRouter)).toEqualTypeOf<typeof emptyRouter>()
145+
it('throw on conflict error map', () => {
146+
builder.router({ ping: {} as ContractProcedure<any, any, { BASE: typeof baseErrorMap['BASE'] }> })
147+
// @ts-expect-error conflict
148+
builder.router({ ping: {} as ContractProcedure<any, any, { BASE: { message: string } }> })
149+
})
130150

131-
// @ts-expect-error - invalid router
132-
builder.router(invalidRouter)
151+
it('only required partial match error map', () => {
152+
expectTypeOf(builder.router({ ping: {} as ContractProcedure<any, any, { OTHER: { status: number } }> })).toEqualTypeOf<{
153+
ping: DecoratedContractProcedure<any, any, { OTHER: { status: number } } & typeof baseErrorMap>
154+
}>()
133155
})
134156
})

packages/contract/src/builder.test.ts

Lines changed: 55 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,48 @@ beforeEach(() => {
1616
vi.clearAllMocks()
1717
})
1818

19-
describe('to ContractRouterBuilder', () => {
20-
const builder = new ContractBuilder()
19+
const baseErrorMap = {
20+
BASE: {
21+
status: 500,
22+
data: z.object({
23+
message: z.string(),
24+
}),
25+
},
26+
}
27+
28+
const builder = new ContractBuilder({ errorMap: baseErrorMap })
29+
30+
const schema = z.object({ val: z.string().transform(val => Number(val)) })
31+
32+
describe('self chainable', () => {
33+
it('errors', () => {
34+
const errors = {
35+
BAD: {
36+
status: 500,
37+
data: schema,
38+
},
39+
}
40+
41+
const applied = builder.errors(errors)
42+
43+
expect(applied).not.toBe(builder)
44+
expect(applied).toBeInstanceOf(ContractBuilder)
45+
expect(applied['~orpc']).toEqual({
46+
errorMap: {
47+
...baseErrorMap,
48+
...errors,
49+
},
50+
})
51+
})
52+
})
2153

54+
describe('to ContractRouterBuilder', () => {
2255
it('prefix', () => {
2356
expect(builder.prefix('/prefix')).toBeInstanceOf(ContractRouterBuilder)
2457

2558
expect(ContractRouterBuilder).toHaveBeenCalledWith({
2659
prefix: '/prefix',
60+
errorMap: baseErrorMap,
2761
})
2862
})
2963

@@ -32,19 +66,18 @@ describe('to ContractRouterBuilder', () => {
3266

3367
expect(ContractRouterBuilder).toHaveBeenCalledWith({
3468
tags: ['tag1', 'tag2'],
69+
errorMap: baseErrorMap,
3570
})
3671
})
3772
})
3873

3974
describe('to DecoratedContractProcedure', () => {
40-
const builder = new ContractBuilder()
41-
4275
it('route', () => {
4376
const route = { method: 'GET', path: '/path' } as const
4477
const procedure = builder.route(route)
4578

4679
expect(procedure).toBeInstanceOf(DecoratedContractProcedure)
47-
expect(DecoratedContractProcedure).toHaveBeenCalledWith({ route })
80+
expect(DecoratedContractProcedure).toHaveBeenCalledWith({ route, errorMap: baseErrorMap })
4881
})
4982

5083
const schema = z.object({
@@ -56,48 +89,41 @@ describe('to DecoratedContractProcedure', () => {
5689
const procedure = builder.input(schema, example)
5790

5891
expect(procedure).toBeInstanceOf(DecoratedContractProcedure)
59-
expect(DecoratedContractProcedure).toHaveBeenCalledWith({ InputSchema: schema, inputExample: example })
92+
expect(DecoratedContractProcedure).toHaveBeenCalledWith({ InputSchema: schema, inputExample: example, errorMap: baseErrorMap })
6093
})
6194

6295
it('output', () => {
6396
const procedure = builder.output(schema, example)
6497

6598
expect(procedure).toBeInstanceOf(DecoratedContractProcedure)
66-
expect(DecoratedContractProcedure).toHaveBeenCalledWith({ OutputSchema: schema, outputExample: example })
67-
})
68-
69-
it('errors', () => {
70-
const errors = {
71-
BAD: {
72-
status: 500,
73-
data: schema,
74-
},
75-
}
76-
77-
const procedure = builder.errors(errors)
78-
79-
expect(procedure).toBeInstanceOf(DecoratedContractProcedure)
80-
expect(DecoratedContractProcedure).toHaveBeenCalledWith({ errorMap: errors })
99+
expect(DecoratedContractProcedure).toHaveBeenCalledWith({ OutputSchema: schema, outputExample: example, errorMap: baseErrorMap })
81100
})
82101
})
83102

84103
describe('to router', () => {
85-
const builder = new ContractBuilder()
86-
87104
const router = {
88105
a: {
89106
b: {
90-
c: new ContractProcedure({ InputSchema: undefined, OutputSchema: undefined, errorMap: undefined }),
107+
c: new ContractProcedure({ InputSchema: undefined, OutputSchema: undefined, errorMap: baseErrorMap }),
91108
},
92109
},
93110
}
94111

95-
const emptyRouter = {
112+
it('adapt all routers', () => {
113+
const routerFn = vi.fn()
114+
vi.mocked(ContractRouterBuilder).mockReturnValue({
115+
router: routerFn,
116+
} as any)
96117

97-
}
118+
const mockedValue = { __mocked__: true }
119+
routerFn.mockReturnValue(mockedValue)
98120

99-
it('router', () => {
100-
expect(builder.router(router)).toBe(router)
101-
expect(builder.router(emptyRouter)).toBe(emptyRouter)
121+
expect(builder.router(router)).toBe(mockedValue)
122+
expect(ContractRouterBuilder).toBeCalledTimes(1)
123+
expect(ContractRouterBuilder).toBeCalledWith({
124+
errorMap: baseErrorMap,
125+
})
126+
expect(routerFn).toBeCalledTimes(1)
127+
expect(routerFn).toBeCalledWith(router)
102128
})
103129
})

0 commit comments

Comments
 (0)