exisjs 0.7.3 → 0.7.4
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/generate.d.ts +4 -0
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +227 -14
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/index.js +8 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/config/config.js +1 -1
- package/dist/config/config.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/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/cli/commands.mdx +4 -3
- package/dist/docs/content/config.mdx +1 -1
- package/dist/docs/content/errors.mdx +43 -9
- package/dist/docs/content/middleware.mdx +138 -0
- package/dist/docs/content/providers.mdx +89 -0
- package/dist/docs/content/requests.mdx +153 -64
- package/dist/docs/content/responses.mdx +56 -1
- package/dist/docs/content/routing.mdx +117 -0
- package/dist/docs/content/validation.mdx +92 -7
- 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/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/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/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.map +1 -1
- package/dist/router/route-scanner.js +26 -0
- 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 +30 -0
- package/dist/router/types.d.ts.map +1 -1
- package/dist/server/request.d.ts +5 -0
- package/dist/server/request.d.ts.map +1 -1
- package/dist/server/request.js +32 -4
- package/dist/server/request.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 +38 -0
- package/dist/validator/tex.d.ts.map +1 -1
- package/dist/validator/tex.js +126 -17
- package/dist/validator/tex.js.map +1 -1
- package/package.json +5 -1
|
@@ -74,7 +74,9 @@ The `HttpError` class provides static factory methods for standard HTTP status c
|
|
|
74
74
|
- `HttpError.forbidden('Message')` - 403
|
|
75
75
|
- `HttpError.notFound('Resource')` - 404
|
|
76
76
|
- `HttpError.conflict('Message')` - 409
|
|
77
|
+
- `HttpError.payloadTooLarge('Message')` - 413
|
|
77
78
|
- `HttpError.unprocessable('Message', details)` - 422
|
|
79
|
+
- `HttpError.rateLimit('Message')` - 429
|
|
78
80
|
- `HttpError.internal('Message')` - 500
|
|
79
81
|
|
|
80
82
|
### Direct Error Classes & Exceptions
|
|
@@ -82,15 +84,17 @@ The `HttpError` class provides static factory methods for standard HTTP status c
|
|
|
82
84
|
If you prefer a class-based approach, ExisJS exports every standard error as its own class. We also provide `Exception` aliases for every error class. You can import and throw them directly:
|
|
83
85
|
|
|
84
86
|
```typescript filename="example.ts"
|
|
85
|
-
import {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
import {
|
|
88
|
+
NotFoundException,
|
|
89
|
+
UnauthorizedException,
|
|
90
|
+
BadRequestError,
|
|
91
|
+
PayloadTooLargeError
|
|
92
|
+
} from 'exisjs/error'
|
|
93
|
+
|
|
94
|
+
export const uploadMedia = (req, res) => {
|
|
95
|
+
if (req.headers['content-length'] > 10 * 1024 * 1024) {
|
|
96
|
+
throw new PayloadTooLargeError('File exceeds 10MB limit')
|
|
97
|
+
}
|
|
94
98
|
}
|
|
95
99
|
```
|
|
96
100
|
|
|
@@ -108,6 +112,36 @@ When an `HttpError` is thrown, the client will automatically receive a structure
|
|
|
108
112
|
}
|
|
109
113
|
```
|
|
110
114
|
|
|
115
|
+
### Validation Error Responses & Diagnostic Visibility
|
|
116
|
+
|
|
117
|
+
When schema validation fails (via `tex`, Zod, or Yup), ExisJS produces a typed `400 Bad Request` featuring a clean `errors: Record<string, string>` map for seamless frontend form field binding:
|
|
118
|
+
|
|
119
|
+
```json filename="HTTP 400 Bad Request Response"
|
|
120
|
+
{
|
|
121
|
+
"statusCode": 400,
|
|
122
|
+
"error": "Bad Request",
|
|
123
|
+
"message": "Validation Error: email: Invalid email format, age: Field must be >= 18",
|
|
124
|
+
"errors": {
|
|
125
|
+
"email": "Invalid email format",
|
|
126
|
+
"age": "Field must be >= 18"
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
#### Development Terminal Diagnostics
|
|
132
|
+
|
|
133
|
+
In development mode, ExisJS automatically outputs formatted tabular diagnostics to the terminal displaying the exact HTTP part, field, received value, and expected requirement:
|
|
134
|
+
|
|
135
|
+
```text
|
|
136
|
+
[VALIDATION FAILED] POST /auth/register (part: body)
|
|
137
|
+
┌─────────────┬───────────────────────────┬──────────────────┐
|
|
138
|
+
│ Field │ Received │ Expected │
|
|
139
|
+
├─────────────┼───────────────────────────┼──────────────────┤
|
|
140
|
+
│ email │ "alice" │ Valid email │
|
|
141
|
+
│ age │ "16" │ min: 18 │
|
|
142
|
+
└─────────────┴───────────────────────────┴──────────────────┘
|
|
143
|
+
```
|
|
144
|
+
|
|
111
145
|
If you throw an unhandled exception (like a database connection failure or a `TypeError`), ExisJS catches it and securely returns a `500 Internal Server Error`, hiding the stack trace from the client in production mode while logging the full trace to your internal logger.
|
|
112
146
|
|
|
113
147
|
## Error Handling Decision Tree
|
|
@@ -213,3 +213,141 @@ const config: ExisConfig = {
|
|
|
213
213
|
|
|
214
214
|
export default config
|
|
215
215
|
```
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## 9. Request Timeout (`timeout`)
|
|
220
|
+
|
|
221
|
+
The `timeout()` middleware terminates runaway requests and returns `503 Service Unavailable` (or a custom status code).
|
|
222
|
+
|
|
223
|
+
ExisJS defaults to **60 seconds (60000ms)** to provide generous headroom for slow network connections and third-party cloud operations (Cloudinary, AWS S3, OpenAI, Stripe).
|
|
224
|
+
|
|
225
|
+
```typescript filename="src/http/server.ts"
|
|
226
|
+
import { exis } from 'exisjs'
|
|
227
|
+
import { timeout } from 'exisjs/middleware'
|
|
228
|
+
|
|
229
|
+
export default exis({
|
|
230
|
+
async onStart(app) {
|
|
231
|
+
app.use(timeout({
|
|
232
|
+
ms: 60000,
|
|
233
|
+
statusCode: 504,
|
|
234
|
+
message: 'Gateway Timeout connecting to upstream cloud service',
|
|
235
|
+
// Bypass webhooks or long-lived streaming endpoints
|
|
236
|
+
exclude: ['/webhooks/*', '/ai/stream'],
|
|
237
|
+
}))
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Handlers can also dynamically adjust timeouts using `req.setTimeout(ms)` and `req.clearTimeout()`.
|
|
243
|
+
|
|
244
|
+
### Route-Level Timeout Overrides
|
|
245
|
+
|
|
246
|
+
Long-running routes (file uploads, PDF exports, AI inferences, data migrations) can declare custom timeouts directly in the route definition, overriding any global application timeout:
|
|
247
|
+
|
|
248
|
+
```typescript filename="src/http/ai/route.ts"
|
|
249
|
+
import { controller, route } from 'exisjs/router'
|
|
250
|
+
|
|
251
|
+
export default controller({
|
|
252
|
+
heavyTask: route.post('/heavy-task', {
|
|
253
|
+
timeoutMs: 120000, // 2 minutes for this endpoint only
|
|
254
|
+
async handle({ body }) {
|
|
255
|
+
return await generateLargeReport(body)
|
|
256
|
+
}
|
|
257
|
+
}),
|
|
258
|
+
|
|
259
|
+
exportStream: route.get('/export', {
|
|
260
|
+
timeoutMs: 0, // 0 disables timeout completely for infinite SSE / streaming
|
|
261
|
+
async handle({ res }) {
|
|
262
|
+
return streamDataToClient(res)
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
})
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
#### Class-Based (OOP) Paradigm
|
|
269
|
+
|
|
270
|
+
Use the `@Timeout(ms)` decorator on any controller method:
|
|
271
|
+
|
|
272
|
+
```typescript filename="src/http/ai/controller.ts"
|
|
273
|
+
import { Controller, Post, Timeout } from 'exisjs/decorators'
|
|
274
|
+
|
|
275
|
+
@Controller('/ai')
|
|
276
|
+
export default class AiController {
|
|
277
|
+
@Post('/inference')
|
|
278
|
+
@Timeout(120000)
|
|
279
|
+
async runInference() {
|
|
280
|
+
return await computeModel()
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
### Declarative Route Uploads
|
|
288
|
+
|
|
289
|
+
You can configure file uploads directly within functional route declarations:
|
|
290
|
+
|
|
291
|
+
```typescript filename="src/http/media/route.ts"
|
|
292
|
+
import { controller, route } from 'exisjs/router'
|
|
293
|
+
|
|
294
|
+
export default controller({
|
|
295
|
+
upload: route.post('/photo', {
|
|
296
|
+
upload: {
|
|
297
|
+
field: 'file',
|
|
298
|
+
maxSize: '10MB',
|
|
299
|
+
allowedMimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
|
|
300
|
+
destination: 'memory', // 'memory' | 'disk' | 'stream'
|
|
301
|
+
},
|
|
302
|
+
async handle({ file, fields, req }) {
|
|
303
|
+
// file.buffer, file.mimetype, file.filename are immediately accessible
|
|
304
|
+
return { success: true, filename: file.filename }
|
|
305
|
+
}
|
|
306
|
+
})
|
|
307
|
+
})
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Class-Based (OOP) Upload Decorators
|
|
311
|
+
|
|
312
|
+
Use `@UseUpload` or `@Upload` to configure file parsing on controller actions:
|
|
313
|
+
|
|
314
|
+
```typescript filename="src/http/media/controller.ts"
|
|
315
|
+
import { Controller, Post, UseUpload, UploadedFile, Body } from 'exisjs/decorators'
|
|
316
|
+
|
|
317
|
+
@Controller('/media')
|
|
318
|
+
export default class MediaController {
|
|
319
|
+
@Post('/photo')
|
|
320
|
+
@UseUpload({
|
|
321
|
+
field: 'file',
|
|
322
|
+
maxSize: '10MB',
|
|
323
|
+
allowedMimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
|
|
324
|
+
destination: 'memory',
|
|
325
|
+
})
|
|
326
|
+
async uploadPhoto(@UploadedFile() file: any, @Body() fields: any) {
|
|
327
|
+
return { filename: file.filename, size: file.size }
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Express / Connect Multer Bridge (`fromExpress`)
|
|
333
|
+
|
|
334
|
+
Seamlessly run existing Express/Connect middleware such as **Multer** directly in ExisJS without manual Promise wrapping:
|
|
335
|
+
|
|
336
|
+
```typescript filename="src/http/legacy/route.ts"
|
|
337
|
+
import { controller, route } from 'exisjs/router'
|
|
338
|
+
import { fromExpress } from 'exisjs/middleware'
|
|
339
|
+
import multer from 'multer'
|
|
340
|
+
|
|
341
|
+
const upload = multer({ storage: multer.memoryStorage() })
|
|
342
|
+
|
|
343
|
+
export default controller({
|
|
344
|
+
upload: route.post('/',
|
|
345
|
+
fromExpress(upload.single('file')),
|
|
346
|
+
({ req }) => {
|
|
347
|
+
const file = (req.raw as any).file || req.file
|
|
348
|
+
return { success: true, originalname: file?.originalname }
|
|
349
|
+
}
|
|
350
|
+
)
|
|
351
|
+
})
|
|
352
|
+
```
|
|
353
|
+
|
|
@@ -177,3 +177,92 @@ export class OperationContext {
|
|
|
177
177
|
> [!WARNING]
|
|
178
178
|
> **Request Scope Constraints**: A provider marked as `scope: 'request'` **must** be resolved within the lifecycle of an active HTTP request. If you try to `inject()` or resolve a request-scoped provider during application startup, inside a background queue worker, or from a cron job, the container will throw a runtime error.
|
|
179
179
|
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Database Providers & Lifecycle Interop (`exisjs/database`)
|
|
183
|
+
|
|
184
|
+
ExisJS provides first-party lifecycle management and transaction coordination for databases (PostgreSQL, SQLite, MySQL, MongoDB, Prisma, Drizzle, Mongoose).
|
|
185
|
+
|
|
186
|
+
### 1. Registering Database Lifecycle (`registerDatabase`)
|
|
187
|
+
|
|
188
|
+
Registering your database with `registerDatabase` ensures connections are automatically connected on boot and safely closed with zero leaked sockets when the server receives `SIGTERM` / `SIGINT`:
|
|
189
|
+
|
|
190
|
+
```typescript filename="src/http/server.ts"
|
|
191
|
+
import { exis } from 'exisjs'
|
|
192
|
+
import { registerDatabase } from 'exisjs/database'
|
|
193
|
+
import mongoose from 'mongoose'
|
|
194
|
+
|
|
195
|
+
export default exis({
|
|
196
|
+
async onStart(app) {
|
|
197
|
+
// Automatically handles startup and graceful shutdown
|
|
198
|
+
registerDatabase(app, {
|
|
199
|
+
name: 'mongodb',
|
|
200
|
+
connect: async () => {
|
|
201
|
+
await mongoose.connect(process.env.MONGODB_URI!)
|
|
202
|
+
},
|
|
203
|
+
disconnect: async () => {
|
|
204
|
+
await mongoose.disconnect()
|
|
205
|
+
},
|
|
206
|
+
healthCheck: async () => {
|
|
207
|
+
return mongoose.connection.readyState === 1
|
|
208
|
+
},
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### 2. Universal Transaction Runner (`withTransaction`)
|
|
215
|
+
|
|
216
|
+
`withTransaction` provides a duck-typed transaction runner that automatically coordinates transaction lifecycles, commits, aborts, and session cleanups across different ORMs:
|
|
217
|
+
|
|
218
|
+
```typescript filename="src/http/orders/service.ts"
|
|
219
|
+
import { withTransaction } from 'exisjs/database'
|
|
220
|
+
import mongoose from 'mongoose'
|
|
221
|
+
import { Order } from '@/models/Order'
|
|
222
|
+
import { Inventory } from '@/models/Inventory'
|
|
223
|
+
|
|
224
|
+
export async function placeOrder(customerId: string, items: { sku: string; qty: number }[]) {
|
|
225
|
+
return withTransaction(mongoose, async (session) => {
|
|
226
|
+
// Automatically attaches session to queries and commits/aborts on error
|
|
227
|
+
const order = await Order.create([{ customerId, items }], { session })
|
|
228
|
+
|
|
229
|
+
for (const item of items) {
|
|
230
|
+
const updated = await Inventory.findOneAndUpdate(
|
|
231
|
+
{ sku: item.sku, stock: { $gte: item.qty } },
|
|
232
|
+
{ $inc: { stock: -item.qty } },
|
|
233
|
+
{ session, returnDocument: 'after' }
|
|
234
|
+
)
|
|
235
|
+
if (!updated) {
|
|
236
|
+
throw new Error(`Insufficient stock for SKU: ${item.sku}`)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return order[0]
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Works identically with Prisma (`prisma.$transaction`) and Drizzle (`db.transaction`).
|
|
246
|
+
|
|
247
|
+
### 3. Modern Mongoose 8/9 Interop & Connection Pooling
|
|
248
|
+
|
|
249
|
+
ExisJS exports helpers to prevent deprecation warnings in modern Mongoose:
|
|
250
|
+
|
|
251
|
+
```typescript filename="src/models/user.ts"
|
|
252
|
+
import { modernUpdateOptions, mongoPoolOptions } from 'exisjs/database'
|
|
253
|
+
import mongoose from 'mongoose'
|
|
254
|
+
|
|
255
|
+
// Cloud-hardened pool options (maxPoolSize, serverSelectionTimeoutMS, etc.)
|
|
256
|
+
export const poolConfig = mongoPoolOptions({ maxPoolSize: 50 })
|
|
257
|
+
|
|
258
|
+
// Automatically maps legacy { new: true } to { returnDocument: 'after' }
|
|
259
|
+
export async function updateUsername(id: string, name: string) {
|
|
260
|
+
return User.findByIdAndUpdate(
|
|
261
|
+
id,
|
|
262
|
+
{ $set: { name } },
|
|
263
|
+
modernUpdateOptions({ returnDocument: 'after' })
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
|
|
@@ -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)
|
|
@@ -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
|
|