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.
Files changed (111) hide show
  1. package/dist/cli/commands/generate.d.ts +4 -0
  2. package/dist/cli/commands/generate.d.ts.map +1 -1
  3. package/dist/cli/commands/generate.js +227 -14
  4. package/dist/cli/commands/generate.js.map +1 -1
  5. package/dist/cli/index.js +8 -4
  6. package/dist/cli/index.js.map +1 -1
  7. package/dist/config/config.js +1 -1
  8. package/dist/config/config.js.map +1 -1
  9. package/dist/database/index.d.ts +4 -0
  10. package/dist/database/index.d.ts.map +1 -0
  11. package/dist/database/index.js +20 -0
  12. package/dist/database/index.js.map +1 -0
  13. package/dist/database/lifecycle.d.ts +52 -0
  14. package/dist/database/lifecycle.d.ts.map +1 -0
  15. package/dist/database/lifecycle.js +122 -0
  16. package/dist/database/lifecycle.js.map +1 -0
  17. package/dist/database/mongoose.d.ts +47 -0
  18. package/dist/database/mongoose.d.ts.map +1 -0
  19. package/dist/database/mongoose.js +44 -0
  20. package/dist/database/mongoose.js.map +1 -0
  21. package/dist/database/transaction.d.ts +39 -0
  22. package/dist/database/transaction.d.ts.map +1 -0
  23. package/dist/database/transaction.js +99 -0
  24. package/dist/database/transaction.js.map +1 -0
  25. package/dist/decorators/index.d.ts +1 -0
  26. package/dist/decorators/index.d.ts.map +1 -1
  27. package/dist/decorators/index.js +1 -0
  28. package/dist/decorators/index.js.map +1 -1
  29. package/dist/decorators/middleware.d.ts +10 -0
  30. package/dist/decorators/middleware.d.ts.map +1 -1
  31. package/dist/decorators/middleware.js +27 -0
  32. package/dist/decorators/middleware.js.map +1 -1
  33. package/dist/decorators/upload.d.ts +37 -0
  34. package/dist/decorators/upload.d.ts.map +1 -0
  35. package/dist/decorators/upload.js +78 -0
  36. package/dist/decorators/upload.js.map +1 -0
  37. package/dist/docs/content/cli/commands.mdx +4 -3
  38. package/dist/docs/content/config.mdx +1 -1
  39. package/dist/docs/content/errors.mdx +43 -9
  40. package/dist/docs/content/middleware.mdx +138 -0
  41. package/dist/docs/content/providers.mdx +89 -0
  42. package/dist/docs/content/requests.mdx +153 -64
  43. package/dist/docs/content/responses.mdx +56 -1
  44. package/dist/docs/content/routing.mdx +117 -0
  45. package/dist/docs/content/validation.mdx +92 -7
  46. package/dist/docs/reference/database.mdx +152 -0
  47. package/dist/docs/reference/decorators.mdx +4 -0
  48. package/dist/docs/reference/error.mdx +3 -1
  49. package/dist/docs/reference/middleware.mdx +84 -2
  50. package/dist/docs/reference/router.mdx +4 -2
  51. package/dist/docs/reference/validator.mdx +43 -5
  52. package/dist/error/errors.d.ts +10 -0
  53. package/dist/error/errors.d.ts.map +1 -1
  54. package/dist/error/errors.js +156 -25
  55. package/dist/error/errors.js.map +1 -1
  56. package/dist/error/index.d.ts +1 -1
  57. package/dist/error/index.d.ts.map +1 -1
  58. package/dist/error/index.js +3 -1
  59. package/dist/error/index.js.map +1 -1
  60. package/dist/middleware/index.d.ts +2 -0
  61. package/dist/middleware/index.d.ts.map +1 -0
  62. package/dist/middleware/index.js +18 -0
  63. package/dist/middleware/index.js.map +1 -0
  64. package/dist/middleware/middleware.d.ts +13 -0
  65. package/dist/middleware/middleware.d.ts.map +1 -1
  66. package/dist/middleware/middleware.js +22 -2
  67. package/dist/middleware/middleware.js.map +1 -1
  68. package/dist/middleware/security.d.ts +13 -3
  69. package/dist/middleware/security.d.ts.map +1 -1
  70. package/dist/middleware/security.js +98 -14
  71. package/dist/middleware/security.js.map +1 -1
  72. package/dist/middleware/upload.d.ts +82 -0
  73. package/dist/middleware/upload.d.ts.map +1 -0
  74. package/dist/middleware/upload.js +387 -0
  75. package/dist/middleware/upload.js.map +1 -0
  76. package/dist/router/index.d.ts +3 -1
  77. package/dist/router/index.d.ts.map +1 -1
  78. package/dist/router/index.js +6 -1
  79. package/dist/router/index.js.map +1 -1
  80. package/dist/router/route-builder.d.ts +11 -2
  81. package/dist/router/route-builder.d.ts.map +1 -1
  82. package/dist/router/route-builder.js.map +1 -1
  83. package/dist/router/route-scanner.d.ts.map +1 -1
  84. package/dist/router/route-scanner.js +26 -0
  85. package/dist/router/route-scanner.js.map +1 -1
  86. package/dist/router/router.d.ts.map +1 -1
  87. package/dist/router/router.js +58 -2
  88. package/dist/router/router.js.map +1 -1
  89. package/dist/router/types.d.ts +30 -0
  90. package/dist/router/types.d.ts.map +1 -1
  91. package/dist/server/request.d.ts +5 -0
  92. package/dist/server/request.d.ts.map +1 -1
  93. package/dist/server/request.js +32 -4
  94. package/dist/server/request.js.map +1 -1
  95. package/dist/testing/mocks.d.ts +3 -1
  96. package/dist/testing/mocks.d.ts.map +1 -1
  97. package/dist/testing/mocks.js +7 -1
  98. package/dist/testing/mocks.js.map +1 -1
  99. package/dist/validator/index.d.ts +1 -1
  100. package/dist/validator/index.d.ts.map +1 -1
  101. package/dist/validator/index.js +3 -1
  102. package/dist/validator/index.js.map +1 -1
  103. package/dist/validator/tex-types.d.ts +5 -1
  104. package/dist/validator/tex-types.d.ts.map +1 -1
  105. package/dist/validator/tex-types.js +4 -0
  106. package/dist/validator/tex-types.js.map +1 -1
  107. package/dist/validator/tex.d.ts +38 -0
  108. package/dist/validator/tex.d.ts.map +1 -1
  109. package/dist/validator/tex.js +126 -17
  110. package/dist/validator/tex.js.map +1 -1
  111. package/package.json +5 -1
