@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.
- package/dist/fetch.js +60 -22
- package/dist/fetch.js.map +1 -1
- package/dist/src/adapters/fetch.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +11 -11
- package/src/adapters/fetch.test.ts +159 -3
- package/src/adapters/fetch.ts +73 -29
- package/src/procedure-builder.test.ts +2 -2
- package/src/procedure-implementer.test.ts +3 -3
- package/src/procedure.test.ts +4 -4
- package/src/router-implementer.test.ts +4 -4
- package/src/router.test.ts +5 -5
@@ -195,7 +195,7 @@ describe('procedure throw error', () => {
|
|
195
195
|
.input(z.object({}))
|
196
196
|
.output(z.string())
|
197
197
|
.handler(() => {
|
198
|
-
return '
|
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(['
|
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('
|
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
|
+
})
|
package/src/adapters/fetch.ts
CHANGED
@@ -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')
|
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
|
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 [
|
101
|
+
const [matches, params_] = routing.match(
|
100
102
|
requestOptions.request.method,
|
101
103
|
pathname,
|
102
104
|
)
|
103
|
-
|
104
|
-
|
105
|
-
|
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
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
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
|
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
|
-
|
212
|
+
try {
|
213
|
+
const { body, headers } = serializer.serialize(error.toJSON())
|
193
214
|
|
194
|
-
|
195
|
-
|
196
|
-
|
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: '
|
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: '
|
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: '
|
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: '
|
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: '
|
199
|
+
name: 'unnoq',
|
200
200
|
}
|
201
201
|
})
|
202
202
|
|
package/src/procedure.test.ts
CHANGED
@@ -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 '
|
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 '
|
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 '
|
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(() => '
|
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 '
|
21
|
+
return 'unnoq'
|
22
22
|
})
|
23
23
|
|
24
24
|
const p2 = osw.nested.p2.handler(() => {
|
25
|
-
return '
|
25
|
+
return 'unnoq'
|
26
26
|
})
|
27
27
|
|
28
28
|
const p3 = osw.nested2.p3.handler(() => {
|
29
|
-
return '
|
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(() => '
|
79
|
+
.handler(() => 'unnoq'),
|
80
80
|
nested: {
|
81
81
|
p2: p2,
|
82
82
|
},
|
package/src/router.test.ts
CHANGED
@@ -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: '
|
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: '
|
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 '
|
124
|
+
return 'unnoq'
|
125
125
|
}),
|
126
126
|
|
127
127
|
nested: osw.nested.router({
|
128
128
|
p2: osw.nested.p2.handler(() => {
|
129
|
-
return '
|
129
|
+
return 'unnoq'
|
130
130
|
}),
|
131
131
|
}),
|
132
132
|
|
133
133
|
nested2: {
|
134
134
|
p3: osw.nested2.p3.handler(() => {
|
135
|
-
return '
|
135
|
+
return 'unnoq'
|
136
136
|
}),
|
137
137
|
},
|
138
138
|
})
|