Skip to content

Commit ed15210

Browse files
authored
feat(contract)!: stricter implementation for contracts (#97)
* contract: procedure * contract: builder & router * update old tests * improve * route tests * 100% test coverage for contract * server * fix * fix types * improve * fix * fix * fix * fix tests * strict route * strict error map * strict contract when implement * remove hidden contract when implement
1 parent 43889a7 commit ed15210

81 files changed

Lines changed: 976 additions & 639 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.
Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import type { ReadonlyDeep } from '@orpc/shared'
2+
import type { ContractBuilder, GetInitialRoute, MergeContractBuilderConfig } from './builder'
3+
import type { StrictErrorMap } from './error-map'
14
import type { ContractProcedure } from './procedure'
25
import type { ContractProcedureBuilder } from './procedure-builder'
36
import type { ContractProcedureBuilderWithInput } from './procedure-builder-with-input'
47
import type { ContractProcedureBuilderWithOutput } from './procedure-builder-with-output'
8+
import type { MergeRoute, StrictRoute } from './route'
59
import type { AdaptedContractRouter, ContractRouterBuilder } from './router-builder'
610
import { z } from 'zod'
7-
import { ContractBuilder } from './builder'
811

912
const schema = z.object({ value: z.string() })
1013

@@ -16,24 +19,24 @@ const baseErrorMap = {
1619
},
1720
}
1821

19-
const builder = new ContractBuilder({
20-
errorMap: baseErrorMap,
21-
OutputSchema: undefined,
22-
InputSchema: undefined,
23-
config: {
24-
initialRoute: {
25-
description: 'from initial',
26-
},
27-
},
28-
})
22+
type BaseErrorMap = StrictErrorMap<typeof baseErrorMap>
23+
24+
type Config = ReadonlyDeep<{ initialRoute: { description: 'from initial' } }>
25+
26+
const builder = {} as ContractBuilder<Config, BaseErrorMap>
2927

