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.
Files changed (162) hide show
  1. package/dist/cli/commands/build.d.ts +4 -2
  2. package/dist/cli/commands/build.d.ts.map +1 -1
  3. package/dist/cli/commands/build.js +80 -22
  4. package/dist/cli/commands/build.js.map +1 -1
  5. package/dist/cli/commands/dev.d.ts.map +1 -1
  6. package/dist/cli/commands/dev.js +121 -27
  7. package/dist/cli/commands/dev.js.map +1 -1
  8. package/dist/cli/commands/doctor.d.ts.map +1 -1
  9. package/dist/cli/commands/doctor.js +5 -2
  10. package/dist/cli/commands/doctor.js.map +1 -1
  11. package/dist/cli/commands/generate.d.ts +5 -0
  12. package/dist/cli/commands/generate.d.ts.map +1 -1
  13. package/dist/cli/commands/generate.js +252 -14
  14. package/dist/cli/commands/generate.js.map +1 -1
  15. package/dist/cli/commands/start.d.ts.map +1 -1
  16. package/dist/cli/commands/start.js +17 -5
  17. package/dist/cli/commands/start.js.map +1 -1
  18. package/dist/cli/index.js +16 -5
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/cli/manifest.d.ts.map +1 -1
  21. package/dist/cli/manifest.js +12 -4
  22. package/dist/cli/manifest.js.map +1 -1
  23. package/dist/config/config.js +1 -1
  24. package/dist/config/config.js.map +1 -1
  25. package/dist/config/env.d.ts.map +1 -1
  26. package/dist/config/env.js +8 -0
  27. package/dist/config/env.js.map +1 -1
  28. package/dist/database/index.d.ts +4 -0
  29. package/dist/database/index.d.ts.map +1 -0
  30. package/dist/database/index.js +20 -0
  31. package/dist/database/index.js.map +1 -0
  32. package/dist/database/lifecycle.d.ts +52 -0
  33. package/dist/database/lifecycle.d.ts.map +1 -0
  34. package/dist/database/lifecycle.js +122 -0
  35. package/dist/database/lifecycle.js.map +1 -0
  36. package/dist/database/mongoose.d.ts +47 -0
  37. package/dist/database/mongoose.d.ts.map +1 -0
  38. package/dist/database/mongoose.js +44 -0
  39. package/dist/database/mongoose.js.map +1 -0
  40. package/dist/database/transaction.d.ts +39 -0
  41. package/dist/database/transaction.d.ts.map +1 -0
  42. package/dist/database/transaction.js +99 -0
  43. package/dist/database/transaction.js.map +1 -0
  44. package/dist/decorators/constants.d.ts +2 -0
  45. package/dist/decorators/constants.d.ts.map +1 -1
  46. package/dist/decorators/index.d.ts +1 -0
  47. package/dist/decorators/index.d.ts.map +1 -1
  48. package/dist/decorators/index.js +1 -0
  49. package/dist/decorators/index.js.map +1 -1
  50. package/dist/decorators/middleware.d.ts +10 -0
  51. package/dist/decorators/middleware.d.ts.map +1 -1
  52. package/dist/decorators/middleware.js +27 -0
  53. package/dist/decorators/middleware.js.map +1 -1
  54. package/dist/decorators/upload.d.ts +37 -0
  55. package/dist/decorators/upload.d.ts.map +1 -0
  56. package/dist/decorators/upload.js +78 -0
  57. package/dist/decorators/upload.js.map +1 -0
  58. package/dist/docs/content/boundaries.mdx +33 -3
  59. package/dist/docs/content/cli/commands.mdx +4 -3
  60. package/dist/docs/content/config.mdx +27 -1
  61. package/dist/docs/content/errors.mdx +81 -11
  62. package/dist/docs/content/middleware.mdx +138 -0
  63. package/dist/docs/content/providers.mdx +89 -0
  64. package/dist/docs/content/requests.mdx +191 -69
  65. package/dist/docs/content/responses.mdx +99 -6
  66. package/dist/docs/content/routing.mdx +117 -0
  67. package/dist/docs/content/security.mdx +17 -4
  68. package/dist/docs/content/structure.mdx +91 -12
  69. package/dist/docs/content/validation.mdx +131 -10
  70. package/dist/docs/reference/database.mdx +152 -0
  71. package/dist/docs/reference/decorators.mdx +4 -0
  72. package/dist/docs/reference/error.mdx +3 -1
  73. package/dist/docs/reference/middleware.mdx +84 -2
  74. package/dist/docs/reference/router.mdx +4 -2
  75. package/dist/docs/reference/validator.mdx +43 -5
  76. package/dist/error/errors.d.ts +10 -0
  77. package/dist/error/errors.d.ts.map +1 -1
  78. package/dist/error/errors.js +156 -25
  79. package/dist/error/errors.js.map +1 -1
  80. package/dist/error/index.d.ts +1 -1
  81. package/dist/error/index.d.ts.map +1 -1
  82. package/dist/error/index.js +3 -1
  83. package/dist/error/index.js.map +1 -1
  84. package/dist/lib/start-server.js +16 -0
  85. package/dist/lib/start-server.js.map +1 -1
  86. package/dist/middleware/index.d.ts +2 -0
  87. package/dist/middleware/index.d.ts.map +1 -0
  88. package/dist/middleware/index.js +18 -0
  89. package/dist/middleware/index.js.map +1 -0
  90. package/dist/middleware/middleware.d.ts +13 -0
  91. package/dist/middleware/middleware.d.ts.map +1 -1
  92. package/dist/middleware/middleware.js +22 -2
  93. package/dist/middleware/middleware.js.map +1 -1
  94. package/dist/middleware/rate-limit.d.ts.map +1 -1
  95. package/dist/middleware/rate-limit.js +9 -0
  96. package/dist/middleware/rate-limit.js.map +1 -1
  97. package/dist/middleware/security.d.ts +13 -3
  98. package/dist/middleware/security.d.ts.map +1 -1
  99. package/dist/middleware/security.js +98 -14
  100. package/dist/middleware/security.js.map +1 -1
  101. package/dist/middleware/upload.d.ts +82 -0
  102. package/dist/middleware/upload.d.ts.map +1 -0
  103. package/dist/middleware/upload.js +387 -0
  104. package/dist/middleware/upload.js.map +1 -0
  105. package/dist/router/boundary.d.ts +12 -0
  106. package/dist/router/boundary.d.ts.map +1 -1
  107. package/dist/router/boundary.js.map +1 -1
  108. package/dist/router/index.d.ts +3 -1
  109. package/dist/router/index.d.ts.map +1 -1
  110. package/dist/router/index.js +6 -1
  111. package/dist/router/index.js.map +1 -1
  112. package/dist/router/route-builder.d.ts +11 -2
  113. package/dist/router/route-builder.d.ts.map +1 -1
  114. package/dist/router/route-builder.js.map +1 -1
  115. package/dist/router/route-scanner.d.ts +1 -0
  116. package/dist/router/route-scanner.d.ts.map +1 -1
  117. package/dist/router/route-scanner.js +219 -8
  118. package/dist/router/route-scanner.js.map +1 -1
  119. package/dist/router/router.d.ts.map +1 -1
  120. package/dist/router/router.js +58 -2
  121. package/dist/router/router.js.map +1 -1
  122. package/dist/router/types.d.ts +33 -1
  123. package/dist/router/types.d.ts.map +1 -1
  124. package/dist/sanitize/index.d.ts +6 -6
  125. package/dist/sanitize/index.d.ts.map +1 -1
  126. package/dist/sanitize/index.js +8 -8
  127. package/dist/sanitize/index.js.map +1 -1
  128. package/dist/server/app.d.ts +1 -0
  129. package/dist/server/app.d.ts.map +1 -1
  130. package/dist/server/app.js +4 -0
  131. package/dist/server/app.js.map +1 -1
  132. package/dist/server/engines/node-engine.d.ts.map +1 -1
  133. package/dist/server/engines/node-engine.js +1 -0
  134. package/dist/server/engines/node-engine.js.map +1 -1
  135. package/dist/server/request-handler.d.ts.map +1 -1
  136. package/dist/server/request-handler.js +15 -1
  137. package/dist/server/request-handler.js.map +1 -1
  138. package/dist/server/request.d.ts +11 -0
  139. package/dist/server/request.d.ts.map +1 -1
  140. package/dist/server/request.js +125 -36
  141. package/dist/server/request.js.map +1 -1
  142. package/dist/server/response.d.ts +3 -1
  143. package/dist/server/response.d.ts.map +1 -1
  144. package/dist/server/response.js +98 -10
  145. package/dist/server/response.js.map +1 -1
  146. package/dist/testing/mocks.d.ts +3 -1
  147. package/dist/testing/mocks.d.ts.map +1 -1
  148. package/dist/testing/mocks.js +7 -1
  149. package/dist/testing/mocks.js.map +1 -1
  150. package/dist/validator/index.d.ts +1 -1
  151. package/dist/validator/index.d.ts.map +1 -1
  152. package/dist/validator/index.js +3 -1
  153. package/dist/validator/index.js.map +1 -1
  154. package/dist/validator/tex-types.d.ts +5 -1
  155. package/dist/validator/tex-types.d.ts.map +1 -1
  156. package/dist/validator/tex-types.js +4 -0
  157. package/dist/validator/tex-types.js.map +1 -1
  158. package/dist/validator/tex.d.ts +45 -7
  159. package/dist/validator/tex.d.ts.map +1 -1
  160. package/dist/validator/tex.js +341 -20
  161. package/dist/validator/tex.js.map +1 -1
  162. package/package.json +12 -8
