ts-procedures 2.1.1 → 3.0.1

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 (46) hide show
  1. package/build/errors.d.ts +2 -1
  2. package/build/errors.js +3 -2
  3. package/build/errors.js.map +1 -1
  4. package/build/errors.test.d.ts +1 -0
  5. package/build/errors.test.js +40 -0
  6. package/build/errors.test.js.map +1 -0
  7. package/build/implementations/http/express-rpc/index.d.ts +3 -2
  8. package/build/implementations/http/express-rpc/index.js +6 -6
  9. package/build/implementations/http/express-rpc/index.js.map +1 -1
  10. package/build/implementations/http/express-rpc/index.test.js +93 -93
  11. package/build/implementations/http/express-rpc/index.test.js.map +1 -1
  12. package/build/implementations/http/hono-rpc/index.d.ts +83 -0
  13. package/build/implementations/http/hono-rpc/index.js +148 -0
  14. package/build/implementations/http/hono-rpc/index.js.map +1 -0
  15. package/build/implementations/http/hono-rpc/index.test.d.ts +1 -0
  16. package/build/implementations/http/hono-rpc/index.test.js +647 -0
  17. package/build/implementations/http/hono-rpc/index.test.js.map +1 -0
  18. package/build/implementations/http/hono-rpc/types.d.ts +28 -0
  19. package/build/implementations/http/hono-rpc/types.js +2 -0
  20. package/build/implementations/http/hono-rpc/types.js.map +1 -0
  21. package/build/implementations/types.d.ts +1 -1
  22. package/build/index.d.ts +12 -0
  23. package/build/index.js +29 -7
  24. package/build/index.js.map +1 -1
  25. package/build/index.test.js +65 -0
  26. package/build/index.test.js.map +1 -1
  27. package/build/schema/parser.js +3 -0
  28. package/build/schema/parser.js.map +1 -1
  29. package/build/schema/parser.test.js +18 -0
  30. package/build/schema/parser.test.js.map +1 -1
  31. package/package.json +10 -5
  32. package/src/errors.test.ts +53 -0
  33. package/src/errors.ts +4 -2
  34. package/src/implementations/http/README.md +172 -0
  35. package/src/implementations/http/express-rpc/README.md +151 -242
  36. package/src/implementations/http/express-rpc/index.test.ts +93 -93
  37. package/src/implementations/http/express-rpc/index.ts +15 -7
  38. package/src/implementations/http/hono-rpc/README.md +293 -0
  39. package/src/implementations/http/hono-rpc/index.test.ts +847 -0
  40. package/src/implementations/http/hono-rpc/index.ts +202 -0
  41. package/src/implementations/http/hono-rpc/types.ts +33 -0
  42. package/src/implementations/types.ts +2 -1
  43. package/src/index.test.ts +83 -0
  44. package/src/index.ts +34 -8
  45. package/src/schema/parser.test.ts +26 -0
  46. package/src/schema/parser.ts +5 -1
