@orpc/server 0.0.0 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -195,7 +195,7 @@ describe('procedure throw error', () => {
195
195
  .input(z.object({}))
196
196
  .output(z.string())
197
197
  .handler(() => {
198
- return 'dinwwwh'
198
+ return 'unnoq'
199
199
  }),
200
200
  })
201
201
 
@@ -332,7 +332,7 @@ describe('file upload', () => {
332
332
 
333
333
  const blob1 = new Blob(['hello'], { type: 'text/plain;charset=utf-8' })
334
334
  const blob2 = new Blob(['"world"'], { type: 'image/png' })
335
- const blob3 = new Blob(['dinwwwh'], { type: 'application/octet-stream' })
335
+ const blob3 = new Blob(['unnoq'], { type: 'application/octet-stream' })
336
336
 
337
337
  it('single file', async () => {
338
338
  const rForm = new FormData()
@@ -358,7 +358,7 @@ describe('file upload', () => {
358
358
  expect(file0).toBeInstanceOf(File)
359
359
  expect(file0.name).toBe('blob')
360
360
  expect(file0.type).toBe('application/octet-stream')
361
- expect(await file0.text()).toBe('dinwwwh')
361
+ expect(await file0.text()).toBe('unnoq')
362
362
  })
363
363
 
364
364
  it('multiple file', async () => {
@@ -397,3 +397,159 @@ describe('file upload', () => {
397
397
  expect(await file1.text()).toBe('"world"')
398
398
  })
399
399
  })
400
+
401
+ describe('accept header', () => {
402
+ const router = os.router({
403
+ ping: os.handler(async () => 'pong'),
404
+ })
405
+ const handler = createFetchHandler({
406
+ router,
407
+ })
408
+
409
+ it('application/json', async () => {
410
+ const response = await handler({
411
+ prefix: '/orpc',
412
+ request: new Request('http://localhost/orpc/ping', {
413
+ method: 'POST',
414
+ headers: {
415
+ Accept: 'application/json',
416
+ },
417
+ }),
418
+ })
419
+
420
+ expect(response.headers.get('Content-Type')).toEqual('application/json')
421
+
422
+ expect(await response.json()).toEqual('pong')
423
+ })
424
+
425
+ it('multipart/form-data', async () => {
426
+ const response = await handler({
427
+ prefix: '/orpc',
428
+ request: new Request('http://localhost/orpc/ping', {
429
+ method: 'POST',
430
+ headers: {
431
+ Accept: 'multipart/form-data',
432
+ },
433
+ }),
434
+ })
435
+
436
+ expect(response.headers.get('Content-Type')).toContain(
437
+ 'multipart/form-data',
438
+ )
439
+
440
+ const form = await response.formData()
441
+ expect(form.get('')).toEqual('pong')
442
+ })
443
+
444
+ it('application/x-www-form-urlencoded', async () => {
445
+ const response = await handler({
446
+ prefix: '/orpc',
447
+ request: new Request('http://localhost/orpc/ping', {
448
+ method: 'POST',
449
+ headers: {
450
+ Accept: 'application/x-www-form-urlencoded',
451
+ },
452
+ }),
453
+ })
454
+
455
+ expect(response.headers.get('Content-Type')).toEqual(
456
+ 'application/x-www-form-urlencoded',
457
+ )
458
+
459
+ const params = new URLSearchParams(await response.text())
460
+ expect(params.get('')).toEqual('pong')
461
+ })
462
+
463
+ it('*/*', async () => {
464
+ const response = await handler({
465
+ prefix: '/orpc',
466
+ request: new Request('http://localhost/orpc/ping', {
467
+ method: 'POST',
468
+ headers: {
469
+ Accept: '*/*',
470
+ },
471
+ }),
472
+ })
473
+
474
+ expect(response.headers.get('Content-Type')).toEqual('application/json')
475
+ expect(await response.json()).toEqual('pong')
476
+ })
477
+
478
+ it('invalid', async () => {
479
+ const response = await handler({
480
+ prefix: '/orpc',
481
+ request: new Request('http://localhost/orpc/ping', {
482
+ method: 'POST',
483
+ headers: {
484
+ Accept: 'invalid',
485
+ },
486
+ }),
487
+ })
488
+
489
+ expect(response.headers.get('Content-Type')).toEqual('application/json')
490
+ expect(await response.json()).toEqual({
491
+ code: 'NOT_ACCEPTABLE',
492
+ message: 'Unsupported content-type: invalid',
493
+ status: 406,
494
+ })
495
+ })
496
+ })
497
+
498
+ describe('dynamic params', () => {
499
+ const router = os.router({
500
+ deep: os
501
+ .route({
502
+ method: 'GET',
503
+ path: '/{id}/{id2}',
504
+ })
505
+ .input(
506
+ z.object({
507
+ id: z.number(),
508
+ id2: z.string(),
509
+ }),
510
+ )
511
+ .handler((input) => input),
512
+
513
+ find: os
514
+ .route({
515
+ method: 'GET',
516
+ path: '/{id}',
517
+ })
518
+ .input(
519
+ z.object({
520
+ id: z.number(),
521
+ }),
522
+ )
523
+ .handler((input) => input),
524
+ })
525
+
526
+ const handlers = [
527
+ createFetchHandler({
528
+ router,
529
+ }),
530
+ createFetchHandler({
531
+ router,
532
+ serverless: true,
533
+ }),
534
+ ]
535
+
536
+ it.each(handlers)('should handle dynamic params', async (handler) => {
537
+ const response = await handler({
538
+ request: new Request('http://localhost/123'),
539
+ })
540
+
541
+ expect(response.status).toEqual(200)
542
+ expect(response.headers.get('Content-Type')).toEqual('application/json')
543
+ expect(await response.json()).toEqual({ id: 123 })
544
+ })
545
+
546
+ it.each(handlers)('should handle deep dynamic params', async (handler) => {
547
+ const response = await handler({
548
+ request: new Request('http://localhost/123/dfdsfds'),
549
+ })
550
+
551
+ expect(response.status).toEqual(200)
552
+ expect(response.headers.get('Content-Type')).toEqual('application/json')
553
+ expect(await response.json()).toEqual({ id: 123, id2: 'dfdsfds' })
554
+ })
555
+ })
@@ -10,6 +10,7 @@ import {
10
10
  type PartialOnUndefinedDeep,
11
11
  get,
12
12
  isPlainObject,
13
+ mapValues,
13
14
  trim,
14
15
  } from '@orpc/shared'
15
16
  import { ORPCError } from '@orpc/shared/error'
@@ -18,6 +19,7 @@ import {
18
19
  ORPCSerializer,
19
20
  OpenAPIDeserializer,
20
21
  OpenAPISerializer,
22
+ zodCoerce,
21
23
  } from '@orpc/transformer'
22
24
  import { LinearRouter } from 'hono/router/linear-router'
23
25
  import { RegExpRouter } from 'hono/router/reg-exp-router'
@@ -73,7 +75,7 @@ export function createFetchHandler<TRouter extends Router<any>>(
73
75
  return async (requestOptions) => {
74
76
  const isORPCTransformer =
75
77
  requestOptions.request.headers.get(ORPC_HEADER) === ORPC_HEADER_VALUE
76
- const accept = requestOptions.request.headers.get('Accept') ?? undefined
78
+ const accept = requestOptions.request.headers.get('Accept') || undefined
77
79
 
78
80
  const serializer = isORPCTransformer
79
81
  ? new ORPCSerializer()
@@ -86,7 +88,7 @@ export function createFetchHandler<TRouter extends Router<any>>(
86
88
 
87
89
  let path: string[] | undefined
88
90
  let procedure: WELL_DEFINED_PROCEDURE | undefined
89
- let params: Record<string, string | number> | undefined
91
+ let params: Record<string, string> | undefined
90
92
 
91
93
  if (isORPCTransformer) {
92
94
  path = trim(pathname, '/').split('/').map(decodeURIComponent)
@@ -96,13 +98,28 @@ export function createFetchHandler<TRouter extends Router<any>>(
96
98
  procedure = val
97
99
  }
98
100
  } else {
99
- const [[match]] = routing.match(
101
+ const [matches, params_] = routing.match(
100
102
  requestOptions.request.method,
101
103
  pathname,
102
104
  )
103
- path = match?.[0][0]
104
- procedure = match?.[0][1]
105
- params = match?.[1]
105
+
106
+ const [match] = matches.sort((a, b) => {
107
+ return Object.keys(a[1]).length - Object.keys(b[1]).length
108
+ })
109
+
110
+ if (match) {
111
+ path = match[0][0]
112
+ procedure = match[0][1]
113
+
114
+ if (params_) {
115
+ params = mapValues(
116
+ (match as any)[1]!,
117
+ (v) => params_[v as number]!,
118
+ )
119
+ } else {
120
+ params = match[1] as Record<string, string>
121
+ }
122
+ }
106
123
 
107
124
  if (!path || !procedure) {
108
125
  path = trim(pathname, '/').split('/').map(decodeURIComponent)
@@ -148,18 +165,28 @@ export function createFetchHandler<TRouter extends Router<any>>(
148
165
  })()
149
166
 
150
167
  const input = (() => {
151
- if (
152
- params &&
153
- Object.keys(params).length > 0 &&
154
- (input_ === undefined || isPlainObject(input_))
155
- ) {
156
- return {
157
- ...params,
158
- ...input_,
159
- }
168
+ if (!params || Object.keys(params).length === 0) {
169
+ return input_
170
+ }
171
+
172
+ const coercedParams = procedure.zz$p.contract.zz$cp.InputSchema
173
+ ? (zodCoerce(
174
+ procedure.zz$p.contract.zz$cp.InputSchema,
175
+ { ...params },
176
+ {
177
+ bracketNotation: true,
178
+ },
179
+ ) as object)
180
+ : params
181
+
182
+ if (input_ !== undefined && !isPlainObject(input_)) {
183
+ return coercedParams
160
184
  }
161
185
 
162
- return input_
186
+ return {
187
+ ...coercedParams,
188
+ ...input_,
189
+ }
163
190
  })()
164
191
 
165
192
  const caller = createProcedureCaller({
@@ -180,21 +207,28 @@ export function createFetchHandler<TRouter extends Router<any>>(
180
207
  })
181
208
  })
182
209
  } catch (e) {
183
- const error =
184
- e instanceof ORPCError
185
- ? e
186
- : new ORPCError({
187
- code: 'INTERNAL_SERVER_ERROR',
188
- message: 'Internal server error',
189
- cause: e,
190
- })
210
+ const error = toORPCError(e)
191
211
 
192
- const { body, headers } = serializer.serialize(error.toJSON())
212
+ try {
213
+ const { body, headers } = serializer.serialize(error.toJSON())
193
214
 
194
- return new Response(body, {
195
- status: error.status,
196
- headers: headers,
197
- })
215
+ return new Response(body, {
216
+ status: error.status,
217
+ headers: headers,
218
+ })
219
+ } catch (e) {
220
+ const error = toORPCError(e)
221
+
222
+ // fallback to OpenAPI serializer (without accept) when expected serializer has failed
223
+ const { body, headers } = new OpenAPISerializer().serialize(
224
+ error.toJSON(),
225
+ )
226
+
227
+ return new Response(body, {
228
+ status: error.status,
229
+ headers: headers,
230
+ })
231
+ }
198
232
  }
199
233
  }
200
234
  }
@@ -226,3 +260,13 @@ export type FetchHandlerOptions<TRouter extends Router<any>> = {
226
260
  export interface FetchHandler<TRouter extends Router<any>> {
227
261
  (options: FetchHandlerOptions<TRouter>): Promise<Response>
228
262
  }
263
+
264
+ function toORPCError(e: unknown): ORPCError<any, any> {
265
+ return e instanceof ORPCError
266
+ ? e
267
+ : new ORPCError({
268
+ code: 'INTERNAL_SERVER_ERROR',
269
+ message: 'Internal server error',
270
+ cause: e,
271
+ })
272
+ }
@@ -8,7 +8,7 @@ import type { Meta } from './types'
8
8
  const schema1 = z.object({ id: z.string() })
9
9
  const example1 = { id: '1' }
10
10
  const schema2 = z.object({ name: z.string() })
11
- const example2 = { name: 'dinwwwh' }
11
+ const example2 = { name: 'unnoq' }
12
12
 
13
13
  const builder = new ProcedureBuilder<
14
14
  { auth: boolean },
@@ -200,7 +200,7 @@ describe('handler', () => {
200
200
  expectTypeOf(meta).toEqualTypeOf<Meta<unknown>>()
201
201
 
202
202
  return {
203
- name: 'dinwwwh',
203
+ name: 'unnoq',
204
204
  }
205
205
  })
206
206
 
@@ -145,7 +145,7 @@ describe('handler', () => {
145
145
  expectTypeOf(meta).toEqualTypeOf<Meta<unknown>>()
146
146
 
147
147
  return {
148
- name: 'dinwwwh',
148
+ name: 'unnoq',
149
149
  }
150
150
  })
151
151
 
@@ -166,7 +166,7 @@ describe('handler', () => {
166
166
  expectTypeOf(meta).toEqualTypeOf<Meta<unknown>>()
167
167
 
168
168
  return {
169
- name: 'dinwwwh',
169
+ name: 'unnoq',
170
170
  }
171
171
  })
172
172
 
@@ -196,7 +196,7 @@ describe('handler', () => {
196
196
  expectTypeOf(meta).toEqualTypeOf<Meta<unknown>>()
197
197
 
198
198
  return {
199
- name: 'dinwwwh',
199
+ name: 'unnoq',
200
200
  }
201
201
  })
202
202
 
@@ -157,7 +157,7 @@ describe('route method', () => {
157
157
 
158
158
  test('prefix method', () => {
159
159
  const p = os.context<{ auth: boolean }>().handler(() => {
160
- return 'dinwwwh'
160
+ return 'unnoq'
161
161
  })
162
162
 
163
163
  const p2 = p.prefix('/test')
@@ -168,7 +168,7 @@ test('prefix method', () => {
168
168
  .context<{ auth: boolean }>()
169
169
  .route({ path: '/test1' })
170
170
  .handler(() => {
171
- return 'dinwwwh'
171
+ return 'unnoq'
172
172
  })
173
173
 
174
174
  const p4 = p3.prefix('/test')
@@ -183,7 +183,7 @@ describe('use middleware', () => {
183
183
  return { context: { postId: 'string' } }
184
184
  })
185
185
  .handler(() => {
186
- return 'dinwwwh'
186
+ return 'unnoq'
187
187
  })
188
188
 
189
189
  const p2 = p1
@@ -246,7 +246,7 @@ describe('use middleware', () => {
246
246
  const mid2 = vi.fn()
247
247
  const mid3 = vi.fn()
248
248
 
249
- const p1 = os.use(mid1).handler(() => 'dinwwwh')
249
+ const p1 = os.use(mid1).handler(() => 'unnoq')
250
250
  const p2 = p1.use(mid2).use(mid3)
251
251
 
252
252
  expect(p2.zz$p.middlewares).toEqual([mid3, mid2, mid1])
@@ -18,15 +18,15 @@ const cr = oc.router({
18
18
  const osw = os.context<{ auth: boolean }>().contract(cr)
19
19
 
20
20
  const p1 = osw.p1.handler(() => {
21
- return 'dinwwwh'
21
+ return 'unnoq'
22
22
  })
23
23
 
24
24
  const p2 = osw.nested.p2.handler(() => {
25
- return 'dinwwwh'
25
+ return 'unnoq'
26
26
  })
27
27
 
28
28
  const p3 = osw.nested2.p3.handler(() => {
29
- return 'dinwwwh'
29
+ return 'unnoq'
30
30
  })
31
31
 
32
32
  it('required all procedure match', () => {
@@ -76,7 +76,7 @@ it('required all procedure match', () => {
76
76
  p1: os
77
77
  .input(z.string())
78
78
  .output(z.string())
79
- .handler(() => 'dinwwwh'),
79
+ .handler(() => 'unnoq'),
80
80
  nested: {
81
81
  p2: p2,
82
82
  },
@@ -12,7 +12,7 @@ it('require procedure match context', () => {
12
12
 
13
13
  // @ts-expect-error userId is not match
14
14
  ping2: osw.context<{ userId: number }>().handler(() => {
15
- return { name: 'dinwwwh' }
15
+ return { name: 'unnoq' }
16
16
  }),
17
17
 
18
18
  nested: {
@@ -22,7 +22,7 @@ it('require procedure match context', () => {
22
22
 
23
23
  // @ts-expect-error userId is not match
24
24
  ping2: osw.context<{ userId: number }>().handler(() => {
25
- return { name: 'dinwwwh' }
25
+ return { name: 'unnoq' }
26
26
  }),
27
27
  },
28
28
  })
@@ -121,18 +121,18 @@ it('toContractRouter', () => {
121
121
 
122
122
  const router = osw.router({
123
123
  p1: osw.p1.handler(() => {
124
- return 'dinwwwh'
124
+ return 'unnoq'
125
125
  }),
126
126
 
127
127
  nested: osw.nested.router({
128
128
  p2: osw.nested.p2.handler(() => {
129
- return 'dinwwwh'
129
+ return 'unnoq'
130
130
  }),
131
131
  }),
132
132
 
133
133
  nested2: {
134
134
  p3: osw.nested2.p3.handler(() => {
135
- return 'dinwwwh'
135
+ return 'unnoq'
136
136
  }),
137
137
  },
138
138
  })