@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.
- 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
|
})
|