@@ -0,0 +1,293 @@
1
+ # HonoRPCAppBuilder
2
+
3
+ Hono integration for `ts-procedures` that creates type-safe RPC endpoints as POST routes. Works with Bun, Deno, Cloudflare Workers, and Node.js.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install ts-procedures hono
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import { Hono } from 'hono'
15
+ import { Procedures } from 'ts-procedures'
16
+ import { HonoRPCAppBuilder, RPCConfig } from 'ts-procedures/hono-rpc'
17
+ import { v } from 'suretype'
18
+
19
+ // Define your context type
20
+ type AppContext = { userId: string }
21
+
22
+ // Create a procedure factory
23
+ const RPC = Procedures<AppContext, RPCConfig>()
24
+
25
+ // Define procedures
26
+ RPC.Create(
27
+ 'GetUser',
28
+ {
29
+ scope: ['users', 'profile'],
30
+ version: 1,
31
+ schema: {
32
+ params: v.object({ id: v.string() }),
33
+ returnType: v.object({ id: v.string(), name: v.string() })
34
+ }
35
+ },
36
+ async (ctx, params) => {
37
+ return { id: params.id, name: 'John Doe' }
38
+ }
39
+ )
40
+
41
+ // Build the Hono app
42
+ const builder = new HonoRPCAppBuilder({ pathPrefix: '/rpc' })
43
+ .register(RPC, (c) => ({
44
+ userId: c.req.header('x-user-id') || 'anonymous'
45
+ }))
46
+
47
+ const app = builder.build()
48
+
49
+ // Bun
50
+ export default app
51
+
52
+ // Node.js
53
+ // import { serve } from '@hono/node-server'
54
+ // serve(app)
55
+
56
+ // POST /rpc/users/profile/get-user/1 → { id: "123", name: "John Doe" }
57
+ ```
58
+
59
+ ## Configuration
60
+
61
+ ```typescript
62
+ type HonoRPCAppBuilderConfig = {
63
+ app?: Hono // Existing Hono app (optional)
64
+ pathPrefix?: string // Prefix for all routes (e.g., '/rpc/v1')
65
+ onRequestStart?: (c: Context) => void
66
+ onRequestEnd?: (c: Context) => void
67
+ onSuccess?: (procedure: TProcedureRegistration, c: Context) => void
68
+ error?: (procedure: TProcedureRegistration, c: Context, error: Error) => Response | Promise<Response>
69
+ }
70
+ ```
71
+
72
+ | Option | Type | Description |
73
+ |--------|------|---------------------------------------------------|
74
+ | `app` | `Hono` | Use existing Hono app instead of creating new one |
75
+ | `pathPrefix` | `string` | Prefix all routes (e.g., `/rpc/v1`) |
76
+ | `onRequestStart` | `(c) => void` | Called at start of each request |
77
+ | `onRequestEnd` | `(c) => void` | Called after handler completes |
78
+ | `onSuccess` | `(proc, c) => void` | Called on successful handler execution |
79
+ | `error` | `(proc, c, err) => Response` | Custom error handler (must return Response) |
80
+
81
+ ## Context Resolution
82
+
83
+ The context resolver receives the Hono `Context` object:
84
+
85
+ ```typescript
86
+ builder.register(RPC, (c: Context) => ({
87
+ userId: c.req.header('x-user-id') || 'anonymous',
88
+ userAgent: c.req.header('user-agent'),
89
+ ip: c.req.raw.headers.get('cf-connecting-ip') // Cloudflare
90
+ }))
91
+
92
+ // Async context resolution
93
+ builder.register(RPC, async (c) => {
94
+ const token = c.req.header('authorization')?.replace('Bearer ', '')
95
+ const user = await verifyToken(token)
96
+ return { userId: user.id, roles: user.roles }
97
+ })
98
+ ```
99
+
100
+ ## Error Handling
101
+
102
+ Custom error handler receives the procedure, context, and error. **Must return a Response:**
103
+
104
+ ```typescript
105
+ const builder = new HonoRPCAppBuilder({
106
+ error: (procedure, c, error) => {
107
+ console.error(`Error in ${procedure.name}:`, error)
108
+
109
+ if (error instanceof ValidationError) {
110
+ return c.json({ error: error.message, code: 'VALIDATION_ERROR' }, 400)
111
+ }
112
+
113
+ if (error instanceof AuthError) {
114
+ return c.json({ error: 'Unauthorized', code: 'AUTH_ERROR' }, 401)
115
+ }
116
+
117
+ return c.json({ error: 'Internal server error' }, 500)
118
+ }
119
+ })
120
+ ```
121
+
122
+ **Default error handling:** Returns `{ error: message }` with status 500.
123
+
124
+ ## Using Existing Hono App
125
+
126
+ You can add RPC routes to an existing Hono application:
127
+
128
+ ```typescript
129
+ const app = new Hono()
130
+
131
+ // Add custom middleware and routes
132
+ app.use('*', cors())
133
+ app.get('/custom', (c) => c.json({ custom: true }))
134
+
135
+ const builder = new HonoRPCAppBuilder({ app })
136
+ .register(RPC, contextResolver)
137
+
138
+ builder.build() // Adds RPC routes to existing app
139
+ ```
140
+
141
+ ## Runtime Compatibility
142
+
143
+ HonoRPCAppBuilder works across all Hono-supported runtimes:
144
+
145
+ ### Bun
146
+
147
+ ```typescript
148
+ const app = builder.build()
149
+ export default app
150
+ ```
151
+
152
+ ### Node.js
153
+
154
+ ```typescript
155
+ import { serve } from '@hono/node-server'
156
+
157
+ const app = builder.build()
158
+ serve(app)
159
+ ```
160
+
161
+ ### Deno
162
+
163
+ ```typescript
164
+ import { serve } from 'https://deno.land/std/http/server.ts'
165
+
166
+ const app = builder.build()
167
+ serve(app.fetch)
168
+ ```
169
+
170
+ ### Cloudflare Workers
171
+
172
+ ```typescript
173
+ const app = builder.build()
174
+ export default app
175
+ ```
176
+
177
+ ## API Reference
178
+
179
+ ### Constructor
180
+
181
+ ```typescript
182
+ new HonoRPCAppBuilder(config?: HonoRPCAppBuilderConfig)
183
+ ```
184
+
185
+ ### Methods
186
+
187
+ | Method | Signature | Description |
188
+ |--------|-----------|-------------|
189
+ | `register` | `register<T>(factory, context): this` | Register procedure factory with context |
190
+ | `build` | `build(): Hono` | Build routes and return app |
191
+ | `makeRPCHttpRoutePath` | `makeRPCHttpRoutePath(config: RPCConfig): string` | Generate route path |
192
+
193
+ ### Static Methods
194
+
195
+ | Method | Signature | Description |
196
+ |--------|-----------|-------------|
197
+ | `makeRPCHttpRoutePath` | `static makeRPCHttpRoutePath({ config, prefix }): string` | Generate route path with custom prefix |
198
+
199
+ ### Properties
200
+
201
+ | Property | Type | Description |
202
+ |----------|------|-------------|
203
+ | `app` | `Hono` | The Hono application instance |
204
+ | `docs` | `RPCHttpRouteDoc[]` | Route documentation (after `build()`) |
205
+ | `config` | `HonoRPCAppBuilderConfig` | The configuration object |
206
+
207
+ ## TypeScript Types
208
+
209
+ ```typescript
210
+ import {
211
+ HonoRPCAppBuilder,
212
+ HonoRPCAppBuilderConfig,
213
+ RPCConfig,
214
+ RPCHttpRouteDoc
215
+ } from 'ts-procedures/hono-rpc'
216
+ ```
217
+
218
+ ## Full Example
219
+
220
+ ```typescript
221
+ import { Hono, Context } from 'hono'
222
+ import { Procedures } from 'ts-procedures'
223
+ import { HonoRPCAppBuilder, RPCConfig } from 'ts-procedures/hono-rpc'
224
+ import { v } from 'suretype'
225
+
226
+ // Context types
227
+ type PublicContext = { source: 'public' }
228
+ type AuthContext = { source: 'auth'; userId: string }
229
+
230
+ // Create factories
231
+ const PublicRPC = Procedures<PublicContext, RPCConfig>()
232
+ const AuthRPC = Procedures<AuthContext, RPCConfig>()
233
+
234
+ // Public procedures
235
+ PublicRPC.Create('HealthCheck', { scope: 'health', version: 1 }, async () => ({
236
+ status: 'ok'
237
+ }))
238
+
239
+ PublicRPC.Create('GetVersion', { scope: ['system', 'version'], version: 1 }, async () => ({
240
+ version: '1.0.0'
241
+ }))
242
+
243
+ // Authenticated procedures
244
+ AuthRPC.Create(
245
+ 'GetProfile',
246
+ {
247
+ scope: ['users', 'profile'],
248
+ version: 1,
249
+ schema: { returnType: v.object({ userId: v.string() }) }
250
+ },
251
+ async (ctx) => ({ userId: ctx.userId })
252
+ )
253
+
254
+ AuthRPC.Create(
255
+ 'UpdateProfile',
256
+ {
257
+ scope: ['users', 'profile'],
258
+ version: 2,
259
+ schema: { params: v.object({ name: v.string() }) }
260
+ },
261
+ async (ctx, params) => ({ userId: ctx.userId, name: params.name })
262
+ )
263
+
264
+ // Build app
265
+ const builder = new HonoRPCAppBuilder({
266
+ pathPrefix: '/rpc',
267
+ onRequestStart: (c) => console.log(`→ ${c.req.method} ${c.req.path}`),
268
+ onRequestEnd: (c) => console.log(`← completed`),
269
+ onSuccess: (proc) => console.log(`✓ ${proc.name}`),
270
+ error: (proc, c, err) => {
271
+ console.error(`✗ ${proc.name}:`, err.message)
272
+ return c.json({ error: err.message }, 500)
273
+ }
274
+ })
275
+
276
+ builder
277
+ .register(PublicRPC, () => ({ source: 'public' as const }))
278
+ .register(AuthRPC, (c) => ({
279
+ source: 'auth' as const,
280
+ userId: c.req.header('x-user-id') || 'anonymous'
281
+ }))
282
+
283
+ const app = builder.build()
284
+
285
+ // Generated routes:
286
+ // POST /rpc/health/1
287
+ // POST /rpc/system/version/get-version/1
288
+ // POST /rpc/users/profile/get-user/1
289
+ // POST /rpc/users/profile/get-user/2
290
+
291
+ console.log('Routes:', builder.docs.map(d => d.path))
292
+ export default app
293
+ ```