exisjs 0.7.11 → 0.7.13
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 +195 -39
- package/dist/cli/commands/dev.d.ts +4 -0
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +136 -32
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/run.d.ts +7 -0
- package/dist/cli/commands/run.d.ts.map +1 -0
- package/dist/cli/commands/run.js +99 -0
- package/dist/cli/commands/run.js.map +1 -0
- package/dist/cli/index.js +29 -13
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/manifest.d.ts.map +1 -1
- package/dist/cli/manifest.js +18 -3
- package/dist/cli/manifest.js.map +1 -1
- package/dist/cli/routes.d.ts +6 -0
- package/dist/cli/routes.d.ts.map +1 -1
- package/dist/cli/routes.js +224 -135
- package/dist/cli/routes.js.map +1 -1
- package/dist/config/config.d.ts +2 -1
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js.map +1 -1
- package/dist/config/types.d.ts +30 -9
- package/dist/config/types.d.ts.map +1 -1
- package/dist/database/lifecycle.d.ts +6 -1
- package/dist/database/lifecycle.d.ts.map +1 -1
- package/dist/database/lifecycle.js +15 -4
- package/dist/database/lifecycle.js.map +1 -1
- package/dist/decorators/params.d.ts +21 -0
- package/dist/decorators/params.d.ts.map +1 -1
- package/dist/decorators/params.js +21 -1
- package/dist/decorators/params.js.map +1 -1
- package/dist/di/container.d.ts +15 -6
- package/dist/di/container.d.ts.map +1 -1
- package/dist/di/container.js +26 -3
- package/dist/di/container.js.map +1 -1
- package/dist/di/index.d.ts +1 -1
- package/dist/di/index.d.ts.map +1 -1
- package/dist/di/index.js.map +1 -1
- package/dist/di/inject.d.ts +2 -1
- package/dist/di/inject.d.ts.map +1 -1
- package/dist/di/inject.js +2 -1
- package/dist/di/inject.js.map +1 -1
- package/dist/docs/BENCHMARKS.md +4 -4
- package/dist/docs/content/README.md +43 -44
- package/dist/docs/content/boundaries.mdx +141 -48
- package/dist/docs/content/cli/commands.mdx +161 -107
- package/dist/docs/content/cli/overview.mdx +61 -46
- package/dist/docs/content/cli/usage.mdx +107 -30
- package/dist/docs/content/config.mdx +145 -147
- package/dist/docs/content/controllers.mdx +193 -464
- package/dist/docs/content/cron.mdx +183 -69
- package/dist/docs/content/decorators.mdx +449 -154
- package/dist/docs/content/errors.mdx +202 -305
- package/dist/docs/content/exisjs-mcp.mdx +59 -49
- package/dist/docs/content/installation.mdx +243 -157
- package/dist/docs/content/introduction.mdx +220 -48
- package/dist/docs/content/middleware.mdx +171 -497
- package/dist/docs/content/modules.mdx +141 -83
- package/dist/docs/content/providers.mdx +216 -180
- package/dist/docs/content/requests.mdx +168 -488
- package/dist/docs/content/responses.mdx +200 -533
- package/dist/docs/content/routing.mdx +239 -558
- package/dist/docs/content/security.mdx +233 -57
- package/dist/docs/content/structure.mdx +151 -240
- package/dist/docs/content/swagger.mdx +192 -91
- package/dist/docs/content/testing.mdx +205 -74
- package/dist/docs/content/validation.mdx +479 -341
- package/dist/docs/reference/README.md +34 -2
- package/dist/docs/reference/config.mdx +277 -73
- package/dist/docs/reference/context.mdx +197 -106
- package/dist/docs/reference/cron.mdx +266 -152
- package/dist/docs/reference/database.mdx +135 -53
- package/dist/docs/reference/decorators.mdx +234 -116
- package/dist/docs/reference/di.mdx +146 -112
- package/dist/docs/reference/error.mdx +155 -178
- package/dist/docs/reference/exis.mdx +141 -70
- package/dist/docs/reference/logger.mdx +110 -126
- package/dist/docs/reference/middleware.mdx +235 -158
- package/dist/docs/reference/plugin.mdx +136 -64
- package/dist/docs/reference/router.mdx +277 -251
- package/dist/docs/reference/sanitize.mdx +91 -87
- package/dist/docs/reference/swagger.mdx +280 -100
- package/dist/docs/reference/testing.mdx +223 -89
- package/dist/docs/reference/validator.mdx +194 -206
- package/dist/lib/inspect-routes.d.ts +2 -0
- package/dist/lib/inspect-routes.d.ts.map +1 -0
- package/dist/lib/inspect-routes.js +148 -0
- package/dist/lib/inspect-routes.js.map +1 -0
- package/dist/lib/start-server.js +13 -8
- package/dist/lib/start-server.js.map +1 -1
- package/dist/module/module.d.ts +6 -3
- package/dist/module/module.d.ts.map +1 -1
- package/dist/module/module.js +17 -3
- package/dist/module/module.js.map +1 -1
- package/dist/router/controller-registrar.d.ts.map +1 -1
- package/dist/router/controller-registrar.js +26 -4
- package/dist/router/controller-registrar.js.map +1 -1
- package/dist/router/scanner/cron-mounter.d.ts.map +1 -1
- package/dist/router/scanner/cron-mounter.js +9 -2
- package/dist/router/scanner/cron-mounter.js.map +1 -1
- package/dist/server/app.d.ts +3 -1
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/app.js +29 -8
- package/dist/server/app.js.map +1 -1
- package/dist/server/define.d.ts.map +1 -1
- package/dist/server/define.js +5 -0
- package/dist/server/define.js.map +1 -1
- package/dist/server/request.d.ts +2 -0
- package/dist/server/request.d.ts.map +1 -1
- package/dist/server/request.js +11 -0
- package/dist/server/request.js.map +1 -1
- package/dist/swagger/generator.d.ts.map +1 -1
- package/dist/swagger/generator.js +130 -10
- package/dist/swagger/generator.js.map +1 -1
- package/dist/telemetry/index.d.ts +12 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/index.js +121 -0
- package/dist/telemetry/index.js.map +1 -0
- package/dist/testing/client.d.ts +1 -0
- package/dist/testing/client.d.ts.map +1 -1
- package/dist/testing/client.js +4 -1
- package/dist/testing/client.js.map +1 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +20 -4
- package/dist/testing/index.js.map +1 -1
- package/dist/validator/builder.d.ts +26 -1
- package/dist/validator/builder.d.ts.map +1 -1
- package/dist/validator/builder.js +31 -0
- package/dist/validator/builder.js.map +1 -1
- package/dist/validator/engine.d.ts +23 -0
- package/dist/validator/engine.d.ts.map +1 -1
- package/dist/validator/engine.js +228 -31
- package/dist/validator/engine.js.map +1 -1
- package/dist/validator/index.d.ts +8 -4
- package/dist/validator/index.d.ts.map +1 -1
- package/dist/validator/index.js +4 -1
- package/dist/validator/index.js.map +1 -1
- package/dist/validator/tex-types.d.ts +15 -0
- package/dist/validator/tex-types.d.ts.map +1 -1
- package/dist/validator/tex-types.js +102 -0
- package/dist/validator/tex-types.js.map +1 -1
- package/dist/validator/tex.d.ts +1 -1
- package/dist/validator/tex.d.ts.map +1 -1
- package/dist/validator/tex.js +3 -1
- package/dist/validator/tex.js.map +1 -1
- package/dist/validator/types.d.ts +1 -0
- package/dist/validator/types.d.ts.map +1 -1
- package/package.json +47 -14
- package/dist/docs/content/dataloader.mdx +0 -170
- package/dist/docs/reference/dataloader.mdx +0 -91
|
@@ -1,595 +1,275 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
title: 'Request Object (ExisRequest)'
|
|
3
|
+
description: 'API reference and usage guide for ExisRequest: parameters, query strings, headers, body parsing, multipart uploads, IP resolution, and abort signals.'
|
|
4
|
+
nextUrl: '/docs/responses'
|
|
5
|
+
nextTitle: 'Response Helpers & Streaming'
|
|
4
6
|
---
|
|
5
7
|
|
|
6
|
-
#
|
|
8
|
+
# Request Object (`ExisRequest`)
|
|
7
9
|
|
|
8
|
-
ExisJS wraps the
|
|
10
|
+
ExisJS wraps the standard Node.js `IncomingMessage` in an `ExisRequest` instance. It provides parsed properties, lazy body decoding, multipart upload helpers, IP resolution behind reverse proxies, and integration with the Web standard `AbortSignal`.
|
|
9
11
|
|
|
10
12
|
---
|
|
11
13
|
|
|
12
|
-
##
|
|
14
|
+
## Core Request Properties
|
|
15
|
+
|
|
16
|
+
| Property | Type | Description |
|
|
17
|
+
| :--- | :--- | :--- |
|
|
18
|
+
| `req.params` | `TParams` | Dynamic route parameters extracted from URL segments (e.g. `/:id`) |
|
|
19
|
+
| `req.query` | `TQuery` | Parsed URL query parameters (e.g. `?page=1&limit=20`) |
|
|
20
|
+
| `req.body` | `TBody` | Parsed request body payload (JSON, URL-encoded, or multipart fields) |
|
|
21
|
+
| `req.headers` | `IncomingHttpHeaders` | Incoming HTTP headers map (case-insensitive keys) |
|
|
22
|
+
| `req.cookies` | `Record<string, string>` | Parsed request cookies dictionary |
|
|
23
|
+
| `req.file` | `ExisFile` | First uploaded file from multipart form data |
|
|
24
|
+
| `req.files` | `ExisFile[]` | All uploaded files from multipart form data |
|
|
25
|
+
| `req.ip` | `string` | Resolved client IP address (evaluates trusted proxy headers) |
|
|
26
|
+
| `req.ips` | `string[]` | Array of IP addresses when traversed through proxy hops |
|
|
27
|
+
| `req.hostname` | `string` | Resolved hostname (from `Host` or `X-Forwarded-Host`) |
|
|
28
|
+
| `req.protocol` | `'http' \| 'https'` | Request protocol |
|
|
29
|
+
| `req.secure` | `boolean` | `true` when request is over HTTPS (`req.protocol === 'https'`) |
|
|
30
|
+
| `req.method` | `string` | HTTP method verb (`GET`, `POST`, `PUT`, etc.) |
|
|
31
|
+
| `req.path` | `string` | Normalized pathname without query parameters |
|
|
32
|
+
| `req.originalUrl` | `string` | Raw URL string including query parameters |
|
|
33
|
+
| `req.signal` | `AbortSignal` | Standard `AbortSignal` triggered when client cancels connection |
|
|
34
|
+
| `req.fresh` | `boolean` | `true` if client cache matches current `ETag` or `Last-Modified` |
|
|
35
|
+
| `req.stale` | `boolean` | Inverse of `req.fresh` |
|
|
36
|
+
| `req.user` | `ExisUser` | Authenticated user identity attached by auth guards |
|
|
37
|
+
| `req.session` | `Record<string, any>` | Session data store when session middleware is enabled |
|
|
38
|
+
| `req.log` | `Logger` | Request-scoped Pino logger with trace metadata |
|
|
39
|
+
| `req.requestId` | `string` | Unique request identifier header |
|
|
40
|
+
| `req.raw` | `IncomingMessage` | Underlying Node.js `IncomingMessage` stream |
|
|
13
41
|
|
|
14
|
-
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Accessing Request Data
|
|
45
|
+
|
|
46
|
+
### In Functional Routes
|
|
15
47
|
|
|
16
|
-
|
|
17
|
-
- **`req.query`**: An object containing the URL query string parameters (e.g., `?id=123`).
|
|
18
|
-
- **`req.params`**: An object containing route parameters (e.g., `/:id`).
|
|
48
|
+
In functional route handlers, `req` is available directly on the handler context alongside destructured convenience properties (`body`, `query`, `params`, `headers`):
|
|
19
49
|
|
|
20
|
-
|
|
50
|
+
```typescript filename="src/http/users/route.ts"
|
|
51
|
+
import { controller, route } from 'exisjs/router'
|
|
21
52
|
|
|
22
|
-
```typescript filename="src/http/route.ts"
|
|
23
53
|
export default controller({
|
|
24
|
-
updateUser: route.
|
|
25
|
-
handle(
|
|
26
|
-
const id =
|
|
27
|
-
const force =
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
54
|
+
updateUser: route.put('/:id', {
|
|
55
|
+
handle: async ({ params, query, body, req }) => {
|
|
56
|
+
const id = params.id
|
|
57
|
+
const force = query.force === 'true'
|
|
58
|
+
const clientIp = req.ip
|
|
59
|
+
|
|
60
|
+
return { id, force, clientIp, updated: body }
|
|
61
|
+
},
|
|
62
|
+
}),
|
|
31
63
|
})
|
|
32
64
|
```
|
|
33
65
|
|
|
34
|
-
|
|
66
|
+
### In Class-Based (OOP) Controllers
|
|
67
|
+
|
|
68
|
+
In OOP controllers, access `req` using the `@Req()` decorator or extract specific properties using `@Param()`, `@Query()`, `@Body()`, and `@Headers()`:
|
|
35
69
|
|
|
36
|
-
```typescript filename="src/http/route.ts"
|
|
37
|
-
import { Controller,
|
|
70
|
+
```typescript filename="src/http/users/route.ts"
|
|
71
|
+
import { Controller, Put, Param, Query, Body, Req } from 'exisjs/decorators'
|
|
72
|
+
import type { Request } from 'exisjs/router'
|
|
38
73
|
|
|
39
74
|
@Controller()
|
|
40
75
|
export default class UsersController {
|
|
41
|
-
@
|
|
76
|
+
@Put('/:id')
|
|
42
77
|
async updateUser(
|
|
43
78
|
@Param('id') id: string,
|
|
44
79
|
@Query('force') force: string,
|
|
45
|
-
@Body()
|
|
80
|
+
@Body() body: any,
|
|
81
|
+
@Req() req: Request
|
|
46
82
|
) {
|
|
47
|
-
|
|
83
|
+
const clientIp = req.ip
|
|
84
|
+
return { id, force: force === 'true', clientIp, updated: body }
|
|
48
85
|
}
|
|
49
86
|
}
|
|
50
87
|
```
|
|
51
88
|
|
|
52
89
|
---
|
|
53
90
|
|
|
54
|
-
##
|
|
91
|
+
## On-Demand Body Parsing & Raw Streams
|
|
55
92
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
#### Functional Paradigm
|
|
93
|
+
When dealing with webhooks (e.g. Stripe, GitHub), HMAC signature verification, or custom binary payloads, you can access the unparsed stream directly:
|
|
59
94
|
|
|
60
95
|
```typescript
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
async handle(req, res) {
|
|
64
|
-
// Parses application/json
|
|
65
|
-
const data = await req.json<{ key: string }>()
|
|
66
|
-
|
|
67
|
-
// Parses raw text (text/plain)
|
|
68
|
-
const text = await req.text()
|
|
69
|
-
}
|
|
70
|
-
})
|
|
71
|
-
})
|
|
72
|
-
```
|
|
96
|
+
// 1. Read raw binary Buffer (ideal for webhook signature verification)
|
|
97
|
+
const rawBuffer = await req.buffer()
|
|
73
98
|
|
|
74
|
-
|
|
99
|
+
// 2. Read raw UTF-8 string
|
|
100
|
+
const rawText = await req.text()
|
|
75
101
|
|
|
76
|
-
|
|
77
|
-
|
|
102
|
+
// 3. Parse JSON body on demand
|
|
103
|
+
const data = await req.json<{ username: string }>()
|
|
78
104
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
@Post('/process')
|
|
82
|
-
async processData(@Req() req: any) {
|
|
83
|
-
// Parses application/json
|
|
84
|
-
const data = await req.json<{ key: string }>()
|
|
85
|
-
|
|
86
|
-
// Parses raw text (text/plain)
|
|
87
|
-
const text = await req.text()
|
|
88
|
-
}
|
|
89
|
-
}
|
|
105
|
+
// 4. Parse multipart/form-data or x-www-form-urlencoded
|
|
106
|
+
const { fields, files } = await req.formData()
|
|
90
107
|
```
|
|
91
108
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
## 3. File Uploads & Multipart Form Data
|
|
109
|
+
### Webhooks with `@RawBody()` (Class Controllers)
|
|
95
110
|
|
|
96
|
-
|
|
111
|
+
In class-based controllers, use the `@RawBody()` parameter decorator to inject the raw `Buffer` or decoded string directly:
|
|
97
112
|
|
|
98
|
-
|
|
113
|
+
```typescript filename="src/http/webhooks/route.ts"
|
|
114
|
+
import { Controller, Post, RawBody, Headers } from 'exisjs/decorators'
|
|
99
115
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
import { tex } from 'exisjs/validator'
|
|
107
|
-
|
|
108
|
-
export default controller({
|
|
109
|
-
uploadPhoto: route.post('/', {
|
|
110
|
-
upload: {
|
|
111
|
-
field: 'photo',
|
|
112
|
-
maxBytes: 10 * 1024 * 1024, // 10MB limit
|
|
113
|
-
mimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
|
|
114
|
-
},
|
|
115
|
-
body: tex.object({
|
|
116
|
-
title: tex.string({ min: 1 }),
|
|
117
|
-
}),
|
|
118
|
-
handle({ req }) {
|
|
119
|
-
const file = req.file! // Typed UploadedFile
|
|
120
|
-
console.log(`Uploaded ${file.originalname} (${file.size} bytes)`)
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
success: true,
|
|
124
|
-
filename: file.filename,
|
|
125
|
-
mimetype: file.mimetype,
|
|
126
|
-
title: req.body.title,
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
})
|
|
130
|
-
})
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
#### Class-Based (OOP) Paradigm
|
|
134
|
-
|
|
135
|
-
```typescript filename="src/http/upload/route.ts"
|
|
136
|
-
import { Controller, Post, UseUpload, UploadedFile, Body } from 'exisjs/decorators'
|
|
137
|
-
import { tex } from 'exisjs/validator'
|
|
138
|
-
import type { UploadedFile as IUploadedFile } from 'exisjs/router'
|
|
139
|
-
|
|
140
|
-
const TitleSchema = tex.object({
|
|
141
|
-
title: tex.string({ min: 1 }),
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
@Controller('/upload')
|
|
145
|
-
export default class UploadController {
|
|
146
|
-
@Post('/')
|
|
147
|
-
@UseUpload({
|
|
148
|
-
field: 'photo',
|
|
149
|
-
maxBytes: 10 * 1024 * 1024,
|
|
150
|
-
mimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
|
|
151
|
-
})
|
|
152
|
-
async uploadPhoto(
|
|
153
|
-
@UploadedFile() file: IUploadedFile,
|
|
154
|
-
@Body(TitleSchema) body: { title: string }
|
|
116
|
+
@Controller('/webhooks')
|
|
117
|
+
export default class WebhookController {
|
|
118
|
+
@Post('/stripe')
|
|
119
|
+
async handleStripe(
|
|
120
|
+
@RawBody() payload: Buffer,
|
|
121
|
+
@Headers('stripe-signature') sig: string
|
|
155
122
|
) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
return {
|
|
159
|
-
success: true,
|
|
160
|
-
filename: file.filename,
|
|
161
|
-
mimetype: file.mimetype,
|
|
162
|
-
title: body.title,
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
### B. First-Party `fileUpload` Middleware
|
|
169
|
-
|
|
170
|
-
ExisJS exports a dedicated `fileUpload` middleware from `exisjs/middleware` supporting single, multiple, and multi-field uploads:
|
|
171
|
-
|
|
172
|
-
#### Functional Paradigm
|
|
173
|
-
|
|
174
|
-
```typescript filename="src/http/documents/route.ts"
|
|
175
|
-
import { controller, route } from 'exisjs/router'
|
|
176
|
-
import { fileUpload } from 'exisjs/middleware'
|
|
177
|
-
|
|
178
|
-
export default controller({
|
|
179
|
-
// Single file upload
|
|
180
|
-
single: route.post('/single',
|
|
181
|
-
fileUpload.single('document', {
|
|
182
|
-
maxBytes: 15 * 1024 * 1024, // 15MB
|
|
183
|
-
dest: './uploads/docs',
|
|
184
|
-
}),
|
|
185
|
-
({ req }) => {
|
|
186
|
-
return { file: req.file }
|
|
187
|
-
}
|
|
188
|
-
),
|
|
189
|
-
|
|
190
|
-
// Multiple files with the same field name
|
|
191
|
-
gallery: route.post('/gallery',
|
|
192
|
-
fileUpload.array('photos', 5, {
|
|
193
|
-
mimeTypes: ['image/*'],
|
|
194
|
-
}),
|
|
195
|
-
({ req }) => {
|
|
196
|
-
const files = req.files // UploadedFile[]
|
|
197
|
-
return { count: files?.length }
|
|
198
|
-
}
|
|
199
|
-
),
|
|
200
|
-
|
|
201
|
-
// Multiple distinct fields
|
|
202
|
-
profile: route.post('/profile',
|
|
203
|
-
fileUpload.fields([
|
|
204
|
-
{ name: 'avatar', maxCount: 1 },
|
|
205
|
-
{ name: 'cover', maxCount: 1 },
|
|
206
|
-
]),
|
|
207
|
-
({ req }) => {
|
|
208
|
-
// req.files is a dictionary Record<string, UploadedFile[]>
|
|
209
|
-
return { files: req.files }
|
|
210
|
-
}
|
|
211
|
-
),
|
|
212
|
-
})
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
#### Class-Based (OOP) Paradigm
|
|
216
|
-
|
|
217
|
-
```typescript filename="src/http/documents/route.ts"
|
|
218
|
-
import { Controller, Post, Use, UploadedFile, UploadedFiles } from 'exisjs/decorators'
|
|
219
|
-
import { fileUpload } from 'exisjs/middleware'
|
|
220
|
-
import type { UploadedFile as IUploadedFile } from 'exisjs/router'
|
|
221
|
-
|
|
222
|
-
@Controller('/documents')
|
|
223
|
-
export default class DocumentsController {
|
|
224
|
-
// Single file upload
|
|
225
|
-
@Post('/single')
|
|
226
|
-
@Use(fileUpload.single('document', { maxBytes: 15 * 1024 * 1024, dest: './uploads/docs' }))
|
|
227
|
-
async uploadSingle(@UploadedFile() file: IUploadedFile) {
|
|
228
|
-
return { file }
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Multiple files with the same field name
|
|
232
|
-
@Post('/gallery')
|
|
233
|
-
@Use(fileUpload.array('photos', 5, { mimeTypes: ['image/*'] }))
|
|
234
|
-
async uploadGallery(@UploadedFiles() files: IUploadedFile[]) {
|
|
235
|
-
return { count: files.length, files }
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Multiple distinct fields
|
|
239
|
-
@Post('/profile')
|
|
240
|
-
@Use(fileUpload.fields([{ name: 'avatar', maxCount: 1 }, { name: 'cover', maxCount: 1 }]))
|
|
241
|
-
async uploadProfile(@UploadedFiles() files: Record<string, IUploadedFile[]>) {
|
|
242
|
-
return { files }
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
### C. Integrating Express/Connect Middleware (e.g. Multer)
|
|
248
|
-
|
|
249
|
-
If your enterprise project relies on existing Express middleware like **Multer** (for custom storage engines, S3 direct-to-cloud streams, or specialized filters), ExisJS provides the `fromExpress()` bridge:
|
|
250
|
-
|
|
251
|
-
#### Functional Paradigm
|
|
252
|
-
|
|
253
|
-
```typescript filename="src/http/legacy/route.ts"
|
|
254
|
-
import { controller, route } from 'exisjs/router'
|
|
255
|
-
import { fromExpress } from 'exisjs/middleware'
|
|
256
|
-
import multer from 'multer'
|
|
257
|
-
|
|
258
|
-
const upload = multer({
|
|
259
|
-
storage: multer.memoryStorage(),
|
|
260
|
-
limits: { fileSize: 10 * 1024 * 1024 },
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
export default controller({
|
|
264
|
-
upload: route.post('/',
|
|
265
|
-
fromExpress(upload.single('file')),
|
|
266
|
-
({ req }) => {
|
|
267
|
-
// Access multer's req.file or req.body seamlessly
|
|
268
|
-
const file = (req.raw as any).file || req.file
|
|
269
|
-
return { success: true, originalname: file?.originalname }
|
|
270
|
-
}
|
|
271
|
-
)
|
|
272
|
-
})
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
#### Class-Based (OOP) Paradigm
|
|
276
|
-
|
|
277
|
-
```typescript filename="src/http/legacy/route.ts"
|
|
278
|
-
import { Controller, Post, Use, Req } from 'exisjs/decorators'
|
|
279
|
-
import { fromExpress } from 'exisjs/middleware'
|
|
280
|
-
import type { Request } from 'exisjs/router'
|
|
281
|
-
import multer from 'multer'
|
|
282
|
-
|
|
283
|
-
const upload = multer({
|
|
284
|
-
storage: multer.memoryStorage(),
|
|
285
|
-
limits: { fileSize: 10 * 1024 * 1024 },
|
|
286
|
-
})
|
|
287
|
-
|
|
288
|
-
@Controller('/legacy')
|
|
289
|
-
export default class LegacyUploadController {
|
|
290
|
-
@Post('/upload')
|
|
291
|
-
@Use(fromExpress(upload.single('file')))
|
|
292
|
-
async upload(@Req() req: Request) {
|
|
293
|
-
const file = (req.raw as any).file || req.file
|
|
294
|
-
return { success: true, originalname: file?.originalname }
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
### D. Manual `req.formData()` & Streaming Uploads
|
|
300
|
-
|
|
301
|
-
For manual control over incoming multipart streams without route middleware:
|
|
302
|
-
|
|
303
|
-
#### Functional Paradigm
|
|
304
|
-
|
|
305
|
-
```typescript filename="src/http/upload/route.ts"
|
|
306
|
-
import { controller, route } from 'exisjs/router'
|
|
307
|
-
|
|
308
|
-
export default controller({
|
|
309
|
-
uploadAvatar: route.post('/avatar', {
|
|
310
|
-
async handle({ req }) {
|
|
311
|
-
// Parse on-demand
|
|
312
|
-
const { fields, files } = await req.formData()
|
|
313
|
-
const uploadedFile = files['profile_pic']
|
|
314
|
-
|
|
315
|
-
if (uploadedFile) {
|
|
316
|
-
// Save to disk safely with automated collision-free naming
|
|
317
|
-
const savedPath = await uploadedFile.saveToDisk('./uploads/avatars')
|
|
318
|
-
return { success: true, path: savedPath }
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
return { success: false, error: 'No file uploaded' }
|
|
322
|
-
}
|
|
323
|
-
})
|
|
324
|
-
})
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
#### Class-Based (OOP) Paradigm
|
|
328
|
-
|
|
329
|
-
```typescript filename="src/http/upload/route.ts"
|
|
330
|
-
import { Controller, Post, Req } from 'exisjs/decorators'
|
|
331
|
-
import type { Request } from 'exisjs/router'
|
|
332
|
-
|
|
333
|
-
@Controller('/upload')
|
|
334
|
-
export default class ManualUploadController {
|
|
335
|
-
@Post('/avatar')
|
|
336
|
-
async uploadAvatar(@Req() req: Request) {
|
|
337
|
-
const { fields, files } = await req.formData()
|
|
338
|
-
const uploadedFile = files['profile_pic']
|
|
339
|
-
|
|
340
|
-
if (uploadedFile) {
|
|
341
|
-
const savedPath = await uploadedFile.saveToDisk('./uploads/avatars')
|
|
342
|
-
return { success: true, path: savedPath }
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
return { success: false, error: 'No file uploaded' }
|
|
123
|
+
return stripe.webhooks.constructEvent(payload, sig, process.env.STRIPE_SECRET!)
|
|
346
124
|
}
|
|
347
125
|
}
|
|
348
126
|
```
|
|
349
127
|
|
|
350
128
|
---
|
|
351
129
|
|
|
352
|
-
##
|
|
130
|
+
## Headers & Content Negotiation
|
|
353
131
|
|
|
354
|
-
ExisJS
|
|
132
|
+
ExisJS provides helper methods for header inspection and RFC content negotiation:
|
|
355
133
|
|
|
356
|
-
|
|
134
|
+
```typescript
|
|
135
|
+
// Case-insensitive header retrieval
|
|
136
|
+
const auth = req.get('Authorization') // or req.header('authorization')
|
|
357
137
|
|
|
358
|
-
|
|
359
|
-
{
|
|
360
|
-
|
|
361
|
-
"error": "Payload Too Large",
|
|
362
|
-
"message": "Request payload exceeded size limit of 10485760 bytes. Increase server.bodyLimit in exis.config.ts to allow larger payloads."
|
|
138
|
+
// Check Content-Type
|
|
139
|
+
if (req.is('json')) {
|
|
140
|
+
// Content-Type contains application/json
|
|
363
141
|
}
|
|
364
|
-
```
|
|
365
142
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
export default {
|
|
370
|
-
server: {
|
|
371
|
-
// Set custom maximum payload limit in bytes (e.g. 50MB)
|
|
372
|
-
bodyLimit: 50 * 1024 * 1024,
|
|
373
|
-
}
|
|
374
|
-
}
|
|
143
|
+
// Content negotiation - returns best supported match
|
|
144
|
+
const format = req.accepts('json', 'html', 'text')
|
|
145
|
+
const language = req.acceptsLanguages('en', 'es', 'de')
|
|
375
146
|
```
|
|
376
147
|
|
|
377
148
|
---
|
|
378
149
|
|
|
379
|
-
##
|
|
380
|
-
|
|
381
|
-
ExisJS ships with a **60-second default request timeout** in its `timeout()` middleware, offering ample headroom for slow network clients and third-party cloud operations (Cloudinary, AWS S3, OpenAI, Stripe).
|
|
382
|
-
|
|
383
|
-
### Dynamic In-Handler Control
|
|
384
|
-
|
|
385
|
-
Handlers can dynamically extend or cancel the active timeout on the fly using `req.setTimeout()` and `req.clearTimeout()`, or declaratively via `@Timeout(ms)`:
|
|
386
|
-
|
|
387
|
-
#### Functional Paradigm
|
|
388
|
-
|
|
389
|
-
```typescript filename="src/http/ai/route.ts"
|
|
390
|
-
import { controller, route } from 'exisjs/router'
|
|
391
|
-
|
|
392
|
-
export default controller({
|
|
393
|
-
generateVideo: route.post('/', {
|
|
394
|
-
timeoutMs: 180000, // 1. Declarative 3-minute timeout for this endpoint
|
|
395
|
-
async handle({ req }) {
|
|
396
|
-
// 2. Or dynamically adjust timeout during processing
|
|
397
|
-
req.setTimeout(240000)
|
|
398
|
-
|
|
399
|
-
try {
|
|
400
|
-
const result = await longRunningAiTask()
|
|
401
|
-
return { success: true, result }
|
|
402
|
-
} finally {
|
|
403
|
-
req.clearTimeout()
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
})
|
|
407
|
-
})
|
|
408
|
-
```
|
|
150
|
+
## Proxy & IP Address Resolution
|
|
409
151
|
|
|
410
|
-
|
|
152
|
+
When running behind reverse proxies, load balancers, or CDNs (such as AWS ALB, Cloudflare, NGINX, or Render), configure `trustProxy` in `exis.config.ts`:
|
|
411
153
|
|
|
412
|
-
```typescript filename="
|
|
413
|
-
import {
|
|
414
|
-
import type { Request } from 'exisjs/router'
|
|
154
|
+
```typescript filename="exis.config.ts"
|
|
155
|
+
import type { ExisConfig } from 'exisjs/config'
|
|
415
156
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
@Post('/generate-video')
|
|
419
|
-
@Timeout(180000) // 1. Declarative 3-minute timeout
|
|
420
|
-
async generateVideo(@Req() req: Request) {
|
|
421
|
-
// 2. Or dynamically adjust on the fly
|
|
422
|
-
req.setTimeout(240000)
|
|
423
|
-
|
|
424
|
-
try {
|
|
425
|
-
const result = await longRunningAiTask()
|
|
426
|
-
return { success: true, result }
|
|
427
|
-
} finally {
|
|
428
|
-
req.clearTimeout()
|
|
429
|
-
}
|
|
430
|
-
}
|
|
157
|
+
const config: ExisConfig = {
|
|
158
|
+
trustProxy: true, // Evaluates CF-Connecting-IP, X-Real-IP, and X-Forwarded-For
|
|
431
159
|
}
|
|
432
|
-
```
|
|
433
160
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
## 6. HTTP Headers & Content Negotiation
|
|
437
|
-
|
|
438
|
-
```typescript
|
|
439
|
-
// Get a specific header (case-insensitive)
|
|
440
|
-
const authHeader = req.get('Authorization')
|
|
441
|
-
// or
|
|
442
|
-
const authHeader = req.header('authorization')
|
|
443
|
-
|
|
444
|
-
// Check the Content-Type
|
|
445
|
-
if (req.is('application/json')) { ... }
|
|
446
|
-
|
|
447
|
-
// Content Negotiation (returns the best match)
|
|
448
|
-
const format = req.accepts('html', 'json', 'text')
|
|
449
|
-
const language = req.acceptsLanguages('en', 'es', 'fr')
|
|
161
|
+
export default config
|
|
450
162
|
```
|
|
451
163
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
ExisJS handles reverse proxies, CDNs, and dual-stack IP networks automatically. When deployed behind Cloudflare, AWS ALB, NGINX, Render, or GCP, set `trustProxy: true` in your `exis.config.ts` or define `TRUST_PROXY=true` in your environment variables.
|
|
457
|
-
|
|
458
|
-
When proxy trust is enabled, ExisJS evaluates industry-standard proxy and CDN headers in priority order:
|
|
459
|
-
1. `CF-Connecting-IP` (Cloudflare)
|
|
460
|
-
2. `True-Client-IP` (Akamai / Cloudflare Enterprise)
|
|
461
|
-
3. `X-Real-IP` / `X-Client-IP` (NGINX / Reverse Proxies)
|
|
462
|
-
4. `X-Forwarded-For` (Standard multi-hop proxy chains)
|
|
463
|
-
|
|
464
|
-
- **`req.ip`**: The real client's IP address. Automatically strips proxy hops, normalizes IPv6-mapped IPv4 dual-stack addresses (e.g., `::ffff:192.168.1.50` $\to$ `192.168.1.50`), and converts IPv6 loopback `::1` $\to$ `127.0.0.1` for consistent rate limiting and geo-IP lookups.
|
|
465
|
-
- **`req.ips`**: An array of IPs if multiple proxies were traversed, each normalized cleanly.
|
|
466
|
-
- **`req.hostname`**: The resolved hostname (from `Host` or `X-Forwarded-Host`).
|
|
467
|
-
- **`req.protocol`**: Returns `'http'` or `'https'`.
|
|
468
|
-
- **`req.secure`**: A boolean (true if `req.protocol === 'https'`).
|
|
164
|
+
When enabled:
|
|
165
|
+
- `req.ip` automatically resolves the client IP, normalizes IPv6 loopback (`::1` $\to$ `127.0.0.1`), and strips IPv4-mapped IPv6 prefixes (`::ffff:192.168.1.1` $\to$ `192.168.1.1`).
|
|
166
|
+
- `req.ips` provides the complete upstream proxy chain.
|
|
167
|
+
- `req.protocol` resolves to `'https'` when `X-Forwarded-Proto: https` is present.
|
|
469
168
|
|
|
470
169
|
---
|
|
471
170
|
|
|
472
|
-
##
|
|
473
|
-
|
|
474
|
-
ExisJS provides a first-class standard Web API `req.signal: AbortSignal` tied directly to the underlying HTTP connection lifecycle.
|
|
171
|
+
## Connection Cancellation & `req.signal`
|
|
475
172
|
|
|
476
|
-
|
|
173
|
+
ExisJS links `req.signal` to the lifecycle of the incoming HTTP connection. If a client closes their browser, navigates away, or aborts an HTTP request, `req.signal` fires an abort event.
|
|
477
174
|
|
|
478
|
-
|
|
175
|
+
Pass `req.signal` to downstream database queries or outgoing HTTP calls to avoid running orphaned operations:
|
|
479
176
|
|
|
480
177
|
```typescript filename="src/http/reports/route.ts"
|
|
481
178
|
import { controller, route } from 'exisjs/router'
|
|
482
179
|
|
|
483
180
|
export default controller({
|
|
484
|
-
|
|
485
|
-
async
|
|
486
|
-
// 1. Pass
|
|
487
|
-
const upstream = await fetch('https://
|
|
488
|
-
signal: req.signal
|
|
181
|
+
exportReport: route.get('/', {
|
|
182
|
+
handle: async ({ req }) => {
|
|
183
|
+
// 1. Pass signal to fetch calls
|
|
184
|
+
const upstream = await fetch('https://analytics.internal/data', {
|
|
185
|
+
signal: req.signal,
|
|
489
186
|
})
|
|
490
|
-
const
|
|
187
|
+
const data = await upstream.json()
|
|
491
188
|
|
|
492
|
-
// 2. Pass
|
|
493
|
-
const
|
|
494
|
-
signal: req.signal
|
|
189
|
+
// 2. Pass signal to database queries
|
|
190
|
+
const report = await db.query('SELECT * FROM large_dataset', {
|
|
191
|
+
signal: req.signal,
|
|
495
192
|
})
|
|
496
193
|
|
|
497
|
-
return {
|
|
498
|
-
}
|
|
499
|
-
})
|
|
194
|
+
return { data, report }
|
|
195
|
+
},
|
|
196
|
+
}),
|
|
500
197
|
})
|
|
501
198
|
```
|
|
502
199
|
|
|
503
|
-
|
|
200
|
+
---
|
|
504
201
|
|
|
505
|
-
|
|
506
|
-
import { Controller, Get, Req } from 'exisjs/decorators'
|
|
507
|
-
import type { Request } from 'exisjs/router'
|
|
202
|
+
## Request Timeouts
|
|
508
203
|
|
|
509
|
-
|
|
510
|
-
export default class ReportsController {
|
|
511
|
-
@Get('/')
|
|
512
|
-
async heavyReport(@Req() req: Request) {
|
|
513
|
-
// 1. Pass req.signal to fetch()
|
|
514
|
-
const upstream = await fetch('https://api.external.com/data', {
|
|
515
|
-
signal: req.signal,
|
|
516
|
-
})
|
|
517
|
-
const payload = await upstream.json()
|
|
518
|
-
|
|
519
|
-
// 2. Pass req.signal to your database queries
|
|
520
|
-
const results = await db.query(sql`SELECT * FROM heavy_analytics`, {
|
|
521
|
-
signal: req.signal,
|
|
522
|
-
})
|
|
523
|
-
|
|
524
|
-
return { payload, results }
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
```
|
|
204
|
+
Dynamic timeout controls allow extending or resetting execution limits for long-running batch operations:
|
|
528
205
|
|
|
529
|
-
|
|
206
|
+
```typescript
|
|
207
|
+
// Set active request timeout to 3 minutes
|
|
208
|
+
req.setTimeout(180000)
|
|
530
209
|
|
|
531
|
-
|
|
210
|
+
// Clear the active timeout
|
|
211
|
+
req.clearTimeout()
|
|
212
|
+
```
|
|
532
213
|
|
|
533
|
-
|
|
214
|
+
---
|
|
534
215
|
|
|
535
|
-
|
|
536
|
-
- **`req.requestId`**: The unique Trace ID (via `requestId`).
|
|
537
|
-
- **`req.user`**: The authenticated user payload (via `JWT` auth middleware).
|
|
538
|
-
- **`req.session`**: The stateful session object (via `session` middleware).
|
|
539
|
-
- **`req.fresh`**: Returns `true` if the client's cache is still valid (ETag match), meaning you can send a 304 response.
|
|
216
|
+
## Custom Context Typing (`TContext`)
|
|
540
217
|
|
|
541
|
-
|
|
218
|
+
Middlewares can attach custom data to the request. You can strongly type this context either per-route or globally:
|
|
542
219
|
|
|
543
|
-
|
|
220
|
+
### Per-Route Generic Context
|
|
544
221
|
|
|
545
|
-
|
|
222
|
+
```typescript filename="src/http/admin/route.ts"
|
|
223
|
+
import { controller, route } from 'exisjs/router'
|
|
546
224
|
|
|
547
|
-
|
|
548
|
-
|
|
225
|
+
interface AdminContext {
|
|
226
|
+
adminUser: { id: string; role: 'superadmin' | 'moderator' }
|
|
227
|
+
}
|
|
549
228
|
|
|
550
|
-
export default
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
229
|
+
export default controller({
|
|
230
|
+
dashboard: route.get<unknown, Record<string, string>, Record<string, string>, AdminContext>('/', {
|
|
231
|
+
handle: async ({ adminUser, req }) => {
|
|
232
|
+
return { activeAdmin: adminUser.id }
|
|
233
|
+
},
|
|
234
|
+
}),
|
|
555
235
|
})
|
|
556
236
|
```
|
|
557
237
|
|
|
558
|
-
###
|
|
559
|
-
|
|
560
|
-
To provide a better developer experience, ExisJS allows you to define a **custom typed context** for your application. This completely eliminates the need for global `.d.ts` augmentations and `as any` casts.
|
|
238
|
+
### App-Wide Context with `createRouter`
|
|
561
239
|
|
|
562
|
-
|
|
240
|
+
Define a typed router factory to eliminate type assertions across your entire project:
|
|
563
241
|
|
|
564
242
|
```typescript filename="src/http/router.ts"
|
|
565
|
-
import { createRouter } from
|
|
243
|
+
import { createRouter } from 'exisjs/router'
|
|
566
244
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
workspace: string;
|
|
245
|
+
export interface AppContext {
|
|
246
|
+
user: { id: string; email: string }
|
|
247
|
+
tenantId: string
|
|
571
248
|
}
|
|
572
249
|
|
|
573
|
-
|
|
574
|
-
export const { route, controller } = createRouter<AppContext>();
|
|
250
|
+
export const { route, controller } = createRouter<AppContext>()
|
|
575
251
|
```
|
|
576
252
|
|
|
577
|
-
|
|
253
|
+
Use your local router in route files to automatically infer injected context:
|
|
578
254
|
|
|
579
|
-
```typescript filename="src/http/
|
|
580
|
-
import {
|
|
255
|
+
```typescript filename="src/http/profile/route.ts"
|
|
256
|
+
import { controller, route } from '../router'
|
|
581
257
|
|
|
582
258
|
export default controller({
|
|
583
|
-
getProfile: route.get(
|
|
584
|
-
async
|
|
585
|
-
//
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
});
|
|
259
|
+
getProfile: route.get('/', {
|
|
260
|
+
handle: async ({ user, tenantId }) => {
|
|
261
|
+
// user and tenantId are strongly typed without casting
|
|
262
|
+
return { user, tenantId }
|
|
263
|
+
},
|
|
264
|
+
}),
|
|
265
|
+
})
|
|
591
266
|
```
|
|
592
267
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## Next Steps
|
|
271
|
+
|
|
272
|
+
- **[Response Helpers & Streaming](/docs/responses)**: Formatting JSON envelopes, custom headers, cookies, and SSE.
|
|
273
|
+
- **[Validation Guide](/docs/validation)**: Building validation schemas with `tex`.
|
|
274
|
+
- **[Controllers](/docs/controllers)**: Parameter decorators and class-based controllers.
|
|
275
|
+
- **[Boundaries](/docs/boundaries)**: Attaching authentication context via middleware cascades.
|