@orpc/server 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. package/dist/chunk-ACLC6USM.js +262 -0
  2. package/dist/chunk-ACLC6USM.js.map +1 -0
  3. package/dist/fetch.js +648 -0
  4. package/dist/fetch.js.map +1 -0
  5. package/dist/index.js +403 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/src/adapters/fetch.d.ts +36 -0
  8. package/dist/src/adapters/fetch.d.ts.map +1 -0
  9. package/dist/src/builder.d.ts +49 -0
  10. package/dist/src/builder.d.ts.map +1 -0
  11. package/dist/src/index.d.ts +15 -0
  12. package/dist/src/index.d.ts.map +1 -0
  13. package/dist/src/middleware.d.ts +16 -0
  14. package/dist/src/middleware.d.ts.map +1 -0
  15. package/dist/src/procedure-builder.d.ts +31 -0
  16. package/dist/src/procedure-builder.d.ts.map +1 -0
  17. package/dist/src/procedure-caller.d.ts +33 -0
  18. package/dist/src/procedure-caller.d.ts.map +1 -0
  19. package/dist/src/procedure-implementer.d.ts +19 -0
  20. package/dist/src/procedure-implementer.d.ts.map +1 -0
  21. package/dist/src/procedure.d.ts +29 -0
  22. package/dist/src/procedure.d.ts.map +1 -0
  23. package/dist/src/router-builder.d.ts +22 -0
  24. package/dist/src/router-builder.d.ts.map +1 -0
  25. package/dist/src/router-caller.d.ts +36 -0
  26. package/dist/src/router-caller.d.ts.map +1 -0
  27. package/dist/src/router-implementer.d.ts +20 -0
  28. package/dist/src/router-implementer.d.ts.map +1 -0
  29. package/dist/src/router.d.ts +14 -0
  30. package/dist/src/router.d.ts.map +1 -0
  31. package/dist/src/types.d.ts +18 -0
  32. package/dist/src/types.d.ts.map +1 -0
  33. package/dist/src/utils.d.ts +4 -0
  34. package/dist/src/utils.d.ts.map +1 -0
  35. package/dist/tsconfig.tsbuildinfo +1 -0
  36. package/package.json +57 -0
  37. package/src/adapters/fetch.test.ts +399 -0
  38. package/src/adapters/fetch.ts +228 -0
  39. package/src/builder.test.ts +362 -0
  40. package/src/builder.ts +236 -0
  41. package/src/index.ts +16 -0
  42. package/src/middleware.test.ts +279 -0
  43. package/src/middleware.ts +119 -0
  44. package/src/procedure-builder.test.ts +219 -0
  45. package/src/procedure-builder.ts +158 -0
  46. package/src/procedure-caller.test.ts +210 -0
  47. package/src/procedure-caller.ts +165 -0
  48. package/src/procedure-implementer.test.ts +215 -0
  49. package/src/procedure-implementer.ts +103 -0
  50. package/src/procedure.test.ts +312 -0
  51. package/src/procedure.ts +251 -0
  52. package/src/router-builder.test.ts +106 -0
  53. package/src/router-builder.ts +120 -0
  54. package/src/router-caller.test.ts +173 -0
  55. package/src/router-caller.ts +95 -0
  56. package/src/router-implementer.test.ts +116 -0
  57. package/src/router-implementer.ts +110 -0
  58. package/src/router.test.ts +142 -0
  59. package/src/router.ts +69 -0
  60. package/src/types.test.ts +18 -0
  61. package/src/types.ts +28 -0
  62. package/src/utils.test.ts +243 -0
  63. package/src/utils.ts +95 -0
