exisjs 0.7.3 → 0.7.5
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/dist/cli/commands/build.d.ts +4 -2
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +80 -22
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +121 -27
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/doctor.js +5 -2
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/generate.d.ts +5 -0
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +252 -14
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/start.d.ts.map +1 -1
- package/dist/cli/commands/start.js +17 -5
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/index.js +16 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/manifest.d.ts.map +1 -1
- package/dist/cli/manifest.js +12 -4
- package/dist/cli/manifest.js.map +1 -1
- package/dist/config/config.js +1 -1
- package/dist/config/config.js.map +1 -1
- package/dist/config/env.d.ts.map +1 -1
- package/dist/config/env.js +8 -0
- package/dist/config/env.js.map +1 -1
- package/dist/database/index.d.ts +4 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +20 -0
- package/dist/database/index.js.map +1 -0
- package/dist/database/lifecycle.d.ts +52 -0
- package/dist/database/lifecycle.d.ts.map +1 -0
- package/dist/database/lifecycle.js +122 -0
- package/dist/database/lifecycle.js.map +1 -0
- package/dist/database/mongoose.d.ts +47 -0
- package/dist/database/mongoose.d.ts.map +1 -0
- package/dist/database/mongoose.js +44 -0
- package/dist/database/mongoose.js.map +1 -0
- package/dist/database/transaction.d.ts +39 -0
- package/dist/database/transaction.d.ts.map +1 -0
- package/dist/database/transaction.js +99 -0
- package/dist/database/transaction.js.map +1 -0
- package/dist/decorators/constants.d.ts +2 -0
- package/dist/decorators/constants.d.ts.map +1 -1
- package/dist/decorators/index.d.ts +1 -0
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/decorators/index.js +1 -0
- package/dist/decorators/index.js.map +1 -1
- package/dist/decorators/middleware.d.ts +10 -0
- package/dist/decorators/middleware.d.ts.map +1 -1
- package/dist/decorators/middleware.js +27 -0
- package/dist/decorators/middleware.js.map +1 -1
- package/dist/decorators/upload.d.ts +37 -0
- package/dist/decorators/upload.d.ts.map +1 -0
- package/dist/decorators/upload.js +78 -0
- package/dist/decorators/upload.js.map +1 -0
- package/dist/docs/content/boundaries.mdx +33 -3
- package/dist/docs/content/cli/commands.mdx +4 -3
- package/dist/docs/content/config.mdx +27 -1
- package/dist/docs/content/errors.mdx +81 -11
- package/dist/docs/content/middleware.mdx +138 -0
- package/dist/docs/content/providers.mdx +89 -0
- package/dist/docs/content/requests.mdx +191 -69
- package/dist/docs/content/responses.mdx +99 -6
- package/dist/docs/content/routing.mdx +117 -0
- package/dist/docs/content/security.mdx +17 -4
- package/dist/docs/content/structure.mdx +91 -12
- package/dist/docs/content/validation.mdx +131 -10
- package/dist/docs/reference/database.mdx +152 -0
- package/dist/docs/reference/decorators.mdx +4 -0
- package/dist/docs/reference/error.mdx +3 -1
- package/dist/docs/reference/middleware.mdx +84 -2
- package/dist/docs/reference/router.mdx +4 -2
- package/dist/docs/reference/validator.mdx +43 -5
- package/dist/error/errors.d.ts +10 -0
- package/dist/error/errors.d.ts.map +1 -1
- package/dist/error/errors.js +156 -25
- package/dist/error/errors.js.map +1 -1
- package/dist/error/index.d.ts +1 -1
- package/dist/error/index.d.ts.map +1 -1
- package/dist/error/index.js +3 -1
- package/dist/error/index.js.map +1 -1
- package/dist/lib/start-server.js +16 -0
- package/dist/lib/start-server.js.map +1 -1
- package/dist/middleware/index.d.ts +2 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +18 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/middleware.d.ts +13 -0
- package/dist/middleware/middleware.d.ts.map +1 -1
- package/dist/middleware/middleware.js +22 -2
- package/dist/middleware/middleware.js.map +1 -1
- package/dist/middleware/rate-limit.d.ts.map +1 -1
- package/dist/middleware/rate-limit.js +9 -0
- package/dist/middleware/rate-limit.js.map +1 -1
- package/dist/middleware/security.d.ts +13 -3
- package/dist/middleware/security.d.ts.map +1 -1
- package/dist/middleware/security.js +98 -14
- package/dist/middleware/security.js.map +1 -1
- package/dist/middleware/upload.d.ts +82 -0
- package/dist/middleware/upload.d.ts.map +1 -0
- package/dist/middleware/upload.js +387 -0
- package/dist/middleware/upload.js.map +1 -0
- package/dist/router/boundary.d.ts +12 -0
- package/dist/router/boundary.d.ts.map +1 -1
- package/dist/router/boundary.js.map +1 -1
- package/dist/router/index.d.ts +3 -1
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +6 -1
- package/dist/router/index.js.map +1 -1
- package/dist/router/route-builder.d.ts +11 -2
- package/dist/router/route-builder.d.ts.map +1 -1
- package/dist/router/route-builder.js.map +1 -1
- package/dist/router/route-scanner.d.ts +1 -0
- package/dist/router/route-scanner.d.ts.map +1 -1
- package/dist/router/route-scanner.js +219 -8
- package/dist/router/route-scanner.js.map +1 -1
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/router.js +58 -2
- package/dist/router/router.js.map +1 -1
- package/dist/router/types.d.ts +33 -1
- package/dist/router/types.d.ts.map +1 -1
- package/dist/sanitize/index.d.ts +6 -6
- package/dist/sanitize/index.d.ts.map +1 -1
- package/dist/sanitize/index.js +8 -8
- package/dist/sanitize/index.js.map +1 -1
- package/dist/server/app.d.ts +1 -0
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/app.js +4 -0
- package/dist/server/app.js.map +1 -1
- package/dist/server/engines/node-engine.d.ts.map +1 -1
- package/dist/server/engines/node-engine.js +1 -0
- package/dist/server/engines/node-engine.js.map +1 -1
- package/dist/server/request-handler.d.ts.map +1 -1
- package/dist/server/request-handler.js +15 -1
- package/dist/server/request-handler.js.map +1 -1
- package/dist/server/request.d.ts +11 -0
- package/dist/server/request.d.ts.map +1 -1
- package/dist/server/request.js +125 -36
- package/dist/server/request.js.map +1 -1
- package/dist/server/response.d.ts +3 -1
- package/dist/server/response.d.ts.map +1 -1
- package/dist/server/response.js +98 -10
- package/dist/server/response.js.map +1 -1
- package/dist/testing/mocks.d.ts +3 -1
- package/dist/testing/mocks.d.ts.map +1 -1
- package/dist/testing/mocks.js +7 -1
- package/dist/testing/mocks.js.map +1 -1
- package/dist/validator/index.d.ts +1 -1
- package/dist/validator/index.d.ts.map +1 -1
- package/dist/validator/index.js +3 -1
- package/dist/validator/index.js.map +1 -1
- package/dist/validator/tex-types.d.ts +5 -1
- package/dist/validator/tex-types.d.ts.map +1 -1
- package/dist/validator/tex-types.js +4 -0
- package/dist/validator/tex-types.js.map +1 -1
- package/dist/validator/tex.d.ts +45 -7
- package/dist/validator/tex.d.ts.map +1 -1
- package/dist/validator/tex.js +341 -20
- package/dist/validator/tex.js.map +1 -1
- package/package.json +12 -8
|
@@ -91,112 +91,201 @@ export default class ProcessController {
|
|
|
91
91
|
|
|
92
92
|
---
|
|
93
93
|
|
|
94
|
-
## 3. File Uploads & Form Data
|
|
94
|
+
## 3. File Uploads & Multipart Form Data
|
|
95
95
|
|
|
96
|
-
Handling `multipart/form-data` in Node.js can be difficult. ExisJS provides
|
|
96
|
+
Handling `multipart/form-data` in Node.js can be difficult. ExisJS provides multiple robust, high-performance approaches:
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
### A. Declarative Route Uploads (Recommended)
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
You can declare file upload requirements directly inside your route schema. ExisJS automatically executes the multipart parser, validates MIME types, enforces byte caps, and populates `req.file` or `req.files` along with non-file fields in `req.body`:
|
|
101
|
+
|
|
102
|
+
```typescript filename="src/http/upload/route.ts"
|
|
103
|
+
import { controller, route } from 'exisjs/router'
|
|
104
|
+
import { tex } from 'exisjs/validator'
|
|
105
|
+
|
|
106
|
+
export default controller({
|
|
107
|
+
uploadPhoto: route.post('/', {
|
|
108
|
+
upload: {
|
|
109
|
+
field: 'photo',
|
|
110
|
+
maxBytes: 10 * 1024 * 1024, // 10MB limit
|
|
111
|
+
mimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
|
|
112
|
+
},
|
|
113
|
+
body: tex.object({
|
|
114
|
+
title: tex.string({ min: 1 }),
|
|
115
|
+
}),
|
|
116
|
+
handle({ req }) {
|
|
117
|
+
const file = req.file! // Typed UploadedFile
|
|
118
|
+
console.log(`Uploaded ${file.originalname} (${file.size} bytes)`)
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
success: true,
|
|
122
|
+
filename: file.filename,
|
|
123
|
+
mimetype: file.mimetype,
|
|
124
|
+
title: req.body.title,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### B. First-Party `fileUpload` Middleware
|
|
132
|
+
|
|
133
|
+
ExisJS exports a dedicated `fileUpload` middleware from `exisjs/middleware` supporting single, multiple, and multi-field uploads:
|
|
134
|
+
|
|
135
|
+
```typescript filename="src/http/documents/route.ts"
|
|
136
|
+
import { controller, route } from 'exisjs/router'
|
|
137
|
+
import { fileUpload } from 'exisjs/middleware'
|
|
138
|
+
|
|
139
|
+
export default controller({
|
|
140
|
+
// Single file upload
|
|
141
|
+
single: route.post('/single',
|
|
142
|
+
fileUpload.single('document', {
|
|
143
|
+
maxBytes: 15 * 1024 * 1024, // 15MB
|
|
144
|
+
dest: './uploads/docs',
|
|
145
|
+
}),
|
|
146
|
+
({ req }) => {
|
|
147
|
+
return { file: req.file }
|
|
148
|
+
}
|
|
149
|
+
),
|
|
150
|
+
|
|
151
|
+
// Multiple files with the same field name
|
|
152
|
+
gallery: route.post('/gallery',
|
|
153
|
+
fileUpload.array('photos', 5, {
|
|
154
|
+
mimeTypes: ['image/*'],
|
|
155
|
+
}),
|
|
156
|
+
({ req }) => {
|
|
157
|
+
const files = req.files // UploadedFile[]
|
|
158
|
+
return { count: files?.length }
|
|
159
|
+
}
|
|
160
|
+
),
|
|
161
|
+
|
|
162
|
+
// Multiple distinct fields
|
|
163
|
+
profile: route.post('/profile',
|
|
164
|
+
fileUpload.fields([
|
|
165
|
+
{ name: 'avatar', maxCount: 1 },
|
|
166
|
+
{ name: 'cover', maxCount: 1 },
|
|
167
|
+
]),
|
|
168
|
+
({ req }) => {
|
|
169
|
+
// req.files is a dictionary Record<string, UploadedFile[]>
|
|
170
|
+
return { files: req.files }
|
|
171
|
+
}
|
|
172
|
+
),
|
|
173
|
+
})
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### C. Integrating Express/Connect Middleware (e.g. Multer)
|
|
177
|
+
|
|
178
|
+
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:
|
|
179
|
+
|
|
180
|
+
```typescript filename="src/http/legacy/route.ts"
|
|
181
|
+
import { controller, route } from 'exisjs/router'
|
|
182
|
+
import { fromExpress } from 'exisjs/middleware'
|
|
183
|
+
import multer from 'multer'
|
|
184
|
+
|
|
185
|
+
const upload = multer({
|
|
186
|
+
storage: multer.memoryStorage(),
|
|
187
|
+
limits: { fileSize: 10 * 1024 * 1024 },
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
export default controller({
|
|
191
|
+
upload: route.post('/',
|
|
192
|
+
fromExpress(upload.single('file')),
|
|
193
|
+
({ req }) => {
|
|
194
|
+
// Access multer's req.file or req.body seamlessly
|
|
195
|
+
const file = (req.raw as any).file || req.file
|
|
196
|
+
return { success: true, originalname: file?.originalname }
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
})
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### D. Manual `req.formData()` & Streaming Uploads
|
|
203
|
+
|
|
204
|
+
For manual control over incoming multipart streams without route middleware:
|
|
101
205
|
|
|
102
206
|
```typescript filename="src/http/upload/route.ts"
|
|
103
207
|
import { controller, route } from 'exisjs/router'
|
|
104
208
|
|
|
105
209
|
export default controller({
|
|
106
210
|
uploadAvatar: route.post('/avatar', {
|
|
107
|
-
async handle(req
|
|
108
|
-
//
|
|
211
|
+
async handle({ req }) {
|
|
212
|
+
// Parse on-demand
|
|
109
213
|
const { fields, files } = await req.formData()
|
|
110
|
-
|
|
111
|
-
// 2. Access the file (e.g., from an input named "profile_pic")
|
|
112
214
|
const uploadedFile = files['profile_pic']
|
|
113
215
|
|
|
114
216
|
if (uploadedFile) {
|
|
115
|
-
//
|
|
217
|
+
// Save to disk safely with automated collision-free naming
|
|
116
218
|
const savedPath = await uploadedFile.saveToDisk('./uploads/avatars')
|
|
117
219
|
return { success: true, path: savedPath }
|
|
118
220
|
}
|
|
119
221
|
|
|
120
|
-
return { success: false, error:
|
|
222
|
+
return { success: false, error: 'No file uploaded' }
|
|
121
223
|
}
|
|
122
224
|
})
|
|
123
225
|
})
|
|
124
226
|
```
|
|
125
227
|
|
|
126
|
-
|
|
228
|
+
---
|
|
127
229
|
|
|
128
|
-
|
|
129
|
-
import { Controller, Post, UploadedFile } from 'exisjs/decorators'
|
|
230
|
+
## 4. Body Size Limits & HTTP 413
|
|
130
231
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
232
|
+
ExisJS comes pre-configured with a generous **10MB default body limit** (`10485760` bytes) across JSON, form-data, and raw payloads, preventing premature drops during photo or document uploads.
|
|
233
|
+
|
|
234
|
+
If a client sends a payload exceeding the configured threshold, ExisJS halts parsing immediately and responds with `413 Payload Too Large`:
|
|
235
|
+
|
|
236
|
+
```json
|
|
237
|
+
{
|
|
238
|
+
"statusCode": 413,
|
|
239
|
+
"error": "Payload Too Large",
|
|
240
|
+
"message": "Request payload exceeded size limit of 10485760 bytes. Increase server.bodyLimit in exis.config.ts to allow larger payloads."
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
To configure custom limits globally across your app, update `exis.config.ts`:
|
|
245
|
+
|
|
246
|
+
```typescript filename="exis.config.ts"
|
|
247
|
+
export default {
|
|
248
|
+
server: {
|
|
249
|
+
// Set custom maximum payload limit in bytes (e.g. 50MB)
|
|
250
|
+
bodyLimit: 50 * 1024 * 1024,
|
|
142
251
|
}
|
|
143
252
|
}
|
|
144
253
|
```
|
|
145
254
|
|
|
146
|
-
|
|
255
|
+
---
|
|
147
256
|
|
|
148
|
-
|
|
257
|
+
## 5. Dynamic Request Timeouts
|
|
149
258
|
|
|
150
|
-
|
|
259
|
+
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).
|
|
151
260
|
|
|
152
|
-
|
|
261
|
+
### Dynamic In-Handler Control
|
|
262
|
+
|
|
263
|
+
Handlers can dynamically extend or cancel the active timeout on the fly using `req.setTimeout()` and `req.clearTimeout()`:
|
|
264
|
+
|
|
265
|
+
```typescript filename="src/http/ai/route.ts"
|
|
153
266
|
import { controller, route } from 'exisjs/router'
|
|
154
267
|
|
|
155
268
|
export default controller({
|
|
156
|
-
|
|
157
|
-
async handle(req
|
|
158
|
-
// 1.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
uploadedFiles: files.map(f => f.filename),
|
|
168
|
-
customFields: fields
|
|
269
|
+
generateVideo: route.post('/', {
|
|
270
|
+
async handle({ req }) {
|
|
271
|
+
// 1. Extend timeout for heavy AI pass generation (e.g. 3 minutes)
|
|
272
|
+
req.setTimeout(180000)
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
const result = await longRunningAiTask()
|
|
276
|
+
return { success: true, result }
|
|
277
|
+
} finally {
|
|
278
|
+
// 2. Clear timeout once work completes
|
|
279
|
+
req.clearTimeout()
|
|
169
280
|
}
|
|
170
281
|
}
|
|
171
282
|
})
|
|
172
283
|
})
|
|
173
284
|
```
|
|
174
285
|
|
|
175
|
-
#### Class-Based (OOP) Paradigm
|
|
176
|
-
|
|
177
|
-
```typescript
|
|
178
|
-
import { Controller, Post, Req } from 'exisjs/decorators'
|
|
179
|
-
import { streamUpload } from 'exisjs/storage'
|
|
180
|
-
|
|
181
|
-
@Controller()
|
|
182
|
-
export default class UploadController {
|
|
183
|
-
@Post('/cover')
|
|
184
|
-
async uploadCover(@Req() req: any) {
|
|
185
|
-
// Both req.streamUpload() and the exported helper from exisjs/storage work.
|
|
186
|
-
const { fields, files } = await streamUpload(req, { dest: './uploads/covers' })
|
|
187
|
-
|
|
188
|
-
return {
|
|
189
|
-
success: true,
|
|
190
|
-
uploadedFiles: files.map(f => f.filename),
|
|
191
|
-
customFields: fields
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
```
|
|
196
|
-
|
|
197
286
|
---
|
|
198
287
|
|
|
199
|
-
##
|
|
288
|
+
## 6. HTTP Headers & Content Negotiation
|
|
200
289
|
|
|
201
290
|
```typescript
|
|
202
291
|
// Get a specific header (case-insensitive)
|
|
@@ -214,19 +303,52 @@ const language = req.acceptsLanguages('en', 'es', 'fr')
|
|
|
214
303
|
|
|
215
304
|
---
|
|
216
305
|
|
|
217
|
-
##
|
|
306
|
+
## 7. Network Information & IPv6 Normalization
|
|
218
307
|
|
|
219
|
-
ExisJS handles trust-proxies. If you are behind Cloudflare, Nginx, or an AWS Load Balancer, simply set `trustProxy: true` in your `exis()` config, and these properties will automatically resolve the true client IP from `X-Forwarded-For`.
|
|
308
|
+
ExisJS handles trust-proxies and dual-stack IP networks automatically. If you are behind Cloudflare, Nginx, or an AWS Load Balancer, simply set `trustProxy: true` in your `exis()` config, and these properties will automatically resolve the true client IP from `X-Forwarded-For`.
|
|
220
309
|
|
|
221
|
-
- **`req.ip`**: The client's IP address.
|
|
222
|
-
- **`req.ips`**: An array of IPs if multiple proxies were traversed.
|
|
310
|
+
- **`req.ip`**: The client's IP address. Automatically 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.
|
|
311
|
+
- **`req.ips`**: An array of IPs if multiple proxies were traversed, each normalized cleanly.
|
|
223
312
|
- **`req.hostname`**: The resolved hostname (from `Host` or `X-Forwarded-Host`).
|
|
224
313
|
- **`req.protocol`**: Returns `'http'` or `'https'`.
|
|
225
314
|
- **`req.secure`**: A boolean (true if `req.protocol === 'https'`).
|
|
226
315
|
|
|
227
316
|
---
|
|
228
317
|
|
|
229
|
-
##
|
|
318
|
+
## 8. Request Cancellation & Abort Signals (`req.signal`)
|
|
319
|
+
|
|
320
|
+
ExisJS provides a first-class standard Web API `req.signal: AbortSignal` tied directly to the underlying HTTP connection lifecycle.
|
|
321
|
+
|
|
322
|
+
When a client closes their browser tab, cancels a download, or aborts an HTTP request, `req.signal` immediately triggers an `abort` event. You can pass `req.signal` directly to ORMs, database drivers, and HTTP clients to cancel expensive in-flight operations and eliminate wasted server resources.
|
|
323
|
+
|
|
324
|
+
#### Example: Passing to Database Queries & Upstream Fetches
|
|
325
|
+
|
|
326
|
+
```typescript filename="src/http/reports/route.ts"
|
|
327
|
+
import { controller, route } from 'exisjs/router'
|
|
328
|
+
|
|
329
|
+
export default controller({
|
|
330
|
+
heavyReport: route.get('/', {
|
|
331
|
+
async handle({ req }) {
|
|
332
|
+
// 1. Pass req.signal to fetch() — if the client leaves, fetch aborts immediately
|
|
333
|
+
const upstream = await fetch('https://api.external.com/data', {
|
|
334
|
+
signal: req.signal
|
|
335
|
+
})
|
|
336
|
+
const payload = await upstream.json()
|
|
337
|
+
|
|
338
|
+
// 2. Pass req.signal to your database queries (Prisma, Drizzle, pg, etc.)
|
|
339
|
+
const results = await db.query(sql`SELECT * FROM heavy_analytics`, {
|
|
340
|
+
signal: req.signal
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
return { payload, results }
|
|
344
|
+
}
|
|
345
|
+
})
|
|
346
|
+
})
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## 9. Context & Metadata
|
|
230
352
|
|
|
231
353
|
As your request moves through ExisJS Middlewares, various properties are attached to `req` for your convenience:
|
|
232
354
|
|
|
@@ -132,7 +132,62 @@ export default class UsersController {
|
|
|
132
132
|
|
|
133
133
|
---
|
|
134
134
|
|
|
135
|
-
## 3.
|
|
135
|
+
## 3. Standardized Pagination Envelopes (`paginate`)
|
|
136
|
+
|
|
137
|
+
To eliminate pagination inconsistency across different endpoints, ExisJS exports standard `paginate()` and `getPaginationSkip()` utilities directly from `exisjs/validator` (as well as `exisjs/response`).
|
|
138
|
+
|
|
139
|
+
```typescript filename="src/http/products/route.ts"
|
|
140
|
+
import { controller, route } from 'exisjs/router'
|
|
141
|
+
import { tex, paginate, getPaginationSkip } from 'exisjs/validator'
|
|
142
|
+
|
|
143
|
+
export default controller({
|
|
144
|
+
listProducts: route.get('/', {
|
|
145
|
+
query: tex.pagination({ defaultLimit: 15, maxLimit: 50 }),
|
|
146
|
+
async handle({ query }) {
|
|
147
|
+
// 1. Automatically extracts { skip, limit, page } with fallback defaults
|
|
148
|
+
const { skip, limit, page } = getPaginationSkip(query)
|
|
149
|
+
|
|
150
|
+
// 2. Fetch items and count total from your database
|
|
151
|
+
const [items, total] = await Promise.all([
|
|
152
|
+
db.product.findMany({ skip, take: limit, orderBy: { createdAt: 'desc' } }),
|
|
153
|
+
db.product.count(),
|
|
154
|
+
])
|
|
155
|
+
|
|
156
|
+
// 3. Return standardized pagination envelope
|
|
157
|
+
return paginate(items, total, { page, limit })
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### The Standardized Pagination Envelope
|
|
164
|
+
|
|
165
|
+
Calling `paginate(items, total, { page, limit })` produces a uniform envelope:
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"data": [
|
|
170
|
+
{ "id": 1, "name": "Mechanical Keyboard" },
|
|
171
|
+
{ "id": 2, "name": "Ergonomic Mouse" }
|
|
172
|
+
],
|
|
173
|
+
"pagination": {
|
|
174
|
+
"page": 1,
|
|
175
|
+
"limit": 15,
|
|
176
|
+
"total": 42,
|
|
177
|
+
"totalPages": 3,
|
|
178
|
+
"hasNext": true,
|
|
179
|
+
"hasPrev": false,
|
|
180
|
+
"hasNextPage": true,
|
|
181
|
+
"hasPrevPage": false
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Both `hasNext`/`hasPrev` and `hasNextPage`/`hasPrevPage` are provided out-of-the-box, ensuring full compatibility with both cursor-style and page-number frontend UI components.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## 4. Setting Headers & Status
|
|
136
191
|
|
|
137
192
|
The `ExisResponse` object implements a chainable API for HTTP headers and status codes.
|
|
138
193
|
|
|
@@ -155,10 +210,12 @@ res.status(404)
|
|
|
155
210
|
|
|
156
211
|
## 3. Cookies
|
|
157
212
|
|
|
158
|
-
ExisJS provides native helper methods for setting and clearing cookies safely.
|
|
213
|
+
ExisJS provides native helper methods for setting and clearing cookies safely, supporting modern web standards including CHIPS (Partitioned Cookies) and Priority hints.
|
|
214
|
+
|
|
215
|
+
### Setting Cookies
|
|
159
216
|
|
|
160
217
|
```typescript
|
|
161
|
-
// Set a secure, HTTP-only cookie
|
|
218
|
+
// Set a secure, HTTP-only session cookie
|
|
162
219
|
res.cookie('session_id', '123456', {
|
|
163
220
|
maxAge: 86400, // 1 day in seconds
|
|
164
221
|
httpOnly: true,
|
|
@@ -166,15 +223,49 @@ res.cookie('session_id', '123456', {
|
|
|
166
223
|
sameSite: 'Strict'
|
|
167
224
|
})
|
|
168
225
|
|
|
169
|
-
//
|
|
226
|
+
// Cross-site embedded iframe cookie with CHIPS (Partitioned)
|
|
227
|
+
res.cookie('widget_pref', 'dark_mode', {
|
|
228
|
+
sameSite: 'None', // Automatically forces `secure: true` per modern browser specs
|
|
229
|
+
partitioned: true, // Cookies Having Independent Partitioned State (CHIPS)
|
|
230
|
+
priority: 'high', // Hints browser cookie retention under storage pressure ('low' | 'medium' | 'high')
|
|
231
|
+
path: '/'
|
|
232
|
+
})
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
#### Cookie Options Reference
|
|
236
|
+
|
|
237
|
+
| Option | Type | Default | Description |
|
|
238
|
+
| :--- | :--- | :--- | :--- |
|
|
239
|
+
| `maxAge` | `number` | `undefined` | Lifetime of the cookie in seconds (sets both `Max-Age` and computed `Expires`). |
|
|
240
|
+
| `expires` | `Date` | `undefined` | Absolute expiration date for the cookie. |
|
|
241
|
+
| `httpOnly` | `boolean` | `false` | Mitigates XSS by preventing client-side JavaScript access. |
|
|
242
|
+
| `secure` | `boolean` | `false` | Requires HTTPS. *Automatically set to `true` if `sameSite: 'None'` is provided.* |
|
|
243
|
+
| `sameSite` | `'Strict' \| 'Lax' \| 'None' \| boolean` | `undefined` | Controls cross-site request cookie inclusion. |
|
|
244
|
+
| `partitioned` | `boolean` | `false` | Enables CHIPS for third-party contexts across top-level site partitions. |
|
|
245
|
+
| `priority` | `'low' \| 'medium' \| 'high'` | `undefined` | RFC 6265bis storage priority hint when client cookie capacity is exhausted. |
|
|
246
|
+
| `path` | `string` | `'/'` | Path where the cookie is valid. |
|
|
247
|
+
| `domain` | `string` | `undefined` | Domain scope for the cookie. |
|
|
248
|
+
|
|
249
|
+
### Clearing Cookies
|
|
250
|
+
|
|
251
|
+
When clearing cookies, browsers require the matching `path` and `domain` used when the cookie was originally set:
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
// Clear default root cookie
|
|
170
255
|
res.clearCookie('session_id')
|
|
256
|
+
|
|
257
|
+
// Clear scoped cookie matching original domain & path
|
|
258
|
+
res.clearCookie('tenant_token', {
|
|
259
|
+
path: '/api',
|
|
260
|
+
domain: '.example.com'
|
|
261
|
+
})
|
|
171
262
|
```
|
|
172
263
|
|
|
173
264
|
---
|
|
174
265
|
|
|
175
266
|
## 4. File Downloads & Streaming
|
|
176
267
|
|
|
177
|
-
ExisJS makes streaming files simple, bypassing V8 memory bottlenecks by piping native Node streams directly to the socket.
|
|
268
|
+
ExisJS makes streaming files simple, bypassing V8 memory bottlenecks by piping native Node streams directly to the socket with automated resource cleanup.
|
|
178
269
|
|
|
179
270
|
### `res.download()`
|
|
180
271
|
Automatically reads a file from disk, sets the `Content-Disposition` header to force a download in the browser, and infers the correct MIME type based on the file extension.
|
|
@@ -208,7 +299,9 @@ export default class ReportController {
|
|
|
208
299
|
```
|
|
209
300
|
|
|
210
301
|
### `res.sendStream()`
|
|
211
|
-
|
|
302
|
+
Pipe any Node.js `Readable` stream directly to the client with built-in leak prevention:
|
|
303
|
+
- **Client Disconnect Cleanup**: If the client aborts or closes the connection prematurely, the source stream is immediately destroyed (`stream.destroy()`), releasing underlying file handles or external sockets.
|
|
304
|
+
- **Unhandled Error Catching**: Errors emitted during stream consumption are trapped gracefully, preventing unhandled stream crashes.
|
|
212
305
|
|
|
213
306
|
#### Functional Paradigm
|
|
214
307
|
|
|
@@ -452,6 +452,123 @@ export default class SearchController {
|
|
|
452
452
|
|
|
453
453
|
|
|
454
454
|
|
|
455
|
+
## Declarative Route Options (Uploads, Timeouts & Validation)
|
|
456
|
+
|
|
457
|
+
ExisJS allows configuring input validation schemas, multipart file uploads, and per-route timeouts directly inside the route definition without manual middleware plumbing.
|
|
458
|
+
|
|
459
|
+
### Declarative File Uploads
|
|
460
|
+
|
|
461
|
+
Specify `upload` in your route definition to automatically handle `multipart/form-data`. Uploaded files are parsed and attached to `req.file` or `req.files`, while non-file form fields are populated in `req.body`.
|
|
462
|
+
|
|
463
|
+
#### Functional Paradigm
|
|
464
|
+
```typescript filename="src/http/avatar/route.ts"
|
|
465
|
+
import { controller, route } from 'exisjs/router'
|
|
466
|
+
import { tex } from 'exisjs/validator'
|
|
467
|
+
|
|
468
|
+
export default controller({
|
|
469
|
+
uploadAvatar: route.post('/', {
|
|
470
|
+
upload: {
|
|
471
|
+
field: 'avatar',
|
|
472
|
+
maxBytes: 5 * 1024 * 1024, // 5MB limit
|
|
473
|
+
mimeTypes: ['image/png', 'image/jpeg', 'image/webp'],
|
|
474
|
+
},
|
|
475
|
+
body: tex.object({
|
|
476
|
+
caption: tex.string({ optional: true }),
|
|
477
|
+
}),
|
|
478
|
+
handle({ req }) {
|
|
479
|
+
const file = req.file! // UploadedFile { filename, buffer, size, mimetype }
|
|
480
|
+
const caption = req.body?.caption
|
|
481
|
+
|
|
482
|
+
return {
|
|
483
|
+
success: true,
|
|
484
|
+
filename: file.filename,
|
|
485
|
+
size: file.size,
|
|
486
|
+
caption,
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
})
|
|
490
|
+
})
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
#### Class-Based (OOP) Paradigm
|
|
494
|
+
```typescript filename="src/http/avatar/route.ts"
|
|
495
|
+
import { Controller, Post, Body } from 'exisjs/decorators'
|
|
496
|
+
import { Upload, UploadedFile } from 'exisjs/decorators'
|
|
497
|
+
import type { UploadedFile as UploadedFileType } from 'exisjs/middleware'
|
|
498
|
+
|
|
499
|
+
@Controller('/avatar')
|
|
500
|
+
export default class AvatarController {
|
|
501
|
+
@Post('/')
|
|
502
|
+
@Upload('avatar', {
|
|
503
|
+
maxBytes: 5 * 1024 * 1024,
|
|
504
|
+
mimeTypes: ['image/png', 'image/jpeg', 'image/webp'],
|
|
505
|
+
})
|
|
506
|
+
upload(
|
|
507
|
+
@UploadedFile() file: UploadedFileType,
|
|
508
|
+
@Body('caption') caption?: string
|
|
509
|
+
) {
|
|
510
|
+
return {
|
|
511
|
+
success: true,
|
|
512
|
+
filename: file.filename,
|
|
513
|
+
size: file.size,
|
|
514
|
+
caption,
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
### Route-Level Timeout Overrides
|
|
521
|
+
|
|
522
|
+
While ExisJS provides a generous 60-second default global timeout, long-running batch jobs, PDF generation, or third-party AI integrations can override the timeout at the individual route level:
|
|
523
|
+
|
|
524
|
+
#### Functional Paradigm
|
|
525
|
+
```typescript filename="src/http/reports/route.ts"
|
|
526
|
+
import { controller, route } from 'exisjs/router'
|
|
527
|
+
|
|
528
|
+
export default controller({
|
|
529
|
+
generateAnnualReport: route.post('/annual', {
|
|
530
|
+
timeout: 120000, // 2 minutes specifically for this route
|
|
531
|
+
async handle() {
|
|
532
|
+
// Intensive document rendering or AI workflow
|
|
533
|
+
const reportUrl = await buildReport()
|
|
534
|
+
return { success: true, reportUrl }
|
|
535
|
+
}
|
|
536
|
+
})
|
|
537
|
+
})
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### Query Pagination Schemas
|
|
541
|
+
|
|
542
|
+
ExisJS provides the `tex.pagination()` primitive for declarative pagination, sorting, and ordering query parameters. String query parameters are automatically coerced into numbers:
|
|
543
|
+
|
|
544
|
+
```typescript filename="src/http/products/route.ts"
|
|
545
|
+
import { controller, route } from 'exisjs/router'
|
|
546
|
+
import { tex, paginate, getPaginationSkip } from 'exisjs/validator'
|
|
547
|
+
|
|
548
|
+
const ProductQuerySchema = tex.pagination({
|
|
549
|
+
defaultLimit: 20,
|
|
550
|
+
maxLimit: 100,
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
export default controller({
|
|
554
|
+
list: route.get('/', {
|
|
555
|
+
query: ProductQuerySchema,
|
|
556
|
+
async handle({ query }) {
|
|
557
|
+
// query.page and query.limit are guaranteed positive numbers
|
|
558
|
+
const { skip, limit, page } = getPaginationSkip(query)
|
|
559
|
+
|
|
560
|
+
// Fetch data using your preferred database query
|
|
561
|
+
const [items, total] = await Promise.all([
|
|
562
|
+
db.products.findMany({ skip, take: limit }),
|
|
563
|
+
db.products.count()
|
|
564
|
+
])
|
|
565
|
+
|
|
566
|
+
return paginate(items, total, { page, limit })
|
|
567
|
+
}
|
|
568
|
+
})
|
|
569
|
+
})
|
|
570
|
+
```
|
|
571
|
+
|
|
455
572
|
## Dependency Injection in Functional Controllers
|
|
456
573
|
|
|
457
574
|
ExisJS natively supports **Request-Scoped Dependency Injection** directly inside functional routes. You don't need to use classes to get the benefits of DI!
|
|
@@ -41,7 +41,7 @@ export default exis({
|
|
|
41
41
|
|
|
42
42
|
## 2. In-Memory Rate Limiting
|
|
43
43
|
|
|
44
|
-
The core `rateLimit` middleware provides high-performance in-memory and off-heap native rate limiting:
|
|
44
|
+
The core `rateLimit` middleware provides high-performance in-memory and off-heap native rate limiting with standard HTTP response headers:
|
|
45
45
|
|
|
46
46
|
```typescript filename="src/http/boundary.ts"
|
|
47
47
|
import { defineBoundary } from 'exisjs/router'
|
|
@@ -51,12 +51,25 @@ export const config = defineBoundary({})
|
|
|
51
51
|
|
|
52
52
|
// Apply rate limiting chain step across the boundary
|
|
53
53
|
export const limiter = rateLimit({
|
|
54
|
-
windowMs: 60 * 1000, // 1 minute
|
|
55
|
-
max: 100,
|
|
56
|
-
message: 'Too many requests from this IP, please try again after a minute'
|
|
54
|
+
windowMs: 60 * 1000, // 1 minute window
|
|
55
|
+
max: 100, // 100 requests per window
|
|
56
|
+
message: 'Too many requests from this IP, please try again after a minute',
|
|
57
|
+
keyGenerator: (req) => req.ip // Uses normalized IPv4/IPv6 client IP
|
|
57
58
|
})
|
|
58
59
|
```
|
|
59
60
|
|
|
61
|
+
### Standard Rate Limit Headers
|
|
62
|
+
|
|
63
|
+
`rateLimit` automatically emits standard IETF draft and legacy rate-limiting headers on every response:
|
|
64
|
+
|
|
65
|
+
| Header | Description | Example |
|
|
66
|
+
| :--- | :--- | :--- |
|
|
67
|
+
| `RateLimit-Limit` | Maximum request quota for the configured window. | `100` |
|
|
68
|
+
| `RateLimit-Remaining` | Remaining requests permitted within the current window. | `42` |
|
|
69
|
+
| `RateLimit-Reset` | Time remaining in seconds until the window quota resets. | `18` |
|
|
70
|
+
| `Retry-After` | Included on `429 Too Many Requests` (RFC 6585) indicating seconds to wait. | `18` |
|
|
71
|
+
| `X-RateLimit-*` | Legacy header equivalents for backwards compatibility. | `X-RateLimit-Reset: 18` |
|
|
72
|
+
|
|
60
73
|
> [!NOTE]
|
|
61
74
|
> The core `rateLimit` middleware runs using an in-memory native off-heap store. For distributed, multi-instance rate limiting backed by Redis, use `@exisjs/cache`.
|
|
62
75
|
|