@orpc/server 0.0.0

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.
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,228 @@
1
+ /// <reference lib="dom" />
2
+
3
+ import {
4
+ type HTTPPath,
5
+ ORPC_HEADER,
6
+ ORPC_HEADER_VALUE,
7
+ standardizeHTTPPath,
8
+ } from '@orpc/contract'
9
+ import {
10
+ type PartialOnUndefinedDeep,
11
+ get,
12
+ isPlainObject,
13
+ trim,
14
+ } from '@orpc/shared'
15
+ import { ORPCError } from '@orpc/shared/error'
16
+ import {
17
+ ORPCDeserializer,
18
+ ORPCSerializer,
19
+ OpenAPIDeserializer,
20
+ OpenAPISerializer,
21
+ } from '@orpc/transformer'
22
+ import { LinearRouter } from 'hono/router/linear-router'
23
+ import { RegExpRouter } from 'hono/router/reg-exp-router'
24
+ import { type WELL_DEFINED_PROCEDURE, isProcedure } from '../procedure'
25
+ import { createProcedureCaller } from '../procedure-caller'
26
+ import type { Router } from '../router'
27
+ import type { Meta, Promisable } from '../types'
28
+ import { hook } from '../utils'
29
+
30
+ export interface CreateFetchHandlerOptions<TRouter extends Router<any>> {
31
+ router: TRouter
32
+
33
+ hooks?: (
34
+ context: TRouter extends Router<infer UContext> ? UContext : never,
35
+ meta: Meta<unknown>,
36
+ ) => Promisable<void>
37
+
38
+ /**
39
+ * It will help improve the cold start time. But it will increase the performance.
40
+ *
41
+ * @default false
42
+ */
43
+ serverless?: boolean
44
+ }
45
+
46
+ export function createFetchHandler<TRouter extends Router<any>>(
47
+ options: CreateFetchHandlerOptions<TRouter>,
48
+ ): FetchHandler<TRouter> {
49
+ const routing = options.serverless
50
+ ? new LinearRouter<[string[], WELL_DEFINED_PROCEDURE]>()
51
+ : new RegExpRouter<[string[], WELL_DEFINED_PROCEDURE]>()
52
+
53
+ const addRouteRecursively = (router: Router<any>, basePath: string[]) => {
54
+ for (const key in router) {
55
+ const currentPath = [...basePath, key]
56
+ const item = router[key] as WELL_DEFINED_PROCEDURE | Router<any>
57
+
58
+ if (isProcedure(item)) {
59
+ if (item.zz$p.contract.zz$cp.path) {
60
+ const method = item.zz$p.contract.zz$cp.method ?? 'POST'
61
+ const path = openAPIPathToRouterPath(item.zz$p.contract.zz$cp.path)
62
+
63
+ routing.add(method, path, [currentPath, item])
64
+ }
65
+ } else {
66
+ addRouteRecursively(item, currentPath)
67
+ }
68
+ }
69
+ }
70
+
71
+ addRouteRecursively(options.router, [])
72
+
73
+ return async (requestOptions) => {
74
+ const isORPCTransformer =
75
+ requestOptions.request.headers.get(ORPC_HEADER) === ORPC_HEADER_VALUE
76
+ const accept = requestOptions.request.headers.get('Accept') ?? undefined
77
+
78
+ const serializer = isORPCTransformer
79
+ ? new ORPCSerializer()
80
+ : new OpenAPISerializer({ accept })
81
+
82
+ try {
83
+ return await hook(async (hooks) => {
84
+ const url = new URL(requestOptions.request.url)
85
+ const pathname = `/${trim(url.pathname.replace(requestOptions.prefix ?? '', ''), '/')}`
86
+
87
+ let path: string[] | undefined
88
+ let procedure: WELL_DEFINED_PROCEDURE | undefined
89
+ let params: Record<string, string | number> | undefined
90
+
91
+ if (isORPCTransformer) {
92
+ path = trim(pathname, '/').split('/').map(decodeURIComponent)
93
+ const val = get(options.router, path)
94
+
95
+ if (isProcedure(val)) {
96
+ procedure = val
97
+ }
98
+ } else {
99
+ const [[match]] = routing.match(
100
+ requestOptions.request.method,
101
+ pathname,
102
+ )
103
+ path = match?.[0][0]
104
+ procedure = match?.[0][1]
105
+ params = match?.[1]
106
+
107
+ if (!path || !procedure) {
108
+ path = trim(pathname, '/').split('/').map(decodeURIComponent)
109
+
110
+ const val = get(options.router, path)
111
+
112
+ if (isProcedure(val)) {
113
+ procedure = val
114
+ }
115
+ }
116
+ }
117
+
118
+ if (!path || !procedure) {
119
+ throw new ORPCError({ code: 'NOT_FOUND', message: 'Not found' })
120
+ }
121
+
122
+ const meta: Meta<unknown> = {
123
+ ...hooks,
124
+ procedure,
125
+ path: path,
126
+ internal: false,
127
+ }
128
+
129
+ await options.hooks?.(requestOptions.context as any, meta)
130
+
131
+ const deserializer = isORPCTransformer
132
+ ? new ORPCDeserializer()
133
+ : new OpenAPIDeserializer({
134
+ schema: procedure.zz$p.contract.zz$cp.InputSchema,
135
+ })
136
+
137
+ const input_ = await (async () => {
138
+ try {
139
+ return await deserializer.deserialize(requestOptions.request)
140
+ } catch (e) {
141
+ throw new ORPCError({
142
+ code: 'BAD_REQUEST',
143
+ message:
144
+ 'Cannot parse request. Please check the request body and Content-Type header.',
145
+ cause: e,
146
+ })
147
+ }
148
+ })()
149
+
150
+ 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
+ }
160
+ }
161
+
162
+ return input_
163
+ })()
164
+
165
+ const caller = createProcedureCaller({
166
+ context: requestOptions.context,
167
+ internal: false,
168
+ validate: true,
169
+ procedure,
170
+ path,
171
+ })
172
+
173
+ const output = await caller(input)
174
+
175
+ const { body, headers } = serializer.serialize(output)
176
+
177
+ return new Response(body, {
178
+ status: 200,
179
+ headers,
180
+ })
181
+ })
182
+ } 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
+ })
191
+
192
+ const { body, headers } = serializer.serialize(error.toJSON())
193
+
194
+ return new Response(body, {
195
+ status: error.status,
196
+ headers: headers,
197
+ })
198
+ }
199
+ }
200
+ }
201
+
202
+ function openAPIPathToRouterPath(path: HTTPPath): string {
203
+ return standardizeHTTPPath(path).replace(/\{([^}]+)\}/g, ':$1')
204
+ }
205
+
206
+ export type FetchHandlerOptions<TRouter extends Router<any>> = {
207
+ /**
208
+ * The request need to be handled.
209
+ */
210
+ request: Request
211
+
212
+ /**
213
+ * Remove the prefix from the request path.
214
+ *
215
+ * @example /orpc
216
+ * @example /api
217
+ */
218
+ prefix?: string
219
+ } & PartialOnUndefinedDeep<{
220
+ /**
221
+ * The context used to handle the request.
222
+ */
223
+ context: TRouter extends Router<infer UContext> ? UContext : never
224
+ }>
225
+
226
+ export interface FetchHandler<TRouter extends Router<any>> {
227
+ (options: FetchHandlerOptions<TRouter>): Promise<Response>
228
+ }
@@ -0,0 +1,362 @@
1
+ import { oc } from '@orpc/contract'
2
+ import { z } from 'zod'
3
+ import {
4
+ os,
5
+ type Builder,
6
+ type DecoratedMiddleware,
7
+ type DecoratedProcedure,
8
+ type Meta,
9
+ ProcedureBuilder,
10
+ ProcedureImplementer,
11
+ RouterImplementer,
12
+ isProcedure,
13
+ } from '.'
14
+ import { RouterBuilder } from './router-builder'
15
+
16
+ test('context method', () => {
17
+ expectTypeOf<
18
+ typeof os extends Builder<infer TContext, any> ? TContext : never
19
+ >().toEqualTypeOf<undefined | Record<string, unknown>>()
20
+
21
+ const os2 = os.context<{ foo: 'bar' }>()
22
+
23
+ expectTypeOf<
24
+ typeof os2 extends Builder<infer TContext, any> ? TContext : never
25
+ >().toEqualTypeOf<{ foo: 'bar' }>()
26
+
27
+ const os3 = os.context<{ foo: 'bar' }>().context()
28
+
29
+ expectTypeOf<
30
+ typeof os3 extends Builder<infer TContext, any> ? TContext : never
31
+ >().toEqualTypeOf<{ foo: 'bar' }>()
32
+ })
33
+
34
+ describe('use middleware', () => {
35
+ type Context = { auth: boolean }
36
+
37
+ const osw = os.context<Context>()
38
+
39
+ it('infer types', () => {
40
+ osw.use((input, context, meta) => {
41
+ expectTypeOf(input).toEqualTypeOf<unknown>()
42
+ expectTypeOf(context).toEqualTypeOf<Context>()
43
+ expectTypeOf(meta).toEqualTypeOf<Meta<unknown>>()
44
+ })
45
+ })
46
+
47
+ it('can map context', () => {
48
+ osw
49
+ .use(() => {
50
+ return { context: { userId: '1' } }
51
+ })
52
+ .use((_, context) => {
53
+ expectTypeOf(context).toMatchTypeOf<Context & { userId: string }>()
54
+ })
55
+ })
56
+
57
+ it('can map input', () => {
58
+ osw
59
+ // @ts-expect-error mismatch input
60
+ .use((input: { postId: string }) => {})
61
+ .use(
62
+ (input: { postId: string }) => {
63
+ return { context: { user: '1' } }
64
+ },
65
+ (input) => {
66
+ expectTypeOf(input).toEqualTypeOf<unknown>()
67
+ return { postId: '1' }
68
+ },
69
+ )
70
+ .handler((_, context) => {
71
+ expectTypeOf(context).toMatchTypeOf<{ user: string }>()
72
+ })
73
+ })
74
+ })
75
+
76
+ describe('create middleware', () => {
77
+ it('infer types', () => {
78
+ const mid = os
79
+ .context<{ auth: boolean }>()
80
+ .middleware((input, context, meta) => {
81
+ expectTypeOf(input).toEqualTypeOf<unknown>()
82
+ expectTypeOf(context).toEqualTypeOf<{ auth: boolean }>()
83
+ expectTypeOf(meta).toEqualTypeOf<Meta<unknown>>()
84
+ })
85
+
86
+ expectTypeOf(mid).toEqualTypeOf<
87
+ DecoratedMiddleware<{ auth: boolean }, undefined, unknown, unknown>
88
+ >()
89
+ })
90
+
91
+ it('map context', () => {
92
+ const mid = os.context<{ auth: boolean }>().middleware(() => {
93
+ return { context: { userId: '1' } }
94
+ })
95
+
96
+ expectTypeOf(mid).toEqualTypeOf<
97
+ DecoratedMiddleware<
98
+ { auth: boolean },
99
+ { userId: string },
100
+ unknown,
101
+ unknown
102
+ >
103
+ >()
104
+ })
105
+ })
106
+
107
+ test('router method', () => {
108
+ const pingContract = oc.input(z.string()).output(z.string())
109
+ const userFindContract = oc
110
+ .input(z.object({ id: z.string() }))
111
+ .output(z.object({ name: z.string() }))
112
+
113
+ const contract = oc.router({
114
+ ping: pingContract,
115
+ user: {
116
+ find: userFindContract,
117
+ },
118
+
119
+ user2: oc.router({
120
+ find: userFindContract,
121
+ }),
122
+
123
+ router: userFindContract,
124
+ })
125
+
126
+ const osw = os.contract(contract)
127
+
128
+ expect(osw.ping).instanceOf(ProcedureImplementer)
129
+ expect(osw.ping.zz$pi.contract).toEqual(pingContract)
130
+
131
+ expect(osw.user).instanceOf(RouterImplementer)
132
+
133
+ expect(osw.user.find).instanceOf(ProcedureImplementer)
134
+ expect(osw.user.find.zz$pi.contract).toEqual(userFindContract)
135
+
136
+ // Because of the router keyword is special, we can't use instanceof
137
+ expect(osw.router.zz$pi.contract).toEqual(userFindContract)
138
+ expect(
139
+ osw.router.handler(() => {
140
+ return { name: '' }
141
+ }),
142
+ ).toSatisfy(isProcedure)
143
+ })
144
+
145
+ describe('define procedure builder', () => {
146
+ const osw = os.context<{ auth: boolean }>()
147
+ const schema1 = z.object({})
148
+ const example1 = {}
149
+ const schema2 = z.object({ a: z.string() })
150
+ const example2 = { a: '' }
151
+
152
+ test('input method', () => {
153
+ const builder = osw.input(schema1, example1)
154
+
155
+ expectTypeOf(builder).toEqualTypeOf<
156
+ ProcedureBuilder<{ auth: boolean }, undefined, typeof schema1, undefined>
157
+ >()
158
+
159
+ expect(builder).instanceOf(ProcedureBuilder)
160
+ expect(builder.zz$pb.middlewares).toBe(undefined)
161
+ expect(builder.zz$pb).toMatchObject({
162
+ contract: {
163
+ zz$cp: {
164
+ InputSchema: schema1,
165
+ inputExample: example1,
166
+ },
167
+ },
168
+ })
169
+ })
170
+
171
+ test('output method', () => {
172
+ const builder = osw.output(schema2, example2)
173
+
174
+ expectTypeOf(builder).toEqualTypeOf<
175
+ ProcedureBuilder<{ auth: boolean }, undefined, undefined, typeof schema2>
176
+ >()
177
+
178
+ expect(builder).instanceOf(ProcedureBuilder)
179
+ expect(builder.zz$pb.middlewares).toBe(undefined)
180
+ expect(builder.zz$pb).toMatchObject({
181
+ contract: {
182
+ zz$cp: {
183
+ OutputSchema: schema2,
184
+ outputExample: example2,
185
+ },
186
+ },
187
+ })
188
+ })
189
+
190
+ test('route method', () => {
191
+ const builder = osw.route({
192
+ method: 'GET',
193
+ path: '/test',
194
+ deprecated: true,
195
+ description: 'des',
196
+ summary: 'sum',
197
+ tags: ['cccc'],
198
+ })
199
+
200
+ expectTypeOf(builder).toEqualTypeOf<
201
+ ProcedureBuilder<{ auth: boolean }, undefined, undefined, undefined>
202
+ >()
203
+
204
+ expect(builder).instanceOf(ProcedureBuilder)
205
+ expect(builder.zz$pb.middlewares).toBe(undefined)
206
+ expect(builder.zz$pb).toMatchObject({
207
+ contract: {
208
+ zz$cp: {
209
+ method: 'GET',
210
+ path: '/test',
211
+ deprecated: true,
212
+ description: 'des',
213
+ summary: 'sum',
214
+ tags: ['cccc'],
215
+ },
216
+ },
217
+ })
218
+ })
219
+
220
+ test('with middlewares', () => {
221
+ const mid = os.middleware(() => {
222
+ return {
223
+ context: {
224
+ userId: 'string',
225
+ },
226
+ }
227
+ })
228
+
229
+ const mid2 = os.middleware(() => {
230
+ return {
231
+ context: {
232
+ mid2: true,
233
+ },
234
+ }
235
+ })
236
+
237
+ const osw = os.context<{ auth: boolean }>().use(mid).use(mid2)
238
+
239
+ const builder1 = osw.input(schema1)
240
+ const builder2 = osw.output(schema2)
241
+ const builder3 = osw.route({ method: 'GET', path: '/test' })
242
+
243
+ expectTypeOf(builder1).toEqualTypeOf<
244
+ ProcedureBuilder<
245
+ { auth: boolean },
246
+ { userId: string } & { mid2: boolean },
247
+ typeof schema1,
248
+ undefined
249
+ >
250
+ >()
251
+
252
+ expectTypeOf(builder2).toEqualTypeOf<
253
+ ProcedureBuilder<
254
+ { auth: boolean },
255
+ { userId: string } & { mid2: boolean },
256
+ undefined,
257
+ typeof schema2
258
+ >
259
+ >()
260
+
261
+ expectTypeOf(builder3).toEqualTypeOf<
262
+ ProcedureBuilder<
263
+ { auth: boolean },
264
+ { userId: string } & { mid2: boolean },
265
+ undefined,
266
+ undefined
267
+ >
268
+ >()
269
+
270
+ expect(builder1.zz$pb.middlewares).toEqual([mid, mid2])
271
+ expect(builder2.zz$pb.middlewares).toEqual([mid, mid2])
272
+ expect(builder3.zz$pb.middlewares).toEqual([mid, mid2])
273
+ })
274
+ })
275
+
276
+ describe('handler method', () => {
277
+ it('without middlewares', () => {
278
+ const osw = os.context<{ auth: boolean }>()
279
+
280
+ const procedure = osw.handler((input, context, meta) => {
281
+ expectTypeOf(input).toEqualTypeOf<unknown>()
282
+ expectTypeOf(context).toEqualTypeOf<{ auth: boolean }>()
283
+ expectTypeOf(meta).toEqualTypeOf<Meta<unknown>>()
284
+ })
285
+
286
+ expectTypeOf(procedure).toEqualTypeOf<
287
+ DecoratedProcedure<
288
+ { auth: boolean },
289
+ undefined,
290
+ undefined,
291
+ undefined,
292
+ void
293
+ >
294
+ >()
295
+
296
+ expect(isProcedure(procedure)).toBe(true)
297
+ expect(procedure.zz$p.middlewares).toBe(undefined)
298
+ })
299
+
300
+ it('with middlewares', () => {
301
+ const mid = os.middleware(() => {
302
+ return {
303
+ context: {
304
+ userId: 'string',
305
+ },
306
+ }
307
+ })
308
+
309
+ const osw = os.context<{ auth: boolean }>().use(mid)
310
+
311
+ const procedure = osw.handler((input, context, meta) => {
312
+ expectTypeOf(input).toEqualTypeOf<unknown>()
313
+ expectTypeOf(context).toMatchTypeOf<{ auth: boolean }>()
314
+ expectTypeOf(meta).toEqualTypeOf<Meta<unknown>>()
315
+ })
316
+
317
+ expectTypeOf(procedure).toEqualTypeOf<
318
+ DecoratedProcedure<
319
+ { auth: boolean },
320
+ { userId: string },
321
+ undefined,
322
+ undefined,
323
+ void
324
+ >
325
+ >()
326
+
327
+ expect(isProcedure(procedure)).toBe(true)
328
+ expect(procedure.zz$p.middlewares).toEqual([mid])
329
+ })
330
+ })
331
+
332
+ test('prefix', () => {
333
+ const builder = os
334
+ .context<{ auth: boolean }>()
335
+ .use(() => {
336
+ return { context: { userId: '1' } }
337
+ })
338
+ .prefix('/api')
339
+
340
+ expectTypeOf(builder).toEqualTypeOf<
341
+ RouterBuilder<{ auth: boolean }, { userId: string }>
342
+ >()
343
+
344
+ expect(builder).instanceOf(RouterBuilder)
345
+ expect(builder.zz$rb.prefix).toEqual('/api')
346
+ })
347
+
348
+ test('tags', () => {
349
+ const builder = os
350
+ .context<{ auth: boolean }>()
351
+ .use(() => {
352
+ return { context: { userId: '1' } }
353
+ })
354
+ .tags('user', 'user2')
355
+
356
+ expectTypeOf(builder).toEqualTypeOf<
357
+ RouterBuilder<{ auth: boolean }, { userId: string }>
358
+ >()
359
+
360
+ expect(builder).instanceOf(RouterBuilder)
361
+ expect(builder.zz$rb.tags).toEqual(['user', 'user2'])
362
+ })