@@ -0,0 +1,312 @@
1
+ import { ContractProcedure, DecoratedContractProcedure } from '@orpc/contract'
2
+ import { z } from 'zod'
3
+ import { os, type Meta } from '.'
4
+ import {
5
+ type DecoratedProcedure,
6
+ Procedure,
7
+ decorateProcedure,
8
+ isProcedure,
9
+ } from './procedure'
10
+
11
+ it('isProcedure', () => {
12
+ expect(
13
+ isProcedure(
14
+ decorateProcedure(
15
+ new Procedure({
16
+ contract: new ContractProcedure({
17
+ InputSchema: undefined,
18
+ OutputSchema: undefined,
19
+ }),
20
+ handler: () => {},
21
+ }),
22
+ ),
23
+ ),
24
+ ).toBe(true)
25
+ expect({
26
+ zz$p: {
27
+ contract: new DecoratedContractProcedure({
28
+ InputSchema: undefined,
29
+ OutputSchema: undefined,
30
+ }),
31
+ handler: () => {},
32
+ },
33
+ }).toSatisfy(isProcedure)
34
+
35
+ expect({
36
+ zz$p: {
37
+ contract: new DecoratedContractProcedure({
38
+ InputSchema: undefined,
39
+ OutputSchema: undefined,
40
+ }),
41
+ },
42
+ }).not.toSatisfy(isProcedure)
43
+
44
+ expect({
45
+ zz$p: {
46
+ handler: () => {},
47
+ },
48
+ }).not.toSatisfy(isProcedure)
49
+
50
+ expect({}).not.toSatisfy(isProcedure)
51
+ expect(12233).not.toSatisfy(isProcedure)
52
+ expect('12233').not.toSatisfy(isProcedure)
53
+ expect(undefined).not.toSatisfy(isProcedure)
54
+ expect(null).not.toSatisfy(isProcedure)
55
+ })
56
+
57
+ describe('route method', () => {
58
+ it('sets route options correctly', () => {
59
+ const p = os.context<{ auth: boolean }>().handler(() => {
60
+ return 'test'
61
+ })
62
+
63
+ const p2 = p.route({ path: '/test', method: 'GET' })
64
+
65
+ expect(p2.zz$p.contract.zz$cp.path).toBe('/test')
66
+ expect(p2.zz$p.contract.zz$cp.method).toBe('GET')
67
+ })
68
+
69
+ it('preserves existing context and handler', () => {
70
+ const handler = () => 'test'
71
+ const p = os.context<{ auth: boolean }>().handler(handler)
72
+
73
+ const p2 = p.route({ path: '/test' })
74
+
75
+ expect(p2.zz$p.handler).toBe(handler)
76
+ // Context type is preserved through the route method
77
+ expectTypeOf(p2).toEqualTypeOf<
78
+ DecoratedProcedure<
79
+ { auth: boolean },
80
+ undefined,
81
+ undefined,
82
+ undefined,
83
+ string
84
+ >
85
+ >()
86
+ })
87
+
88
+ it('works with prefix method', () => {
89
+ const p = os
90
+ .context<{ auth: boolean }>()
91
+ .route({ path: '/api', method: 'POST' })
92
+ .handler(() => 'test')
93
+
94
+ const p2 = p.prefix('/v1')
95
+
96
+ expect(p2.zz$p.contract.zz$cp.path).toBe('/v1/api')
97
+ expect(p2.zz$p.contract.zz$cp.method).toBe('POST')
98
+ })
99
+
100
+ it('works with middleware', () => {
101
+ const mid = vi.fn(() => ({ context: { userId: '1' } }))
102
+
103
+ const p = os
104
+ .context<{ auth: boolean }>()
105
+ .route({ path: '/test' })
106
+ .use(mid)
107
+ .handler((input, context) => {
108
+ expectTypeOf(context).toEqualTypeOf<
109
+ { auth: boolean } & { userId: string }
110
+ >()
111
+ return 'test'
112
+ })
113
+
114
+ expect(p.zz$p.contract.zz$cp.path).toBe('/test')
115
+ expect(p.zz$p.middlewares).toEqual([mid])
116
+ })
117
+
118
+ it('overrides existing route options', () => {
119
+ const p = os
120
+ .context<{ auth: boolean }>()
121
+ .route({ path: '/test1', method: 'GET' })
122
+ .handler(() => 'test')
123
+
124
+ const p2 = p.route({ path: '/test2', method: 'POST' })
125
+
126
+ expect(p2.zz$p.contract.zz$cp.path).toBe('/test2')
127
+ expect(p2.zz$p.contract.zz$cp.method).toBe('POST')
128
+ })
129
+
130
+ it('preserves input/output schemas', () => {
131
+ const inputSchema = z.object({ id: z.number() })
132
+ const outputSchema = z.string()
133
+ const p = os
134
+ .context<{ auth: boolean }>()
135
+ .input(inputSchema)
136
+ .output(outputSchema)
137
+ .route({ path: '/test' })
138
+ .handler((input) => {
139
+ expectTypeOf(input).toEqualTypeOf<{ id: number }>()
140
+ return 'test'
141
+ })
142
+
143
+ const p2 = p.route({ path: '/test2' })
144
+
145
+ // Type checking that schemas are preserved
146
+ expectTypeOf(p2).toEqualTypeOf<
147
+ DecoratedProcedure<
148
+ { auth: boolean },
149
+ undefined,
150
+ typeof inputSchema,
151
+ typeof outputSchema,
152
+ string
153
+ >
154
+ >()
155
+ })
156
+ })
157
+
158
+ test('prefix method', () => {
159
+ const p = os.context<{ auth: boolean }>().handler(() => {
160
+ return 'dinwwwh'
161
+ })
162
+
163
+ const p2 = p.prefix('/test')
164
+
165
+ expect(p2.zz$p.contract.zz$cp.path).toBe(undefined)
166
+
167
+ const p3 = os
168
+ .context<{ auth: boolean }>()
169
+ .route({ path: '/test1' })
170
+ .handler(() => {
171
+ return 'dinwwwh'
172
+ })
173
+
174
+ const p4 = p3.prefix('/test')
175
+ expect(p4.zz$p.contract.zz$cp.path).toBe('/test/test1')
176
+ })
177
+
178
+ describe('use middleware', () => {
179
+ it('infer types', () => {
180
+ const p1 = os
181
+ .context<{ auth: boolean }>()
182
+ .use(() => {
183
+ return { context: { postId: 'string' } }
184
+ })
185
+ .handler(() => {
186
+ return 'dinwwwh'
187
+ })
188
+
189
+ const p2 = p1
190
+ .use((input, context, meta) => {
191
+ expectTypeOf(input).toEqualTypeOf<unknown>()
192
+ expectTypeOf(context).toEqualTypeOf<
193
+ { auth: boolean } & { postId: string }
194
+ >()
195
+ expectTypeOf(meta).toEqualTypeOf<Meta<string>>()
196
+
197
+ return {
198
+ context: {
199
+ userId: '1',
200
+ },
201
+ }
202
+ })
203
+ .use((input, context, meta) => {
204
+ expectTypeOf(input).toEqualTypeOf<unknown>()
205
+ expectTypeOf(context).toEqualTypeOf<
206
+ { userId: string } & { postId: string } & { auth: boolean }
207
+ >()
208
+ expectTypeOf(meta).toEqualTypeOf<Meta<string>>()
209
+ })
210
+
211
+ expectTypeOf(p2).toEqualTypeOf<
212
+ DecoratedProcedure<
213
+ { auth: boolean },
214
+ { postId: string } & { userId: string },
215
+ undefined,
216
+ undefined,
217
+ string
218
+ >
219
+ >()
220
+ })
221
+
222
+ it('can map input', () => {
223
+ const mid = (input: { id: number }) => {}
224
+
225
+ os.input(z.object({ postId: z.number() })).use(mid, (input) => {
226
+ expectTypeOf(input).toEqualTypeOf<{ postId: number }>()
227
+
228
+ return {
229
+ id: input.postId,
230
+ }
231
+ })
232
+
233
+ // @ts-expect-error mismatch input
234
+ os.input(z.object({ postId: z.number() })).use(mid)
235
+
236
+ // @ts-expect-error mismatch input
237
+ os.input(z.object({ postId: z.number() })).use(mid, (input) => {
238
+ return {
239
+ wrong: input.postId,
240
+ }
241
+ })
242
+ })
243
+
244
+ it('add middlewares to beginning', () => {
245
+ const mid1 = vi.fn()
246
+ const mid2 = vi.fn()
247
+ const mid3 = vi.fn()
248
+
249
+ const p1 = os.use(mid1).handler(() => 'dinwwwh')
250
+ const p2 = p1.use(mid2).use(mid3)
251
+
252
+ expect(p2.zz$p.middlewares).toEqual([mid3, mid2, mid1])
253
+ })
254
+ })
255
+
256
+ describe('server action', () => {
257
+ it('only accept undefined context', () => {
258
+ expectTypeOf(os.handler(() => {})).toMatchTypeOf<(...args: any[]) => any>()
259
+ expectTypeOf(
260
+ os.context<{ auth: boolean } | undefined>().handler(() => {}),
261
+ ).toMatchTypeOf<(...args: any[]) => any>()
262
+ expectTypeOf(
263
+ os.context<{ auth: boolean }>().handler(() => {}),
264
+ ).not.toMatchTypeOf<(...args: any[]) => any>()
265
+ })
266
+
267
+ it('infer types', () => {
268
+ const p = os
269
+ .input(z.object({ id: z.number() }))
270
+ .output(z.string())
271
+ .handler(() => 'string')
272
+
273
+ expectTypeOf(p).toMatchTypeOf<
274
+ (input: { id: number } | FormData) => Promise<string>
275
+ >()
276
+
277
+ const p2 = os.input(z.object({ id: z.number() })).handler(() => 12333)
278
+
279
+ expectTypeOf(p2).toMatchTypeOf<
280
+ (input: { id: number } | FormData) => Promise<number>
281
+ >()
282
+ })
283
+
284
+ it('works with input', async () => {
285
+ const p = os
286
+ .input(z.object({ id: z.number(), date: z.date() }))
287
+ .handler(async (input, context) => {
288
+ expect(context).toBe(undefined)
289
+ return input
290
+ })
291
+
292
+ expect(await p({ id: 123, date: new Date('2022-01-01') })).toEqual({
293
+ id: 123,
294
+ date: new Date('2022-01-01'),
295
+ })
296
+ })
297
+
298
+ it('can deserialize form data', async () => {
299
+ const p = os
300
+ .input(z.object({ id: z.number(), nested: z.object({ date: z.date() }) }))
301
+ .handler(async (input) => input)
302
+
303
+ const form = new FormData()
304
+ form.append('id', '123')
305
+ form.append('nested[date]', '2022-01-01')
306
+
307
+ expect(await p(form)).toEqual({
308
+ id: 123,
309
+ nested: { date: new Date('2022-01-01') },
310
+ })
311
+ })
312
+ })
@@ -0,0 +1,251 @@
1
+ import {
2
+ type ContractProcedure,
3
+ DecoratedContractProcedure,
4
+ type HTTPPath,
5
+ type SchemaInput,
6
+ type SchemaOutput,
7
+ isContractProcedure,
8
+ } from '@orpc/contract'
9
+ import type { RouteOptions, Schema } from '@orpc/contract'
10
+ import { OpenAPIDeserializer } from '@orpc/transformer'
11
+ import {
12
+ type MapInputMiddleware,
13
+ type Middleware,
14
+ decorateMiddleware,
15
+ } from './middleware'
16
+ import { createProcedureCaller } from './procedure-caller'
17
+ import type { Context, MergeContext, Meta, Promisable } from './types'
18
+
19
+ export class Procedure<
20
+ TContext extends Context,
21
+ TExtraContext extends Context,
22
+ TInputSchema extends Schema,
23
+ TOutputSchema extends Schema,
24
+ THandlerOutput extends SchemaOutput<TOutputSchema>,
25
+ > {
26
+ constructor(
27
+ public zz$p: {
28
+ middlewares?: Middleware<any, any, any, any>[]
29
+ contract: ContractProcedure<TInputSchema, TOutputSchema>
30
+ handler: ProcedureHandler<
31
+ TContext,
32
+ TExtraContext,
33
+ TInputSchema,
34
+ TOutputSchema,
35
+ THandlerOutput
36
+ >
37
+ },
38
+ ) {}
39
+ }
40
+
41
+ export type DecoratedProcedure<
42
+ TContext extends Context,
43
+ TExtraContext extends Context,
44
+ TInputSchema extends Schema,
45
+ TOutputSchema extends Schema,
46
+ THandlerOutput extends SchemaOutput<TOutputSchema>,
47
+ > = Procedure<
48
+ TContext,
49
+ TExtraContext,
50
+ TInputSchema,
51
+ TOutputSchema,
52
+ THandlerOutput
53
+ > & {
54
+ prefix(
55
+ prefix: HTTPPath,
56
+ ): DecoratedProcedure<
57
+ TContext,
58
+ TExtraContext,
59
+ TInputSchema,
60
+ TOutputSchema,
61
+ THandlerOutput
62
+ >
63
+
64
+ route(
65
+ opts: RouteOptions,
66
+ ): DecoratedProcedure<
67
+ TContext,
68
+ TExtraContext,
69
+ TInputSchema,
70
+ TOutputSchema,
71
+ THandlerOutput
72
+ >
73
+
74
+ use<
75
+ UExtraContext extends
76
+ | Partial<MergeContext<Context, MergeContext<TContext, TExtraContext>>>
77
+ | undefined = undefined,
78
+ >(
79
+ middleware: Middleware<
80
+ MergeContext<TContext, TExtraContext>,
81
+ UExtraContext,
82
+ SchemaOutput<TInputSchema>,
83
+ SchemaOutput<TOutputSchema, THandlerOutput>
84
+ >,
85
+ ): DecoratedProcedure<
86
+ TContext,
87
+ MergeContext<TExtraContext, UExtraContext>,
88
+ TInputSchema,
89
+ TOutputSchema,
90
+ THandlerOutput
91
+ >
92
+
93
+ use<
94
+ UExtraContext extends
95
+ | Partial<MergeContext<Context, MergeContext<TContext, TExtraContext>>>
96
+ | undefined = undefined,
97
+ UMappedInput = unknown,
98
+ >(
99
+ middleware: Middleware<
100
+ MergeContext<TContext, TExtraContext>,
101
+ UExtraContext,
102
+ UMappedInput,
103
+ SchemaOutput<TOutputSchema, THandlerOutput>
104
+ >,
105
+ mapInput: MapInputMiddleware<
106
+ SchemaOutput<TInputSchema, THandlerOutput>,
107
+ UMappedInput
108
+ >,
109
+ ): DecoratedProcedure<
110
+ TContext,
111
+ MergeContext<TExtraContext, UExtraContext>,
112
+ TInputSchema,
113
+ TOutputSchema,
114
+ THandlerOutput
115
+ >
116
+ } & (undefined extends TContext
117
+ ? (
118
+ input: SchemaInput<TInputSchema> | FormData,
119
+ ) => Promise<SchemaOutput<TOutputSchema, THandlerOutput>>
120
+ : // biome-ignore lint/complexity/noBannedTypes: {} seem has no side-effect on this case
121
+ {})
122
+
123
+ export interface ProcedureHandler<
124
+ TContext extends Context,
125
+ TExtraContext extends Context,
126
+ TInputSchema extends Schema,
127
+ TOutputSchema extends Schema,
128
+ TOutput extends SchemaOutput<TOutputSchema>,
129
+ > {
130
+ (
131
+ input: SchemaOutput<TInputSchema>,
132
+ context: MergeContext<TContext, TExtraContext>,
133
+ meta: Meta<unknown>,
134
+ ): Promisable<SchemaInput<TOutputSchema, TOutput>>
135
+ }
136
+
137
+ const DECORATED_PROCEDURE_SYMBOL = Symbol('DECORATED_PROCEDURE')
138
+
139
+ export function decorateProcedure<
140
+ TContext extends Context,
141
+ TExtraContext extends Context,
142
+ TInputSchema extends Schema,
143
+ TOutputSchema extends Schema,
144
+ THandlerOutput extends SchemaOutput<TOutputSchema>,
145
+ >(
146
+ procedure: Procedure<
147
+ TContext,
148
+ TExtraContext,
149
+ TInputSchema,
150
+ TOutputSchema,
151
+ THandlerOutput
152
+ >,
153
+ ): DecoratedProcedure<
154
+ TContext,
155
+ TExtraContext,
156
+ TInputSchema,
157
+ TOutputSchema,
158
+ THandlerOutput
159
+ > {
160
+ if (DECORATED_PROCEDURE_SYMBOL in procedure) {
161
+ return procedure as any
162
+ }
163
+
164
+ const serverAction = async (input: unknown) => {
165
+ const input_ = (() => {
166
+ if (!(input instanceof FormData)) return input
167
+
168
+ const transformer = new OpenAPIDeserializer({
169
+ schema: procedure.zz$p.contract.zz$cp.InputSchema,
170
+ })
171
+
172
+ return transformer.deserializeAsFormData(input)
173
+ })()
174
+
175
+ const procedureCaller = createProcedureCaller({
176
+ procedure,
177
+ context: undefined as any,
178
+ internal: false,
179
+ validate: true,
180
+ })
181
+
182
+ return await procedureCaller(input_ as any)
183
+ }
184
+
185
+ return Object.assign(serverAction, {
186
+ [DECORATED_PROCEDURE_SYMBOL]: true,
187
+ zz$p: procedure.zz$p,
188
+
189
+ prefix(prefix: HTTPPath) {
190
+ return decorateProcedure({
191
+ zz$p: {
192
+ ...procedure.zz$p,
193
+ contract: DecoratedContractProcedure.decorate(
194
+ procedure.zz$p.contract,
195
+ ).prefix(prefix),
196
+ },
197
+ })
198
+ },
199
+
200
+ route(opts: RouteOptions) {
201
+ return decorateProcedure({
202
+ zz$p: {
203
+ ...procedure.zz$p,
204
+ contract: DecoratedContractProcedure.decorate(
205
+ procedure.zz$p.contract,
206
+ ).route(opts),
207
+ },
208
+ })
209
+ },
210
+
211
+ use(
212
+ middleware: Middleware<any, any, any, any>,
213
+ mapInput?: MapInputMiddleware<any, any>,
214
+ ) {
215
+ const middleware_ = mapInput
216
+ ? decorateMiddleware(middleware).mapInput(mapInput)
217
+ : middleware
218
+
219
+ return decorateProcedure({
220
+ zz$p: {
221
+ ...procedure.zz$p,
222
+ middlewares: [middleware_, ...(procedure.zz$p.middlewares ?? [])],
223
+ },
224
+ })
225
+ },
226
+ }) as any
227
+ }
228
+
229
+ export type WELL_DEFINED_PROCEDURE = Procedure<
230
+ Context,
231
+ Context,
232
+ Schema,
233
+ Schema,
234
+ unknown
235
+ >
236
+
237
+ export function isProcedure(item: unknown): item is WELL_DEFINED_PROCEDURE {
238
+ if (item instanceof Procedure) return true
239
+
240
+ return (
241
+ (typeof item === 'object' || typeof item === 'function') &&
242
+ item !== null &&
243
+ 'zz$p' in item &&
244
+ typeof item.zz$p === 'object' &&
245
+ item.zz$p !== null &&
246
+ 'contract' in item.zz$p &&
247
+ isContractProcedure(item.zz$p.contract) &&
248
+ 'handler' in item.zz$p &&
249
+ typeof item.zz$p.handler === 'function'
250
+ )
251
+ }
@@ -0,0 +1,106 @@
1
+ import { ContractProcedure } from '@orpc/contract'
2
+ import { z } from 'zod'
3
+ import { os, Procedure, decorateProcedure, isProcedure } from '.'
4
+ import { RouterBuilder } from './router-builder'
5
+
6
+ const builder = new RouterBuilder<undefined, undefined>({})
7
+ const ping = os
8
+ .route({ method: 'GET', path: '/ping', tags: ['ping'] })
9
+ .handler(() => 'ping')
10
+ const pong = os
11
+ .output(z.object({ id: z.string() }))
12
+ .handler(() => ({ id: '123' }))
13
+
14
+ describe('prefix', () => {
15
+ it('chainable prefix', () => {
16
+ expect(builder.prefix('/1').prefix('/2').prefix('/3').zz$rb.prefix).toEqual(
17
+ '/1/2/3',
18
+ )
19
+ })
20
+
21
+ it('router', () => {
22
+ const router = builder
23
+ .prefix('/api')
24
+ .prefix('/users')
25
+ .router({ ping: ping, pong })
26
+
27
+ expect(router.ping.zz$p.contract.zz$cp.path).toEqual('/api/users/ping')
28
+ expect(router.pong.zz$p.contract.zz$cp.path).toEqual(undefined)
29
+ })
30
+ })
31
+
32
+ describe('tags', () => {
33
+ it('chainable tags', () => {
34
+ expect(builder.tags('1', '2').tags('3').tags('4').zz$rb.tags).toEqual([
35
+ '1',
36
+ '2',
37
+ '3',
38
+ '4',
39
+ ])
40
+ })
41
+
42
+ it('router', () => {
43
+ const router = builder
44
+ .tags('api')
45
+ .tags('users')
46
+ .router({ ping: ping, pong })
47
+
48
+ expect(router.ping.zz$p.contract.zz$cp.tags).toEqual([
49
+ 'ping',
50
+ 'api',
51
+ 'users',
52
+ ])
53
+ expect(router.pong.zz$p.contract.zz$cp.tags).toEqual(['api', 'users'])
54
+ })
55
+ })
56
+
57
+ describe('middleware', () => {
58
+ const mid1 = vi.fn()
59
+ const mid2 = vi.fn()
60
+ const mid3 = vi.fn()
61
+
62
+ it('chainable middleware', () => {
63
+ expect(builder.use(mid1).use(mid2).use(mid3).zz$rb.middlewares).toEqual([
64
+ mid1,
65
+ mid2,
66
+ mid3,
67
+ ])
68
+ })
69
+
70
+ it('router', () => {
71
+ const router = builder.use(mid1).use(mid2).router({ ping: ping, pong })
72
+
73
+ expect(router.ping.zz$p.middlewares).toEqual([mid1, mid2])
74
+ expect(router.pong.zz$p.middlewares).toEqual([mid1, mid2])
75
+ })
76
+
77
+ it('decorate items', () => {
78
+ const ping = new Procedure({
79
+ contract: new ContractProcedure({
80
+ InputSchema: undefined,
81
+ OutputSchema: undefined,
82
+ }),
83
+ handler: () => {},
84
+ })
85
+
86
+ const decorated = decorateProcedure({
87
+ zz$p: {
88
+ contract: new ContractProcedure({
89
+ InputSchema: undefined,
90
+ OutputSchema: undefined,
91
+ }),
92
+ handler: () => {},
93
+ },
94
+ })
95
+
96
+ const router = builder.router({ ping, nested: { ping } })
97
+
98
+ expectTypeOf(router).toEqualTypeOf<{
99
+ ping: typeof decorated
100
+ nested: { ping: typeof decorated }
101
+ }>()
102
+
103
+ expect(router.ping).satisfies(isProcedure)
104
+ expect(router.nested.ping).satisfies(isProcedure)
105
+ })
106
+ })