3028
describe('ContractBuilder', () => {
3129
it('is a contract procedure', () => {
32-
expectTypeOf(builder).toMatchTypeOf<ContractProcedure<undefined, undefined, typeof baseErrorMap>>()
30+
expectTypeOf(builder).toMatchTypeOf<ContractProcedure<undefined, undefined, BaseErrorMap, { description: string }>>()
3331
})
3432

3533
it('.config', () => {
36-
expectTypeOf(builder.config({ initialRoute: { description: 'from config' } })).toEqualTypeOf<typeof builder>()
34+
expectTypeOf(builder.config({ initialRoute: { description: 'from config' } })).toEqualTypeOf<
35+
ContractBuilder<
36+
MergeContractBuilderConfig<Config, ReadonlyDeep<{ initialRoute: { description: 'from config' } }>>,
37+
BaseErrorMap
38+
>
39+
>()
3740

3841
// @ts-expect-error - invalid method
3942
builder.config({ initialRoute: { method: 'HI' } })
@@ -43,52 +46,61 @@ describe('ContractBuilder', () => {
4346
const errors = { BAD_GATEWAY: { data: schema } } as const
4447

4548
expectTypeOf(builder.errors(errors))
46-
.toEqualTypeOf<ContractBuilder<typeof baseErrorMap & typeof errors>>()
49+
.toEqualTypeOf < ContractBuilder<Config, BaseErrorMap & StrictErrorMap<typeof errors>>>()
4750

4851
// @ts-expect-error - not allow redefine error map
4952
builder.errors({ BASE: baseErrorMap.BASE })
5053
})
5154

5255
it('.route', () => {
53-
expectTypeOf(builder.route({ method: 'GET' })).toEqualTypeOf<ContractProcedureBuilder<typeof baseErrorMap>>()
56+
expectTypeOf(builder.route({ method: 'GET' })).toEqualTypeOf<
57+
ContractProcedureBuilder<
58+
BaseErrorMap,
59+
MergeRoute<StrictRoute<GetInitialRoute<Config>>, ReadonlyDeep<{ method: 'GET' }>>
60+
>
61+
>()
5462

5563
// @ts-expect-error - invalid method
5664
builder.route({ method: 'HE' })
5765
})
5866

5967
it('.input', () => {
6068
expectTypeOf(builder.input(schema)).toEqualTypeOf<
61-
ContractProcedureBuilderWithInput<typeof schema, typeof baseErrorMap >
69+
ContractProcedureBuilderWithInput<
70+
typeof schema,
71+
BaseErrorMap,
72+
StrictRoute<GetInitialRoute<Config>>
73+
>
6274
>()
6375
})
6476

6577
it('.output', () => {
6678
expectTypeOf(builder.output(schema)).toEqualTypeOf<
67-
ContractProcedureBuilderWithOutput<typeof schema, typeof baseErrorMap >
79+
ContractProcedureBuilderWithOutput<typeof schema, BaseErrorMap, StrictRoute<GetInitialRoute<Config>>>
6880
>()
6981
})
7082

7183
it('.prefix', () => {
72-
expectTypeOf(builder.prefix('/api')).toEqualTypeOf<ContractRouterBuilder<typeof baseErrorMap>>()
84+
expectTypeOf(builder.prefix('/api')).toEqualTypeOf<ContractRouterBuilder<BaseErrorMap, '/api', undefined>>()
7385

7486
// @ts-expect-error - invalid prefix
7587
builder.prefix(1)
7688
})
7789

7890
it('.tag', () => {
79-
expectTypeOf(builder.tag('tag1', 'tag2')).toEqualTypeOf<ContractRouterBuilder<typeof baseErrorMap>>()
91+
expectTypeOf(builder.tag('tag1', 'tag2')).toEqualTypeOf<ContractRouterBuilder<BaseErrorMap, undefined, ['tag1', 'tag2']>>()
8092

8193
// @ts-expect-error - invalid tag
8294
builder.tag(1)
8395
})
8496

8597
it('.router', () => {
8698
const router = {
87-
ping: {} as ContractProcedure<undefined, typeof schema, typeof baseErrorMap>,
88-
pong: {} as ContractProcedure<typeof schema, undefined, Record<never, never>>,
99+
ping: {} as ContractProcedure<undefined, typeof schema, BaseErrorMap, { description: string }>,
100+
pong: {} as ContractProcedure<typeof schema, undefined, Record<never, never>, Record<never, never>>,
89101
}
90102

91-
expectTypeOf(builder.router(router)).toEqualTypeOf<AdaptedContractRouter<typeof router, typeof baseErrorMap>>()
103+
expectTypeOf(builder.router(router)).toEqualTypeOf<AdaptedContractRouter<typeof router, BaseErrorMap, undefined, undefined>>()
92104

93105
const invalidErrorMap = {
94106
BASE: {
@@ -99,7 +111,7 @@ describe('ContractBuilder', () => {
99111

100112
builder.router({
101113
// @ts-expect-error - error map is not match
102-
ping: {} as ContractProcedure<undefined, typeof schema, typeof invalidErrorMap>,
114+
ping: {} as ContractProcedure<undefined, typeof schema, typeof invalidErrorMap, Record<never, never>>,
103115
})
104116
})
105117
})

packages/contract/src/builder.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ const builder = new ContractBuilder({
3636
description: 'from initial',
3737
},
3838
},
39+
route: {
40+
description: 'from initial',
41+
},
3942
})
4043

4144
beforeEach(() => {

packages/contract/src/builder.ts

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,53 @@
11
import type { ErrorMap, ErrorMapGuard, ErrorMapSuggestions, StrictErrorMap } from './error-map'
2-
import type { ContractProcedureDef, RouteOptions } from './procedure'
2+
import type { ContractProcedureDef } from './procedure'
3+
import type { HTTPPath, MergeRoute, Route, StrictRoute } from './route'
34
import type { ContractRouter } from './router'
45
import type { AdaptedContractRouter } from './router-builder'
5-
import type { HTTPPath, Schema, SchemaInput, SchemaOutput } from './types'
6+
import type { Schema, SchemaInput, SchemaOutput } from './types'
67
import { ContractProcedure } from './procedure'
78
import { ContractProcedureBuilder } from './procedure-builder'
89
import { ContractProcedureBuilderWithInput } from './procedure-builder-with-input'
910
import { ContractProcedureBuilderWithOutput } from './procedure-builder-with-output'
11+
import { mergeRoute } from './route'
1012
import { ContractRouterBuilder } from './router-builder'
1113

1214
export interface ContractBuilderConfig {
13-
initialRoute?: RouteOptions
15+
initialRoute?: Route
1416
}
1517

16-
export interface ContractBuilderDef<TErrorMap extends ErrorMap> extends ContractProcedureDef<undefined, undefined, TErrorMap> {
17-
config: ContractBuilderConfig
18+
export type MergeContractBuilderConfig<A extends ContractBuilderConfig, B extends ContractBuilderConfig> = Omit<A, keyof B> & B
19+
20+
export type GetInitialRoute<T extends ContractBuilderConfig> = T['initialRoute'] extends Route
21+
? T['initialRoute']
22+
: Record<never, never>
23+
24+
export interface ContractBuilderDef<TConfig extends ContractBuilderConfig, TErrorMap extends ErrorMap>
25+
extends ContractProcedureDef<undefined, undefined, TErrorMap, StrictRoute<GetInitialRoute<TConfig>>> {
26+
config: TConfig
1827
}
1928

20-
export class ContractBuilder<TErrorMap extends ErrorMap> extends ContractProcedure<undefined, undefined, TErrorMap> {
21-
declare '~orpc': ContractBuilderDef<TErrorMap>
29+
export class ContractBuilder<TConfig extends ContractBuilderConfig, TErrorMap extends ErrorMap>
30+
extends ContractProcedure<undefined, undefined, TErrorMap, GetInitialRoute<TConfig>> {
31+
declare '~orpc': ContractBuilderDef<TConfig, TErrorMap>
2232

23-
constructor(def: ContractBuilderDef<TErrorMap>) {
33+
constructor(def: ContractBuilderDef<TConfig, TErrorMap>) {
2434
super(def)
35+
this['~orpc'].config = def.config
2536
}
2637

27-
config(config: ContractBuilderConfig): ContractBuilder<TErrorMap> {
38+
config<const U extends ContractBuilderConfig>(config: U): ContractBuilder<MergeContractBuilderConfig<TConfig, U>, TErrorMap> {
2839
return new ContractBuilder({
2940
...this['~orpc'],
3041
config: {
3142
...this['~orpc'].config,
3243
...config,
33-
},
44+
} as any,
3445
})
3546
}
3647

37-
errors<const U extends ErrorMap & ErrorMapGuard<TErrorMap> & ErrorMapSuggestions>(errors: U): ContractBuilder<U & TErrorMap> {
48+
errors<const U extends ErrorMap & ErrorMapGuard<TErrorMap> & ErrorMapSuggestions>(
49+
errors: U,
50+
): ContractBuilder<TConfig, StrictErrorMap<U> & TErrorMap> {
3851
return new ContractBuilder({
3952
...this['~orpc'],
4053
errorMap: {
@@ -44,55 +57,58 @@ export class ContractBuilder<TErrorMap extends ErrorMap> extends ContractProcedu
4457
})
4558
}
4659

47-
route(route: RouteOptions): ContractProcedureBuilder<TErrorMap> {
60+
route<const U extends Route>(route: U): ContractProcedureBuilder<TErrorMap, MergeRoute<StrictRoute<GetInitialRoute<TConfig>>, U>> {
4861
return new ContractProcedureBuilder({
49-
route: {
50-
...this['~orpc'].config.initialRoute,
51-
...route,
52-
},
62+
route: mergeRoute(this['~orpc'].route, route),
5363
InputSchema: undefined,
5464
OutputSchema: undefined,
5565
errorMap: this['~orpc'].errorMap,
5666
})
5767
}
5868

59-
input<U extends Schema>(schema: U, example?: SchemaInput<U>): ContractProcedureBuilderWithInput<U, TErrorMap> {
69+
input<U extends Schema>(schema: U, example?: SchemaInput<U>): ContractProcedureBuilderWithInput<U, TErrorMap, StrictRoute<GetInitialRoute<TConfig>>> {
6070
return new ContractProcedureBuilderWithInput({
61-
route: this['~orpc'].config.initialRoute,
71+
route: this['~orpc'].route,
6272
InputSchema: schema,
6373
inputExample: example,
6474
OutputSchema: undefined,
6575
errorMap: this['~orpc'].errorMap,
6676
})
6777
}
6878

69-
output<U extends Schema>(schema: U, example?: SchemaOutput<U>): ContractProcedureBuilderWithOutput<U, TErrorMap> {
79+
output<U extends Schema>(schema: U, example?: SchemaOutput<U>): ContractProcedureBuilderWithOutput<U, TErrorMap, StrictRoute<GetInitialRoute<TConfig>>> {
7080
return new ContractProcedureBuilderWithOutput({
71-
route: this['~orpc'].config.initialRoute,
81+
route: this['~orpc'].route,
7282
OutputSchema: schema,
7383
outputExample: example,
7484
InputSchema: undefined,
7585
errorMap: this['~orpc'].errorMap,
7686
})
7787
}
7888

79-
prefix(prefix: HTTPPath): ContractRouterBuilder<TErrorMap> {
89+
prefix<U extends HTTPPath>(prefix: U): ContractRouterBuilder<TErrorMap, U, undefined> {
8090
return new ContractRouterBuilder({
8191
prefix,
8292
errorMap: this['~orpc'].errorMap,
93+
tags: undefined,
8394
})
8495
}
8596

86-
tag(...tags: string[]): ContractRouterBuilder<TErrorMap> {
97+
tag<U extends string[]>(...tags: U): ContractRouterBuilder<TErrorMap, undefined, U> {
8798
return new ContractRouterBuilder({
8899
tags,
89100
errorMap: this['~orpc'].errorMap,
101+
prefix: undefined,
90102
})
91103
}
92104

93-
router<T extends ContractRouter<ErrorMap & Partial<StrictErrorMap<TErrorMap>>>>(router: T): AdaptedContractRouter<T, TErrorMap> {
105+
router<T extends ContractRouter<ErrorMap & Partial<TErrorMap>>>(
106+
router: T,
107+
): AdaptedContractRouter<T, TErrorMap, undefined, undefined> {
94108
return new ContractRouterBuilder({
95109
errorMap: this['~orpc'].errorMap,
110+
prefix: undefined,
111+
tags: undefined,
96112
}).router(router)
97113
}
98114
}

packages/contract/src/config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import type { HTTPMethod, InputStructure } from './types'
1+
import type { HTTPMethod, InputStructure, Route } from './route'
22

33
export interface ContractConfig {
44
defaultMethod: HTTPMethod
55
defaultSuccessStatus: number
66
defaultSuccessDescription: string
77
defaultInputStructure: InputStructure
88
defaultOutputStructure: InputStructure
9+
defaultInitialRoute: Route
910
}
1011

1112
const DEFAULT_CONFIG: ContractConfig = {
@@ -14,6 +15,7 @@ const DEFAULT_CONFIG: ContractConfig = {
1415
defaultSuccessDescription: 'OK',
1516
defaultInputStructure: 'compact',
1617
defaultOutputStructure: 'compact',
18+
defaultInitialRoute: {},
1719
}
1820

1921
export function fallbackContractConfig<T extends keyof ContractConfig>(key: T, value: ContractConfig[T] | undefined): ContractConfig[T] {

packages/contract/src/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,17 @@ export * from './procedure-builder-with-input'
1515
export * from './procedure-builder-with-output'
1616
export * from './procedure-client'
1717
export * from './procedure-decorated'
18+
export * from './route'
1819
export * from './router'
1920
export * from './router-builder'
2021
export * from './router-client'
2122
export * from './schema-utils'
2223
export * from './types'
2324

24-
export const oc = new ContractBuilder<Record<never, never>>({
25+
export const oc = new ContractBuilder({
26+
config: {},
27+
route: {},
2528
errorMap: {},
2629
InputSchema: undefined,
2730
OutputSchema: undefined,
28-
config: {},
2931
})

0 commit comments

Comments
 (0)