@@ -23,6 +23,7 @@ my-backend/
23
23
  ├── src/
24
24
  │ ├── http/
25
25
  │ │ ├── server.ts # Application entry point
26
+ │ │ ├── error.ts # Centralized framework exception handler (onError)
26
27
  │ │ ├── route.ts # Root routes (e.g. GET /api)
27
28
  │ │ ├── boundary.ts # Root boundary (config + request pipeline)
28
29
  │ │ ├── users/
@@ -72,6 +73,7 @@ Both styles are automatically detected by the route scanner, CLI manifest genera
72
73
  | `schema.ts` / `*.schema.ts` | `tex` validation schemas, imported by route files. Mandatory for every route accepting input. | Handlers, routing |
73
74
  | `service.ts` / `*.service.ts` | Business logic and data access. DI-injectable. | Routing, response shaping |
74
75
  | `boundary.ts` / `*.boundary.ts` | Folder-scoped config + request pipeline for everything in this folder and below. | — |
76
+ | `error.ts` / `*.error.ts` | Centralized exception and validation error handler (`onError`). Auto-mounted by the framework. | Business logic, routes |
75
77
 
76
78
  ---
77
79
 
@@ -172,14 +174,16 @@ A `boundary.ts` applies to every route in its folder and all subfolders. It has
172
174
  two independent halves:
