ts-procedures 3.0.2 → 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 +6 -3
- package/build/implementations/http/express-rpc/index.js +18 -6
- package/build/implementations/http/express-rpc/index.js.map +1 -1
- package/build/implementations/http/express-rpc/index.test.js +153 -0
- 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 +6 -3
- package/build/implementations/http/hono-rpc/index.js +18 -6
- package/build/implementations/http/hono-rpc/index.js.map +1 -1
- package/build/implementations/http/hono-rpc/index.test.js +153 -0
- 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/index.test.ts +225 -0
- package/src/implementations/http/express-rpc/index.ts +39 -10
- package/src/implementations/http/express-rpc/types.ts +8 -25
- package/src/implementations/http/hono-rpc/README.md +82 -42
- package/src/implementations/http/hono-rpc/index.test.ts +225 -0
- package/src/implementations/http/hono-rpc/index.ts +40 -11
- package/src/implementations/http/hono-rpc/types.ts +8 -25
- package/src/implementations/types.ts +39 -1
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import express from 'express'
|
|
2
2
|
import { kebabCase } from 'es-toolkit/string'
|
|
3
3
|
import { Procedures, TProcedureRegistration } from '../../../index.js'
|
|
4
|
-
import {
|
|
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 { ExpressFactoryItem
|
|
12
|
+
import { ExpressFactoryItem } from './types.js'
|
|
7
13
|
|
|
8
14
|
export type { RPCConfig, RPCHttpRouteDoc }
|
|
9
15
|
|
|
@@ -122,7 +128,7 @@ export class ExpressRPCAppBuilder {
|
|
|
122
128
|
private factories: ExpressFactoryItem<any>[] = []
|
|
123
129
|
|
|
124
130
|
private _app: express.Express = express()
|
|
125
|
-
private _docs: RPCHttpRouteDoc[] = []
|
|
131
|
+
private _docs: (RPCHttpRouteDoc & object)[] = []
|
|
126
132
|
|
|
127
133
|
get app(): express.Express {
|
|
128
134
|
return this._app
|
|
@@ -137,14 +143,21 @@ export class ExpressRPCAppBuilder {
|
|
|
137
143
|
* @param factory - The procedure factory created by Procedures<Context, RPCConfig>()
|
|
138
144
|
* @param factoryContext - The context for procedure handlers. Can be a direct value,
|
|
139
145
|
* a sync function (req) => Context, or an async function (req) => Promise<Context>
|
|
146
|
+
* @param extendProcedureDoc - A custom function to extend the generated RPC route documentation for each procedure.
|
|
140
147
|
*/
|
|
141
148
|
register<TFactory extends ProceduresFactory>(
|
|
142
149
|
factory: TFactory,
|
|
143
150
|
factoryContext:
|
|
144
151
|
| ExtractContext<TFactory>
|
|
145
|
-
| ((req: express.Request) => ExtractContext<TFactory> | Promise<ExtractContext<TFactory>>)
|
|
152
|
+
| ((req: express.Request) => ExtractContext<TFactory> | Promise<ExtractContext<TFactory>>),
|
|
153
|
+
extendProcedureDoc?: (params: {
|
|
154
|
+
/* RPC App builder base http route doc */
|
|
155
|
+
base: RPCHttpRouteDoc
|
|
156
|
+
/* Procedure registration */
|
|
157
|
+
procedure: TProcedureRegistration<any, ExtractConfig<TFactory>>
|
|
158
|
+
}) => Record<string, any>
|
|
146
159
|
): this {
|
|
147
|
-
this.factories.push({ factory, factoryContext } as ExpressFactoryItem<any>)
|
|
160
|
+
this.factories.push({ factory, factoryContext, extendProcedureDoc } as ExpressFactoryItem<any>)
|
|
148
161
|
return this
|
|
149
162
|
}
|
|
150
163
|
|
|
@@ -153,9 +166,9 @@ export class ExpressRPCAppBuilder {
|
|
|
153
166
|
* @return express.Application
|
|
154
167
|
*/
|
|
155
168
|
build(): express.Application {
|
|
156
|
-
this.factories.forEach(({ factory, factoryContext }) => {
|
|
169
|
+
this.factories.forEach(({ factory, factoryContext, extendProcedureDoc }) => {
|
|
157
170
|
factory.getProcedures().map((procedure: TProcedureRegistration<any, RPCConfig>) => {
|
|
158
|
-
const route = this.buildRpcHttpRouteDoc(procedure)
|
|
171
|
+
const route = this.buildRpcHttpRouteDoc(procedure, extendProcedureDoc)
|
|
159
172
|
|
|
160
173
|
this._docs.push(route)
|
|
161
174
|
|
|
@@ -198,14 +211,17 @@ export class ExpressRPCAppBuilder {
|
|
|
198
211
|
* Generates the RPC HTTP route for the given procedure.
|
|
199
212
|
* @param procedure
|
|
200
213
|
*/
|
|
201
|
-
private buildRpcHttpRouteDoc(
|
|
214
|
+
private buildRpcHttpRouteDoc(
|
|
215
|
+
procedure: TProcedureRegistration<any, RPCConfig>,
|
|
216
|
+
extendProcedureDoc: ExpressFactoryItem['extendProcedureDoc']
|
|
217
|
+
): RPCHttpRouteDoc {
|
|
202
218
|
const { config } = procedure
|
|
203
219
|
const path = ExpressRPCAppBuilder.makeRPCHttpRoutePath({
|
|
204
220
|
name: procedure.name,
|
|
205
221
|
config,
|
|
206
222
|
prefix: this.config?.pathPrefix,
|
|
207
223
|
})
|
|
208
|
-
const method = 'post' // RPCs use POST method
|
|
224
|
+
const method = 'post' as const // RPCs use POST method
|
|
209
225
|
const jsonSchema: { body?: object; response?: object } = {}
|
|
210
226
|
|
|
211
227
|
if (config.schema?.params) {
|
|
@@ -215,10 +231,23 @@ export class ExpressRPCAppBuilder {
|
|
|
215
231
|
jsonSchema.response = config.schema.returnType
|
|
216
232
|
}
|
|
217
233
|
|
|
218
|
-
|
|
234
|
+
const base = {
|
|
235
|
+
name: procedure.name,
|
|
236
|
+
version: config.version,
|
|
237
|
+
scope: config.scope,
|
|
219
238
|
path,
|
|
220
239
|
method,
|
|
221
240
|
jsonSchema,
|
|
222
241
|
}
|
|
242
|
+
let extendedDoc: object = {}
|
|
243
|
+
|
|
244
|
+
if (extendProcedureDoc) {
|
|
245
|
+
extendedDoc = extendProcedureDoc({ base, procedure })
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
...extendedDoc,
|
|
250
|
+
...base,
|
|
251
|
+
}
|
|
223
252
|
}
|
|
224
253
|
}
|
|
@@ -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 express from 'express'
|
|
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 ExpressFactoryItem<TFactory = ReturnType<typeof Procedures<any, RPCConfig>>> = {
|
|
29
6
|
factory: TFactory
|
|
30
7
|
factoryContext:
|
|
31
8
|
| ExtractContext<TFactory>
|
|
32
9
|
| ((req: express.Request) => 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
|
}
|
|
@@ -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,6 +100,41 @@ 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:**
|
|
@@ -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
|
)
|
|
@@ -270,14 +307,14 @@ const builder = new HonoRPCAppBuilder({
|
|
|
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
|
```
|
|
@@ -724,6 +724,231 @@ describe('HonoRPCAppBuilder', () => {
|
|
|
724
724
|
})
|
|
725
725
|
})
|
|
726
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
|
+
|
|
727
952
|
// --------------------------------------------------------------------------
|
|
728
953
|
// Integration Test
|
|
729
954
|
// --------------------------------------------------------------------------
|