@orpc/server 0.0.0-unsafe-pr-2-20241118033608

Sign up to get free protection for your applications and to get access to all the features.
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,142 @@
1
+ import { oc } from '@orpc/contract'
2
+ import { z } from 'zod'
3
+ import { os, type RouterWithContract, toContractRouter } from '.'
4
+
5
+ it('require procedure match context', () => {
6
+ const osw = os.context<{ auth: boolean; userId: string }>()
7
+
8
+ osw.router({
9
+ ping: osw.context<{ auth: boolean }>().handler(() => {
10
+ return { pong: 'ping' }
11
+ }),
12
+
13
+ // @ts-expect-error userId is not match
14
+ ping2: osw.context<{ userId: number }>().handler(() => {
15
+ return { name: 'unnoq' }
16
+ }),
17
+
18
+ nested: {
19
+ ping: osw.context<{ auth: boolean }>().handler(() => {
20
+ return { pong: 'ping' }
21
+ }),
22
+
23
+ // @ts-expect-error userId is not match
24
+ ping2: osw.context<{ userId: number }>().handler(() => {
25
+ return { name: 'unnoq' }
26
+ }),
27
+ },
28
+ })
29
+ })
30
+
31
+ it('require match contract', () => {
32
+ const pingContract = oc.route({ method: 'GET', path: '/ping' })
33
+ const pongContract = oc.input(z.string()).output(z.string())
34
+ const ping = os.contract(pingContract).handler(() => {
35
+ return 'ping'
36
+ })
37
+ const pong = os.contract(pongContract).handler(() => {
38
+ return 'pong'
39
+ })
40
+
41
+ const contract = oc.router({
42
+ ping: pingContract,
43
+ pong: pongContract,
44
+
45
+ nested: oc.router({
46
+ ping: pingContract,
47
+ pong: pongContract,
48
+ }),
49
+ })
50
+
51
+ const _1: RouterWithContract<undefined, typeof contract> = {
52
+ ping,
53
+ pong,
54
+
55
+ nested: {
56
+ ping,
57
+ pong,
58
+ },
59
+ }
60
+
61
+ const _2: RouterWithContract<undefined, typeof contract> = {
62
+ ping,
63
+ pong,
64
+
65
+ nested: os.contract(contract.nested).router({
66
+ ping,
67
+ pong,
68
+ }),
69
+ }
70
+
71
+ const _3: RouterWithContract<undefined, typeof contract> = {
72
+ ping,
73
+ pong,
74
+
75
+ // @ts-expect-error missing nested.ping
76
+ nested: {
77
+ pong,
78
+ },
79
+ }
80
+
81
+ const _4: RouterWithContract<undefined, typeof contract> = {
82
+ ping,
83
+ pong,
84
+
85
+ nested: {
86
+ ping,
87
+ // @ts-expect-error nested.pong is mismatch
88
+ pong: os.handler(() => 'ping'),
89
+ },
90
+ }
91
+
92
+ // @ts-expect-error missing pong
93
+ const _5: RouterWithContract<undefined, typeof contract> = {
94
+ ping,
95
+
96
+ nested: {
97
+ ping,
98
+ pong,
99
+ },
100
+ }
101
+ })
102
+
103
+ it('toContractRouter', () => {
104
+ const p1 = oc.input(z.string()).output(z.string())
105
+ const p2 = oc.output(z.string())
106
+ const p3 = oc.route({ method: 'GET', path: '/test' })
107
+
108
+ const contract = oc.router({
109
+ p1: p1,
110
+
111
+ nested: oc.router({
112
+ p2: p2,
113
+ }),
114
+
115
+ nested2: {
116
+ p3: p3,
117
+ },
118
+ })
119
+
120
+ const osw = os.contract(contract)
121
+
122
+ const router = osw.router({
123
+ p1: osw.p1.handler(() => {
124
+ return 'unnoq'
125
+ }),
126
+
127
+ nested: osw.nested.router({
128
+ p2: osw.nested.p2.handler(() => {
129
+ return 'unnoq'
130
+ }),
131
+ }),
132
+
133
+ nested2: {
134
+ p3: osw.nested2.p3.handler(() => {
135
+ return 'unnoq'
136
+ }),
137
+ },
138
+ })
139
+
140
+ expect(toContractRouter(router)).toEqual(contract)
141
+ expect(toContractRouter(contract)).toEqual(contract)
142
+ })
package/src/router.ts ADDED
@@ -0,0 +1,69 @@
1
+ import {
2
+ type ContractProcedure,
3
+ type ContractRouter,
4
+ isContractProcedure,
5
+ } from '@orpc/contract'
6
+ import {
7
+ type DecoratedProcedure,
8
+ type Procedure,
9
+ isProcedure,
10
+ } from './procedure'
11
+ import type { Context } from './types'
12
+
13
+ export interface Router<TContext extends Context> {
14
+ [k: string]: Procedure<TContext, any, any, any, any> | Router<TContext>
15
+ }
16
+
17
+ export type HandledRouter<TRouter extends Router<any>> = {
18
+ [K in keyof TRouter]: TRouter[K] extends Procedure<
19
+ infer UContext,
20
+ infer UExtraContext,
21
+ infer UInputSchema,
22
+ infer UOutputSchema,
23
+ infer UHandlerOutput
24
+ >
25
+ ? DecoratedProcedure<
26
+ UContext,
27
+ UExtraContext,
28
+ UInputSchema,
29
+ UOutputSchema,
30
+ UHandlerOutput
31
+ >
32
+ : TRouter[K] extends Router<any>
33
+ ? HandledRouter<TRouter[K]>
34
+ : never
35
+ }
36
+
37
+ export type RouterWithContract<
38
+ TContext extends Context,
39
+ TContract extends ContractRouter,
40
+ > = {
41
+ [K in keyof TContract]: TContract[K] extends ContractProcedure<
42
+ infer UInputSchema,
43
+ infer UOutputSchema
44
+ >
45
+ ? Procedure<TContext, any, UInputSchema, UOutputSchema, any>
46
+ : TContract[K] extends ContractRouter
47
+ ? RouterWithContract<TContext, TContract[K]>
48
+ : never
49
+ }
50
+
51
+ export function toContractRouter(
52
+ router: ContractRouter | Router<any>,
53
+ ): ContractRouter {
54
+ const contract: ContractRouter = {}
55
+
56
+ for (const key in router) {
57
+ const item = router[key]
58
+
59
+ if (isContractProcedure(item)) {
60
+ contract[key] = item
61
+ } else if (isProcedure(item)) {
62
+ contract[key] = item.zz$p.contract
63
+ } else {
64
+ contract[key] = toContractRouter(item as any)
65
+ }
66
+ }
67
+
68
+ return contract
69
+ }
@@ -0,0 +1,18 @@
1
+ import type { MergeContext } from './types'
2
+
3
+ test('MergeContext', () => {
4
+ expectTypeOf<MergeContext<undefined, undefined>>().toEqualTypeOf<undefined>()
5
+ expectTypeOf<MergeContext<undefined, { foo: string }>>().toEqualTypeOf<{
6
+ foo: string
7
+ }>()
8
+ expectTypeOf<MergeContext<{ foo: string }, undefined>>().toEqualTypeOf<{
9
+ foo: string
10
+ }>()
11
+ expectTypeOf<MergeContext<{ foo: string }, { foo: string }>>().toEqualTypeOf<{
12
+ foo: string
13
+ }>()
14
+ expectTypeOf<MergeContext<{ foo: string }, { bar: string }>>().toMatchTypeOf<{
15
+ foo: string
16
+ bar: string
17
+ }>()
18
+ })
package/src/types.ts ADDED
@@ -0,0 +1,28 @@
1
+ import type { WELL_DEFINED_PROCEDURE } from './procedure'
2
+
3
+ export type Context = Record<string, unknown> | undefined
4
+
5
+ export type MergeContext<
6
+ TA extends Context,
7
+ TB extends Context,
8
+ > = TA extends undefined ? TB : TB extends undefined ? TA : TA & TB
9
+
10
+ export interface Meta<T> extends Hooks<T> {
11
+ path: string[]
12
+ internal: boolean
13
+ procedure: WELL_DEFINED_PROCEDURE
14
+ }
15
+
16
+ export type Promisable<T> = T | Promise<T>
17
+
18
+ export interface UnsubscribeFn {
19
+ (): void
20
+ }
21
+
22
+ export interface Hooks<T> {
23
+ onSuccess: (fn: (output: T) => Promisable<void>) => UnsubscribeFn
24
+ onError: (fn: (error: unknown) => Promisable<void>) => UnsubscribeFn
25
+ onFinish: (
26
+ fn: (output: T | undefined, error: unknown | undefined) => Promisable<void>,
27
+ ) => UnsubscribeFn
28
+ }
@@ -0,0 +1,243 @@
1
+ import { hook, mergeContext } from './utils'
2
+
3
+ test('mergeContext', () => {
4
+ expect(mergeContext(undefined, undefined)).toBe(undefined)
5
+ expect(mergeContext(undefined, { foo: 'bar' })).toEqual({ foo: 'bar' })
6
+ expect(mergeContext({ foo: 'bar' }, undefined)).toEqual({ foo: 'bar' })
7
+ expect(mergeContext({ foo: 'bar' }, { foo: 'bar' })).toEqual({ foo: 'bar' })
8
+ expect(mergeContext({ foo: 'bar' }, { bar: 'bar' })).toEqual({
9
+ foo: 'bar',
10
+ bar: 'bar',
11
+ })
12
+ expect(mergeContext({ foo: 'bar' }, { bar: 'bar', foo: 'bar1' })).toEqual({
13
+ foo: 'bar1',
14
+ bar: 'bar',
15
+ })
16
+ })
17
+
18
+ describe('hook', async () => {
19
+ it('on success', async () => {
20
+ const onSuccess = vi.fn()
21
+ const onError = vi.fn()
22
+ const onFinish = vi.fn()
23
+
24
+ await hook(async (hooks) => {
25
+ hooks.onSuccess(onSuccess)
26
+ hooks.onError(onError)
27
+ hooks.onFinish(onFinish)
28
+
29
+ return 'foo'
30
+ })
31
+
32
+ expect(onSuccess).toHaveBeenCalledWith('foo')
33
+ expect(onError).not.toHaveBeenCalled()
34
+ expect(onFinish).toHaveBeenCalledWith('foo', undefined)
35
+ })
36
+
37
+ it('on failed', async () => {
38
+ const onSuccess = vi.fn()
39
+ const onError = vi.fn()
40
+ const onFinish = vi.fn()
41
+ const error = new Error('foo')
42
+
43
+ try {
44
+ await hook(async (hooks) => {
45
+ hooks.onSuccess(onSuccess)
46
+ hooks.onError(onError)
47
+ hooks.onFinish(onFinish)
48
+
49
+ throw error
50
+ })
51
+ } catch (e) {
52
+ expect(e).toBe(error)
53
+ }
54
+
55
+ expect(onSuccess).not.toHaveBeenCalled()
56
+ expect(onError).toHaveBeenCalledWith(error)
57
+ expect(onFinish).toHaveBeenCalledWith(undefined, error)
58
+ })
59
+
60
+ it('throw the last error', async () => {
61
+ const error = new Error('foo')
62
+ const error1 = new Error('1')
63
+ const error2 = new Error('2')
64
+
65
+ try {
66
+ await hook(async (hooks) => {
67
+ hooks.onSuccess(() => {
68
+ throw error
69
+ })
70
+ })
71
+ } catch (e) {
72
+ expect(e).toBe(error)
73
+ }
74
+
75
+ try {
76
+ await hook(async (hooks) => {
77
+ hooks.onError(() => {
78
+ throw error1
79
+ })
80
+ })
81
+ } catch (e) {
82
+ expect(e).toBe(error1)
83
+ }
84
+
85
+ try {
86
+ await hook(async (hooks) => {
87
+ hooks.onFinish(() => {
88
+ throw error2
89
+ })
90
+ })
91
+ } catch (e) {
92
+ expect(e).toBe(error2)
93
+ }
94
+
95
+ try {
96
+ await hook(async (hooks) => {
97
+ hooks.onError((e) => {
98
+ expect(e).toBe(error)
99
+ throw error1
100
+ })
101
+ hooks.onFinish((_, error) => {
102
+ expect(error).toBe(error1)
103
+ throw error2
104
+ })
105
+
106
+ throw error
107
+ })
108
+ } catch (e) {
109
+ expect(e).toBe(error2)
110
+ }
111
+ })
112
+
113
+ it('Fist In Last Out', async () => {
114
+ const ref = { value: 0 }
115
+ const hooked = hook(async (hooks) => {
116
+ hooks.onSuccess(() => {
117
+ expect(ref).toEqual({ value: 2 })
118
+ ref.value++
119
+ })
120
+ hooks.onSuccess(() => {
121
+ expect(ref).toEqual({ value: 1 })
122
+ ref.value++
123
+ })
124
+ hooks.onSuccess(() => {
125
+ expect(ref).toEqual({ value: 0 })
126
+ ref.value++
127
+ })
128
+
129
+ hooks.onFinish(() => {
130
+ expect(ref).toEqual({ value: 5 })
131
+ ref.value++
132
+ })
133
+ hooks.onFinish(() => {
134
+ expect(ref).toEqual({ value: 4 })
135
+ ref.value++
136
+ })
137
+ hooks.onFinish(() => {
138
+ expect(ref).toEqual({ value: 3 })
139
+ ref.value++
140
+ })
141
+ })
142
+
143
+ await expect(hooked).resolves.toEqual(undefined)
144
+ expect(ref).toEqual({ value: 6 })
145
+ })
146
+
147
+ it('Fist In Last Out - onError', async () => {
148
+ const ref = { value: 0 }
149
+ const hooked = hook(async (hooks) => {
150
+ hooks.onError(() => {
151
+ expect(ref).toEqual({ value: 2 })
152
+ ref.value++
153
+ })
154
+ hooks.onError(() => {
155
+ expect(ref).toEqual({ value: 1 })
156
+ ref.value++
157
+ })
158
+ hooks.onError(() => {
159
+ expect(ref).toEqual({ value: 0 })
160
+ ref.value++
161
+ })
162
+
163
+ hooks.onFinish(() => {
164
+ expect(ref).toEqual({ value: 5 })
165
+ ref.value++
166
+ })
167
+ hooks.onFinish(() => {
168
+ expect(ref).toEqual({ value: 4 })
169
+ ref.value++
170
+ })
171
+ hooks.onFinish(() => {
172
+ expect(ref).toEqual({ value: 3 })
173
+ ref.value++
174
+ })
175
+
176
+ throw new Error('foo')
177
+ })
178
+
179
+ await expect(hooked).rejects.toThrow('foo')
180
+ expect(ref).toEqual({ value: 6 })
181
+ })
182
+
183
+ it('ensure run every onSuccess and onFinish even on throw many times', async () => {
184
+ const error = new Error('foo')
185
+ const error1 = new Error('1')
186
+ const error2 = new Error('2')
187
+
188
+ const onError = vi.fn(() => {
189
+ throw error
190
+ })
191
+ const onFinish = vi.fn(() => {
192
+ throw error1
193
+ })
194
+
195
+ try {
196
+ await hook(async (hooks) => {
197
+ hooks.onError(onError)
198
+ hooks.onError(onError)
199
+ hooks.onError(onError)
200
+ hooks.onFinish(onFinish)
201
+ hooks.onFinish(onFinish)
202
+ hooks.onFinish(onFinish)
203
+
204
+ throw error2
205
+ })
206
+ } catch (e) {
207
+ expect(e).toBe(error1)
208
+ }
209
+
210
+ expect(onError).toHaveBeenCalledTimes(3)
211
+ expect(onFinish).toHaveBeenCalledTimes(3)
212
+ })
213
+
214
+ it('can unsubscribe', async () => {
215
+ const onSuccess = vi.fn()
216
+ const onError = vi.fn()
217
+ const onFinish = vi.fn()
218
+
219
+ await hook(async (hooks) => {
220
+ hooks.onSuccess(onSuccess)()
221
+ hooks.onSuccess(onSuccess)()
222
+ hooks.onError(onError)()
223
+ hooks.onFinish(onFinish)()
224
+
225
+ return 'foo'
226
+ })
227
+
228
+ await expect(
229
+ hook(async (hooks) => {
230
+ hooks.onSuccess(onSuccess)()
231
+ hooks.onSuccess(onSuccess)()
232
+ hooks.onError(onError)()
233
+ hooks.onFinish(onFinish)()
234
+
235
+ throw new Error('foo')
236
+ }),
237
+ ).rejects.toThrow('foo')
238
+
239
+ expect(onSuccess).not.toHaveBeenCalled()
240
+ expect(onError).not.toHaveBeenCalled()
241
+ expect(onFinish).not.toHaveBeenCalled()
242
+ })
243
+ })
package/src/utils.ts ADDED
@@ -0,0 +1,95 @@
1
+ import type { Context, Hooks, MergeContext, Promisable } from './types'
2
+
3
+ export function mergeContext<A extends Context, B extends Context>(
4
+ a: A,
5
+ b: B,
6
+ ): MergeContext<A, B> {
7
+ if (!a) return b as any
8
+ if (!b) return a as any
9
+
10
+ return {
11
+ ...a,
12
+ ...b,
13
+ } as any
14
+ }
15
+
16
+ export async function hook<T>(
17
+ fn: (hooks: Hooks<T>) => Promisable<T>,
18
+ ): Promise<T> {
19
+ const onSuccessFns: ((output: T) => Promisable<void>)[] = []
20
+ const onErrorFns: ((error: unknown) => Promisable<void>)[] = []
21
+ const onFinishFns: ((
22
+ output: T | undefined,
23
+ error: unknown | undefined,
24
+ ) => Promisable<void>)[] = []
25
+
26
+ const hooks: Hooks<T> = {
27
+ onSuccess(fn) {
28
+ onSuccessFns.unshift(fn)
29
+
30
+ return () => {
31
+ const index = onSuccessFns.indexOf(fn)
32
+ if (index !== -1) onSuccessFns.splice(index, 1)
33
+ }
34
+ },
35
+
36
+ onError(fn) {
37
+ onErrorFns.unshift(fn)
38
+
39
+ return () => {
40
+ const index = onErrorFns.indexOf(fn)
41
+ if (index !== -1) onErrorFns.splice(index, 1)
42
+ }
43
+ },
44
+
45
+ onFinish(fn) {
46
+ onFinishFns.unshift(fn)
47
+
48
+ return () => {
49
+ const index = onFinishFns.indexOf(fn)
50
+ if (index !== -1) onFinishFns.splice(index, 1)
51
+ }
52
+ },
53
+ }
54
+
55
+ let error: unknown = undefined
56
+ let output: T | undefined = undefined
57
+
58
+ try {
59
+ output = await fn(hooks)
60
+
61
+ for (const onSuccessFn of onSuccessFns) {
62
+ await onSuccessFn(output)
63
+ }
64
+
65
+ return output
66
+ } catch (e) {
67
+ error = e
68
+
69
+ for (const onErrorFn of onErrorFns) {
70
+ try {
71
+ await onErrorFn(error)
72
+ } catch (e) {
73
+ error = e
74
+ }
75
+ }
76
+
77
+ throw error
78
+ } finally {
79
+ let hasNewError = false
80
+
81
+ for (const onFinishFn of onFinishFns) {
82
+ try {
83
+ await onFinishFn(output, error)
84
+ } catch (e) {
85
+ error = e
86
+ hasNewError = true
87
+ }
88
+ }
89
+
90
+ if (hasNewError) {
91
+ // biome-ignore lint/correctness/noUnsafeFinally: this behavior is expected
92
+ throw error
93
+ }
94
+ }
95
+ }