ts-procedures 2.1.0 → 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.
Files changed (70) 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.js +40 -0
  5. package/build/errors.test.js.map +1 -0
  6. package/build/implementations/http/express-rpc/index.d.ts +36 -35
  7. package/build/implementations/http/express-rpc/index.js +29 -13
  8. package/build/implementations/http/express-rpc/index.js.map +1 -1
  9. package/build/implementations/http/express-rpc/index.test.js +146 -92
  10. package/build/implementations/http/express-rpc/index.test.js.map +1 -1
  11. package/build/implementations/http/hono-rpc/index.d.ts +83 -0
  12. package/build/implementations/http/hono-rpc/index.js +148 -0
  13. package/build/implementations/http/hono-rpc/index.js.map +1 -0
  14. package/build/implementations/http/hono-rpc/index.test.js +647 -0
  15. package/build/implementations/http/hono-rpc/index.test.js.map +1 -0
  16. package/build/implementations/http/hono-rpc/types.d.ts +28 -0
  17. package/build/implementations/http/hono-rpc/types.js.map +1 -0
  18. package/build/implementations/types.d.ts +1 -1
  19. package/build/index.d.ts +12 -0
  20. package/build/index.js +29 -7
  21. package/build/index.js.map +1 -1
  22. package/build/index.test.js +65 -0
  23. package/build/index.test.js.map +1 -1
  24. package/build/schema/parser.js +3 -0
  25. package/build/schema/parser.js.map +1 -1
  26. package/build/schema/parser.test.js +18 -0
  27. package/build/schema/parser.test.js.map +1 -1
  28. package/package.json +8 -2
  29. package/src/errors.test.ts +53 -0
  30. package/src/errors.ts +4 -2
  31. package/src/implementations/http/README.md +172 -0
  32. package/src/implementations/http/express-rpc/README.md +151 -228
  33. package/src/implementations/http/express-rpc/index.test.ts +167 -93
  34. package/src/implementations/http/express-rpc/index.ts +67 -38
  35. package/src/implementations/http/hono-rpc/README.md +293 -0
  36. package/src/implementations/http/hono-rpc/index.test.ts +847 -0
  37. package/src/implementations/http/hono-rpc/index.ts +202 -0
  38. package/src/implementations/http/hono-rpc/types.ts +33 -0
  39. package/src/implementations/types.ts +2 -1
  40. package/src/index.test.ts +83 -0
  41. package/src/index.ts +34 -8
  42. package/src/schema/parser.test.ts +26 -0
  43. package/src/schema/parser.ts +5 -1
  44. package/build/implementations/http/client/index.js +0 -2
  45. package/build/implementations/http/client/index.js.map +0 -1
  46. package/build/implementations/http/express/example/factories.d.ts +0 -97
  47. package/build/implementations/http/express/example/factories.js +0 -4
  48. package/build/implementations/http/express/example/factories.js.map +0 -1
  49. package/build/implementations/http/express/example/procedures/auth.d.ts +0 -1
  50. package/build/implementations/http/express/example/procedures/auth.js +0 -22
  51. package/build/implementations/http/express/example/procedures/auth.js.map +0 -1
  52. package/build/implementations/http/express/example/procedures/users.d.ts +0 -1
  53. package/build/implementations/http/express/example/procedures/users.js +0 -30
  54. package/build/implementations/http/express/example/procedures/users.js.map +0 -1
  55. package/build/implementations/http/express/example/server.d.ts +0 -3
  56. package/build/implementations/http/express/example/server.js +0 -49
  57. package/build/implementations/http/express/example/server.js.map +0 -1
  58. package/build/implementations/http/express/example/server.test.d.ts +0 -1
  59. package/build/implementations/http/express/example/server.test.js +0 -110
  60. package/build/implementations/http/express/example/server.test.js.map +0 -1
  61. package/build/implementations/http/express/index.d.ts +0 -35
  62. package/build/implementations/http/express/index.js +0 -75
  63. package/build/implementations/http/express/index.js.map +0 -1
  64. package/build/implementations/http/express/index.test.js +0 -329
  65. package/build/implementations/http/express/index.test.js.map +0 -1
  66. package/build/implementations/http/express/types.d.ts +0 -17
  67. package/build/implementations/http/express/types.js.map +0 -1
  68. /package/build/{implementations/http/client/index.d.ts → errors.test.d.ts} +0 -0
  69. /package/build/implementations/http/{express → hono-rpc}/index.test.d.ts +0 -0
  70. /package/build/implementations/http/{express → hono-rpc}/types.js +0 -0
@@ -6,6 +6,31 @@ import { castArray } from 'es-toolkit/compat'
6
6
  import { ExpressFactoryItem, ExtractContext, ProceduresFactory } from './types.js'
7
7
 
8
8
  export type { RPCConfig, RPCHttpRouteDoc }