173
175
 
174
176
  1. **Config** — pure data, via `defineBoundary()` (functional) or `@Boundary()`
175
- (OOP argument). CORS, headers, DI providers, excluded paths.
176
- 2. **Pipeline** — real code, auto-detected by function signature. No imports,
177
+ (OOP argument). CORS, headers, DI providers, excluded paths, and lifecycle hooks.
178
+ 2. **Pipeline & Hooks** — real code, auto-detected by function signature. No imports,
177
179
  no manual registration required.
178
180
 
179
- | Signature | Detected as | Runs |
180
- | ---------------------------------------------------------------------------- | ------------ | ---------------------------------------------------- |
181
- | `(req, res, next)` | A chain step | In file/declaration order, before the route |
182
- | `(ctx, next)` — default export (functional) or a method named `handle` (OOP) | The wrapper | Around all chain steps and the nested route/boundary |
181
+ | Signature | Detected as | Runs |
182
+ | :--- | :--- | :--- |
183
+ | `(req, res, next)` | Chain Step | In file/declaration order, before the route handler |
184
+ | `beforeHandle(req, res)` | Boundary Guard Hook | Immediately before route execution. Short-circuits on `false` or when headers sent |
185
+ | `afterHandle(req, res, data)` | Response Hook | Immediately after route execution. Receives returned data to transform or audit |
186
+ | `(ctx, next)` — default export (functional) or `handle(ctx, next)` (OOP) | Pipeline Wrapper | Around all chain steps and the nested route/boundary |
183
187
 
