ts-procedures 5.3.0 → 5.4.1
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/README.md +90 -0
- package/agent_config/claude-code/agents/ts-procedures-architect.md +15 -0
- package/agent_config/claude-code/skills/guide/anti-patterns.md +106 -0
- package/agent_config/claude-code/skills/guide/api-reference.md +150 -4
- package/agent_config/claude-code/skills/guide/patterns.md +155 -0
- package/agent_config/claude-code/skills/review/checklist.md +22 -0
- package/agent_config/claude-code/skills/scaffold/SKILL.md +3 -1
- package/agent_config/claude-code/skills/scaffold/templates/hono-api.md +169 -0
- package/agent_config/copilot/copilot-instructions.md +35 -0
- package/agent_config/cursor/cursorrules +35 -0
- package/build/implementations/http/hono-api/index.d.ts +102 -0
- package/build/implementations/http/hono-api/index.js +339 -0
- package/build/implementations/http/hono-api/index.js.map +1 -0
- package/build/implementations/http/hono-api/index.test.d.ts +1 -0
- package/build/implementations/http/hono-api/index.test.js +983 -0
- package/build/implementations/http/hono-api/index.test.js.map +1 -0
- package/build/implementations/http/hono-api/types.d.ts +13 -0
- package/build/implementations/http/hono-api/types.js +2 -0
- package/build/implementations/http/hono-api/types.js.map +1 -0
- package/build/implementations/types.d.ts +44 -0
- package/build/index.d.ts +28 -6
- package/build/index.js +28 -0
- package/build/index.js.map +1 -1
- package/build/schema/compute-schema.d.ts +5 -0
- package/build/schema/compute-schema.js +8 -1
- package/build/schema/compute-schema.js.map +1 -1
- package/build/schema/parser.d.ts +6 -5
- package/build/schema/parser.js +54 -0
- package/build/schema/parser.js.map +1 -1
- package/package.json +8 -4
- package/src/errors.test.ts +0 -163
- package/src/errors.ts +0 -107
- package/src/exports.ts +0 -7
- package/src/implementations/http/README.md +0 -217
- package/src/implementations/http/express-rpc/README.md +0 -281
- package/src/implementations/http/express-rpc/index.test.ts +0 -957
- package/src/implementations/http/express-rpc/index.ts +0 -265
- package/src/implementations/http/express-rpc/types.ts +0 -16
- package/src/implementations/http/hono-rpc/README.md +0 -358
- package/src/implementations/http/hono-rpc/index.test.ts +0 -1075
- package/src/implementations/http/hono-rpc/index.ts +0 -237
- package/src/implementations/http/hono-rpc/types.ts +0 -16
- package/src/implementations/http/hono-stream/README.md +0 -526
- package/src/implementations/http/hono-stream/index.test.ts +0 -1676
- package/src/implementations/http/hono-stream/index.ts +0 -435
- package/src/implementations/http/hono-stream/types.ts +0 -29
- package/src/implementations/types.ts +0 -75
- package/src/index.test.ts +0 -1194
- package/src/index.ts +0 -435
- package/src/schema/compute-schema.test.ts +0 -128
- package/src/schema/compute-schema.ts +0 -67
- package/src/schema/extract-json-schema.test.ts +0 -25
- package/src/schema/extract-json-schema.ts +0 -15
- package/src/schema/parser.test.ts +0 -182
- package/src/schema/parser.ts +0 -148
- package/src/schema/resolve-schema-lib.test.ts +0 -19
- package/src/schema/resolve-schema-lib.ts +0 -29
- package/src/schema/types.ts +0 -20
- package/src/stack-utils.test.ts +0 -94
- package/src/stack-utils.ts +0 -129
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
import express from 'express'
|
|
2
|
-
import { kebabCase } from 'es-toolkit/string'
|
|
3
|
-
import { Procedures, TProcedureRegistration } from '../../../index.js'
|
|
4
|
-
import {
|
|
5
|
-
ExtractConfig,
|
|
6
|
-
ExtractContext,
|
|
7
|
-
ProceduresFactory,
|
|
8
|
-
RPCConfig,
|
|
9
|
-
RPCHttpRouteDoc,
|
|
10
|
-
} from '../../types.js'
|
|
11
|
-
import { castArray } from 'es-toolkit/compat'
|
|
12
|
-
import { ExpressFactoryItem } from './types.js'
|
|
13
|
-
|
|
14
|
-
export type { RPCConfig, RPCHttpRouteDoc }
|
|
15
|
-
|
|
16
|
-
export type ExpressRPCAppBuilderConfig = {
|
|
17
|
-
/**
|
|
18
|
-
* An existing Express application instance to use.
|
|
19
|
-
* When provided, ensure to set up necessary middleware (e.g., json/body parser) beforehand.
|
|
20
|
-
* If not provided, a new instance will be created.
|
|
21
|
-
*/
|
|
22
|
-
app?: express.Express
|
|
23
|
-
/** Optional path prefix for all RPC routes. */
|
|
24
|
-
pathPrefix?: string
|
|
25
|
-
onRequestStart?: (req: express.Request) => void
|
|
26
|
-
onRequestEnd?: (req: express.Request, res: express.Response) => void
|
|
27
|
-
onSuccess?: (
|
|
28
|
-
procedure: TProcedureRegistration,
|
|
29
|
-
req: express.Request,
|
|
30
|
-
res: express.Response
|
|
31
|
-
) => void
|
|
32
|
-
/**
|
|
33
|
-
* Error handler called when a procedure throws an error.
|
|
34
|
-
* @param procedure
|
|
35
|
-
* @param req
|
|
36
|
-
* @param res
|
|
37
|
-
* @param error
|
|
38
|
-
*/
|
|
39
|
-
onError?: (
|
|
40
|
-
procedure: TProcedureRegistration,
|
|
41
|
-
req: express.Request,
|
|
42
|
-
res: express.Response,
|
|
43
|
-
error: Error
|
|
44
|
-
) => void
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Builder class for creating an Express application with RPC routes.
|
|
49
|
-
*
|
|
50
|
-
* Usage:
|
|
51
|
-
* const PublicRPC = Procedures<PublicRPCContext, RPCConfig>()
|
|
52
|
-
* const ProtectedRPC = Procedures<ProtectedRPCContext, RPCConfig>()
|
|
53
|
-
*
|
|
54
|
-
* const rpcApp = new ExpressRPCAppBuilder()
|
|
55
|
-
* .register(PublicRPC, (req): Promise<PublicRPCContext> => { /* context resolution logic * / })
|
|
56
|
-
* .register(ProtectedRPC, (req): Promise<ProtectedRPCContext> => { /* context resolution logic * / })
|
|
57
|
-
* .build();
|
|
58
|
-
*
|
|
59
|
-
* const app = rpcApp.app; // Express application
|
|
60
|
-
* const docs = rpcApp.docs; // RPC route documentation
|
|
61
|
-
*/
|
|
62
|
-
export class ExpressRPCAppBuilder {
|
|
63
|
-
/**
|
|
64
|
-
* Constructor for ExpressRPCAppBuilder.
|
|
65
|
-
*
|
|
66
|
-
* @param config
|
|
67
|
-
*/
|
|
68
|
-
constructor(readonly config?: ExpressRPCAppBuilderConfig) {
|
|
69
|
-
if (config?.app) {
|
|
70
|
-
this._app = config.app
|
|
71
|
-
} else {
|
|
72
|
-
// Default middleware if no app is provided
|
|
73
|
-
this._app.use(express.json())
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (config?.onRequestStart) {
|
|
77
|
-
this._app.use((req, res, next) => {
|
|
78
|
-
config.onRequestStart!(req)
|
|
79
|
-
next()
|
|
80
|
-
})
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (config?.onRequestEnd) {
|
|
84
|
-
this._app.use((req, res, next) => {
|
|
85
|
-
res.on('finish', () => {
|
|
86
|
-
config.onRequestEnd!(req, res)
|
|
87
|
-
})
|
|
88
|
-
next()
|
|
89
|
-
})
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Generates the RPC route path based on the RPC configuration.
|
|
95
|
-
* The RPCConfig name can be a string or an array of strings to form nested paths.
|
|
96
|
-
*
|
|
97
|
-
* Example
|
|
98
|
-
* name: ['string', 'string-string', 'string']
|
|
99
|
-
* path: /string/string-string/string/version
|
|
100
|
-
* @param config
|
|
101
|
-
*/
|
|
102
|
-
static makeRPCHttpRoutePath({
|
|
103
|
-
name,
|
|
104
|
-
config,
|
|
105
|
-
prefix,
|
|
106
|
-
}: {
|
|
107
|
-
name: string
|
|
108
|
-
prefix?: string
|
|
109
|
-
config: RPCConfig
|
|
110
|
-
}) {
|
|
111
|
-
const normalizedPrefix = prefix ? (prefix.startsWith('/') ? prefix : `/${prefix}`) : ''
|
|
112
|
-
|
|
113
|
-
return `${normalizedPrefix}/${castArray(config.scope).map(kebabCase).join('/')}/${kebabCase(name)}/${String(config.version).trim()}`
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Instance method wrapper for makeRPCHttpRoutePath that uses the builder's pathPrefix.
|
|
118
|
-
* @param config - The RPC configuration
|
|
119
|
-
*/
|
|
120
|
-
makeRPCHttpRoutePath(name: string, config: RPCConfig): string {
|
|
121
|
-
return ExpressRPCAppBuilder.makeRPCHttpRoutePath({
|
|
122
|
-
name,
|
|
123
|
-
config,
|
|
124
|
-
prefix: this.config?.pathPrefix,
|
|
125
|
-
})
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
private factories: ExpressFactoryItem<any>[] = []
|
|
129
|
-
|
|
130
|
-
private _app: express.Express = express()
|
|
131
|
-
private _docs: (RPCHttpRouteDoc & object)[] = []
|
|
132
|
-
|
|
133
|
-
get app(): express.Express {
|
|
134
|
-
return this._app
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
get docs(): RPCHttpRouteDoc[] {
|
|
138
|
-
return this._docs
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Registers a procedure factory with its context.
|
|
143
|
-
* @param factory - The procedure factory created by Procedures<Context, RPCConfig>()
|
|
144
|
-
* @param factoryContext - The context for procedure handlers. Can be a direct value,
|
|
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.
|
|
147
|
-
*/
|
|
148
|
-
register<TFactory extends ProceduresFactory>(
|
|
149
|
-
factory: TFactory,
|
|
150
|
-
factoryContext:
|
|
151
|
-
| 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>
|
|
159
|
-
): this {
|
|
160
|
-
this.factories.push({ factory, factoryContext, extendProcedureDoc } as ExpressFactoryItem<any>)
|
|
161
|
-
return this
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Builds and returns the Express application with registered RPC routes.
|
|
166
|
-
* @return express.Application
|
|
167
|
-
*/
|
|
168
|
-
build(): express.Application {
|
|
169
|
-
this.factories.forEach(({ factory, factoryContext, extendProcedureDoc }) => {
|
|
170
|
-
factory.getProcedures().map((procedure: TProcedureRegistration<any, RPCConfig>) => {
|
|
171
|
-
const route = this.buildRpcHttpRouteDoc(procedure, extendProcedureDoc)
|
|
172
|
-
|
|
173
|
-
this._docs.push(route)
|
|
174
|
-
|
|
175
|
-
this._app[route.method](route.path, async (req, res) => {
|
|
176
|
-
try {
|
|
177
|
-
const context =
|
|
178
|
-
typeof factoryContext === 'function'
|
|
179
|
-
? await factoryContext(req)
|
|
180
|
-
: (factoryContext as ExtractContext<typeof factory>)
|
|
181
|
-
|
|
182
|
-
let ac: AbortController | undefined
|
|
183
|
-
const ctxWithSignal = Object.defineProperty({ ...context }, 'signal', {
|
|
184
|
-
get() {
|
|
185
|
-
if (!ac) {
|
|
186
|
-
ac = new AbortController()
|
|
187
|
-
req.on('close', () => { if (!res.writableFinished) ac!.abort() })
|
|
188
|
-
}
|
|
189
|
-
return ac.signal
|
|
190
|
-
},
|
|
191
|
-
enumerable: true,
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
res.json(await procedure.handler(ctxWithSignal, req.body))
|
|
195
|
-
if (this.config?.onSuccess) {
|
|
196
|
-
this.config.onSuccess(procedure, req, res)
|
|
197
|
-
}
|
|
198
|
-
// if status not set, set to 200
|
|
199
|
-
if (!res.status) {
|
|
200
|
-
res.status(200)
|
|
201
|
-
}
|
|
202
|
-
} catch (error) {
|
|
203
|
-
if (this.config?.onError) {
|
|
204
|
-
this.config.onError(procedure, req, res, error as Error)
|
|
205
|
-
return
|
|
206
|
-
}
|
|
207
|
-
if (!res.status) {
|
|
208
|
-
res.status(500)
|
|
209
|
-
}
|
|
210
|
-
// if no res.json set, set default error message
|
|
211
|
-
if (!res.headersSent) {
|
|
212
|
-
res.json({ error: (error as Error).message })
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
})
|
|
216
|
-
})
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
return this._app
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Generates the RPC HTTP route for the given procedure.
|
|
224
|
-
* @param procedure
|
|
225
|
-
*/
|
|
226
|
-
private buildRpcHttpRouteDoc(
|
|
227
|
-
procedure: TProcedureRegistration<any, RPCConfig>,
|
|
228
|
-
extendProcedureDoc: ExpressFactoryItem['extendProcedureDoc']
|
|
229
|
-
): RPCHttpRouteDoc {
|
|
230
|
-
const { config } = procedure
|
|
231
|
-
const path = ExpressRPCAppBuilder.makeRPCHttpRoutePath({
|
|
232
|
-
name: procedure.name,
|
|
233
|
-
config,
|
|
234
|
-
prefix: this.config?.pathPrefix,
|
|
235
|
-
})
|
|
236
|
-
const method = 'post' as const // RPCs use POST method
|
|
237
|
-
const jsonSchema: { body?: Record<string, unknown>; response?: Record<string, unknown> } = {}
|
|
238
|
-
|
|
239
|
-
if (config.schema?.params) {
|
|
240
|
-
jsonSchema.body = config.schema.params
|
|
241
|
-
}
|
|
242
|
-
if (config.schema?.returnType) {
|
|
243
|
-
jsonSchema.response = config.schema.returnType
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const base = {
|
|
247
|
-
name: procedure.name,
|
|
248
|
-
version: config.version,
|
|
249
|
-
scope: config.scope,
|
|
250
|
-
path,
|
|
251
|
-
method,
|
|
252
|
-
jsonSchema,
|
|
253
|
-
}
|
|
254
|
-
let extendedDoc: object = {}
|
|
255
|
-
|
|
256
|
-
if (extendProcedureDoc) {
|
|
257
|
-
extendedDoc = extendProcedureDoc({ base, procedure })
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
return {
|
|
261
|
-
...extendedDoc,
|
|
262
|
-
...base,
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { ExtractConfig, ExtractContext, RPCConfig, RPCHttpRouteDoc } from '../../types.js'
|
|
2
|
-
import { Procedures, TProcedureRegistration } from '../../../index.js'
|
|
3
|
-
import express from 'express'
|
|
4
|
-
|
|
5
|
-
export type ExpressFactoryItem<TFactory = ReturnType<typeof Procedures<any, RPCConfig>>> = {
|
|
6
|
-
factory: TFactory
|
|
7
|
-
factoryContext:
|
|
8
|
-
| ExtractContext<TFactory>
|
|
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>
|
|
16
|
-
}
|
|
@@ -1,358 +0,0 @@
|
|
|
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/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' }).register(RPC, (c) => ({
|
|
43
|
-
userId: c.req.header('x-user-id') || 'anonymous',
|
|
44
|
-
}))
|
|
45
|
-
|
|
46
|
-
const app = builder.build()
|
|
47
|
-
|
|
48
|
-
// Bun
|
|
49
|
-
export default app
|
|
50
|
-
|
|
51
|
-
// Node.js
|
|
52
|
-
// import { serve } from '@hono/node-server'
|
|
53
|
-
// serve(app)
|
|
54
|
-
|
|
55
|
-
// POST /rpc/users/profile/get-user/1 → { id: "123", name: "John Doe" }
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## Configuration
|
|
59
|
-
|
|
60
|
-
```typescript
|
|
61
|
-
type HonoRPCAppBuilderConfig = {
|
|
62
|
-
app?: Hono // Existing Hono app (optional)
|
|
63
|
-
pathPrefix?: string // Prefix for all routes (e.g., '/rpc/v1')
|
|
64
|
-
onRequestStart?: (c: Context) => void
|
|
65
|
-
onRequestEnd?: (c: Context) => void
|
|
66
|
-
onSuccess?: (procedure: TProcedureRegistration, c: Context) => void
|
|
67
|
-
error?: (
|
|
68
|
-
procedure: TProcedureRegistration,
|
|
69
|
-
c: Context,
|
|
70
|
-
error: Error
|
|
71
|
-
) => Response | Promise<Response>
|
|
72
|
-
}
|
|
73
|
-
```
|
|
74
|
-
|
|
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) |
|
|
83
|
-
|
|
84
|
-
## Context Resolution
|
|
85
|
-
|
|
86
|
-
The context resolver receives the Hono `Context` object:
|
|
87
|
-
|
|
88
|
-
```typescript
|
|
89
|
-
builder.register(RPC, (c: Context) => ({
|
|
90
|
-
userId: c.req.header('x-user-id') || 'anonymous',
|
|
91
|
-
userAgent: c.req.header('user-agent'),
|
|
92
|
-
ip: c.req.raw.headers.get('cf-connecting-ip'), // Cloudflare
|
|
93
|
-
}))
|
|
94
|
-
|
|
95
|
-
// Async context resolution
|
|
96
|
-
builder.register(RPC, async (c) => {
|
|
97
|
-
const token = c.req.header('authorization')?.replace('Bearer ', '')
|
|
98
|
-
const user = await verifyToken(token)
|
|
99
|
-
return { userId: user.id, roles: user.roles }
|
|
100
|
-
})
|
|
101
|
-
```
|
|
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
|
-
|
|
138
|
-
## Abort Signal
|
|
139
|
-
|
|
140
|
-
`HonoRPCAppBuilder` automatically injects the HTTP request's `AbortSignal` (`c.req.raw.signal`) into the handler context. When a client disconnects mid-request, `ctx.signal` aborts, cancelling any signal-aware operations:
|
|
141
|
-
|
|
142
|
-
```typescript
|
|
143
|
-
RPC.Create(
|
|
144
|
-
'SlowQuery',
|
|
145
|
-
{ scope: 'data', version: 1 },
|
|
146
|
-
async (ctx, params) => {
|
|
147
|
-
// Automatically cancelled if client disconnects
|
|
148
|
-
const response = await fetch('https://slow-api.example.com/data', {
|
|
149
|
-
signal: ctx.signal,
|
|
150
|
-
})
|
|
151
|
-
return response.json()
|
|
152
|
-
}
|
|
153
|
-
)
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
To use `ctx.signal` with type safety, include `signal: AbortSignal` in your context type:
|
|
157
|
-
|
|
158
|
-
```typescript
|
|
159
|
-
type AppContext = { userId: string; signal: AbortSignal }
|
|
160
|
-
const RPC = Procedures<AppContext, RPCConfig>()
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
## Error Handling
|
|
164
|
-
|
|
165
|
-
Custom error handler receives the procedure, context, and error. **Must return a Response:**
|
|
166
|
-
|
|
167
|
-
```typescript
|
|
168
|
-
const builder = new HonoRPCAppBuilder({
|
|
169
|
-
onError: (procedure, c, error) => {
|
|
170
|
-
console.error(`Error in ${procedure.name}:`, error)
|
|
171
|
-
|
|
172
|
-
if (error instanceof ValidationError) {
|
|
173
|
-
return c.json({ error: error.message, code: 'VALIDATION_ERROR' }, 400)
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (error instanceof AuthError) {
|
|
177
|
-
return c.json({ error: 'Unauthorized', code: 'AUTH_ERROR' }, 401)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return c.json({ error: 'Internal server error' }, 500)
|
|
181
|
-
},
|
|
182
|
-
})
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
**Default error handling:** Returns `{ error: message }` with status 500.
|
|
186
|
-
|
|
187
|
-
## Using Existing Hono App
|
|
188
|
-
|
|
189
|
-
You can add RPC routes to an existing Hono application:
|
|
190
|
-
|
|
191
|
-
```typescript
|
|
192
|
-
const app = new Hono()
|
|
193
|
-
|
|
194
|
-
// Add custom middleware and routes
|
|
195
|
-
app.use('*', cors())
|
|
196
|
-
app.get('/custom', (c) => c.json({ custom: true }))
|
|
197
|
-
|
|
198
|
-
const builder = new HonoRPCAppBuilder({ app }).register(RPC, contextResolver)
|
|
199
|
-
|
|
200
|
-
builder.build() // Adds RPC routes to existing app
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
## Runtime Compatibility
|
|
204
|
-
|
|
205
|
-
HonoRPCAppBuilder works across all Hono-supported runtimes:
|
|
206
|
-
|
|
207
|
-
### Bun
|
|
208
|
-
|
|
209
|
-
```typescript
|
|
210
|
-
const app = builder.build()
|
|
211
|
-
export default app
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
### Node.js
|
|
215
|
-
|
|
216
|
-
```typescript
|
|
217
|
-
import { serve } from '@hono/node-server'
|
|
218
|
-
|
|
219
|
-
const app = builder.build()
|
|
220
|
-
serve(app)
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
### Deno
|
|
224
|
-
|
|
225
|
-
```typescript
|
|
226
|
-
import { serve } from 'https://deno.land/std/http/server.ts'
|
|
227
|
-
|
|
228
|
-
const app = builder.build()
|
|
229
|
-
serve(app.fetch)
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
### Cloudflare Workers
|
|
233
|
-
|
|
234
|
-
```typescript
|
|
235
|
-
const app = builder.build()
|
|
236
|
-
export default app
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
## API Reference
|
|
240
|
-
|
|
241
|
-
### Constructor
|
|
242
|
-
|
|
243
|
-
```typescript
|
|
244
|
-
new HonoRPCAppBuilder(config?: HonoRPCAppBuilderConfig)
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
### Methods
|
|
248
|
-
|
|
249
|
-
| Method | Signature | Description |
|
|
250
|
-
| ---------------------- | ------------------------------------------------- | ------------------------------------------------------------------ |
|
|
251
|
-
| `register` | `register<T>(factory, context, options?): this` | Register procedure factory with context and optional doc extension |
|
|
252
|
-
| `build` | `build(): Hono` | Build routes and return app |
|
|
253
|
-
| `makeRPCHttpRoutePath` | `makeRPCHttpRoutePath(config: RPCConfig): string` | Generate route path |
|
|
254
|
-
|
|
255
|
-
### Static Methods
|
|
256
|
-
|
|
257
|
-
| Method | Signature | Description |
|
|
258
|
-
| ---------------------- | --------------------------------------------------------- | -------------------------------------- |
|
|
259
|
-
| `makeRPCHttpRoutePath` | `static makeRPCHttpRoutePath({ config, prefix }): string` | Generate route path with custom prefix |
|
|
260
|
-
|
|
261
|
-
### Properties
|
|
262
|
-
|
|
263
|
-
| Property | Type | Description |
|
|
264
|
-
| -------- | ------------------------- | ------------------------------------- |
|
|
265
|
-
| `app` | `Hono` | The Hono application instance |
|
|
266
|
-
| `docs` | `RPCHttpRouteDoc[]` | Route documentation (after `build()`) |
|
|
267
|
-
| `config` | `HonoRPCAppBuilderConfig` | The configuration object |
|
|
268
|
-
|
|
269
|
-
## TypeScript Types
|
|
270
|
-
|
|
271
|
-
```typescript
|
|
272
|
-
import {
|
|
273
|
-
HonoRPCAppBuilder,
|
|
274
|
-
HonoRPCAppBuilderConfig,
|
|
275
|
-
RPCConfig,
|
|
276
|
-
RPCHttpRouteDoc,
|
|
277
|
-
} from 'ts-procedures/hono-rpc'
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
## Full Example
|
|
281
|
-
|
|
282
|
-
```typescript
|
|
283
|
-
import { Hono, Context } from 'hono'
|
|
284
|
-
import { Procedures } from 'ts-procedures'
|
|
285
|
-
import { HonoRPCAppBuilder, RPCConfig } from 'ts-procedures/hono-rpc'
|
|
286
|
-
import { v } from 'suretype'
|
|
287
|
-
|
|
288
|
-
// Context types
|
|
289
|
-
type PublicContext = { source: 'public' }
|
|
290
|
-
type AuthContext = { source: 'auth'; userId: string }
|
|
291
|
-
|
|
292
|
-
// Create factories
|
|
293
|
-
const PublicRPC = Procedures<PublicContext, RPCConfig>()
|
|
294
|
-
const AuthRPC = Procedures<AuthContext, RPCConfig>()
|
|
295
|
-
|
|
296
|
-
// Public procedures
|
|
297
|
-
PublicRPC.Create('HealthCheck', { scope: 'health', version: 1 }, async () => ({
|
|
298
|
-
status: 'ok',
|
|
299
|
-
}))
|
|
300
|
-
|
|
301
|
-
PublicRPC.Create('GetVersion', { scope: ['system', 'version'], version: 1 }, async () => ({
|
|
302
|
-
version: '1.0.0',
|
|
303
|
-
}))
|
|
304
|
-
|
|
305
|
-
// Authenticated procedures
|
|
306
|
-
AuthRPC.Create(
|
|
307
|
-
'GetProfile',
|
|
308
|
-
{
|
|
309
|
-
scope: ['users', 'profile'],
|
|
310
|
-
version: 1,
|
|
311
|
-
schema: { returnType: v.object({ userId: v.string() }) },
|
|
312
|
-
},
|
|
313
|
-
async (ctx) => ({ userId: ctx.userId })
|
|
314
|
-
)
|
|
315
|
-
|
|
316
|
-
AuthRPC.Create(
|
|
317
|
-
'UpdateProfile',
|
|
318
|
-
{
|
|
319
|
-
scope: ['users', 'profile'],
|
|
320
|
-
version: 2,
|
|
321
|
-
schema: { params: v.object({ name: v.string() }) },
|
|
322
|
-
},
|
|
323
|
-
async (ctx, params) => ({ userId: ctx.userId, name: params.name })
|
|
324
|
-
)
|
|
325
|
-
|
|
326
|
-
// Build app
|
|
327
|
-
const builder = new HonoRPCAppBuilder({
|
|
328
|
-
pathPrefix: '/rpc',
|
|
329
|
-
onRequestStart: (c) => console.log(`→ ${c.req.method} ${c.req.path}`),
|
|
330
|
-
onRequestEnd: (c) => console.log(`← completed`),
|
|
331
|
-
onSuccess: (proc) => console.log(`✓ ${proc.name}`),
|
|
332
|
-
onError: (proc, c, err) => {
|
|
333
|
-
console.error(`✗ ${proc.name}:`, err.message)
|
|
334
|
-
return c.json({ error: err.message }, 500)
|
|
335
|
-
},
|
|
336
|
-
})
|
|
337
|
-
|
|
338
|
-
builder
|
|
339
|
-
.register(PublicRPC, () => ({ source: 'public' as const }))
|
|
340
|
-
.register(AuthRPC, (c) => ({
|
|
341
|
-
source: 'auth' as const,
|
|
342
|
-
userId: c.req.header('x-user-id') || 'anonymous',
|
|
343
|
-
}))
|
|
344
|
-
|
|
345
|
-
const app = builder.build()
|
|
346
|
-
|
|
347
|
-
// Generated routes:
|
|
348
|
-
// POST /rpc/health/1
|
|
349
|
-
// POST /rpc/system/version/get-version/1
|
|
350
|
-
// POST /rpc/users/profile/get-user/1
|
|
351
|
-
// POST /rpc/users/profile/get-user/2
|
|
352
|
-
|
|
353
|
-
console.log(
|
|
354
|
-
'Routes:',
|
|
355
|
-
builder.docs.map((d) => d.path)
|
|
356
|
-
)
|
|
357
|
-
export default app
|
|
358
|
-
```
|