ts-procedures 3.0.1 → 3.1.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/implementations/http/express-rpc/index.d.ts +14 -4
- package/build/implementations/http/express-rpc/index.js +20 -8
- package/build/implementations/http/express-rpc/index.js.map +1 -1
- package/build/implementations/http/express-rpc/index.test.js +162 -3
- package/build/implementations/http/express-rpc/index.test.js.map +1 -1
- package/build/implementations/http/express-rpc/types.d.ts +6 -23
- package/build/implementations/http/hono-rpc/index.d.ts +13 -4
- package/build/implementations/http/hono-rpc/index.js +20 -8
- package/build/implementations/http/hono-rpc/index.js.map +1 -1
- package/build/implementations/http/hono-rpc/index.test.js +158 -2
- package/build/implementations/http/hono-rpc/index.test.js.map +1 -1
- package/build/implementations/http/hono-rpc/types.d.ts +6 -23
- package/build/implementations/types.d.ts +32 -1
- package/package.json +1 -1
- package/src/implementations/http/express-rpc/README.md +2 -2
- package/src/implementations/http/express-rpc/index.test.ts +234 -3
- package/src/implementations/http/express-rpc/index.ts +49 -13
- package/src/implementations/http/express-rpc/types.ts +8 -25
- package/src/implementations/http/hono-rpc/README.md +84 -44
- package/src/implementations/http/hono-rpc/index.test.ts +230 -2
- package/src/implementations/http/hono-rpc/index.ts +49 -14
- package/src/implementations/http/hono-rpc/types.ts +8 -25
- package/src/implementations/types.ts +39 -1
|
@@ -30,8 +30,8 @@ RPC.Create(
|
|
|
30
30
|
version: 1,
|
|
31
31
|
schema: {
|
|
32
32
|
params: v.object({ id: v.string() }),
|
|
33
|
-
returnType: v.object({ id: v.string(), name: v.string() })
|
|
34
|
-
}
|
|
33
|
+
returnType: v.object({ id: v.string(), name: v.string() }),
|
|
34
|
+
},
|
|
35
35
|
},
|
|
36
36
|
async (ctx, params) => {
|
|
37
37
|
return { id: params.id, name: 'John Doe' }
|
|
@@ -39,10 +39,9 @@ RPC.Create(
|
|
|
39
39
|
)
|
|
40
40
|
|
|
41
41
|
// Build the Hono app
|
|
42
|
-
const builder = new HonoRPCAppBuilder({ pathPrefix: '/rpc' })
|
|
43
|
-
.
|
|
44
|
-
|
|
45
|
-
}))
|
|
42
|
+
const builder = new HonoRPCAppBuilder({ pathPrefix: '/rpc' }).register(RPC, (c) => ({
|
|
43
|
+
userId: c.req.header('x-user-id') || 'anonymous',
|
|
44
|
+
}))
|
|
46
45
|
|
|
47
46
|
const app = builder.build()
|
|
48
47
|
|
|
@@ -60,23 +59,27 @@ export default app
|
|
|
60
59
|
|
|
61
60
|
```typescript
|
|
62
61
|
type HonoRPCAppBuilderConfig = {
|
|
63
|
-
app?: Hono
|
|
64
|
-
pathPrefix?: string
|
|
62
|
+
app?: Hono // Existing Hono app (optional)
|
|
63
|
+
pathPrefix?: string // Prefix for all routes (e.g., '/rpc/v1')
|
|
65
64
|
onRequestStart?: (c: Context) => void
|
|
66
65
|
onRequestEnd?: (c: Context) => void
|
|
67
66
|
onSuccess?: (procedure: TProcedureRegistration, c: Context) => void
|
|
68
|
-
error?: (
|
|
67
|
+
error?: (
|
|
68
|
+
procedure: TProcedureRegistration,
|
|
69
|
+
c: Context,
|
|
70
|
+
error: Error
|
|
71
|
+
) => Response | Promise<Response>
|
|
69
72
|
}
|
|
70
73
|
```
|
|
71
74
|
|
|
72
|
-
| Option
|
|
73
|
-
|
|
74
|
-
| `app`
|
|
75
|
-
| `pathPrefix`
|
|
76
|
-
| `onRequestStart` | `(c) => void`
|
|
77
|
-
| `onRequestEnd`
|
|
78
|
-
| `onSuccess`
|
|
79
|
-
| `error`
|
|
75
|
+
| Option | Type | Description |
|
|
76
|
+
| ---------------- | ---------------------------- | ------------------------------------------------- |
|
|
77
|
+
| `app` | `Hono` | Use existing Hono app instead of creating new one |
|
|
78
|
+
| `pathPrefix` | `string` | Prefix all routes (e.g., `/rpc/v1`) |
|
|
79
|
+
| `onRequestStart` | `(c) => void` | Called at start of each request |
|
|
80
|
+
| `onRequestEnd` | `(c) => void` | Called after handler completes |
|
|
81
|
+
| `onSuccess` | `(proc, c) => void` | Called on successful handler execution |
|
|
82
|
+
| `error` | `(proc, c, err) => Response` | Custom error handler (must return Response) |
|
|
80
83
|
|
|
81
84
|
## Context Resolution
|
|
82
85
|
|
|
@@ -86,7 +89,7 @@ The context resolver receives the Hono `Context` object:
|
|
|
86
89
|
builder.register(RPC, (c: Context) => ({
|
|
87
90
|
userId: c.req.header('x-user-id') || 'anonymous',
|
|
88
91
|
userAgent: c.req.header('user-agent'),
|
|
89
|
-
ip: c.req.raw.headers.get('cf-connecting-ip')
|
|
92
|
+
ip: c.req.raw.headers.get('cf-connecting-ip'), // Cloudflare
|
|
90
93
|
}))
|
|
91
94
|
|
|
92
95
|
// Async context resolution
|
|
@@ -97,13 +100,48 @@ builder.register(RPC, async (c) => {
|
|
|
97
100
|
})
|
|
98
101
|
```
|
|
99
102
|
|
|
103
|
+
## Extending Procedure Documentation
|
|
104
|
+
|
|
105
|
+
The `register` method accepts an optional third parameter `extendProcedureDoc` that allows you to add custom fields to each procedure's documentation. This is useful for adding metadata like descriptions, tags, or custom fields for API documentation generators.
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
// Example of a factory extending the procedure config:
|
|
109
|
+
type ExtendedRPCConfig = {
|
|
110
|
+
description: string
|
|
111
|
+
tags: string[]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
builder.register(RPC, (c) => ({ userId: c.req.header('x-user-id') || 'anonymous' }), {
|
|
115
|
+
extendProcedureDoc: ({ base, procedure }, { base: RPCHttpRouteDoc, procedure }) =>
|
|
116
|
+
({
|
|
117
|
+
description: `Procedure: ${procedure.name}`,
|
|
118
|
+
tags: Array.isArray(procedure.config.scope)
|
|
119
|
+
? procedure.config.scope
|
|
120
|
+
: [procedure.config.scope],
|
|
121
|
+
}),
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
// Access extended docs after build()
|
|
125
|
+
const app = builder.build()
|
|
126
|
+
console.log(builder.docs) // Each doc now includes description and tags
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The `extendProcedureDoc` callback receives:
|
|
130
|
+
|
|
131
|
+
| Parameter | Type | Description |
|
|
132
|
+
| ----------- | ------------------------ | ------------------------------------------------------------------------------------------ |
|
|
133
|
+
| `base` | `RPCHttpRouteDoc` | The base documentation with `name`, `path`, `method`, `scope`, `version`, and `jsonSchema` |
|
|
134
|
+
| `procedure` | `TProcedureRegistration` | The full procedure registration including `name`, `config`, and `handler` |
|
|
135
|
+
|
|
136
|
+
This allows you to derive documentation fields from procedure config or add static metadata per factory registration.
|
|
137
|
+
|
|
100
138
|
## Error Handling
|
|
101
139
|
|
|
102
140
|
Custom error handler receives the procedure, context, and error. **Must return a Response:**
|
|
103
141
|
|
|
104
142
|
```typescript
|
|
105
143
|
const builder = new HonoRPCAppBuilder({
|
|
106
|
-
|
|
144
|
+
onError: (procedure, c, error) => {
|
|
107
145
|
console.error(`Error in ${procedure.name}:`, error)
|
|
108
146
|
|
|
109
147
|
if (error instanceof ValidationError) {
|
|
@@ -115,7 +153,7 @@ const builder = new HonoRPCAppBuilder({
|
|
|
115
153
|
}
|
|
116
154
|
|
|
117
155
|
return c.json({ error: 'Internal server error' }, 500)
|
|
118
|
-
}
|
|
156
|
+
},
|
|
119
157
|
})
|
|
120
158
|
```
|
|
121
159
|
|
|
@@ -132,10 +170,9 @@ const app = new Hono()
|
|
|
132
170
|
app.use('*', cors())
|
|
133
171
|
app.get('/custom', (c) => c.json({ custom: true }))
|
|
134
172
|
|
|
135
|
-
const builder = new HonoRPCAppBuilder({ app })
|
|
136
|
-
.register(RPC, contextResolver)
|
|
173
|
+
const builder = new HonoRPCAppBuilder({ app }).register(RPC, contextResolver)
|
|
137
174
|
|
|
138
|
-
builder.build()
|
|
175
|
+
builder.build() // Adds RPC routes to existing app
|
|
139
176
|
```
|
|
140
177
|
|
|
141
178
|
## Runtime Compatibility
|
|
@@ -184,25 +221,25 @@ new HonoRPCAppBuilder(config?: HonoRPCAppBuilderConfig)
|
|
|
184
221
|
|
|
185
222
|
### Methods
|
|
186
223
|
|
|
187
|
-
| Method
|
|
188
|
-
|
|
189
|
-
| `register`
|
|
190
|
-
| `build`
|
|
191
|
-
| `makeRPCHttpRoutePath` | `makeRPCHttpRoutePath(config: RPCConfig): string` | Generate route path
|
|
224
|
+
| Method | Signature | Description |
|
|
225
|
+
| ---------------------- | ------------------------------------------------- | ------------------------------------------------------------------ |
|
|
226
|
+
| `register` | `register<T>(factory, context, options?): this` | Register procedure factory with context and optional doc extension |
|
|
227
|
+
| `build` | `build(): Hono` | Build routes and return app |
|
|
228
|
+
| `makeRPCHttpRoutePath` | `makeRPCHttpRoutePath(config: RPCConfig): string` | Generate route path |
|
|
192
229
|
|
|
193
230
|
### Static Methods
|
|
194
231
|
|
|
195
|
-
| Method
|
|
196
|
-
|
|
232
|
+
| Method | Signature | Description |
|
|
233
|
+
| ---------------------- | --------------------------------------------------------- | -------------------------------------- |
|
|
197
234
|
| `makeRPCHttpRoutePath` | `static makeRPCHttpRoutePath({ config, prefix }): string` | Generate route path with custom prefix |
|
|
198
235
|
|
|
199
236
|
### Properties
|
|
200
237
|
|
|
201
|
-
| Property | Type
|
|
202
|
-
|
|
203
|
-
| `app`
|
|
204
|
-
| `docs`
|
|
205
|
-
| `config` | `HonoRPCAppBuilderConfig` | The configuration object
|
|
238
|
+
| Property | Type | Description |
|
|
239
|
+
| -------- | ------------------------- | ------------------------------------- |
|
|
240
|
+
| `app` | `Hono` | The Hono application instance |
|
|
241
|
+
| `docs` | `RPCHttpRouteDoc[]` | Route documentation (after `build()`) |
|
|
242
|
+
| `config` | `HonoRPCAppBuilderConfig` | The configuration object |
|
|
206
243
|
|
|
207
244
|
## TypeScript Types
|
|
208
245
|
|
|
@@ -211,7 +248,7 @@ import {
|
|
|
211
248
|
HonoRPCAppBuilder,
|
|
212
249
|
HonoRPCAppBuilderConfig,
|
|
213
250
|
RPCConfig,
|
|
214
|
-
RPCHttpRouteDoc
|
|
251
|
+
RPCHttpRouteDoc,
|
|
215
252
|
} from 'ts-procedures/hono-rpc'
|
|
216
253
|
```
|
|
217
254
|
|
|
@@ -233,11 +270,11 @@ const AuthRPC = Procedures<AuthContext, RPCConfig>()
|
|
|
233
270
|
|
|
234
271
|
// Public procedures
|
|
235
272
|
PublicRPC.Create('HealthCheck', { scope: 'health', version: 1 }, async () => ({
|
|
236
|
-
status: 'ok'
|
|
273
|
+
status: 'ok',
|
|
237
274
|
}))
|
|
238
275
|
|
|
239
276
|
PublicRPC.Create('GetVersion', { scope: ['system', 'version'], version: 1 }, async () => ({
|
|
240
|
-
version: '1.0.0'
|
|
277
|
+
version: '1.0.0',
|
|
241
278
|
}))
|
|
242
279
|
|
|
243
280
|
// Authenticated procedures
|
|
@@ -246,7 +283,7 @@ AuthRPC.Create(
|
|
|
246
283
|
{
|
|
247
284
|
scope: ['users', 'profile'],
|
|
248
285
|
version: 1,
|
|
249
|
-
schema: { returnType: v.object({ userId: v.string() }) }
|
|
286
|
+
schema: { returnType: v.object({ userId: v.string() }) },
|
|
250
287
|
},
|
|
251
288
|
async (ctx) => ({ userId: ctx.userId })
|
|
252
289
|
)
|
|
@@ -256,7 +293,7 @@ AuthRPC.Create(
|
|
|
256
293
|
{
|
|
257
294
|
scope: ['users', 'profile'],
|
|
258
295
|
version: 2,
|
|
259
|
-
schema: { params: v.object({ name: v.string() }) }
|
|
296
|
+
schema: { params: v.object({ name: v.string() }) },
|
|
260
297
|
},
|
|
261
298
|
async (ctx, params) => ({ userId: ctx.userId, name: params.name })
|
|
262
299
|
)
|
|
@@ -267,17 +304,17 @@ const builder = new HonoRPCAppBuilder({
|
|
|
267
304
|
onRequestStart: (c) => console.log(`→ ${c.req.method} ${c.req.path}`),
|
|
268
305
|
onRequestEnd: (c) => console.log(`← completed`),
|
|
269
306
|
onSuccess: (proc) => console.log(`✓ ${proc.name}`),
|
|
270
|
-
|
|
307
|
+
onError: (proc, c, err) => {
|
|
271
308
|
console.error(`✗ ${proc.name}:`, err.message)
|
|
272
309
|
return c.json({ error: err.message }, 500)
|
|
273
|
-
}
|
|
310
|
+
},
|
|
274
311
|
})
|
|
275
312
|
|
|
276
313
|
builder
|
|
277
314
|
.register(PublicRPC, () => ({ source: 'public' as const }))
|
|
278
315
|
.register(AuthRPC, (c) => ({
|
|
279
316
|
source: 'auth' as const,
|
|
280
|
-
userId: c.req.header('x-user-id') || 'anonymous'
|
|
317
|
+
userId: c.req.header('x-user-id') || 'anonymous',
|
|
281
318
|
}))
|
|
282
319
|
|
|
283
320
|
const app = builder.build()
|
|
@@ -288,6 +325,9 @@ const app = builder.build()
|
|
|
288
325
|
// POST /rpc/users/profile/get-user/1
|
|
289
326
|
// POST /rpc/users/profile/get-user/2
|
|
290
327
|
|
|
291
|
-
console.log(
|
|
328
|
+
console.log(
|
|
329
|
+
'Routes:',
|
|
330
|
+
builder.docs.map((d) => d.path)
|
|
331
|
+
)
|
|
292
332
|
export default app
|
|
293
333
|
```
|
|
@@ -296,7 +296,7 @@ describe('HonoRPCAppBuilder', () => {
|
|
|
296
296
|
return c.json({ customError: error.message }, 400)
|
|
297
297
|
})
|
|
298
298
|
|
|
299
|
-
const builder = new HonoRPCAppBuilder({
|
|
299
|
+
const builder = new HonoRPCAppBuilder({ onError: errorHandler })
|
|
300
300
|
const RPC = Procedures<{}, RPCConfig>()
|
|
301
301
|
|
|
302
302
|
RPC.Create('Test', { scope: 'test', version: 1 }, async () => {
|
|
@@ -616,7 +616,10 @@ describe('HonoRPCAppBuilder', () => {
|
|
|
616
616
|
})
|
|
617
617
|
|
|
618
618
|
test("array scope with procedure name: ['users', 'profile'] + 'GetById' → /users/profile/get-by-id/1", () => {
|
|
619
|
-
const path = builder.makeRPCHttpRoutePath('GetById', {
|
|
619
|
+
const path = builder.makeRPCHttpRoutePath('GetById', {
|
|
620
|
+
scope: ['users', 'profile'],
|
|
621
|
+
version: 1,
|
|
622
|
+
})
|
|
620
623
|
expect(path).toBe('/users/profile/get-by-id/1')
|
|
621
624
|
})
|
|
622
625
|
|
|
@@ -721,6 +724,231 @@ describe('HonoRPCAppBuilder', () => {
|
|
|
721
724
|
})
|
|
722
725
|
})
|
|
723
726
|
|
|
727
|
+
// --------------------------------------------------------------------------
|
|
728
|
+
// extendProcedureDoc Tests
|
|
729
|
+
// --------------------------------------------------------------------------
|
|
730
|
+
describe('extendProcedureDoc', () => {
|
|
731
|
+
test('adds custom properties to generated documentation', () => {
|
|
732
|
+
const builder = new HonoRPCAppBuilder()
|
|
733
|
+
const RPC = Procedures<{}, RPCConfig>()
|
|
734
|
+
|
|
735
|
+
RPC.Create('GetUser', { scope: 'users', version: 1 }, async () => ({ name: 'test' }))
|
|
736
|
+
|
|
737
|
+
builder.register(
|
|
738
|
+
RPC,
|
|
739
|
+
() => ({}),
|
|
740
|
+
({ base, procedure }) => ({
|
|
741
|
+
summary: `Get user endpoint`,
|
|
742
|
+
tags: ['users'],
|
|
743
|
+
operationId: procedure.name,
|
|
744
|
+
})
|
|
745
|
+
)
|
|
746
|
+
builder.build()
|
|
747
|
+
|
|
748
|
+
const doc = builder.docs[0]!
|
|
749
|
+
expect(doc).toHaveProperty('summary', 'Get user endpoint')
|
|
750
|
+
expect(doc).toHaveProperty('tags', ['users'])
|
|
751
|
+
expect(doc).toHaveProperty('operationId', 'GetUser')
|
|
752
|
+
})
|
|
753
|
+
|
|
754
|
+
test('receives correct base and procedure parameters', () => {
|
|
755
|
+
const extendFn = vi.fn(() => ({}))
|
|
756
|
+
const builder = new HonoRPCAppBuilder()
|
|
757
|
+
const RPC = Procedures<{}, RPCConfig>()
|
|
758
|
+
|
|
759
|
+
const paramsSchema = v.object({ id: v.string() })
|
|
760
|
+
RPC.Create(
|
|
761
|
+
'GetItem',
|
|
762
|
+
{ scope: 'items', version: 2, schema: { params: paramsSchema } },
|
|
763
|
+
async () => ({})
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
builder.register(RPC, () => ({}), extendFn)
|
|
767
|
+
builder.build()
|
|
768
|
+
|
|
769
|
+
expect(extendFn).toHaveBeenCalledTimes(1)
|
|
770
|
+
const callArg = extendFn.mock.calls[0]![0 as any] as any
|
|
771
|
+
|
|
772
|
+
// Verify base properties
|
|
773
|
+
expect(callArg.base).toHaveProperty('name', 'GetItem')
|
|
774
|
+
expect(callArg.base).toHaveProperty('version', 2)
|
|
775
|
+
expect(callArg.base).toHaveProperty('scope', 'items')
|
|
776
|
+
expect(callArg.base).toHaveProperty('path', '/items/get-item/2')
|
|
777
|
+
expect(callArg.base).toHaveProperty('method', 'post')
|
|
778
|
+
expect(callArg.base.jsonSchema).toHaveProperty('body')
|
|
779
|
+
|
|
780
|
+
// Verify procedure properties
|
|
781
|
+
expect(callArg.procedure).toHaveProperty('name', 'GetItem')
|
|
782
|
+
expect(callArg.procedure).toHaveProperty('handler')
|
|
783
|
+
expect(callArg.procedure.config).toHaveProperty('scope', 'items')
|
|
784
|
+
expect(callArg.procedure.config).toHaveProperty('version', 2)
|
|
785
|
+
})
|
|
786
|
+
|
|
787
|
+
test('base properties take precedence over extended properties', () => {
|
|
788
|
+
const builder = new HonoRPCAppBuilder()
|
|
789
|
+
const RPC = Procedures<{}, RPCConfig>()
|
|
790
|
+
|
|
791
|
+
RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({}))
|
|
792
|
+
|
|
793
|
+
builder.register(
|
|
794
|
+
RPC,
|
|
795
|
+
() => ({}),
|
|
796
|
+
() => ({
|
|
797
|
+
name: 'OverriddenName',
|
|
798
|
+
path: '/overridden/path',
|
|
799
|
+
method: 'get',
|
|
800
|
+
customField: 'custom-value',
|
|
801
|
+
})
|
|
802
|
+
)
|
|
803
|
+
builder.build()
|
|
804
|
+
|
|
805
|
+
const doc = builder.docs[0]!
|
|
806
|
+
// Base properties should NOT be overridden
|
|
807
|
+
expect(doc.name).toBe('Test')
|
|
808
|
+
expect(doc.path).toBe('/test/test/1')
|
|
809
|
+
expect(doc.method).toBe('post')
|
|
810
|
+
// Custom field should be present
|
|
811
|
+
expect(doc).toHaveProperty('customField', 'custom-value')
|
|
812
|
+
})
|
|
813
|
+
|
|
814
|
+
test('different factories can have different extendProcedureDoc functions', () => {
|
|
815
|
+
const builder = new HonoRPCAppBuilder()
|
|
816
|
+
|
|
817
|
+
const PublicRPC = Procedures<{}, RPCConfig>()
|
|
818
|
+
const AdminRPC = Procedures<{}, RPCConfig>()
|
|
819
|
+
|
|
820
|
+
PublicRPC.Create('GetPublic', { scope: 'public', version: 1 }, async () => ({}))
|
|
821
|
+
AdminRPC.Create('GetAdmin', { scope: 'admin', version: 1 }, async () => ({}))
|
|
822
|
+
|
|
823
|
+
builder
|
|
824
|
+
.register(
|
|
825
|
+
PublicRPC,
|
|
826
|
+
() => ({}),
|
|
827
|
+
() => ({
|
|
828
|
+
security: [],
|
|
829
|
+
tags: ['public'],
|
|
830
|
+
})
|
|
831
|
+
)
|
|
832
|
+
.register(
|
|
833
|
+
AdminRPC,
|
|
834
|
+
() => ({}),
|
|
835
|
+
() => ({
|
|
836
|
+
security: [{ bearerAuth: [] }],
|
|
837
|
+
tags: ['admin'],
|
|
838
|
+
})
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
builder.build()
|
|
842
|
+
|
|
843
|
+
const publicDoc = builder.docs.find((d) => d.name === 'GetPublic')!
|
|
844
|
+
const adminDoc = builder.docs.find((d) => d.name === 'GetAdmin')!
|
|
845
|
+
|
|
846
|
+
expect(publicDoc).toHaveProperty('security', [])
|
|
847
|
+
expect(publicDoc).toHaveProperty('tags', ['public'])
|
|
848
|
+
expect(adminDoc).toHaveProperty('security', [{ bearerAuth: [] }])
|
|
849
|
+
expect(adminDoc).toHaveProperty('tags', ['admin'])
|
|
850
|
+
})
|
|
851
|
+
|
|
852
|
+
test('extendProcedureDoc is called for each procedure in factory', () => {
|
|
853
|
+
const extendFn = vi.fn(() => ({ extended: true }))
|
|
854
|
+
const builder = new HonoRPCAppBuilder()
|
|
855
|
+
const RPC = Procedures<{}, RPCConfig>()
|
|
856
|
+
|
|
857
|
+
RPC.Create('Method1', { scope: 'm1', version: 1 }, async () => ({}))
|
|
858
|
+
RPC.Create('Method2', { scope: 'm2', version: 1 }, async () => ({}))
|
|
859
|
+
RPC.Create('Method3', { scope: 'm3', version: 1 }, async () => ({}))
|
|
860
|
+
|
|
861
|
+
builder.register(RPC, () => ({}), extendFn)
|
|
862
|
+
builder.build()
|
|
863
|
+
|
|
864
|
+
expect(extendFn).toHaveBeenCalledTimes(3)
|
|
865
|
+
|
|
866
|
+
const procedureNames = extendFn.mock.calls.map((call: any) => call[0].procedure.name)
|
|
867
|
+
expect(procedureNames).toContain('Method1')
|
|
868
|
+
expect(procedureNames).toContain('Method2')
|
|
869
|
+
expect(procedureNames).toContain('Method3')
|
|
870
|
+
})
|
|
871
|
+
|
|
872
|
+
test('not providing extendProcedureDoc results in base documentation only', () => {
|
|
873
|
+
const builder = new HonoRPCAppBuilder()
|
|
874
|
+
const RPC = Procedures<{}, RPCConfig>()
|
|
875
|
+
|
|
876
|
+
RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({}))
|
|
877
|
+
|
|
878
|
+
builder.register(RPC, () => ({})) // No extendProcedureDoc
|
|
879
|
+
builder.build()
|
|
880
|
+
|
|
881
|
+
const doc = builder.docs[0]!
|
|
882
|
+
expect(doc.name).toBe('Test')
|
|
883
|
+
expect(doc.path).toBe('/test/test/1')
|
|
884
|
+
expect(doc.method).toBe('post')
|
|
885
|
+
// Should not have any extra properties
|
|
886
|
+
expect(Object.keys(doc)).toEqual(['name', 'version', 'scope', 'path', 'method', 'jsonSchema'])
|
|
887
|
+
})
|
|
888
|
+
|
|
889
|
+
test('extendProcedureDoc can access procedure config for conditional logic', () => {
|
|
890
|
+
const builder = new HonoRPCAppBuilder()
|
|
891
|
+
const RPC = Procedures<{}, RPCConfig>()
|
|
892
|
+
|
|
893
|
+
RPC.Create('PublicEndpoint', { scope: 'public', version: 1 }, async () => ({}))
|
|
894
|
+
RPC.Create('PrivateEndpoint', { scope: 'private', version: 1 }, async () => ({}))
|
|
895
|
+
|
|
896
|
+
builder.register(
|
|
897
|
+
RPC,
|
|
898
|
+
() => ({}),
|
|
899
|
+
({ procedure }) => ({
|
|
900
|
+
isPublic: procedure.config.scope === 'public',
|
|
901
|
+
description: `This is a ${procedure.config.scope} endpoint`,
|
|
902
|
+
})
|
|
903
|
+
)
|
|
904
|
+
builder.build()
|
|
905
|
+
|
|
906
|
+
const publicDoc = builder.docs.find((d) => d.name === 'PublicEndpoint')!
|
|
907
|
+
const privateDoc = builder.docs.find((d) => d.name === 'PrivateEndpoint')!
|
|
908
|
+
|
|
909
|
+
expect(publicDoc).toHaveProperty('isPublic', true)
|
|
910
|
+
expect(publicDoc).toHaveProperty('description', 'This is a public endpoint')
|
|
911
|
+
expect(privateDoc).toHaveProperty('isPublic', false)
|
|
912
|
+
expect(privateDoc).toHaveProperty('description', 'This is a private endpoint')
|
|
913
|
+
})
|
|
914
|
+
|
|
915
|
+
test('extendProcedureDoc can use base jsonSchema for OpenAPI-style docs', () => {
|
|
916
|
+
const builder = new HonoRPCAppBuilder()
|
|
917
|
+
const RPC = Procedures<{}, RPCConfig>()
|
|
918
|
+
|
|
919
|
+
const paramsSchema = v.object({ userId: v.string() })
|
|
920
|
+
const returnSchema = v.object({ name: v.string(), email: v.string() })
|
|
921
|
+
|
|
922
|
+
RPC.Create(
|
|
923
|
+
'GetUser',
|
|
924
|
+
{ scope: 'users', version: 1, schema: { params: paramsSchema, returnType: returnSchema } },
|
|
925
|
+
async () => ({ name: 'test', email: 'test@example.com' })
|
|
926
|
+
)
|
|
927
|
+
|
|
928
|
+
builder.register(
|
|
929
|
+
RPC,
|
|
930
|
+
() => ({}),
|
|
931
|
+
({ base }) => ({
|
|
932
|
+
requestBody: base.jsonSchema.body
|
|
933
|
+
? { content: { 'application/json': { schema: base.jsonSchema.body } } }
|
|
934
|
+
: undefined,
|
|
935
|
+
responses: {
|
|
936
|
+
200: base.jsonSchema.response
|
|
937
|
+
? { content: { 'application/json': { schema: base.jsonSchema.response } } }
|
|
938
|
+
: { description: 'Success' },
|
|
939
|
+
},
|
|
940
|
+
})
|
|
941
|
+
)
|
|
942
|
+
builder.build()
|
|
943
|
+
|
|
944
|
+
const doc = builder.docs[0] as any
|
|
945
|
+
expect(doc).toHaveProperty('requestBody')
|
|
946
|
+
expect(doc.requestBody).toHaveProperty('content')
|
|
947
|
+
expect(doc).toHaveProperty('responses')
|
|
948
|
+
expect(doc.responses).toHaveProperty('200')
|
|
949
|
+
})
|
|
950
|
+
})
|
|
951
|
+
|
|
724
952
|
// --------------------------------------------------------------------------
|
|
725
953
|
// Integration Test
|
|
726
954
|
// --------------------------------------------------------------------------
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { Hono, Context } from 'hono'
|
|
2
2
|
import { kebabCase } from 'es-toolkit/string'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { TProcedureRegistration } from '../../../index.js'
|
|
4
|
+
import {
|
|
5
|
+
ExtractConfig,
|
|
6
|
+
ExtractContext,
|
|
7
|
+
ProceduresFactory,
|
|
8
|
+
RPCConfig,
|
|
9
|
+
RPCHttpRouteDoc,
|
|
10
|
+
} from '../../types.js'
|
|
5
11
|
import { castArray } from 'es-toolkit/compat'
|
|
6
|
-
import { HonoFactoryItem
|
|
12
|
+
import { HonoFactoryItem } from './types.js'
|
|
7
13
|
|
|
8
14
|
export type { RPCConfig, RPCHttpRouteDoc }
|
|
9
15
|
|
|
@@ -18,7 +24,13 @@ export type HonoRPCAppBuilderConfig = {
|
|
|
18
24
|
onRequestStart?: (c: Context) => void
|
|
19
25
|
onRequestEnd?: (c: Context) => void
|
|
20
26
|
onSuccess?: (procedure: TProcedureRegistration, c: Context) => void
|
|
21
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Error handler called when a procedure throws an error.
|
|
29
|
+
* @param procedure
|
|
30
|
+
* @param c
|
|
31
|
+
* @param error
|
|
32
|
+
*/
|
|
33
|
+
onError?: (
|
|
22
34
|
procedure: TProcedureRegistration,
|
|
23
35
|
c: Context,
|
|
24
36
|
error: Error
|
|
@@ -104,7 +116,7 @@ export class HonoRPCAppBuilder {
|
|
|
104
116
|
private factories: HonoFactoryItem<any>[] = []
|
|
105
117
|
|
|
106
118
|
private _app: Hono = new Hono()
|
|
107
|
-
private _docs: RPCHttpRouteDoc[] = []
|
|
119
|
+
private _docs: (RPCHttpRouteDoc & object)[] = []
|
|
108
120
|
|
|
109
121
|
get app(): Hono {
|
|
110
122
|
return this._app
|
|
@@ -119,14 +131,21 @@ export class HonoRPCAppBuilder {
|
|
|
119
131
|
* @param factory - The procedure factory created by Procedures<Context, RPCConfig>()
|
|
120
132
|
* @param factoryContext - The context for procedure handlers. Can be a direct value,
|
|
121
133
|
* a sync function (c) => Context, or an async function (c) => Promise<Context>
|
|
134
|
+
* @param extendProcedureDoc - A custom function to extend the generated RPC route documentation for each procedure.
|
|
122
135
|
*/
|
|
123
136
|
register<TFactory extends ProceduresFactory>(
|
|
124
137
|
factory: TFactory,
|
|
125
138
|
factoryContext:
|
|
126
139
|
| ExtractContext<TFactory>
|
|
127
|
-
| ((c: Context) => ExtractContext<TFactory> | Promise<ExtractContext<TFactory>>)
|
|
140
|
+
| ((c: Context) => ExtractContext<TFactory> | Promise<ExtractContext<TFactory>>),
|
|
141
|
+
extendProcedureDoc?: (params: {
|
|
142
|
+
/* RPC App builder base http route doc */
|
|
143
|
+
base: RPCHttpRouteDoc
|
|
144
|
+
/* Procedure registration */
|
|
145
|
+
procedure: TProcedureRegistration<any, ExtractConfig<TFactory>>
|
|
146
|
+
}) => Record<string, any>
|
|
128
147
|
): this {
|
|
129
|
-
this.factories.push({ factory, factoryContext } as HonoFactoryItem<any>)
|
|
148
|
+
this.factories.push({ factory, factoryContext, extendProcedureDoc } as HonoFactoryItem<any>)
|
|
130
149
|
return this
|
|
131
150
|
}
|
|
132
151
|
|
|
@@ -135,9 +154,9 @@ export class HonoRPCAppBuilder {
|
|
|
135
154
|
* @return Hono
|
|
136
155
|
*/
|
|
137
156
|
build(): Hono {
|
|
138
|
-
this.factories.forEach(({ factory, factoryContext }) => {
|
|
157
|
+
this.factories.forEach(({ factory, factoryContext, extendProcedureDoc }) => {
|
|
139
158
|
factory.getProcedures().map((procedure: TProcedureRegistration<any, RPCConfig>) => {
|
|
140
|
-
const route = this.buildRpcHttpRouteDoc(procedure)
|
|
159
|
+
const route = this.buildRpcHttpRouteDoc(procedure, extendProcedureDoc)
|
|
141
160
|
|
|
142
161
|
this._docs.push(route)
|
|
143
162
|
|
|
@@ -159,8 +178,8 @@ export class HonoRPCAppBuilder {
|
|
|
159
178
|
// Hono returns Response objects via c.json()
|
|
160
179
|
return c.json(result)
|
|
161
180
|
} catch (error) {
|
|
162
|
-
if (this.config?.
|
|
163
|
-
return this.config.
|
|
181
|
+
if (this.config?.onError) {
|
|
182
|
+
return this.config.onError(procedure, c, error as Error)
|
|
164
183
|
}
|
|
165
184
|
// Default error handling
|
|
166
185
|
return c.json({ error: (error as Error).message }, 500)
|
|
@@ -176,14 +195,17 @@ export class HonoRPCAppBuilder {
|
|
|
176
195
|
* Generates the RPC HTTP route for the given procedure.
|
|
177
196
|
* @param procedure
|
|
178
197
|
*/
|
|
179
|
-
private buildRpcHttpRouteDoc(
|
|
198
|
+
private buildRpcHttpRouteDoc(
|
|
199
|
+
procedure: TProcedureRegistration<any, RPCConfig>,
|
|
200
|
+
extendProcedureDoc: HonoFactoryItem['extendProcedureDoc']
|
|
201
|
+
): RPCHttpRouteDoc {
|
|
180
202
|
const { config } = procedure
|
|
181
203
|
const path = HonoRPCAppBuilder.makeRPCHttpRoutePath({
|
|
182
204
|
name: procedure.name,
|
|
183
205
|
config,
|
|
184
206
|
prefix: this.config?.pathPrefix,
|
|
185
207
|
})
|
|
186
|
-
const method = 'post' // RPCs use POST method
|
|
208
|
+
const method = 'post' as const // RPCs use POST method
|
|
187
209
|
const jsonSchema: { body?: object; response?: object } = {}
|
|
188
210
|
|
|
189
211
|
if (config.schema?.params) {
|
|
@@ -193,10 +215,23 @@ export class HonoRPCAppBuilder {
|
|
|
193
215
|
jsonSchema.response = config.schema.returnType
|
|
194
216
|
}
|
|
195
217
|
|
|
196
|
-
|
|
218
|
+
const base = {
|
|
219
|
+
name: procedure.name,
|
|
220
|
+
version: config.version,
|
|
221
|
+
scope: config.scope,
|
|
197
222
|
path,
|
|
198
223
|
method,
|
|
199
224
|
jsonSchema,
|
|
200
225
|
}
|
|
226
|
+
let extendedDoc: object = {}
|
|
227
|
+
|
|
228
|
+
if (extendProcedureDoc) {
|
|
229
|
+
extendedDoc = extendProcedureDoc({ base, procedure })
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
...extendedDoc,
|
|
234
|
+
...base,
|
|
235
|
+
}
|
|
201
236
|
}
|
|
202
237
|
}
|
|
@@ -1,33 +1,16 @@
|
|
|
1
|
-
import { RPCConfig } from '../../types.js'
|
|
2
|
-
import { Procedures } from '../../../index.js'
|
|
1
|
+
import { ExtractConfig, ExtractContext, RPCConfig, RPCHttpRouteDoc } from '../../types.js'
|
|
2
|
+
import { Procedures, TProcedureRegistration } from '../../../index.js'
|
|
3
3
|
import { Context } from 'hono'
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* Extracts the TContext type from a Procedures factory return type.
|
|
7
|
-
* Uses the first parameter of the handler function to infer the context type.
|
|
8
|
-
*/
|
|
9
|
-
export type ExtractContext<TFactory> = TFactory extends {
|
|
10
|
-
getProcedures: () => Array<{ handler: (ctx: infer TContext, ...args: any[]) => any }>
|
|
11
|
-
}
|
|
12
|
-
? TContext
|
|
13
|
-
: never
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Minimal structural type for a Procedures factory.
|
|
17
|
-
* Uses explicit `any` types to avoid variance issues with generic constraints.
|
|
18
|
-
*/
|
|
19
|
-
export type ProceduresFactory = {
|
|
20
|
-
getProcedures: () => Array<{
|
|
21
|
-
name: string
|
|
22
|
-
config: any
|
|
23
|
-
handler: (ctx: any, params?: any) => Promise<any>
|
|
24
|
-
}>
|
|
25
|
-
Create: (...args: any[]) => any
|
|
26
|
-
}
|
|
27
|
-
|
|
28
5
|
export type HonoFactoryItem<TFactory = ReturnType<typeof Procedures<any, RPCConfig>>> = {
|
|
29
6
|
factory: TFactory
|
|
30
7
|
factoryContext:
|
|
31
8
|
| ExtractContext<TFactory>
|
|
32
9
|
| ((c: Context) => ExtractContext<TFactory> | Promise<ExtractContext<TFactory>>)
|
|
10
|
+
extendProcedureDoc?: (params: {
|
|
11
|
+
/* RPC App builder base http route doc */
|
|
12
|
+
base: RPCHttpRouteDoc
|
|
13
|
+
/* Procedure registration */
|
|
14
|
+
procedure: TProcedureRegistration<any, ExtractConfig<TFactory>>
|
|
15
|
+
}) => Record<string, any>
|
|
33
16
|
}
|