184
188
  ### Functional
185
189
 
@@ -190,11 +194,25 @@ import type { Request, Response, Next, BoundaryContext } from "exisjs/router";
190
194
  export const config = defineBoundary({
191
195
  cors: { origin: ["https://myapp.com"], credentials: true },
192
196
  headers: { "X-Powered-By": "ExisJS" },
193
- exclude: [{ path: "/health", method: "GET" }],
197
+ exclude: [{ path: "/admin/health", method: "GET" }],
198
+
199
+ // Pre-handler hook (returns false to block with 403, or sends response)
200
+ beforeHandle(req, res) {
201
+ if (req.headers["x-role"] !== "admin") {
202
+ res.status(403).json({ error: "Admin role required" });
203
+ return false;
204
+ }
205
+ },
206
+
207
+ // Post-handler hook (transforms returned payload or performs async audit)
208
+ afterHandle(req, res, data) {
209
+ res.setHeader("X-Audited-By", "AdminBoundary");
210
+ return { ...data, auditedAt: Date.now() };
211
+ },
194
212
  });
195
213
 
196
- export function auth(req: Request, res: Response, next: Next) {
197
- if (!req.headers["x-api-key"]) throw new UnauthorizedError("Missing API key");
214
+ export function logStep(req: Request, res: Response, next: Next) {
215
+ console.log(`[Admin] ${req.method} ${req.path}`);
198
216
  next();
199
217
  }
200
218
 
@@ -216,14 +234,29 @@ import type { Request, Response, Next, BoundaryContext } from "exisjs/router";
216
234
  @Boundary({
217
235
  cors: { origin: ["https://myapp.com"], credentials: true },
218
236
  headers: { "X-Powered-By": "ExisJS" },
237
+ exclude: ["/admin/health"],
219
238
  })
220
239
  export default class AdminBoundary {
221
- auth(req: Request, res: Response, next: Next) {
222
- if (!req.headers["x-api-key"])
223
- throw new UnauthorizedError("Missing API key");
240
+ // Pre-handler hook
241
+ beforeHandle(req: Request, res: Response) {
242
+ if (req.headers["x-role"] !== "admin") {
243
+ res.status(403).json({ error: "Admin role required" });
244
+ return false;
245
+ }
246
+ }
247
+
248
+ // Post-handler hook
249
+ afterHandle(req: Request, res: Response, data: any) {
250
+ return { ...data, auditedAt: Date.now() };
251
+ }
252
+
253
+ // Chain step
254
+ logStep(req: Request, res: Response, next: Next) {
255
+ console.log(`[Admin] ${req.method} ${req.path}`);
224
256
  next();
225
257
  }
226
258
 
259
+ // Wrapper
227
260
  async handle(ctx: BoundaryContext, next: Next) {
228
261
  try {
229
262
  return { success: true, data: await next() };
@@ -234,6 +267,52 @@ export default class AdminBoundary {
234
267
  }
235
268
  ```
236
269
 
270
+ ---
271
+
272
+ ## `src/http/error.ts` (Framework-Level Exception Handler)
273
+
274
+ ExisJS natively discovers and mounts `src/http/error.ts` (or `error.js`) at boot. It exports an `onError(err, req, res)` handler, giving developers a single, standard place to intercept, log, and format all unhandled or validation errors without writing custom `app.onError` boilerplate inside `server.ts`.
275
+
276
+ ### Functional
277
+
278
+ ```typescript filename="src/http/error.ts"
279
+ import type { Request, Response } from "exisjs";
280
+
281
+ export function onError(err: any, req: Request, res: Response) {
282
+ const status = err.statusCode || err.status || 500;
283
+
284
+ // Format consistent error response across the entire API
285
+ res.status(status).json({
286
+ success: false,
287
+ error: {
288
+ message: err.message || "Internal Server Error",
289
+ code: err.code || "INTERNAL_SERVER_ERROR",
290
+ path: req.path,
291
+ },
292
+ });
293
+ }
294
+ ```
295
+
296
+ ### Class-Based (OOP)
297
+
298
+ ```typescript filename="src/http/error.ts"
299
+ import type { Request, Response } from "exisjs";
300
+
301
+ export default class GlobalErrorHandler {
302
+ onError(err: any, req: Request, res: Response) {
303
+ const status = err.statusCode || err.status || 500;
304
+ res.status(status).json({
305
+ success: false,
306
+ error: {
307
+ message: err.message || "Internal Server Error",
308
+ code: err.code || "INTERNAL_ERROR",
309
+ path: req.path,
310
+ },
311
+ });
312
+ }
313
+ }
314
+ ```
315
+
237
316
  A nested folder's `boundary.ts` always runs _inside_ its parent's `next()` —
238
317
  pipeline depth mirrors folder depth automatically.
239
318
 
@@ -32,17 +32,25 @@ const safeData = userSchema.parse(rawInput)
32
32
 
33
33
  ExisJS supports a comprehensive set of strict validation primitives:
34
34
 
35
- - `tex.string()` - String validation (supports options like `{ min: 5, max: 10 }`).
35
+ - `tex.string()` - String validation (supports options like `{ min: 5, max: 10, pattern: /regex/ }`).
36
36
  - `tex.email()` - Validates standard email formatting.
37
- - `tex.number()` - Strict number validation (fails if passed a string).
37
+ - `tex.number()` - Strict number validation (fails if passed a string unless `coerce: true`).
38
+ - `tex.number({ coerce: true, min: 0, max: 100 })` - Automatically coerces incoming string representations (e.g., query params) into native numbers.
38
39
  - `tex.boolean()` - Strict boolean validation.
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.date()` - Strict Date instance validation.
42
+ - `tex.date({ coerce: true, minDate: new Date('2020-01-01'), maxDate: new Date() })` - Automatically parses ISO 8601 strings, UNIX timestamps, or date strings into native JavaScript `Date` objects with optional boundary checks.
43
+ - `tex.array(schema, { coerce: true, min: 1, max: 10 })` - Validates an array of items. When `{ coerce: true }` is enabled, automatically converts single items or comma-separated query strings (`"apple,banana"`) into typed arrays. Accepts primitive types, `tex.object(...)`, or shorthand plain objects `{ ... }`.
42
44
  - `tex.object({ ... })` - Validates structured JSON objects.
43
45
  - `tex.enum(['A', 'B'])` - Ensures the value matches one of the provided exact strings.
44
46
 
45
- All primitives accept an options object where you can pass `{ optional: true }` to allow `undefined`.
47
+ All primitives accept modifier options or chained helper methods:
48
+ - `.optional()` / `{ optional: true }` - Allows `undefined`.
49
+ - `.nullable()` / `{ nullable: true }` - Allows `null`.
50
+ - `.nullish()` / `{ nullish: true }` - Allows both `null` and `undefined`.
51
+
52
+ > [!TIP]
53
+ > **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
54
 
47
55
  ### Data Sanitization (Trimming & Formatting)
48
56
 
@@ -217,16 +225,129 @@ export default class BooksController {
217
225
  ```
218
226
 
219
227
 
220
- ## Error Handling
228
+ ### Nested Sub-Documents & Array Schemas
229
+
230
+ ExisJS natively validates arbitrarily deep sub-documents and nested arrays off-heap with Rust acceleration:
231
+
232
+ ```typescript filename="src/http/users/schema.ts"
233
+ import { tex } from 'exisjs/validator'
234
+
235
+ export const RegisterUserSchema = tex.object({
236
+ username: tex.string({ min: 3 }),
237
+ profile: tex.object({
238
+ bio: tex.string().nullish(), // allows undefined, null, or ""
239
+ avatarUrl: tex.string({ optional: true }),
240
+ socials: tex.object({
241
+ twitter: tex.string({ optional: true }),
242
+ github: tex.string({ optional: true }),
243
+ }),
244
+ tags: tex.array(tex.string({ min: 2 })),
245
+ }),
246
+ // You can use tex.object(...) OR a plain object schema shorthand!
247
+ items: tex.array({
248
+ sku: tex.string(),
249
+ quantity: tex.number({ min: 1 }),
250
+ }),
251
+ })
252
+ ```
253
+
254
+ #### First-Class Form & Multipart Support
255
+ - **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"`.
256
+ - **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.
257
+ - **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`.
258
+
259
+ ### Query String Array & Date Coercion
260
+
261
+ Query strings in HTTP cannot natively represent arrays or date instances. ExisJS solves this elegantly through `{ coerce: true }`:
262
+
263
+ ```typescript filename="src/http/events/route.ts"
264
+ import { controller, route } from 'exisjs/router'
265
+ import { tex } from 'exisjs/validator'
266
+
267
+ const FilterSchema = tex.object({
268
+ // Coerces ISO 8601 strings (?from=2026-09-01T00:00:00Z) into Date instances
269
+ from: tex.date({ coerce: true, optional: true }),
270
+ to: tex.date({ coerce: true, optional: true }),
271
+
272
+ // Coerces both comma-separated strings (?tags=tech,ai) AND repeated params (?tags=tech&tags=ai)
273
+ // or single values (?tags=tech -> ['tech']) into a typed array of strings
274
+ tags: tex.array(tex.string(), { coerce: true, optional: true }),
275
+
276
+ // Numbers within comma-separated arrays (?ids=1,2,3 -> [1, 2, 3]) are also coerced
277
+ ids: tex.array(tex.number({ coerce: true }), { coerce: true, optional: true })
278
+ })
279
+
280
+ export default controller({
281
+ listEvents: route.get('/events', {
282
+ query: FilterSchema,
283
+ handle({ query, res }) {
284
+ // query.from is Date | undefined
285
+ // query.tags is string[] | undefined
286
+ // query.ids is number[] | undefined
287
+ res.json({ filter: query })
288
+ }
289
+ })
290
+ })
291
+ ```
292
+
293
+ ---
294
+
295
+ ## Pagination Schemas (`tex.pagination`)
296
+
297
+ 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:
298
+
299
+ ```typescript filename="src/http/items/route.ts"
300
+ import { controller, route } from 'exisjs/router'
301
+ import { tex, paginate, getPaginationSkip } from 'exisjs/validator'
302
+
303
+ const ItemQuerySchema = tex.pagination({
304
+ defaultLimit: 20, // Default items per page if omitted
305
+ maxLimit: 100, // Hard cap to prevent memory exhaustion
306
+ })
307
+
308
+ export default controller({
309
+ list: route.get('/', {
310
+ query: ItemQuerySchema,
311
+ async handle({ query }) {
312
+ const { skip, limit, page } = getPaginationSkip(query)
313
+ const items = await fetchItems({ skip, limit, sort: query.sort, order: query.order })
314
+ const total = await countItems()
315
+
316
+ return paginate(items, total, { page, limit })
317
+ }
318
+ })
319
+ })
320
+ ```
321
+
322
+ ---
323
+
324
+ ## Error Handling & Developer Diagnostics
221
325
 
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.
326
+ 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:
223
327
 
224
- ```json filename="Response"
328
+ ```json filename="HTTP 400 Bad Request Response"
225
329
  {
226
330
  "statusCode": 400,
227
331
  "error": "Bad Request",
228
- "message": "Validation Error: body.username is required"
332
+ "message": "Validation Error: username: Field must be at least 4 characters, profile.tags: Expected array of strings",
333
+ "errors": {
334
+ "username": "Field must be at least 4 characters",
335
+ "profile.tags": "Expected array of strings"
336
+ }
229
337
  }
230
338
  ```
231
339
 
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!
340
+ ### Dev Mode Terminal Diagnostics
341
+
342
+ 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:
343
+
344
+ ```text
345
+ [VALIDATION FAILED] POST /visits (part: body)
346
+ ┌─────────────┬───────────────────────────┬──────────────────┐
347
+ │ Field │ Received │ Expected │
348
+ ├─────────────┼───────────────────────────┼──────────────────┤
349
+ │ phone │ "123" │ min: 7, max: 20 │
350
+ │ idProofs │ "[object Object]" │ Array of IDProof │
351
+ └─────────────┴───────────────────────────┴──────────────────┘
352
+ ```
353
+
@@ -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.