@orpc/server 0.0.0 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  })