ts-procedures 2.1.1 → 3.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.
- package/build/errors.d.ts +2 -1
- package/build/errors.js +3 -2
- package/build/errors.js.map +1 -1
- package/build/errors.test.d.ts +1 -0
- package/build/errors.test.js +40 -0
- package/build/errors.test.js.map +1 -0
- package/build/implementations/http/express-rpc/index.d.ts +3 -2
- package/build/implementations/http/express-rpc/index.js +6 -6
- package/build/implementations/http/express-rpc/index.js.map +1 -1
- package/build/implementations/http/express-rpc/index.test.js +93 -93
- package/build/implementations/http/express-rpc/index.test.js.map +1 -1
- package/build/implementations/http/hono-rpc/index.d.ts +83 -0
- package/build/implementations/http/hono-rpc/index.js +148 -0
- package/build/implementations/http/hono-rpc/index.js.map +1 -0
- package/build/implementations/http/hono-rpc/index.test.d.ts +1 -0
- package/build/implementations/http/hono-rpc/index.test.js +647 -0
- package/build/implementations/http/hono-rpc/index.test.js.map +1 -0
- package/build/implementations/http/hono-rpc/types.d.ts +28 -0
- package/build/implementations/http/hono-rpc/types.js +2 -0
- package/build/implementations/http/hono-rpc/types.js.map +1 -0
- package/build/implementations/types.d.ts +1 -1
- package/build/index.d.ts +12 -0
- package/build/index.js +29 -7
- package/build/index.js.map +1 -1
- package/build/index.test.js +65 -0
- package/build/index.test.js.map +1 -1
- package/build/schema/parser.js +3 -0
- package/build/schema/parser.js.map +1 -1
- package/build/schema/parser.test.js +18 -0
- package/build/schema/parser.test.js.map +1 -1
- package/package.json +8 -2
- package/src/errors.test.ts +53 -0
- package/src/errors.ts +4 -2
- package/src/implementations/http/README.md +172 -0
- package/src/implementations/http/express-rpc/README.md +152 -243
- package/src/implementations/http/express-rpc/index.test.ts +93 -93
- package/src/implementations/http/express-rpc/index.ts +15 -7
- package/src/implementations/http/hono-rpc/README.md +293 -0
- package/src/implementations/http/hono-rpc/index.test.ts +847 -0
- package/src/implementations/http/hono-rpc/index.ts +202 -0
- package/src/implementations/http/hono-rpc/types.ts +33 -0
- package/src/implementations/types.ts +2 -1
- package/src/index.test.ts +83 -0
- package/src/index.ts +34 -8
- package/src/schema/parser.test.ts +26 -0
- 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/implementations/http/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/implementations/http/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/implementations/http/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
|
+
```
|