9
+
10
+ export type ExpressRPCAppBuilderConfig = {
11
+ /**
12
+ * An existing Express application instance to use.
13
+ * When provided, ensure to set up necessary middleware (e.g., json/body parser) beforehand.
14
+ * If not provided, a new instance will be created.
15
+ */
16
+ app?: express.Express
17
+ /** Optional path prefix for all RPC routes. */
18
+ pathPrefix?: string
19
+ onRequestStart?: (req: express.Request) => void
20
+ onRequestEnd?: (req: express.Request, res: express.Response) => void
21
+ onSuccess?: (
22
+ procedure: TProcedureRegistration,
23
+ req: express.Request,
24
+ res: express.Response
25
+ ) => void
26
+ error?: (
27
+ procedure: TProcedureRegistration,
28
+ req: express.Request,
29
+ res: express.Response,
30
+ error: Error
31
+ ) => void
32
+ }
33
+
9
34
  /**
10
35
  * Builder class for creating an Express application with RPC routes.
11
36
  *
@@ -27,29 +52,7 @@ export class ExpressRPCAppBuilder {
27
52
  *
28
53
  * @param config
29
54
  */
30
- constructor(
31
- readonly config?: {
32
- /**
33
- * An existing Express application instance to use.
34
- * When provided, ensure to set up necessary middleware (e.g., json/body parser) beforehand.
35
- * If not provided, a new instance will be created.
36
- */
37
- app?: express.Express
38
- onRequestStart?: (req: express.Request) => void
39
- onRequestEnd?: (req: express.Request, res: express.Response) => void
40
- onSuccess?: (
41
- procedure: TProcedureRegistration,
42
- req: express.Request,
43
- res: express.Response
44
- ) => void
45
- error?: (
46
- procedure: TProcedureRegistration,
47
- req: express.Request,
48
- res: express.Response,
49
- error: Error
50
- ) => void
51
- }
52
- ) {
55
+ constructor(readonly config?: ExpressRPCAppBuilderConfig) {
53
56
  if (config?.app) {
54
57
  this._app = config.app
55
58
  } else {
@@ -74,6 +77,41 @@ export class ExpressRPCAppBuilder {
74
77
  }
75
78
  }
76
79
 
80
+ /**
81
+ * Generates the RPC route path based on the RPC configuration.
82
+ * The RPCConfig name can be a string or an array of strings to form nested paths.
83
+ *
84
+ * Example
85
+ * name: ['string', 'string-string', 'string']
86
+ * path: /string/string-string/string/version
87
+ * @param config
88
+ */
89
+ static makeRPCHttpRoutePath({
90
+ name,
91
+ config,
92
+ prefix,
93
+ }: {
94
+ name: string
95
+ prefix?: string
96
+ config: RPCConfig
97
+ }) {
98
+ const normalizedPrefix = prefix ? (prefix.startsWith('/') ? prefix : `/${prefix}`) : ''
99
+
100
+ return `${normalizedPrefix}/${castArray(config.scope).map(kebabCase).join('/')}/${kebabCase(name)}/${String(config.version).trim()}`
101
+ }
102
+
103
+ /**
104
+ * Instance method wrapper for makeRPCHttpRoutePath that uses the builder's pathPrefix.
105
+ * @param config - The RPC configuration
106
+ */
107
+ makeRPCHttpRoutePath(name: string, config: RPCConfig): string {
108
+ return ExpressRPCAppBuilder.makeRPCHttpRoutePath({
109
+ name,
110
+ config,
111
+ prefix: this.config?.pathPrefix,
112
+ })
113
+ }
114
+
77
115
  private factories: ExpressFactoryItem<any>[] = []
78
116
 
79
117
  private _app: express.Express = express()
@@ -153,9 +191,13 @@ export class ExpressRPCAppBuilder {
153
191
  * Generates the RPC HTTP route for the given procedure.
154
192
  * @param procedure
155
193
  */
156
- buildRpcHttpRouteDoc(procedure: TProcedureRegistration<any, RPCConfig>): RPCHttpRouteDoc {
194
+ private buildRpcHttpRouteDoc(procedure: TProcedureRegistration<any, RPCConfig>): RPCHttpRouteDoc {
157
195
  const { config } = procedure
158
- const path = this.makeRPCHttpRoutePath(config)
196
+ const path = ExpressRPCAppBuilder.makeRPCHttpRoutePath({
197
+ name: procedure.name,
198
+ config,
199
+ prefix: this.config?.pathPrefix,
200
+ })
159
201
  const method = 'post' // RPCs use POST method
160
202
  const jsonSchema: { body?: object; response?: object } = {}
161
203
 
@@ -172,17 +214,4 @@ export class ExpressRPCAppBuilder {
172
214
  jsonSchema,
173
215
  }
174
216
  }
175
-
176
- /**
177
- * Generates the RPC route path based on the RPC configuration.
178
- * The RPCConfig name can be a string or an array of strings to form nested paths.
179
- *
180
- * Example
181
- * name: ['string', 'string-string', 'string']
182
- * path: /rpc/string/string-string/string/version
183
- * @param config
184
- */
185
- makeRPCHttpRoutePath(config: RPCConfig) {
186
- return `/rpc/${castArray(config.name).map(kebabCase).join('/')}/${String(config.version).trim()}`
187
- }
188
217
  }
@@ -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
+ ```