ts-procedures 1.1.0 → 2.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/README.md +3 -3
- package/build/implementations/http/client/index.d.ts +1 -0
- package/build/implementations/http/client/index.js +2 -0
- package/build/implementations/http/client/index.js.map +1 -0
- package/build/implementations/http/express/index.d.ts +2 -1
- package/build/implementations/http/express/index.js.map +1 -1
- package/build/implementations/http/express/types.d.ts +17 -0
- package/build/implementations/http/express/types.js +2 -0
- package/build/implementations/http/express/types.js.map +1 -0
- package/build/implementations/http/express-rpc/index.d.ts +82 -0
- package/build/implementations/http/express-rpc/index.js +140 -0
- package/build/implementations/http/express-rpc/index.js.map +1 -0
- package/build/implementations/http/express-rpc/index.test.d.ts +1 -0
- package/build/implementations/http/express-rpc/index.test.js +445 -0
- package/build/implementations/http/express-rpc/index.test.js.map +1 -0
- package/build/implementations/http/express-rpc/types.d.ts +28 -0
- package/build/implementations/http/express-rpc/types.js +2 -0
- package/build/implementations/http/express-rpc/types.js.map +1 -0
- package/build/implementations/types.d.ts +17 -0
- package/build/implementations/types.js +2 -0
- package/build/implementations/types.js.map +1 -0
- package/build/schema/parser.js +2 -1
- package/build/schema/parser.js.map +1 -1
- package/package.json +13 -7
- package/src/implementations/http/express-rpc/README.md +321 -0
- package/src/implementations/http/express-rpc/index.test.ts +614 -0
- package/src/implementations/http/express-rpc/index.ts +180 -0
- package/src/implementations/http/express-rpc/types.ts +29 -0
- package/src/implementations/types.ts +20 -0
- package/src/schema/parser.ts +5 -4
- package/src/schema/types.ts +0 -1
- package/src/implementations/http/express/README.md +0 -351
- package/src/implementations/http/express/example/factories.ts +0 -25
- package/src/implementations/http/express/example/procedures/auth.ts +0 -24
- package/src/implementations/http/express/example/procedures/users.ts +0 -32
- package/src/implementations/http/express/example/server.test.ts +0 -133
- package/src/implementations/http/express/example/server.ts +0 -67
- package/src/implementations/http/express/index.test.ts +0 -526
- package/src/implementations/http/express/index.ts +0 -108
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import { kebabCase } from 'es-toolkit/string'
|
|
3
|
+
import { Procedures, TProcedureRegistration } from '../../../index.js'
|
|
4
|
+
import { RPCConfig, RPCHttpRouteDoc } from '../../types.js'
|
|
5
|
+
import { castArray } from 'es-toolkit/compat'
|
|
6
|
+
import { ExpressFactoryItem, ExtractContext, ProceduresFactory } from './types.js'
|
|
7
|
+
|
|
8
|
+
export type { RPCConfig, RPCHttpRouteDoc }
|
|
9
|
+
/**
|
|
10
|
+
* Builder class for creating an Express application with RPC routes.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* const PublicRPC = Procedures<PublicRPCContext, RPCConfig>()
|
|
14
|
+
* const ProtectedRPC = Procedures<ProtectedRPCContext, RPCConfig>()
|
|
15
|
+
*
|
|
16
|
+
* const rpcApp = new ExpressRPCAppBuilder()
|
|
17
|
+
* .register(PublicRPC, (req): Promise<PublicRPCContext> => { /* context resolution logic * / })
|
|
18
|
+
* .register(ProtectedRPC, (req): Promise<ProtectedRPCContext> => { /* context resolution logic * / })
|
|
19
|
+
* .build();
|
|
20
|
+
*
|
|
21
|
+
* const app = rpcApp.app; // Express application
|
|
22
|
+
* const docs = rpcApp.docs; // RPC route documentation
|
|
23
|
+
*/
|
|
24
|
+
export class ExpressRPCAppBuilder {
|
|
25
|
+
/**
|
|
26
|
+
* Constructor for ExpressRPCAppBuilder.
|
|
27
|
+
*
|
|
28
|
+
* @param config
|
|
29
|
+
*/
|
|
30
|
+
constructor(
|
|
31
|
+
readonly config?: {
|
|
32
|
+
/**
|
|
33
|
+
* An existing Express application instance to use.
|
|
34
|
+
* When provided, ensure to set up necessary middleware (e.g., json/body parser) beforehand.
|
|
35
|
+
* If not provided, a new instance will be created.
|
|
36
|
+
*/
|
|
37
|
+
app?: express.Express
|
|
38
|
+
onRequestStart?: (req: express.Request) => void
|
|
39
|
+
onRequestEnd?: (req: express.Request, res: express.Response) => void
|
|
40
|
+
onSuccess?: (
|
|
41
|
+
procedure: TProcedureRegistration,
|
|
42
|
+
req: express.Request,
|
|
43
|
+
res: express.Response
|
|
44
|
+
) => void
|
|
45
|
+
error?: (
|
|
46
|
+
procedure: TProcedureRegistration,
|
|
47
|
+
req: express.Request,
|
|
48
|
+
res: express.Response,
|
|
49
|
+
error: Error
|
|
50
|
+
) => void
|
|
51
|
+
}
|
|
52
|
+
) {
|
|
53
|
+
if (config?.app) {
|
|
54
|
+
this._app = config.app
|
|
55
|
+
} else {
|
|
56
|
+
// Default middleware if no app is provided
|
|
57
|
+
this._app.use(express.json())
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (config?.onRequestStart) {
|
|
61
|
+
this._app.use((req, res, next) => {
|
|
62
|
+
config.onRequestStart!(req)
|
|
63
|
+
next()
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (config?.onRequestEnd) {
|
|
68
|
+
this._app.use((req, res, next) => {
|
|
69
|
+
res.on('finish', () => {
|
|
70
|
+
config.onRequestEnd!(req, res)
|
|
71
|
+
})
|
|
72
|
+
next()
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private factories: ExpressFactoryItem<any>[] = []
|
|
78
|
+
|
|
79
|
+
private _app: express.Express = express()
|
|
80
|
+
private _docs: RPCHttpRouteDoc[] = []
|
|
81
|
+
|
|
82
|
+
get app(): express.Express {
|
|
83
|
+
return this._app
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get docs(): RPCHttpRouteDoc[] {
|
|
87
|
+
return this._docs
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Registers a procedure factory with its context resolver.
|
|
92
|
+
* @param factory
|
|
93
|
+
* @param contextResolver
|
|
94
|
+
*/
|
|
95
|
+
register<TFactory extends ProceduresFactory>(
|
|
96
|
+
factory: TFactory,
|
|
97
|
+
contextResolver: (req: express.Request) => ExtractContext<TFactory>
|
|
98
|
+
): this {
|
|
99
|
+
this.factories.push({ factory, contextResolver } as ExpressFactoryItem<any>)
|
|
100
|
+
return this
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Builds and returns the Express application with registered RPC routes.
|
|
105
|
+
* @return express.Application
|
|
106
|
+
*/
|
|
107
|
+
build(): express.Application {
|
|
108
|
+
this.factories.forEach(({ factory, contextResolver }) => {
|
|
109
|
+
factory.getProcedures().map((procedure: TProcedureRegistration<any, RPCConfig>) => {
|
|
110
|
+
const route = this.buildRpcHttpRouteDoc(procedure)
|
|
111
|
+
|
|
112
|
+
this._docs.push(route)
|
|
113
|
+
|
|
114
|
+
this._app[route.method](route.path, async (req, res) => {
|
|
115
|
+
try {
|
|
116
|
+
res.json(await procedure.handler(contextResolver(req), req.body))
|
|
117
|
+
if (this.config?.onSuccess) {
|
|
118
|
+
this.config.onSuccess(procedure, req, res)
|
|
119
|
+
}
|
|
120
|
+
// if status not set, set to 200
|
|
121
|
+
if (!res.status) {
|
|
122
|
+
res.status(200)
|
|
123
|
+
}
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (this.config?.error) {
|
|
126
|
+
this.config.error(procedure, req, res, error as Error)
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
if (!res.status) {
|
|
130
|
+
res.status(500)
|
|
131
|
+
}
|
|
132
|
+
// if no res.json set, set default error message
|
|
133
|
+
if (!res.headersSent) {
|
|
134
|
+
res.json({ error: (error as Error).message })
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
return this._app
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Generates the RPC HTTP route for the given procedure.
|
|
146
|
+
* @param procedure
|
|
147
|
+
*/
|
|
148
|
+
buildRpcHttpRouteDoc(procedure: TProcedureRegistration<any, RPCConfig>): RPCHttpRouteDoc {
|
|
149
|
+
const { config } = procedure
|
|
150
|
+
const path = this.makeRPCHttpRoutePath(config)
|
|
151
|
+
const method = 'post' // RPCs use POST method
|
|
152
|
+
const jsonSchema: { body?: object; response?: object } = {}
|
|
153
|
+
|
|
154
|
+
if (config.schema?.params) {
|
|
155
|
+
jsonSchema.body = config.schema.params
|
|
156
|
+
}
|
|
157
|
+
if (config.schema?.returnType) {
|
|
158
|
+
jsonSchema.response = config.schema.returnType
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
path,
|
|
163
|
+
method,
|
|
164
|
+
jsonSchema,
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Generates the RPC route path based on the RPC configuration.
|
|
170
|
+
* The RPCConfig name can be a string or an array of strings to form nested paths.
|
|
171
|
+
*
|
|
172
|
+
* Example
|
|
173
|
+
* name: ['string', 'string-string', 'string']
|
|
174
|
+
* path: /rpc/string/string-string/string/version
|
|
175
|
+
* @param config
|
|
176
|
+
*/
|
|
177
|
+
makeRPCHttpRoutePath(config: RPCConfig) {
|
|
178
|
+
return `/rpc/${castArray(config.name).map(kebabCase).join('/')}/${String(config.version).trim()}`
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { RPCConfig } from '../../types.js'
|
|
2
|
+
import { Procedures } from '../../../index.js'
|
|
3
|
+
import express from 'express'
|
|
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
|
+
} ? TContext : never
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Minimal structural type for a Procedures factory.
|
|
15
|
+
* Uses explicit `any` types to avoid variance issues with generic constraints.
|
|
16
|
+
*/
|
|
17
|
+
export type ProceduresFactory = {
|
|
18
|
+
getProcedures: () => Array<{
|
|
19
|
+
name: string
|
|
20
|
+
config: any
|
|
21
|
+
handler: (ctx: any, params?: any) => Promise<any>
|
|
22
|
+
}>
|
|
23
|
+
Create: (...args: any[]) => any
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type ExpressFactoryItem<TFactory = ReturnType<typeof Procedures<any, RPCConfig>>> = {
|
|
27
|
+
factory: TFactory
|
|
28
|
+
contextResolver: (req: express.Request) => ExtractContext<TFactory>
|
|
29
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Procedures } from '../index.js'
|
|
2
|
+
|
|
3
|
+
export interface RPCConfig {
|
|
4
|
+
name: string | string[]
|
|
5
|
+
version: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type FactoryItem<C> = {
|
|
9
|
+
factory: ReturnType<typeof Procedures<C, RPCConfig>>
|
|
10
|
+
contextResolver: (req: Request) => C
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface RPCHttpRouteDoc {
|
|
14
|
+
path: string
|
|
15
|
+
method: 'post'
|
|
16
|
+
jsonSchema: {
|
|
17
|
+
body?: object
|
|
18
|
+
response?: object
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/schema/parser.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {default as addFormats} from 'ajv-formats'
|
|
1
|
+
import { default as addFormats } from 'ajv-formats'
|
|
2
2
|
import * as AJV from 'ajv'
|
|
3
3
|
import { extractJsonSchema } from './extract-json-schema.js'
|
|
4
4
|
import { TJSONSchema } from './types.js'
|
|
@@ -10,18 +10,19 @@ export type TSchemaParsed = {
|
|
|
10
10
|
|
|
11
11
|
export type TSchemaValidationError = AJV.ErrorObject
|
|
12
12
|
|
|
13
|
-
// @ts-
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
14
|
+
// @ts-expect-error
|
|
14
15
|
const ajv = addFormats(
|
|
15
16
|
new AJV.Ajv({
|
|
16
17
|
allErrors: true,
|
|
17
18
|
coerceTypes: true,
|
|
18
19
|
removeAdditional: true,
|
|
19
|
-
})
|
|
20
|
+
})
|
|
20
21
|
)
|
|
21
22
|
|
|
22
23
|
export function schemaParser(
|
|
23
24
|
schema: { params?: unknown; returnType?: unknown },
|
|
24
|
-
onParseError: (errors: { params?: string; returnType?: string }) => void
|
|
25
|
+
onParseError: (errors: { params?: string; returnType?: string }) => void
|
|
25
26
|
): TSchemaParsed {
|
|
26
27
|
const jsonSchema: TSchemaParsed['jsonSchema'] = {}
|
|
27
28
|
const validation: TSchemaParsed['validation'] = {}
|
package/src/schema/types.ts
CHANGED
|
@@ -1,351 +0,0 @@
|
|
|
1
|
-
# Express Integration Guide
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
`registerExpressRoutes` is a utility function that simplifies setting up an Express server with ts-procedures. It handles:
|
|
6
|
-
|
|
7
|
-
- **Automatic parameter merging** - Combines path params (`:id`), query params, and body into a single `params` object
|
|
8
|
-
- **Schema validation** - Validates merged params against your procedure's schema
|
|
9
|
-
- **Error handling** - Provides hooks for custom error responses
|
|
10
|
-
- **Context injection** - Creates procedure context from Express request/response
|
|
11
|
-
|
|
12
|
-
## Quick Start
|
|
13
|
-
|
|
14
|
-
```typescript
|
|
15
|
-
import express from 'express'
|
|
16
|
-
import { Procedures } from 'ts-procedures'
|
|
17
|
-
import { registerExpressRoutes } from 'ts-procedures/express'
|
|
18
|
-
import { Type } from 'typebox'
|
|
19
|
-
|
|
20
|
-
// Define extended config for HTTP routes
|
|
21
|
-
interface HTTPRouteConfig {
|
|
22
|
-
path: string
|
|
23
|
-
method: 'get' | 'post' | 'put' | 'delete' | 'patch'
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Create factory with route config
|
|
27
|
-
const { Create, getProcedures } = Procedures<{}, HTTPRouteConfig>()
|
|
28
|
-
|
|
29
|
-
// Define a procedure
|
|
30
|
-
Create('GetUser', {
|
|
31
|
-
path: '/users/:id',
|
|
32
|
-
method: 'get',
|
|
33
|
-
schema: {
|
|
34
|
-
params: Type.Object({ id: Type.String() }),
|
|
35
|
-
returnType: Type.Object({ id: Type.String(), name: Type.String() })
|
|
36
|
-
}
|
|
37
|
-
}, async (ctx, params) => {
|
|
38
|
-
return { id: params.id, name: 'John Doe' }
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
// Register routes
|
|
42
|
-
const app = express()
|
|
43
|
-
app.use(express.json())
|
|
44
|
-
|
|
45
|
-
registerExpressRoutes(
|
|
46
|
-
app,
|
|
47
|
-
{ getContext: async (req, res) => ({}) },
|
|
48
|
-
getProcedures()
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
app.listen(3000)
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
## Architecture Pattern
|
|
55
|
-
|
|
56
|
-
### Public vs Protected Factories
|
|
57
|
-
|
|
58
|
-
A common pattern is to separate procedures that require authentication from public endpoints:
|
|
59
|
-
|
|
60
|
-
```typescript
|
|
61
|
-
// factories.ts
|
|
62
|
-
import { Procedures } from 'ts-procedures'
|
|
63
|
-
|
|
64
|
-
export interface ProceduresContext {
|
|
65
|
-
headers: Record<string, string>
|
|
66
|
-
ipAddress: string
|
|
67
|
-
requestId: string
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export interface ProtectedContext extends ProceduresContext {
|
|
71
|
-
userId: string // Required - user must be authenticated
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export interface PublicContext extends ProceduresContext {
|
|
75
|
-
userId?: undefined // Optional - may or may not be authenticated
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export interface HTTPRouteConfig {
|
|
79
|
-
path: string
|
|
80
|
-
method: 'get' | 'post' | 'put' | 'delete' | 'patch'
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export const ProtectedFactory = Procedures<ProtectedContext, HTTPRouteConfig>()
|
|
84
|
-
export const PublicFactory = Procedures<PublicContext, HTTPRouteConfig>()
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
### File Organization
|
|
88
|
-
|
|
89
|
-
```
|
|
90
|
-
src/
|
|
91
|
-
├── factories.ts # Context interfaces and factory setup
|
|
92
|
-
├── procedures/
|
|
93
|
-
│ ├── auth.ts # Public endpoints (login, register)
|
|
94
|
-
│ └── users.ts # Protected endpoints (profile, settings)
|
|
95
|
-
└── server.ts # Express app and route registration
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
## Complete Example
|
|
99
|
-
|
|
100
|
-
### factories.ts
|
|
101
|
-
|
|
102
|
-
```typescript
|
|
103
|
-
import { Procedures } from 'ts-procedures'
|
|
104
|
-
|
|
105
|
-
export interface ProceduresContext {
|
|
106
|
-
headers: Record<string, string>
|
|
107
|
-
ipAddress: string
|
|
108
|
-
requestId: string
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export interface ProtectedContext extends ProceduresContext {
|
|
112
|
-
userId: string
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export interface PublicContext extends ProceduresContext {
|
|
116
|
-
userId?: undefined
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export interface HTTPRouteConfig {
|
|
120
|
-
path: string
|
|
121
|
-
method: 'get' | 'post' | 'put' | 'delete' | 'patch'
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export const ProtectedFactory = Procedures<ProtectedContext, HTTPRouteConfig>()
|
|
125
|
-
export const PublicFactory = Procedures<PublicContext, HTTPRouteConfig>()
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
### procedures/auth.ts
|
|
129
|
-
|
|
130
|
-
```typescript
|
|
131
|
-
import { PublicFactory } from '../factories.js'
|
|
132
|
-
import { Type } from 'typebox'
|
|
133
|
-
|
|
134
|
-
PublicFactory.Create('Authenticate', {
|
|
135
|
-
path: '/authenticate',
|
|
136
|
-
method: 'post',
|
|
137
|
-
schema: {
|
|
138
|
-
params: Type.Object({
|
|
139
|
-
username: Type.String({ minLength: 3 }),
|
|
140
|
-
password: Type.String({ minLength: 6 }),
|
|
141
|
-
}),
|
|
142
|
-
returnType: Type.Object({
|
|
143
|
-
token: Type.String(),
|
|
144
|
-
})
|
|
145
|
-
},
|
|
146
|
-
description: 'Authenticate as a user and obtain a token',
|
|
147
|
-
}, async (ctx, params) => {
|
|
148
|
-
// Verify credentials and return token
|
|
149
|
-
return { token: 'jwt-token-here' }
|
|
150
|
-
})
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
### procedures/users.ts
|
|
154
|
-
|
|
155
|
-
```typescript
|
|
156
|
-
import { ProtectedFactory } from '../factories.js'
|
|
157
|
-
import { Type } from 'typebox'
|
|
158
|
-
|
|
159
|
-
ProtectedFactory.Create('GetUserProfile', {
|
|
160
|
-
path: '/users/user-profile/:id',
|
|
161
|
-
method: 'get',
|
|
162
|
-
schema: {
|
|
163
|
-
params: Type.Object({
|
|
164
|
-
id: Type.String(), // Mapped from :id in path
|
|
165
|
-
}),
|
|
166
|
-
returnType: Type.Object({
|
|
167
|
-
user: Type.Object({
|
|
168
|
-
id: Type.String(),
|
|
169
|
-
name: Type.String(),
|
|
170
|
-
email: Type.String(),
|
|
171
|
-
}),
|
|
172
|
-
})
|
|
173
|
-
},
|
|
174
|
-
description: 'Get the profile of a specific user',
|
|
175
|
-
}, async (ctx, params) => {
|
|
176
|
-
// ctx.userId is guaranteed to exist (ProtectedContext)
|
|
177
|
-
return {
|
|
178
|
-
user: {
|
|
179
|
-
id: params.id,
|
|
180
|
-
name: 'Jane Doe',
|
|
181
|
-
email: 'jane@example.com'
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
})
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
### server.ts
|
|
188
|
-
|
|
189
|
-
```typescript
|
|
190
|
-
import express from 'express'
|
|
191
|
-
import { PublicFactory, ProtectedFactory } from './factories.js'
|
|
192
|
-
import { registerExpressRoutes } from 'ts-procedures/express'
|
|
193
|
-
|
|
194
|
-
// Import procedures for side effects (registers with factories)
|
|
195
|
-
import './procedures/auth.js'
|
|
196
|
-
import './procedures/users.js'
|
|
197
|
-
|
|
198
|
-
const app = express()
|
|
199
|
-
app.use(express.json())
|
|
200
|
-
|
|
201
|
-
// Register public routes (no auth required)
|
|
202
|
-
registerExpressRoutes(
|
|
203
|
-
app,
|
|
204
|
-
{
|
|
205
|
-
getContext: async (req, res) => ({
|
|
206
|
-
ipAddress: req.ip || '',
|
|
207
|
-
requestId: req.headers['x-request-id']?.toString() || crypto.randomUUID(),
|
|
208
|
-
headers: req.headers as Record<string, string>,
|
|
209
|
-
})
|
|
210
|
-
},
|
|
211
|
-
PublicFactory.getProcedures()
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
// Register protected routes (auth required)
|
|
215
|
-
registerExpressRoutes(
|
|
216
|
-
app,
|
|
217
|
-
{
|
|
218
|
-
getContext: async (req, res) => {
|
|
219
|
-
const token = req.headers['authorization']?.replace('Bearer ', '')
|
|
220
|
-
const user = await validateToken(token) // Your auth logic
|
|
221
|
-
|
|
222
|
-
if (!user) {
|
|
223
|
-
res.status(401).json({ error: 'Unauthorized' })
|
|
224
|
-
throw new Error('Unauthorized')
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return {
|
|
228
|
-
userId: user.id,
|
|
229
|
-
ipAddress: req.ip || '',
|
|
230
|
-
requestId: req.headers['x-request-id']?.toString() || crypto.randomUUID(),
|
|
231
|
-
headers: req.headers as Record<string, string>,
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
},
|
|
235
|
-
ProtectedFactory.getProcedures()
|
|
236
|
-
)
|
|
237
|
-
|
|
238
|
-
app.listen(3000)
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
## API Reference
|
|
242
|
-
|
|
243
|
-
```typescript
|
|
244
|
-
registerExpressRoutes<ProceduresContext>(
|
|
245
|
-
app: express.Application,
|
|
246
|
-
callbacks: {
|
|
247
|
-
/** Create procedure context from Express request */
|
|
248
|
-
getContext: (req: express.Request, res: express.Response) => Promise<ProceduresContext>
|
|
249
|
-
|
|
250
|
-
/** Optional handler for procedure errors (default: 500 with error message) */
|
|
251
|
-
onHandlerError?: (error: Error, req: express.Request, res: express.Response) => void
|
|
252
|
-
|
|
253
|
-
/** Optional handler for validation errors (default: 422 with error details) */
|
|
254
|
-
onValidationError?: (
|
|
255
|
-
errors: Array<{ message: string; path: string[] }>,
|
|
256
|
-
req: express.Request,
|
|
257
|
-
res: express.Response
|
|
258
|
-
) => void
|
|
259
|
-
},
|
|
260
|
-
procedures: Array<TProcedureRegistration<ProceduresContext, HTTPRouteConfig>>
|
|
261
|
-
): void
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
## Features
|
|
265
|
-
|
|
266
|
-
### Path Parameter Mapping
|
|
267
|
-
|
|
268
|
-
Path parameters defined with `:paramName` are automatically extracted and merged into the params object:
|
|
269
|
-
|
|
270
|
-
```typescript
|
|
271
|
-
// Route: /users/:id/posts/:postId
|
|
272
|
-
// URL: /users/123/posts/456
|
|
273
|
-
// Params: { id: '123', postId: '456' }
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
### Parameter Merging
|
|
277
|
-
|
|
278
|
-
Parameters are merged in this order (later sources override earlier):
|
|
279
|
-
|
|
280
|
-
1. Path parameters (`:id`)
|
|
281
|
-
2. Query parameters (`?foo=bar`)
|
|
282
|
-
3. Body parameters (JSON body)
|
|
283
|
-
|
|
284
|
-
### Schema Validation
|
|
285
|
-
|
|
286
|
-
When validation fails, the default behavior returns a 422 response:
|
|
287
|
-
|
|
288
|
-
```json
|
|
289
|
-
{
|
|
290
|
-
"error": "Validation Error",
|
|
291
|
-
"details": [
|
|
292
|
-
{ "message": "must be string", "path": ["username"] }
|
|
293
|
-
]
|
|
294
|
-
}
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
### Custom Error Handlers
|
|
298
|
-
|
|
299
|
-
```typescript
|
|
300
|
-
registerExpressRoutes(
|
|
301
|
-
app,
|
|
302
|
-
{
|
|
303
|
-
getContext: async (req, res) => ({}),
|
|
304
|
-
|
|
305
|
-
onValidationError: (errors, req, res) => {
|
|
306
|
-
res.status(400).json({
|
|
307
|
-
code: 'INVALID_INPUT',
|
|
308
|
-
errors: errors.map(e => e.message)
|
|
309
|
-
})
|
|
310
|
-
},
|
|
311
|
-
|
|
312
|
-
onHandlerError: (error, req, res) => {
|
|
313
|
-
if (error instanceof ProcedureError) {
|
|
314
|
-
res.status(error.code || 500).json({ message: error.message })
|
|
315
|
-
} else {
|
|
316
|
-
res.status(500).json({ message: 'Internal Server Error' })
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
},
|
|
320
|
-
getProcedures()
|
|
321
|
-
)
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
### Authentication Pattern
|
|
325
|
-
|
|
326
|
-
The `getContext` callback is the ideal place to handle authentication:
|
|
327
|
-
|
|
328
|
-
```typescript
|
|
329
|
-
getContext: async (req, res) => {
|
|
330
|
-
const token = req.headers['authorization']
|
|
331
|
-
|
|
332
|
-
if (!token) {
|
|
333
|
-
res.status(401).json({ error: 'No token provided' })
|
|
334
|
-
throw new Error('Unauthorized')
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const user = await verifyToken(token)
|
|
338
|
-
|
|
339
|
-
if (!user) {
|
|
340
|
-
res.status(401).json({ error: 'Invalid token' })
|
|
341
|
-
throw new Error('Unauthorized')
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
return {
|
|
345
|
-
userId: user.id,
|
|
346
|
-
// ... other context
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
When `getContext` throws, the route handler stops execution. This allows you to respond with an error and prevent the procedure from running.
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { Procedures } from '../../../../index.js'
|
|
2
|
-
|
|
3
|
-
export interface ProceduresContext {
|
|
4
|
-
headers: Record<string, string>
|
|
5
|
-
ipAddress: string
|
|
6
|
-
requestId: string
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface ProtectedContext extends ProceduresContext {
|
|
10
|
-
userId: string
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface PublicContext extends ProceduresContext {
|
|
14
|
-
// An authenticated client can call public endpoints so userId is optional
|
|
15
|
-
userId?: undefined
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export const ProtectedFactory = Procedures<ProtectedContext,HTTPRouteConfig>()
|
|
19
|
-
|
|
20
|
-
export const PublicFactory = Procedures<PublicContext,HTTPRouteConfig>()
|
|
21
|
-
|
|
22
|
-
export interface HTTPRouteConfig {
|
|
23
|
-
path: string
|
|
24
|
-
method: 'get' | 'post' | 'put' | 'delete' | 'patch'
|
|
25
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { PublicFactory } from '../factories.js'
|
|
2
|
-
import { Type } from 'typebox'
|
|
3
|
-
|
|
4
|
-
PublicFactory.Create('Authenticate', {
|
|
5
|
-
path: '/authenticate',
|
|
6
|
-
method: 'post',
|
|
7
|
-
schema: {
|
|
8
|
-
params: Type.Object({
|
|
9
|
-
username: Type.String({ minLength: 3 }),
|
|
10
|
-
password: Type.String({ minLength: 6 }),
|
|
11
|
-
}),
|
|
12
|
-
returnType: Type.Object({
|
|
13
|
-
token: Type.String(),
|
|
14
|
-
})
|
|
15
|
-
},
|
|
16
|
-
description: 'Authenticate as a user and obtain a token',
|
|
17
|
-
},
|
|
18
|
-
async (ctx, params) => {
|
|
19
|
-
// In a real implementation, you would verify user credentials here, ie: ctx.services.userService.authenticate(params.username, params.password)
|
|
20
|
-
|
|
21
|
-
return {
|
|
22
|
-
token: 'fake-jwt-token',
|
|
23
|
-
}
|
|
24
|
-
})
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { ProtectedFactory } from '../factories.js'
|
|
2
|
-
import { Type } from 'typebox'
|
|
3
|
-
|
|
4
|
-
ProtectedFactory.Create('Get User Profile', {
|
|
5
|
-
path: '/users/user-profile/:id',
|
|
6
|
-
method: 'get',
|
|
7
|
-
schema: {
|
|
8
|
-
params: Type.Object({
|
|
9
|
-
// Our router in this example will map :id to this param
|
|
10
|
-
id: Type.String(),
|
|
11
|
-
}),
|
|
12
|
-
returnType: Type.Object({
|
|
13
|
-
user: Type.Object({
|
|
14
|
-
id: Type.String(),
|
|
15
|
-
name: Type.String(),
|
|
16
|
-
email: Type.String(),
|
|
17
|
-
}),
|
|
18
|
-
})
|
|
19
|
-
},
|
|
20
|
-
description: 'Get the profile of a specific user',
|
|
21
|
-
},
|
|
22
|
-
async (ctx, params) => {
|
|
23
|
-
// In a real implementation, you would fetch the user profile from a database or service here, ie: ctx.services.userService.getUserProfile(params.userId)
|
|
24
|
-
|
|
25
|
-
return {
|
|
26
|
-
user: {
|
|
27
|
-
id: params.id,
|
|
28
|
-
name: 'Jane Doe',
|
|
29
|
-
email: 'test@gmail.com'
|
|
30
|
-
},
|
|
31
|
-
}
|
|
32
|
-
})
|