@@ -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!
@@ -38,11 +38,17 @@ ExisJS supports a comprehensive set of strict validation primitives:
38
38
  - `tex.boolean()` - Strict boolean validation.
39
39
  - `tex.number({ coerce: true })` - Automatically coerces incoming string representations into native JavaScript numbers.
40
40
  - `tex.boolean({ coerce: true })` - Coerces `"true"`/`"1"` and `"false"`/`"0"` into native booleans.
41
- - `tex.array(schema)` - Validates an array of items.
41
+ - `tex.array(schema)` - Validates an array of items. Accepts primitive types, `tex.object(...)`, or shorthand plain objects `{ ... }`.
42
42
  - `tex.object({ ... })` - Validates structured JSON objects.
43
43
  - `tex.enum(['A', 'B'])` - Ensures the value matches one of the provided exact strings.
44
44
 
45
- All primitives accept an options object where you can pass `{ optional: true }` to allow `undefined`.
45
+ All primitives accept modifier options or chained helper methods:
46
+ - `.optional()` / `{ optional: true }` - Allows `undefined`.
47
+ - `.nullable()` / `{ nullable: true }` - Allows `null`.
48
+ - `.nullish()` / `{ nullish: true }` - Allows both `null` and `undefined`.
49
+
50
+ > [!TIP]
51
+ > **Automatic Empty String `""` Normalization**: When forms or query parameters pass empty strings `""` for fields marked as `.optional()`, `.nullable()`, or `.nullish()`, ExisJS automatically converts them to `undefined` or `null` before running type validation. This prevents false positive type errors on numeric, UUID, or enum fields!
46
52
 
47
53
  ### Data Sanitization (Trimming & Formatting)
48
54
 
