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.
@@ -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
- .register(RPC, (c) => ({
44
- userId: c.req.header('x-user-id') || 'anonymous'
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 // Existing Hono app (optional)
64
- pathPrefix?: string // Prefix for all routes (e.g., '/rpc/v1')
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?: (procedure: TProcedureRegistration, c: Context, error: Error) => Response | Promise<Response>
67
+ error?: (
68
+ procedure: TProcedureRegistration,
69
+ c: Context,
70
+ error: Error
71
+ ) => Response | Promise<Response>
69
72
  }
70
73
  ```
71
74
 
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) |
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') // Cloudflare
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
- error: (procedure, c, error) => {
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() // Adds RPC routes to existing app
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 | 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 |
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 | Signature | Description |
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 | Description |
202
- |----------|------|-------------|
203
- | `app` | `Hono` | The Hono application instance |
204
- | `docs` | `RPCHttpRouteDoc[]` | Route documentation (after `build()`) |
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
- error: (proc, c, err) => {
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('Routes:', builder.docs.map(d => d.path))
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({ error: errorHandler })
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', { scope: ['users', 'profile'], version: 1 })
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 { Procedures, TProcedureRegistration } from '../../../index.js'
4
- import { RPCConfig, RPCHttpRouteDoc } from '../../types.js'
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, ExtractContext, ProceduresFactory } from './types.js'
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
- error?: (
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?.error) {
163
- return this.config.error(procedure, c, error as Error)
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(procedure: TProcedureRegistration<any, RPCConfig>): RPCHttpRouteDoc {
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
- return {
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
  }