ts-procedures 2.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/errors.d.ts +2 -1
- package/build/errors.js +3 -2
- package/build/errors.js.map +1 -1
- package/build/errors.test.js +40 -0
- package/build/errors.test.js.map +1 -0
- package/build/implementations/http/express-rpc/index.d.ts +36 -35
- package/build/implementations/http/express-rpc/index.js +29 -13
- package/build/implementations/http/express-rpc/index.js.map +1 -1
- package/build/implementations/http/express-rpc/index.test.js +146 -92
- package/build/implementations/http/express-rpc/index.test.js.map +1 -1
- package/build/implementations/http/hono-rpc/index.d.ts +83 -0
- package/build/implementations/http/hono-rpc/index.js +148 -0
- package/build/implementations/http/hono-rpc/index.js.map +1 -0
- package/build/implementations/http/hono-rpc/index.test.js +647 -0
- package/build/implementations/http/hono-rpc/index.test.js.map +1 -0
- package/build/implementations/http/hono-rpc/types.d.ts +28 -0
- package/build/implementations/http/hono-rpc/types.js.map +1 -0
- package/build/implementations/types.d.ts +1 -1
- package/build/index.d.ts +12 -0
- package/build/index.js +29 -7
- package/build/index.js.map +1 -1
- package/build/index.test.js +65 -0
- package/build/index.test.js.map +1 -1
- package/build/schema/parser.js +3 -0
- package/build/schema/parser.js.map +1 -1
- package/build/schema/parser.test.js +18 -0
- package/build/schema/parser.test.js.map +1 -1
- package/package.json +8 -2
- package/src/errors.test.ts +53 -0
- package/src/errors.ts +4 -2
- package/src/implementations/http/README.md +172 -0
- package/src/implementations/http/express-rpc/README.md +151 -228
- package/src/implementations/http/express-rpc/index.test.ts +167 -93
- package/src/implementations/http/express-rpc/index.ts +67 -38
- package/src/implementations/http/hono-rpc/README.md +293 -0
- package/src/implementations/http/hono-rpc/index.test.ts +847 -0
- package/src/implementations/http/hono-rpc/index.ts +202 -0
- package/src/implementations/http/hono-rpc/types.ts +33 -0
- package/src/implementations/types.ts +2 -1
- package/src/index.test.ts +83 -0
- package/src/index.ts +34 -8
- package/src/schema/parser.test.ts +26 -0
- package/src/schema/parser.ts +5 -1
- package/build/implementations/http/client/index.js +0 -2
- package/build/implementations/http/client/index.js.map +0 -1
- package/build/implementations/http/express/example/factories.d.ts +0 -97
- package/build/implementations/http/express/example/factories.js +0 -4
- package/build/implementations/http/express/example/factories.js.map +0 -1
- package/build/implementations/http/express/example/procedures/auth.d.ts +0 -1
- package/build/implementations/http/express/example/procedures/auth.js +0 -22
- package/build/implementations/http/express/example/procedures/auth.js.map +0 -1
- package/build/implementations/http/express/example/procedures/users.d.ts +0 -1
- package/build/implementations/http/express/example/procedures/users.js +0 -30
- package/build/implementations/http/express/example/procedures/users.js.map +0 -1
- package/build/implementations/http/express/example/server.d.ts +0 -3
- package/build/implementations/http/express/example/server.js +0 -49
- package/build/implementations/http/express/example/server.js.map +0 -1
- package/build/implementations/http/express/example/server.test.d.ts +0 -1
- package/build/implementations/http/express/example/server.test.js +0 -110
- package/build/implementations/http/express/example/server.test.js.map +0 -1
- package/build/implementations/http/express/index.d.ts +0 -35
- package/build/implementations/http/express/index.js +0 -75
- package/build/implementations/http/express/index.js.map +0 -1
- package/build/implementations/http/express/index.test.js +0 -329
- package/build/implementations/http/express/index.test.js.map +0 -1
- package/build/implementations/http/express/types.d.ts +0 -17
- package/build/implementations/http/express/types.js.map +0 -1
- /package/build/{implementations/http/client/index.d.ts → errors.test.d.ts} +0 -0
- /package/build/implementations/http/{express → hono-rpc}/index.test.d.ts +0 -0
- /package/build/implementations/http/{express → hono-rpc}/types.js +0 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# HTTP-RPC Implementations
|
|
2
|
+
|
|
3
|
+
HTTP-RPC builders for `ts-procedures` that create type-safe, versioned RPC endpoints with automatic path generation, schema-based validation, and route documentation.
|
|
4
|
+
|
|
5
|
+
## Available Implementations
|
|
6
|
+
|
|
7
|
+
| Framework | Package | Description |
|
|
8
|
+
|-----------|---------|-------------|
|
|
9
|
+
| [Express](./express-rpc/README.md) | `express-rpc` | Express.js integration |
|
|
10
|
+
| [Hono](./hono-rpc/README.md) | `hono-rpc` | Hono integration (Bun, Deno, Cloudflare Workers, Node.js) |
|
|
11
|
+
|
|
12
|
+
## Core Concepts
|
|
13
|
+
|
|
14
|
+
### RPCConfig Interface
|
|
15
|
+
|
|
16
|
+
All HTTP-RPC implementations use a shared configuration interface:
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
interface RPCConfig {
|
|
20
|
+
scope: string | string[] // Route path segment(s)
|
|
21
|
+
version: number // API version number
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Path Generation
|
|
26
|
+
|
|
27
|
+
Routes are generated using kebab-case conversion with the formula:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
/{pathPrefix}/{scope...}/{procedureName}/{version}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Conversion Examples:**
|
|
34
|
+
|
|
35
|
+
| Scope | Procedure Name | Version | Generated Path |
|
|
36
|
+
|-------|----------------|---------|----------------|
|
|
37
|
+
| `'users'` | `'Create'` | `1` | `/users/create/1` |
|
|
38
|
+
| `'users'` | `'GetById'` | `1` | `/users/get-by-id/1` |
|
|
39
|
+
| `['users', 'admin']` | `'List'` | `1` | `/users/admin/list/1` |
|
|
40
|
+
| `['UserModule', 'permissions']` | `'Update'` | `2` | `/user-module/permissions/update/2` |
|
|
41
|
+
|
|
42
|
+
**With pathPrefix `/api/v1`:**
|
|
43
|
+
|
|
44
|
+
| Scope | Procedure Name | Version | Generated Path |
|
|
45
|
+
|-------|----------------|---------|----------------|
|
|
46
|
+
| `'users'` | `'Create'` | `1` | `/api/v1/users/create/1` |
|
|
47
|
+
| `['users', 'admin']` | `'Delete'` | `2` | `/api/v1/users/admin/delete/2` |
|
|
48
|
+
|
|
49
|
+
### Context Resolution Patterns
|
|
50
|
+
|
|
51
|
+
The `factoryContext` parameter supports three patterns:
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
// 1. Static object
|
|
55
|
+
builder.register(RPC, { userId: 'static-123' })
|
|
56
|
+
|
|
57
|
+
// 2. Sync function
|
|
58
|
+
builder.register(RPC, (req) => ({
|
|
59
|
+
userId: req.headers['x-user-id']
|
|
60
|
+
}))
|
|
61
|
+
|
|
62
|
+
// 3. Async function
|
|
63
|
+
builder.register(RPC, async (req) => {
|
|
64
|
+
const user = await validateToken(req.headers.authorization)
|
|
65
|
+
return { userId: user.id }
|
|
66
|
+
})
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Lifecycle Hooks
|
|
70
|
+
|
|
71
|
+
Hooks execute in the following order:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
onRequestStart → handler → onSuccess → onRequestEnd
|
|
75
|
+
↓
|
|
76
|
+
(on error)
|
|
77
|
+
↓
|
|
78
|
+
error handler
|
|
79
|
+
↓
|
|
80
|
+
onRequestEnd
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
| Hook | Trigger | Use Case |
|
|
84
|
+
|------|---------|----------|
|
|
85
|
+
| `onRequestStart` | Before route handler | Logging, request tracking |
|
|
86
|
+
| `onSuccess` | After successful handler execution | Metrics, audit logging |
|
|
87
|
+
| `onRequestEnd` | After response sent | Cleanup, timing metrics |
|
|
88
|
+
| `error` | On handler error | Custom error responses |
|
|
89
|
+
|
|
90
|
+
### Route Documentation
|
|
91
|
+
|
|
92
|
+
Each registered procedure generates an `RPCHttpRouteDoc`:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
interface RPCHttpRouteDoc {
|
|
96
|
+
path: string // Generated route path
|
|
97
|
+
method: 'post' // Always POST for RPC
|
|
98
|
+
jsonSchema: {
|
|
99
|
+
body?: object // JSON Schema from schema.params
|
|
100
|
+
response?: object // JSON Schema from schema.returnType
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Access documentation via `builder.docs` after calling `build()`:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
const builder = new ExpressRPCAppBuilder()
|
|
109
|
+
builder.register(RPC, () => ({}))
|
|
110
|
+
builder.build()
|
|
111
|
+
|
|
112
|
+
// Generate OpenAPI documentation
|
|
113
|
+
const openApiPaths = builder.docs.reduce((acc, doc) => {
|
|
114
|
+
acc[doc.path] = {
|
|
115
|
+
post: {
|
|
116
|
+
requestBody: doc.jsonSchema.body ? {
|
|
117
|
+
content: { 'application/json': { schema: doc.jsonSchema.body } }
|
|
118
|
+
} : undefined,
|
|
119
|
+
responses: {
|
|
120
|
+
200: doc.jsonSchema.response ? {
|
|
121
|
+
content: { 'application/json': { schema: doc.jsonSchema.response } }
|
|
122
|
+
} : undefined
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return acc
|
|
127
|
+
}, {})
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Builder Pattern
|
|
131
|
+
|
|
132
|
+
All implementations follow the same builder pattern:
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
const builder = new RPCAppBuilder(config)
|
|
136
|
+
.register(PublicRPC, publicContextResolver)
|
|
137
|
+
.register(ProtectedRPC, protectedContextResolver)
|
|
138
|
+
|
|
139
|
+
const app = builder.build()
|
|
140
|
+
const docs = builder.docs
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Key methods:**
|
|
144
|
+
|
|
145
|
+
| Method | Returns | Description |
|
|
146
|
+
|--------|---------|-------------|
|
|
147
|
+
| `register(factory, context)` | `this` | Register a procedure factory with context resolver |
|
|
148
|
+
| `build()` | Framework app | Create routes and return the application |
|
|
149
|
+
| `makeRPCHttpRoutePath(config)` | `string` | Generate path for an RPCConfig |
|
|
150
|
+
|
|
151
|
+
**Properties:**
|
|
152
|
+
|
|
153
|
+
| Property | Type | Description |
|
|
154
|
+
|----------|------|-------------|
|
|
155
|
+
| `app` | Framework app | The underlying framework application |
|
|
156
|
+
| `docs` | `RPCHttpRouteDoc[]` | Route documentation (populated after `build()`) |
|
|
157
|
+
|
|
158
|
+
## Framework Comparison
|
|
159
|
+
|
|
160
|
+
| Aspect | Express | Hono |
|
|
161
|
+
|--------|---------|------|
|
|
162
|
+
| Context param | `req: express.Request` | `c: Context` |
|
|
163
|
+
| Error handler return | `void` (mutates res) | `Response` |
|
|
164
|
+
| Body access | `req.body` | `await c.req.json()` |
|
|
165
|
+
| Header access | `req.headers['x-id']` | `c.req.header('x-id')` |
|
|
166
|
+
| JSON middleware | Auto-added (or manual) | Built-in |
|
|
167
|
+
|
|
168
|
+
## TypeScript Types
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
import { RPCConfig, RPCHttpRouteDoc } from 'ts-procedures/implementations/types'
|
|
172
|
+
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ExpressRPCAppBuilder
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Express.js integration for `ts-procedures` that creates type-safe RPC endpoints as POST routes.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -8,25 +8,17 @@ RPC-style HTTP integration for `ts-procedures` using Express. Creates POST route
|
|
|
8
8
|
npm install ts-procedures express
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
## Import
|
|
12
|
-
|
|
13
|
-
```typescript
|
|
14
|
-
import { ExpressRPCAppBuilder } from 'ts-procedures/express-rpc'
|
|
15
|
-
```
|
|
16
|
-
|
|
17
11
|
## Quick Start
|
|
18
12
|
|
|
19
13
|
```typescript
|
|
14
|
+
import express from 'express'
|
|
20
15
|
import { Procedures } from 'ts-procedures'
|
|
21
|
-
import { ExpressRPCAppBuilder, RPCConfig } from 'ts-procedures/express-rpc'
|
|
16
|
+
import { ExpressRPCAppBuilder, RPCConfig } from 'ts-procedures/implementations/http/express-rpc'
|
|
22
17
|
import { v } from 'suretype'
|
|
23
18
|
|
|
24
19
|
// Define your context type
|
|
25
20
|
type AppContext = { userId: string }
|
|
26
21
|
|
|
27
|
-
// RPC config type
|
|
28
|
-
// type RPCConfig = { name: string | string[]; version: number }
|
|
29
|
-
|
|
30
22
|
// Create a procedure factory
|
|
31
23
|
const RPC = Procedures<AppContext, RPCConfig>()
|
|
32
24
|
|
|
@@ -34,12 +26,12 @@ const RPC = Procedures<AppContext, RPCConfig>()
|
|
|
34
26
|
RPC.Create(
|
|
35
27
|
'GetUser',
|
|
36
28
|
{
|
|
37
|
-
|
|
29
|
+
scope: ['users', 'profile'],
|
|
38
30
|
version: 1,
|
|
39
31
|
schema: {
|
|
40
32
|
params: v.object({ id: v.string() }),
|
|
41
|
-
returnType: v.object({ id: v.string(), name: v.string() })
|
|
42
|
-
}
|
|
33
|
+
returnType: v.object({ id: v.string(), name: v.string() })
|
|
34
|
+
}
|
|
43
35
|
},
|
|
44
36
|
async (ctx, params) => {
|
|
45
37
|
return { id: params.id, name: 'John Doe' }
|
|
@@ -47,285 +39,216 @@ RPC.Create(
|
|
|
47
39
|
)
|
|
48
40
|
|
|
49
41
|
// Build the Express app
|
|
50
|
-
const builder = new ExpressRPCAppBuilder()
|
|
51
|
-
.register(RPC, (req) => ({
|
|
52
|
-
|
|
42
|
+
const builder = new ExpressRPCAppBuilder({ pathPrefix: '/rpc' })
|
|
43
|
+
.register(RPC, (req) => ({
|
|
44
|
+
userId: req.headers['x-user-id'] as string
|
|
45
|
+
}))
|
|
53
46
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
console.log('RPC server running on http://localhost:3000')
|
|
57
|
-
})
|
|
47
|
+
const app = builder.build()
|
|
48
|
+
app.listen(3000)
|
|
58
49
|
|
|
59
|
-
//
|
|
50
|
+
// POST /rpc/users/profile/get-user/1 → { id: "123", name: "John Doe" }
|
|
60
51
|
```
|
|
61
52
|
|
|
62
|
-
##
|
|
63
|
-
|
|
64
|
-
### `ExpressRPCAppBuilder`
|
|
65
|
-
|
|
66
|
-
Builder class for creating an Express application with RPC routes.
|
|
67
|
-
|
|
68
|
-
#### Constructor
|
|
53
|
+
## Configuration
|
|
69
54
|
|
|
70
55
|
```typescript
|
|
71
|
-
|
|
72
|
-
app?: express.Express
|
|
56
|
+
type ExpressRPCAppBuilderConfig = {
|
|
57
|
+
app?: express.Express // Existing Express app (optional)
|
|
58
|
+
pathPrefix?: string // Prefix for all routes (e.g., '/rpc/v1')
|
|
73
59
|
onRequestStart?: (req: express.Request) => void
|
|
74
60
|
onRequestEnd?: (req: express.Request, res: express.Response) => void
|
|
75
61
|
onSuccess?: (procedure: TProcedureRegistration, req: express.Request, res: express.Response) => void
|
|
76
62
|
error?: (procedure: TProcedureRegistration, req: express.Request, res: express.Response, error: Error) => void
|
|
77
|
-
}
|
|
63
|
+
}
|
|
78
64
|
```
|
|
79
65
|
|
|
80
|
-
| Option | Type | Description
|
|
81
|
-
|
|
82
|
-
| `app` | `express.Express` |
|
|
83
|
-
| `
|
|
84
|
-
| `
|
|
85
|
-
| `
|
|
86
|
-
| `
|
|
66
|
+
| Option | Type | Description |
|
|
67
|
+
|--------|------|------------------------------------------------------|
|
|
68
|
+
| `app` | `express.Express` | Use existing Express app instead of creating new one |
|
|
69
|
+
| `pathPrefix` | `string` | Prefix all routes (e.g., `/rpc/v1`) |
|
|
70
|
+
| `onRequestStart` | `(req) => void` | Called at start of each request |
|
|
71
|
+
| `onRequestEnd` | `(req, res) => void` | Called after response finishes |
|
|
72
|
+
| `onSuccess` | `(proc, req, res) => void` | Called on successful handler execution |
|
|
73
|
+
| `error` | `(proc, req, res, err) => void` | Custom error handler |
|
|
87
74
|
|
|
88
|
-
|
|
75
|
+
## Context Resolution
|
|
89
76
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
Registers a procedure factory with its context.
|
|
77
|
+
The context resolver receives the Express `Request` object:
|
|
93
78
|
|
|
94
79
|
```typescript
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
80
|
+
builder.register(RPC, (req: express.Request) => ({
|
|
81
|
+
userId: req.headers['x-user-id'] as string,
|
|
82
|
+
sessionId: req.cookies?.sessionId,
|
|
83
|
+
ip: req.ip
|
|
84
|
+
}))
|
|
100
85
|
|
|
101
|
-
// Async
|
|
86
|
+
// Async context resolution
|
|
102
87
|
builder.register(RPC, async (req) => {
|
|
103
|
-
const
|
|
104
|
-
|
|
88
|
+
const token = req.headers.authorization?.replace('Bearer ', '')
|
|
89
|
+
const user = await verifyToken(token)
|
|
90
|
+
return { userId: user.id, roles: user.roles }
|
|
105
91
|
})
|
|
106
92
|
```
|
|
107
93
|
|
|
108
|
-
|
|
109
|
-
- **factoryContext**: The context for procedure handlers. Can be:
|
|
110
|
-
- A direct value matching the factory's context type
|
|
111
|
-
- A sync function `(req: express.Request) => Context`
|
|
112
|
-
- An async function `(req: express.Request) => Promise<Context>`
|
|
113
|
-
- **Returns**: `this` for method chaining
|
|
114
|
-
|
|
115
|
-
##### `build(): express.Application`
|
|
94
|
+
## Error Handling
|
|
116
95
|
|
|
117
|
-
|
|
96
|
+
Custom error handler receives the procedure, request, response, and error:
|
|
118
97
|
|
|
119
98
|
```typescript
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
#### Properties
|
|
125
|
-
|
|
126
|
-
##### `app: express.Express`
|
|
127
|
-
|
|
128
|
-
The underlying Express application instance.
|
|
99
|
+
const builder = new ExpressRPCAppBuilder({
|
|
100
|
+
error: (procedure, req, res, error) => {
|
|
101
|
+
console.error(`Error in ${procedure.name}:`, error)
|
|
129
102
|
|
|
130
|
-
|
|
103
|
+
if (error instanceof ValidationError) {
|
|
104
|
+
res.status(400).json({ error: error.message, code: 'VALIDATION_ERROR' })
|
|
105
|
+
return
|
|
106
|
+
}
|
|
131
107
|
|
|
132
|
-
|
|
108
|
+
if (error instanceof AuthError) {
|
|
109
|
+
res.status(401).json({ error: 'Unauthorized', code: 'AUTH_ERROR' })
|
|
110
|
+
return
|
|
111
|
+
}
|
|
133
112
|
|
|
134
|
-
|
|
135
|
-
interface RPCHttpRouteDoc {
|
|
136
|
-
path: string // e.g., '/rpc/users/get/1'
|
|
137
|
-
method: 'post'
|
|
138
|
-
jsonSchema: {
|
|
139
|
-
body?: object // JSON Schema for request params
|
|
140
|
-
response?: object // JSON Schema for return type
|
|
113
|
+
res.status(500).json({ error: 'Internal server error' })
|
|
141
114
|
}
|
|
142
|
-
}
|
|
115
|
+
})
|
|
143
116
|
```
|
|
144
117
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
Generates the RPC route path from config. Exposed for testing/utilities.
|
|
148
|
-
|
|
149
|
-
### `buildRpcHttpRouteDoc(procedure): RPCHttpRouteDoc`
|
|
150
|
-
|
|
151
|
-
Generates route documentation for a procedure. Exposed for testing/utilities.
|
|
152
|
-
|
|
153
|
-
## Route Path Generation
|
|
154
|
-
|
|
155
|
-
Routes are generated at `/rpc/{name-segments}/{version}`:
|
|
118
|
+
**Default error handling:** Returns `{ error: message }` with status 500.
|
|
156
119
|
|
|
157
|
-
|
|
158
|
-
|-------------|---------|----------------|
|
|
159
|
-
| `'users'` | `1` | `/rpc/users/1` |
|
|
160
|
-
| `['users', 'get-by-id']` | `1` | `/rpc/users/get-by-id/1` |
|
|
161
|
-
| `'getUserById'` | `2` | `/rpc/get-user-by-id/2` |
|
|
162
|
-
| `'GetUserById'` | `1` | `/rpc/get-user-by-id/1` |
|
|
120
|
+
## Using Existing Express App
|
|
163
121
|
|
|
164
|
-
|
|
165
|
-
- Array names create nested path segments
|
|
166
|
-
- Version is appended as the final segment (raw number, not `v1`)
|
|
167
|
-
|
|
168
|
-
## Multiple Factories
|
|
169
|
-
|
|
170
|
-
Register multiple factories with different contexts:
|
|
122
|
+
When providing an existing Express app, **you must set up JSON parsing middleware**:
|
|
171
123
|
|
|
172
124
|
```typescript
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const AuthRPC = Procedures<AuthContext, RPCConfig>()
|
|
178
|
-
|
|
179
|
-
// Define public procedures
|
|
180
|
-
PublicRPC.Create('HealthCheck', { name: 'health', version: 1 }, async () => ({ status: 'ok' }))
|
|
125
|
+
const app = express()
|
|
126
|
+
app.use(express.json()) // Required!
|
|
127
|
+
app.use(cors())
|
|
128
|
+
app.use(helmet())
|
|
181
129
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
userId: ctx.userId,
|
|
185
|
-
}))
|
|
130
|
+
const builder = new ExpressRPCAppBuilder({ app })
|
|
131
|
+
.register(RPC, contextResolver)
|
|
186
132
|
|
|
187
|
-
//
|
|
188
|
-
const app = new ExpressRPCAppBuilder()
|
|
189
|
-
.register(PublicRPC, { source: 'public' }) // Direct value
|
|
190
|
-
.register(AuthRPC, (req) => ({ // Sync function
|
|
191
|
-
source: 'auth',
|
|
192
|
-
userId: req.headers['x-user-id'] as string,
|
|
193
|
-
}))
|
|
194
|
-
.build()
|
|
133
|
+
builder.build() // Adds RPC routes to existing app
|
|
195
134
|
```
|
|
196
135
|
|
|
197
|
-
|
|
136
|
+
When no `app` is provided, `express.json()` middleware is added automatically.
|
|
198
137
|
|
|
199
|
-
|
|
200
|
-
const app = new ExpressRPCAppBuilder({
|
|
201
|
-
onRequestStart: (req) => {
|
|
202
|
-
console.log(`[${req.method}] ${req.path}`)
|
|
203
|
-
},
|
|
138
|
+
## API Reference
|
|
204
139
|
|
|
205
|
-
|
|
206
|
-
console.log(`Response: ${res.statusCode}`)
|
|
207
|
-
},
|
|
140
|
+
### Constructor
|
|
208
141
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
},
|
|
212
|
-
})
|
|
142
|
+
```typescript
|
|
143
|
+
new ExpressRPCAppBuilder(config?: ExpressRPCAppBuilderConfig)
|
|
213
144
|
```
|
|
214
145
|
|
|
215
|
-
|
|
146
|
+
### Methods
|
|
216
147
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
```typescript
|
|
224
|
-
const app = new ExpressRPCAppBuilder({
|
|
225
|
-
error: (procedure, req, res, error) => {
|
|
226
|
-
console.error(`Error in ${procedure.name}:`, error.message)
|
|
227
|
-
|
|
228
|
-
if (error instanceof ProcedureValidationError) {
|
|
229
|
-
res.status(400).json({ error: 'Validation failed', details: error.errors })
|
|
230
|
-
} else if (error instanceof ProcedureError) {
|
|
231
|
-
res.status(422).json({ error: error.message })
|
|
232
|
-
} else {
|
|
233
|
-
res.status(500).json({ error: 'Internal server error' })
|
|
234
|
-
}
|
|
235
|
-
},
|
|
236
|
-
})
|
|
237
|
-
```
|
|
148
|
+
| Method | Signature | Description |
|
|
149
|
+
|--------|-----------|-------------|
|
|
150
|
+
| `register` | `register<T>(factory, context): this` | Register procedure factory with context |
|
|
151
|
+
| `build` | `build(): express.Application` | Build routes and return app |
|
|
152
|
+
| `makeRPCHttpRoutePath` | `makeRPCHttpRoutePath(config: RPCConfig): string` | Generate route path |
|
|
238
153
|
|
|
239
|
-
###
|
|
154
|
+
### Static Methods
|
|
240
155
|
|
|
241
|
-
|
|
156
|
+
| Method | Signature | Description |
|
|
157
|
+
|--------|-----------|-------------|
|
|
158
|
+
| `makeRPCHttpRoutePath` | `static makeRPCHttpRoutePath({ config, prefix }): string` | Generate route path with custom prefix |
|
|
242
159
|
|
|
243
|
-
|
|
244
|
-
{ "error": "Error message here" }
|
|
245
|
-
```
|
|
160
|
+
### Properties
|
|
246
161
|
|
|
247
|
-
|
|
162
|
+
| Property | Type | Description |
|
|
163
|
+
|----------|------|-------------|
|
|
164
|
+
| `app` | `express.Express` | The Express application instance |
|
|
165
|
+
| `docs` | `RPCHttpRouteDoc[]` | Route documentation (after `build()`) |
|
|
166
|
+
| `config` | `ExpressRPCAppBuilderConfig` | The configuration object |
|
|
248
167
|
|
|
249
|
-
|
|
168
|
+
## TypeScript Types
|
|
250
169
|
|
|
251
170
|
```typescript
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
// [
|
|
259
|
-
// {
|
|
260
|
-
// path: '/rpc/users/get/1',
|
|
261
|
-
// method: 'post',
|
|
262
|
-
// jsonSchema: {
|
|
263
|
-
// body: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
|
|
264
|
-
// response: { type: 'object', properties: { id: { type: 'string' }, name: { type: 'string' } } }
|
|
265
|
-
// }
|
|
266
|
-
// }
|
|
267
|
-
// ]
|
|
171
|
+
import {
|
|
172
|
+
ExpressRPCAppBuilder,
|
|
173
|
+
ExpressRPCAppBuilderConfig,
|
|
174
|
+
RPCConfig,
|
|
175
|
+
RPCHttpRouteDoc
|
|
176
|
+
} from 'ts-procedures/implementations/http/express-rpc'
|
|
268
177
|
```
|
|
269
178
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
## Using an Existing Express App
|
|
179
|
+
## Full Example
|
|
273
180
|
|
|
274
181
|
```typescript
|
|
275
182
|
import express from 'express'
|
|
183
|
+
import { Procedures } from 'ts-procedures'
|
|
184
|
+
import { ExpressRPCAppBuilder, RPCConfig } from 'ts-procedures/implementations/http/express-rpc'
|
|
185
|
+
import { v } from 'suretype'
|
|
276
186
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
app.use(express.json())
|
|
281
|
-
app.use(cors())
|
|
282
|
-
app.use(helmet())
|
|
283
|
-
|
|
284
|
-
// Mount RPC routes
|
|
285
|
-
const rpcApp = new ExpressRPCAppBuilder({ app })
|
|
286
|
-
.register(RPC, factoryContext)
|
|
287
|
-
.build()
|
|
288
|
-
|
|
289
|
-
// Add other routes
|
|
290
|
-
app.get('/health', (req, res) => res.json({ status: 'ok' }))
|
|
187
|
+
// Context types
|
|
188
|
+
type PublicContext = { source: 'public' }
|
|
189
|
+
type AuthContext = { source: 'auth'; userId: string }
|
|
291
190
|
|
|
292
|
-
|
|
293
|
-
|
|
191
|
+
// Create factories
|
|
192
|
+
const PublicRPC = Procedures<PublicContext, RPCConfig>()
|
|
193
|
+
const AuthRPC = Procedures<AuthContext, RPCConfig>()
|
|
294
194
|
|
|
295
|
-
|
|
195
|
+
// Public procedures
|
|
196
|
+
PublicRPC.Create('HealthCheck', { scope: 'health', version: 1 }, async () => ({
|
|
197
|
+
status: 'ok'
|
|
198
|
+
}))
|
|
296
199
|
|
|
297
|
-
|
|
200
|
+
PublicRPC.Create('GetVersion', { scope: ['system', 'version'], version: 1 }, async () => ({
|
|
201
|
+
version: '1.0.0'
|
|
202
|
+
}))
|
|
298
203
|
|
|
299
|
-
|
|
300
|
-
|
|
204
|
+
// Authenticated procedures
|
|
205
|
+
AuthRPC.Create(
|
|
206
|
+
'GetProfile',
|
|
207
|
+
{
|
|
208
|
+
scope: ['users', 'profile'],
|
|
209
|
+
version: 1,
|
|
210
|
+
schema: { returnType: v.object({ userId: v.string() }) }
|
|
211
|
+
},
|
|
212
|
+
async (ctx) => ({ userId: ctx.userId })
|
|
213
|
+
)
|
|
301
214
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
215
|
+
AuthRPC.Create(
|
|
216
|
+
'UpdateProfile',
|
|
217
|
+
{
|
|
218
|
+
scope: ['users', 'profile'],
|
|
219
|
+
version: 2,
|
|
220
|
+
schema: { params: v.object({ name: v.string() }) }
|
|
221
|
+
},
|
|
222
|
+
async (ctx, params) => ({ userId: ctx.userId, name: params.name })
|
|
223
|
+
)
|
|
307
224
|
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
225
|
+
// Build app
|
|
226
|
+
const builder = new ExpressRPCAppBuilder({
|
|
227
|
+
pathPrefix: '/rpc',
|
|
228
|
+
onRequestStart: (req) => console.log(`→ ${req.method} ${req.path}`),
|
|
229
|
+
onRequestEnd: (req, res) => console.log(`← ${res.statusCode}`),
|
|
230
|
+
onSuccess: (proc) => console.log(`✓ ${proc.name}`),
|
|
231
|
+
error: (proc, req, res, err) => {
|
|
232
|
+
console.error(`✗ ${proc.name}:`, err.message)
|
|
233
|
+
res.status(500).json({ error: err.message })
|
|
315
234
|
}
|
|
316
|
-
}
|
|
317
|
-
```
|
|
235
|
+
})
|
|
318
236
|
|
|
319
|
-
|
|
237
|
+
builder
|
|
238
|
+
.register(PublicRPC, () => ({ source: 'public' as const }))
|
|
239
|
+
.register(AuthRPC, (req) => ({
|
|
240
|
+
source: 'auth' as const,
|
|
241
|
+
userId: req.headers['x-user-id'] as string || 'anonymous'
|
|
242
|
+
}))
|
|
320
243
|
|
|
321
|
-
|
|
244
|
+
const app = builder.build()
|
|
322
245
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
246
|
+
// Generated routes:
|
|
247
|
+
// POST /rpc/health/1
|
|
248
|
+
// POST /rpc/system/version/get-version/1
|
|
249
|
+
// POST /rpc/users/profile/get-user/1
|
|
250
|
+
// POST /rpc/users/profile/get-user/2
|
|
328
251
|
|
|
329
|
-
|
|
330
|
-
|
|
252
|
+
console.log('Routes:', builder.docs.map(d => d.path))
|
|
253
|
+
app.listen(3000)
|
|
331
254
|
```
|