@@ -217,16 +223,95 @@ export default class BooksController {
217
223
  ```
218
224
 
219
225
 
220
- ## Error Handling
226
+ ### Nested Sub-Documents & Array Schemas
227
+
228
+ ExisJS natively validates arbitrarily deep sub-documents and nested arrays off-heap with Rust acceleration:
229
+
230
+ ```typescript filename="src/http/users/schema.ts"
231
+ import { tex } from 'exisjs/validator'
232
+
233
+ export const RegisterUserSchema = tex.object({
234
+ username: tex.string({ min: 3 }),
235
+ profile: tex.object({
236
+ bio: tex.string().nullish(), // allows undefined, null, or ""
237
+ avatarUrl: tex.string({ optional: true }),
238
+ socials: tex.object({
239
+ twitter: tex.string({ optional: true }),
240
+ github: tex.string({ optional: true }),
241
+ }),
242
+ tags: tex.array(tex.string({ min: 2 })),
243
+ }),
244
+ // You can use tex.object(...) OR a plain object schema shorthand!
245
+ items: tex.array({
246
+ sku: tex.string(),
247
+ quantity: tex.number({ min: 1 }),
248
+ }),
249
+ })
250
+ ```
221
251
 
222
- If a request fails validation (e.g., missing a required body field), the validation engine will instantly catch it and return a standardized `400 Bad Request` JSON response to the client.
252
+ #### First-Class Form & Multipart Support
253
+ - **Single-Object Coercion**: When HTML forms or mobile clients submit a single item instead of an array (e.g. `{ items: { sku: "ABC", quantity: 1 } }`), the Rust engine automatically coerces it into a single-element array `[ { sku: "ABC", quantity: 1 } ]` rather than throwing `"Must be an array"`.
254
+ - **Stringified JSON Support**: When submitting `multipart/form-data`, stringified JSON arrays (e.g. `items: "[{\"sku\":\"ABC\",\"quantity\":2}]"`) or stringified objects are automatically deserialized and validated.
255
+ - **Type Inference**: TypeScript automatically infers the shape of array elements for both `tex.array(tex.object(...))` and `tex.array({ ... })`, ensuring full autocomplete without falling back to `never`.
256
+
257
+ ---
258
+
259
+ ## Pagination Schemas (`tex.pagination`)
260
+
261
+ ExisJS provides a dedicated schema primitive for query pagination. It automatically coerces incoming string query parameters (`?page=2&limit=25&sort=name&order=asc`) into typed numbers and strings while enforcing sensible bounds:
262
+
263
+ ```typescript filename="src/http/items/route.ts"
264
+ import { controller, route } from 'exisjs/router'
265
+ import { tex, paginate, getPaginationSkip } from 'exisjs/validator'
266
+
267
+ const ItemQuerySchema = tex.pagination({
268
+ defaultLimit: 20, // Default items per page if omitted
269
+ maxLimit: 100, // Hard cap to prevent memory exhaustion
270
+ })
223
271
 
224
- ```json filename="Response"
272
+ export default controller({
273
+ list: route.get('/', {
274
+ query: ItemQuerySchema,
275
+ async handle({ query }) {
276
+ const { skip, limit, page } = getPaginationSkip(query)
277
+ const items = await fetchItems({ skip, limit, sort: query.sort, order: query.order })
278
+ const total = await countItems()
279
+
280
+ return paginate(items, total, { page, limit })
281
+ }
282
+ })
283
+ })
284
+ ```
285
+
286
+ ---
287
+
288
+ ## Error Handling & Developer Diagnostics
289
+
290
+ If a request fails validation, ExisJS intercepts the rejection and returns a standardized `400 Bad Request` containing a machine-readable `errors: Record<string, string>` dictionary for frontend form field mapping:
291
+
292
+ ```json filename="HTTP 400 Bad Request Response"
225
293
  {
226
294
  "statusCode": 400,
227
295
  "error": "Bad Request",
228
- "message": "Validation Error: body.username is required"
296
+ "message": "Validation Error: username: Field must be at least 4 characters, profile.tags: Expected array of strings",
297
+ "errors": {
298
+ "username": "Field must be at least 4 characters",
299
+ "profile.tags": "Expected array of strings"
300
+ }
229
301
  }
230
302
  ```
231
303
 
232
- Because this is built right into the framework, you don't have to manually catch errors or construct complex schema error formatters. It just works exactly as you expect!
304
+ ### Dev Mode Terminal Diagnostics
305
+
306
+ When validation fails in development mode, ExisJS prints an immediate tabular diagnostic to your terminal displaying the failing HTTP part, exact field path, received value, and expected schema constraint:
307
+
308
+ ```text
309
+ [VALIDATION FAILED] POST /visits (part: body)
310
+ ┌─────────────┬───────────────────────────┬──────────────────┐
311
+ │ Field │ Received │ Expected │
312
+ ├─────────────┼───────────────────────────┼──────────────────┤
313
+ │ phone │ "123" │ min: 7, max: 20 │
314
+ │ idProofs │ "[object Object]" │ Array of IDProof │
315
+ └─────────────┴───────────────────────────┴──────────────────┘
316
+ ```
317
+
@@ -0,0 +1,152 @@
1
+ ---
2
+ title: "Database"
3
+ description: "API Reference for ExisJS Database Lifecycle & Transactions (exisjs/database)."
4
+ ---
5
+
6
+ # Database (`exisjs/database`)
7
+
8
+ ExisJS provides a unified, database-agnostic lifecycle and transaction layer. It enables automatic connection management, multi-database health checks, graceful zero-downtime teardowns, and ORM-agnostic transaction runners.
9
+
10
+ ---
11
+
12
+ ## 1. Lifecycle Management
13
+
14
+ ### `registerDatabase(app, options)`
15
+
16
+ Registers a database connection into the application lifecycle. Automatically calls `connect()` during server boot and `disconnect()` during graceful shutdown (`SIGTERM`, `SIGINT`, or `app.close()`).
17
+
18
+ #### Options (`DatabaseRegistrationOptions`)
19
+ - `name` (`string`): Unique identifier for the database connection (e.g. `'postgres'`, `'mongodb'`).
20
+ - `connect` (`() => Promise<void>`): Async callback to establish connection and connection pools.
21
+ - `disconnect` (`() => Promise<void>`): Async callback to close connections gracefully.
22
+ - `healthCheck` (`() => Promise<boolean>`): Optional health check callback returning `true` when healthy.
23
+
24
+ ```typescript filename="src/http/server.ts"
25
+ import { exis } from 'exisjs'
26
+ import { registerDatabase } from 'exisjs/database'
27
+ import { PrismaClient } from '@prisma/client'
28
+
29
+ const prisma = new PrismaClient()
30
+
31
+ export default exis({
32
+ async onStart(app) {
33
+ registerDatabase(app, {
34
+ name: 'prisma-postgres',
35
+ connect: async () => {
36
+ await prisma.$connect()
37
+ },
38
+ disconnect: async () => {
39
+ await prisma.$disconnect()
40
+ },
41
+ healthCheck: async () => {
42
+ await prisma.$queryRaw`SELECT 1`
43
+ return true
44
+ },
45
+ })
46
+ }
47
+ })
48
+ ```
49
+
50
+ ---
51
+
52
+ ### `DatabaseManager`
53
+
54
+ Singleton registry managing all registered database adapters.
55
+
56
+ - `DatabaseManager.register(options)`: Registers a database adapter.
57
+ - `DatabaseManager.connectAll()`: Connects all registered databases concurrently.
58
+ - `DatabaseManager.disconnectAll()`: Gracefully disconnects all databases concurrently.
59
+ - `DatabaseManager.checkHealth()`: Runs health checks across all registered databases and returns `{ healthy: boolean, databases: Record<string, boolean> }`.
60
+ - `DatabaseManager.clear()`: Clears all registered database adapters (useful for test tear-downs).
61
+
62
+ ```typescript filename="src/http/health/route.ts"
63
+ import { controller, route } from 'exisjs/router'
64
+ import { DatabaseManager } from 'exisjs/database'
65
+
66
+ export default controller({
67
+ check: route.get('/health', async () => {
68
+ const health = await DatabaseManager.checkHealth()
69
+ return {
70
+ status: health.healthy ? 'ok' : 'degraded',
71
+ databases: health.databases,
72
+ }
73
+ })
74
+ })
75
+ ```
76
+
77
+ ---
78
+
79
+ ## 2. Universal Transaction Runner
80
+
81
+ ### `withTransaction(client, callback)`
82
+
83
+ A duck-typed transaction runner that coordinates database transaction lifecycles, commits, rollbacks, and session cleanups. Compatible with **Mongoose**, **Prisma**, and **Drizzle**.
84
+
85
+ ```typescript filename="src/services/transfer.ts"
86
+ import { withTransaction } from 'exisjs/database'
87
+ import mongoose from 'mongoose'
88
+ import { Account } from '@/models/Account'
89
+
90
+ export async function transferFunds(fromId: string, toId: string, amount: number) {
91
+ return withTransaction(mongoose, async (session) => {
92
+ // Session is automatically attached to MongoDB queries
93
+ const sender = await Account.findByIdAndUpdate(
94
+ fromId,
95
+ { $inc: { balance: -amount } },
96
+ { session, returnDocument: 'after' }
97
+ )
98
+
99
+ if (!sender || sender.balance < 0) {
100
+ // Throwing an error automatically aborts the transaction and rolls back
101
+ throw new Error('Insufficient funds')
102
+ }
103
+
104
+ const receiver = await Account.findByIdAndUpdate(
105
+ toId,
106
+ { $inc: { balance: amount } },
107
+ { session, returnDocument: 'after' }
108
+ )
109
+
110
+ return { sender, receiver }
111
+ })
112
+ }
113
+ ```
114
+
115
+ Works identically with Prisma:
116
+ ```typescript
117
+ import { withTransaction } from 'exisjs/database'
118
+ import { prisma } from '@/lib/prisma'
119
+
120
+ const result = await withTransaction(prisma, async (tx) => {
121
+ return tx.user.create({ data: { name: 'Alice' } })
122
+ })
123
+ ```
124
+
125
+ ---
126
+
127
+ ## 3. Mongoose 8/9 Interop & Pooling
128
+
129
+ ### `modernUpdateOptions(options?)`
130
+
131
+ Maps deprecated Mongoose options (such as `{ new: true }`) to standard modern options (`{ returnDocument: 'after' }`), eliminating console warnings when upgrading to Mongoose 8 and 9.
132
+
133
+ ```typescript
134
+ import { modernUpdateOptions } from 'exisjs/database'
135
+
136
+ // Passes { returnDocument: 'after' } warning-free
137
+ User.findByIdAndUpdate(id, { $set: updateData }, modernUpdateOptions())
138
+ ```
139
+
140
+ ### `mongoPoolOptions(customOptions?)`
141
+
142
+ Returns pre-configured, production-hardened MongoDB connection pool settings:
143
+
144
+ ```typescript
145
+ import { mongoPoolOptions } from 'exisjs/database'
146
+ import mongoose from 'mongoose'
147
+
148
+ await mongoose.connect(process.env.MONGODB_URI!, mongoPoolOptions({
149
+ maxPoolSize: 100,
150
+ minPoolSize: 10,
151
+ }))
152
+ ```
@@ -68,6 +68,8 @@ Parameter decorators automatically extract data from the incoming request and in
68
68
  - `@Param(nameOrPipe?)`: Injects a URL path parameter (e.g., `id` from `/:id`).
69
69
  - `@Headers(nameOrPipe?)`: Injects an HTTP header by name.
70
70
  - `@QueryParam(nameOrPipe?)` / `@Query(nameOrPipe?)`: Injects a URL query parameter (e.g., `?search=term`).
71
+ - `@UploadedFile()`: Injects the uploaded file (`req.file`) into the method argument.
72
+ - `@UploadedFiles()`: Injects the uploaded files array or dictionary (`req.files`) into the method argument.
71
73
  - `@HostParam(nameOrPipe?)`: Injects a subdomain parameter.
72
74
  - `@Req()`: Injects the raw ExisJS Request object.
73
75
  - `@Res(options?)`: Injects the raw ExisJS Response object (`{ passthrough: true }`).
@@ -80,6 +82,8 @@ Parameter decorators automatically extract data from the incoming request and in
80
82
  ## 3. Middleware & Pipeline Decorators
81
83
 
82
84
  - `@Use(...middlewaresOrGuardsOrInterceptorsOrFilters)`: Unified decorator that binds middlewares, guards (`canActivate`), interceptors (`intercept`), or exception filters (`catch`) to a controller class or method.
85
+ - `@Upload(fieldName, options?)` / `@UseUpload(...)`: Configures multipart file upload handling for a method, populating `@UploadedFile()` or `@UploadedFiles()`.
86
+ - `@Timeout(ms: number | TimeoutOptions)`: Overrides the request timeout for this method (e.g. `@Timeout(120000)`). Passing `0` disables timeouts for long-running streaming or exports.
83
87
  - `@Idempotent(options?)`: Caches responses based on the `Idempotency-Key` header using native in-memory off-heap storage.
84
88
 
85
89
  ---
@@ -61,6 +61,7 @@ export default controller({
61
61
  - `HttpError.forbidden(msg?)` (403)
62
62
  - `HttpError.notFound(resource?)` (404)
63
63
  - `HttpError.conflict(msg)` (409)
64
+ - `HttpError.payloadTooLarge(msg?)` (413)
64
65
  - `HttpError.unprocessable(msg, details?)` (422)
65
66
  - `HttpError.tooManyRequests(msg?)` (429)
66
67
  - `HttpError.internal(msg?)` (500)
@@ -83,7 +84,7 @@ If you catch an `HttpError` or intercept it, you have access to the following pr
83
84
  If you prefer class instantiation (common in NestJS workflows), you can directly instantiate the error classes or their `Exception` aliases.
84
85
 
85
86
  ```typescript filename="src/services/auth.ts"
86
- import { UnauthorizedError, ConflictException } from 'exisjs/error'
87
+ import { UnauthorizedError, ConflictException, PayloadTooLargeError } from 'exisjs/error'
87
88
 
88
89
  export function verifyToken(token: string) {
89
90
  if (!isValid(token)) {
@@ -104,6 +105,7 @@ export function createUser(email: string) {
104
105
  - `ForbiddenError` / `ForbiddenException`
105
106
  - `NotFoundError` / `NotFoundException`
106
107
  - `ConflictError` / `ConflictException`
108
+ - `PayloadTooLargeError` / `PayloadTooLargeException`
107
109
  - `UnprocessableError` / `UnprocessableException`
108
110
  - `RateLimitError` / `RateLimitException`
109
111
  - `InternalError` / `InternalException`
@@ -62,8 +62,67 @@ app.use(helmet())
62
62
  ### `csrf(options)`
63
63
  Implements the Signed Double Submit Cookie pattern for Cross-Site Request Forgery (CSRF) protection.
64
64
 
65
- ### `timeout(ms)`
66
- Automatically times out requests that exceed the specified milliseconds, returning a 503 response.
65
+ ### `timeout(options?)`
66
+ Automatically times out requests that exceed the specified duration, returning a `503 Service Unavailable` response (or custom status code).
67
+
68
+ - **Default**: `60000` (60 seconds)
69
+ - **Options**:
70
+ - `ms` (`number`): Timeout duration in milliseconds.
71
+ - `statusCode` (`number`): Status code to respond with (default: `503`).
72
+ - `message` (`string`): Error message returned to the client.
73
+ - `exclude` (`(string | RegExp)[]`): Array of route paths or regex patterns to exempt from timeout checks.
74
+
75
+ ```typescript
76
+ import { timeout } from 'exisjs/middleware'
77
+
78
+ app.use(timeout({
79
+ ms: 60000,
80
+ exclude: ['/webhooks/*', '/ai/stream'],
81
+ }))
82
+ ```
83
+
84
+ Handlers can dynamically alter the active timeout using `req.setTimeout(ms)` or `req.clearTimeout()`.
85
+
86
+ ### `fileUpload(options?)`
87
+ First-party multipart form data parsing and file upload middleware.
88
+
89
+ - **Methods**:
90
+ - `fileUpload.single(fieldName, options?)`: Parses a single file into `req.file`.
91
+ - `fileUpload.array(fieldName, maxCount?, options?)`: Parses multiple files with the same field into `req.files` (`UploadedFile[]`).
92
+ - `fileUpload.fields([{ name, maxCount }])`: Parses multiple distinct fields into `req.files` (`Record<string, UploadedFile[]>`).
93
+ - `fileUpload.any(options?)`: Accepts any uploaded files.
94
+ - `fileUpload.none(options?)`: Accepts only text fields; rejects file uploads.
95
+ - **Options**:
96
+ - `dest` (`string`): Directory path to save uploaded files to disk automatically.
97
+ - `maxBytes` (`number`): Maximum file size in bytes (shorthand for `limits.fileSize`).
98
+ - `mimeTypes` (`string[]`): Array of permitted MIME types (e.g. `['image/png', 'image/jpeg']`).
99
+ - `limits` (`{ fileSize?: number; files?: number }`): Detailed size and count limits.
100
+
101
+ ```typescript
102
+ import { fileUpload } from 'exisjs/middleware'
103
+
104
+ app.post('/avatar', fileUpload.single('avatar', {
105
+ maxBytes: 5 * 1024 * 1024,
106
+ mimeTypes: ['image/*'],
107
+ }), (req, res) => {
108
+ const file = req.file // UploadedFile { filename, originalname, mimetype, size, buffer }
109
+ res.json({ success: true, filename: file.filename })
110
+ })
111
+ ```
112
+
113
+ ### `fromExpress(middleware)`
114
+ Seamlessly adapts standard Express/Connect middleware (such as `multer`) to run within the ExisJS pipeline.
115
+
116
+ ```typescript
117
+ import { fromExpress } from 'exisjs/middleware'
118
+ import multer from 'multer'
119
+
120
+ const upload = multer({ dest: './uploads' })
121
+
122
+ app.post('/upload', fromExpress(upload.single('file')), (req, res) => {
123
+ res.json({ file: (req.raw as any).file })
124
+ })
125
+ ```
67
126
 
68
127
  ### `hpp()`
69
128
  HTTP Parameter Pollution protection.
@@ -90,6 +149,29 @@ app.use(rateLimit({
90
149
  }))
91
150
  ```
92
151
 
152
+ ### `timeout(options?)`
153
+ Terminates long-running requests that exceed the specified threshold, responding with `503 Service Unavailable` (or custom status). Defaults to 60,000ms.
154
+
155
+ ```typescript
156
+ import { timeout } from 'exisjs/middleware'
157
+
158
+ app.use(timeout({
159
+ ms: 60000,
160
+ statusCode: 504,
161
+ message: 'Gateway Timeout',
162
+ exclude: ['/webhooks/*', '/ai/stream'],
163
+ }))
164
+ ```
165
+
166
+ ### `routeTimeout(msOrOptions)`
167
+ Applies an endpoint-specific timeout. If a global timeout is already active, dynamically overrides the timer for this route. If no global timeout exists, initializes an isolated timeout timer. Passing `0` disables timeouts for streaming/exports.
168
+
169
+ ```typescript
170
+ import { routeTimeout } from 'exisjs/middleware'
171
+
172
+ app.post('/heavy-task', routeTimeout(120000), (req, res) => { ... })
173
+ ```
174
+
93
175
  ### `idempotent(options?)`
94
176
  Caches responses based on the `Idempotency-Key` header into off-heap native memory to prevent duplicate mutations.
95
177
 
@@ -78,9 +78,11 @@ export default controller({
78
78
 
79
79
  ### Options (`RouteConfig`)
80
80
 
81
- - `body`: A validation schema for the request body (e.g., using `v` or Zod).
82
- - `query`: A validation schema for the query string.
81
+ - `body`: A validation schema for the request body (e.g., using `tex` or Zod).
82
+ - `query`: A validation schema for the query string (e.g., `tex.pagination()`).
83
83
  - `params`: A validation schema for the URL path parameters.
84
+ - `upload` (`string | RouteUploadOptions`): Declarative multipart upload configuration (e.g. `{ field: 'photo', maxBytes: 10 * 1024 * 1024, mimeTypes: [...] }`). Automatically mounts `fileUpload` middleware and populates `req.file` / `req.files`.
85
+ - `timeout` (`number`): Per-route timeout override in milliseconds (e.g. `120000`). Overrides global timeout for this endpoint.
84
86
  - `response`: A validation schema to serialize/validate the outgoing response.
85
87
  - `middleware` (`Handler[]`): An array of middleware specific to this route.
86
88
  - `cors` (`boolean | CorsConfig`): Route-specific CORS overrides.
@@ -21,17 +21,50 @@ The `tex` object provides factory functions for every common primitive and data
21
21
 
22
22
  ### Objects & Arrays
23
23
 
24
- - `tex.object({ ... })`: Validates an object against a strict shape.
25
- - `tex.array(schema)`: Validates an array of items matching the given schema.
24
+ - `tex.object({ ... })`: Validates an object against a strict shape. Supports arbitrarily nested sub-documents (`tex.object({ nested: tex.object(...) })`).
25
+ - `tex.array(schema, options?)`: Validates an array of items matching the given schema.
26
+ - **Schema Types**: Accepts primitive types (`tex.string()`), nested `tex.object(...)`, or shorthand plain objects (`tex.array({ ... })`).
27
+ - **TypeScript Inference**: TypeScript automatically infers the element object shape for both `tex.object(...)` and plain objects without falling back to `never`.
28
+ - **Single-Object Coercion**: Automatically coerces single objects (e.g. `{ item: { ... } }` from HTML forms) into single-item arrays `[ { ... } ]` rather than throwing `"Must be an array"`.
29
+ - **Stringified JSON**: Automatically parses and deserializes stringified JSON arrays (e.g. from `multipart/form-data`).
30
+ - **Options**: Supports `{ min, max, dedupe, optional, nullable, nullish }`.
26
31
 
27
32
  ```typescript
28
33
  const UserProfile = tex.object({
29
34
  id: tex.string(),
30
- tags: tex.array(tex.string()),
31
- role: tex.enum(['admin', 'user'])
35
+ profile: tex.object({
36
+ bio: tex.string().nullish(), // allows undefined, null, or ""
37
+ skills: tex.array(tex.string()),
38
+ }),
39
+ // Can use plain object shorthand directly inside tex.array
40
+ items: tex.array({
41
+ id: tex.string(),
42
+ quantity: tex.number({ min: 1 }),
43
+ }),
44
+ role: tex.enum(['admin', 'user']),
32
45
  })
33
46
  ```
34
47
 
48
+ ### Pagination Primitive (`tex.pagination`)
49
+
50
+ - `tex.pagination(options?)`: Pre-compiled query schema validating `page` (default 1), `limit` (with coercion and clamping), `sort`, and `order` (`asc` | `desc`).
51
+
52
+ ```typescript
53
+ import { tex, paginate, getPaginationSkip } from 'exisjs/validator'
54
+
55
+ const paginationSchema = tex.pagination({
56
+ defaultLimit: 20,
57
+ maxLimit: 100,
58
+ })
59
+
60
+ // Database offset calculation helper
61
+ const { skip, limit, page } = getPaginationSkip({ page: 2, limit: 20 })
62
+ // skip = 20, limit = 20, page = 2
63
+
64
+ // Standardized envelope builder
65
+ const paginatedResponse = paginate(items, 150, { page: 2, limit: 20 })
66
+ ```
67
+
35
68
  ---
36
69
 
37
70
  ## Usage in Routes
@@ -78,9 +111,14 @@ export default controller({
78
111
 
79
112
  All validators accept an options object that allows you to configure validation constraints and behaviors.
80
113
 
81
- - `optional`: Marks the field as optional (allows `undefined`).
114
+ - `optional` / `.optional()`: Marks the field as optional (allows `undefined`).
115
+ - `nullable` / `.nullable()`: Marks the field as nullable (allows `null`).
116
+ - `nullish` / `.nullish()`: Marks the field as nullish (allows both `null` and `undefined`).
82
117
  - `coerce`: Instructs the Rust engine to automatically convert types when possible before validating. For example, converting `"123"` into a number, or `3000` into a string.
83
118
 
119
+ > [!NOTE]
120
+ > **Empty String Interop**: If an incoming string is empty (`""`), and the field is configured as `optional`, `nullable`, or `nullish`, ExisJS automatically converts it to `undefined` or `null` before running type checks. This cleanly handles form submissions and query strings without requiring ad-hoc pre-processors.
121
+
84
122
  ```typescript
85
123
  // Type Conversion Examples using { coerce: true }
86
124
  const EnvSchema = tex.object({
@@ -11,6 +11,7 @@ export declare class HttpError extends Error {
11
11
  static notFound(resource?: string): HttpError;
12
12
  static conflict(message: string): HttpError;
13
13
  static unprocessable(message: string, details?: unknown): HttpError;
14
+ static payloadTooLarge(message?: string, details?: unknown): HttpError;
14
15
  static tooManyRequests(message?: string): HttpError;
15
16
  static internal(message?: string): HttpError;
16
17
  static serviceUnavailable(message?: string): HttpError;
@@ -35,6 +36,9 @@ export declare class ConflictError extends HttpError {
35
36
  export declare class UnprocessableError extends HttpError {
36
37
  constructor(message?: string, details?: unknown);
37
38
  }
39
+ export declare class PayloadTooLargeError extends HttpError {
40
+ constructor(message?: string, details?: unknown);
41
+ }
38
42
  export declare class RateLimitError extends HttpError {
39
43
  constructor(message?: string);
40
44
  }
@@ -53,11 +57,17 @@ export declare const UnauthorizedException: typeof UnauthorizedError;
53
57
  export declare const ForbiddenException: typeof ForbiddenError;
54
58
  export declare const NotFoundException: typeof NotFoundError;
55
59
  export declare const ConflictException: typeof ConflictError;
60
+ export declare const PayloadTooLargeException: typeof PayloadTooLargeError;
56
61
  export declare const UnprocessableException: typeof UnprocessableError;
57
62
  export declare const RateLimitException: typeof RateLimitError;
58
63
  export declare const InternalException: typeof InternalError;
59
64
  export declare const ServiceUnavailableException: typeof ServiceUnavailableError;
60
65
  export declare const GatewayTimeoutException: typeof GatewayTimeoutError;
66
+ export declare function renderValidationTable(rows: {
67
+ field: string;
68
+ received: string;
69
+ expected: string;
70
+ }[]): string;
61
71
  export declare function createErrorHandler(isDev?: boolean): ErrorHandler;
62
72
  /**
63
73
  * Async handler wrapper — catches promise rejections.
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/error/errors.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,UAAU,CAAA;AAKrD,qBAAa,SAAU,SAAQ,KAAK;IAClC,SAAgB,UAAU,EAAE,MAAM,CAAA;IAClC,SAAgB,IAAI,EAAE,MAAM,CAAA;IAC5B,SAAgB,aAAa,EAAE,OAAO,CAAA;IACtC,SAAgB,OAAO,CAAC,EAAE,OAAO,CAAA;gBAG/B,OAAO,EAAE,MAAM,EACf,UAAU,SAAM,EAChB,IAAI,SAAmB,EACvB,OAAO,CAAC,EAAE,OAAO;IAenB,MAAM,CAAC,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS;IAIhE,MAAM,CAAC,YAAY,CAAC,OAAO,SAAiB,GAAG,SAAS;IAIxD,MAAM,CAAC,SAAS,CAAC,OAAO,SAAc,GAAG,SAAS;IAIlD,MAAM,CAAC,QAAQ,CAAC,QAAQ,SAAa,GAAG,SAAS;IAIjD,MAAM,CAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS;IAI3C,MAAM,CAAC,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS;IAInE,MAAM,CAAC,eAAe,CAAC,OAAO,SAAsB,GAAG,SAAS;IAIhE,MAAM,CAAC,QAAQ,CAAC,OAAO,SAA0B,GAAG,SAAS;IAI7D,MAAM,CAAC,kBAAkB,CAAC,OAAO,SAAwB,GAAG,SAAS;IAIrE,MAAM,CAAC,cAAc,CAAC,OAAO,SAAoB,GAAG,SAAS;IAI7D,MAAM,IAAI,MAAM;CAUjB;AAED,qBAAa,eAAgB,SAAQ,SAAS;gBAChC,OAAO,SAAgB,EAAE,OAAO,CAAC,EAAE,OAAO;CAIvD;AAED,qBAAa,iBAAkB,SAAQ,SAAS;gBAClC,OAAO,SAAiB;CAIrC;AAED,qBAAa,cAAe,SAAQ,SAAS;gBAC/B,OAAO,SAAc;CAIlC;AAED,qBAAa,aAAc,SAAQ,SAAS;gBAC9B,QAAQ,SAAa;CAIlC;AAED,qBAAa,aAAc,SAAQ,SAAS;gBAC9B,OAAO,SAAa;CAIjC;AAED,qBAAa,kBAAmB,SAAQ,SAAS;gBACnC,OAAO,SAAyB,EAAE,OAAO,CAAC,EAAE,OAAO;CAIhE;AAED,qBAAa,cAAe,SAAQ,SAAS;gBAC/B,OAAO,SAAsB;CAI1C;AAED,qBAAa,aAAc,SAAQ,SAAS;gBAC9B,OAAO,SAA0B;CAI9C;AAED,qBAAa,uBAAwB,SAAQ,SAAS;gBACxC,OAAO,SAAwB;CAI5C;AAED,qBAAa,mBAAoB,SAAQ,SAAS;gBACpC,OAAO,SAAoB;CAIxC;AAGD,eAAO,MAAM,aAAa,kBAAY,CAAA;AACtC,eAAO,MAAM,mBAAmB,wBAAkB,CAAA;AAClD,eAAO,MAAM,qBAAqB,0BAAoB,CAAA;AACtD,eAAO,MAAM,kBAAkB,uBAAiB,CAAA;AAChD,eAAO,MAAM,iBAAiB,sBAAgB,CAAA;AAC9C,eAAO,MAAM,iBAAiB,sBAAgB,CAAA;AAC9C,eAAO,MAAM,sBAAsB,2BAAqB,CAAA;AACxD,eAAO,MAAM,kBAAkB,uBAAiB,CAAA;AAChD,eAAO,MAAM,iBAAiB,sBAAgB,CAAA;AAC9C,eAAO,MAAM,2BAA2B,gCAA0B,CAAA;AAClE,eAAO,MAAM,uBAAuB,4BAAsB,CAAA;AA6G1D,wBAAgB,kBAAkB,CAAC,KAAK,UAAQ,GAAG,YAAY,CAoH9D;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,EAAE,EAAE,OAAO,GAAG,OAAO,CAIjD"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/error/errors.ts"],"names":[],"mappings":"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"}