exisjs 0.7.4 → 0.7.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/build.d.ts +4 -2
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +80 -22
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +229 -64
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/doctor.js +5 -2
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/generate.d.ts +1 -0
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +25 -0
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/start.d.ts.map +1 -1
- package/dist/cli/commands/start.js +65 -15
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/index.js +20 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/manifest.d.ts.map +1 -1
- package/dist/cli/manifest.js +118 -17
- package/dist/cli/manifest.js.map +1 -1
- package/dist/cli/resolve-aliases.d.ts +1 -1
- package/dist/cli/resolve-aliases.d.ts.map +1 -1
- package/dist/cli/resolve-aliases.js +30 -13
- package/dist/cli/resolve-aliases.js.map +1 -1
- package/dist/config/config.d.ts +3 -1
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js.map +1 -1
- package/dist/config/env.d.ts.map +1 -1
- package/dist/config/env.js +8 -0
- package/dist/config/env.js.map +1 -1
- package/dist/config/types.d.ts +2 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/decorators/constants.d.ts +2 -0
- package/dist/decorators/constants.d.ts.map +1 -1
- package/dist/decorators/middleware.d.ts +65 -2
- package/dist/decorators/middleware.d.ts.map +1 -1
- package/dist/decorators/middleware.js +148 -62
- package/dist/decorators/middleware.js.map +1 -1
- package/dist/decorators/params.d.ts +5 -0
- package/dist/decorators/params.d.ts.map +1 -1
- package/dist/decorators/params.js +11 -1
- package/dist/decorators/params.js.map +1 -1
- package/dist/decorators/response.d.ts.map +1 -1
- package/dist/decorators/response.js +32 -22
- package/dist/decorators/response.js.map +1 -1
- package/dist/di/container.d.ts.map +1 -1
- package/dist/di/container.js +6 -1
- package/dist/di/container.js.map +1 -1
- package/dist/docs/content/boundaries.mdx +33 -3
- package/dist/docs/content/cli/commands.mdx +1 -1
- package/dist/docs/content/config.mdx +26 -0
- package/dist/docs/content/controllers.mdx +127 -10
- package/dist/docs/content/decorators.mdx +22 -4
- package/dist/docs/content/errors.mdx +38 -2
- package/dist/docs/content/introduction.mdx +7 -7
- package/dist/docs/content/middleware.mdx +269 -6
- package/dist/docs/content/requests.mdx +217 -10
- package/dist/docs/content/responses.mdx +323 -34
- package/dist/docs/content/routing.mdx +46 -0
- package/dist/docs/content/security.mdx +17 -4
- package/dist/docs/content/structure.mdx +91 -12
- package/dist/docs/content/validation.mdx +226 -11
- package/dist/docs/reference/context.mdx +11 -0
- package/dist/docs/reference/decorators.mdx +14 -4
- package/dist/docs/reference/error.mdx +44 -0
- package/dist/docs/reference/logger.mdx +175 -43
- package/dist/docs/reference/middleware.mdx +31 -0
- package/dist/docs/reference/router.mdx +22 -0
- package/dist/docs/reference/validator.mdx +83 -23
- package/dist/error/errors.d.ts.map +1 -1
- package/dist/error/errors.js +95 -43
- package/dist/error/errors.js.map +1 -1
- package/dist/error/index.d.ts +1 -0
- package/dist/error/index.d.ts.map +1 -1
- package/dist/error/index.js +11 -1
- package/dist/error/index.js.map +1 -1
- package/dist/error/overlay.d.ts +42 -0
- package/dist/error/overlay.d.ts.map +1 -0
- package/dist/error/overlay.js +306 -0
- package/dist/error/overlay.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/start-server.js +49 -7
- package/dist/lib/start-server.js.map +1 -1
- package/dist/logger/index.d.ts +1 -1
- package/dist/logger/index.d.ts.map +1 -1
- package/dist/logger/index.js +9 -1
- package/dist/logger/index.js.map +1 -1
- package/dist/middleware/guard.d.ts +2 -2
- package/dist/middleware/guard.d.ts.map +1 -1
- package/dist/middleware/guard.js.map +1 -1
- package/dist/middleware/ip-filter.d.ts +1 -0
- package/dist/middleware/ip-filter.d.ts.map +1 -1
- package/dist/middleware/ip-filter.js +2 -0
- package/dist/middleware/ip-filter.js.map +1 -1
- package/dist/middleware/middleware.d.ts +2 -2
- package/dist/middleware/middleware.d.ts.map +1 -1
- package/dist/middleware/middleware.js +20 -2
- package/dist/middleware/middleware.js.map +1 -1
- package/dist/middleware/rate-limit.d.ts.map +1 -1
- package/dist/middleware/rate-limit.js +9 -0
- package/dist/middleware/rate-limit.js.map +1 -1
- package/dist/middleware/security.d.ts +43 -1
- package/dist/middleware/security.d.ts.map +1 -1
- package/dist/middleware/security.js +101 -0
- package/dist/middleware/security.js.map +1 -1
- package/dist/response/index.d.ts +229 -6
- package/dist/response/index.d.ts.map +1 -1
- package/dist/response/index.js +272 -2
- package/dist/response/index.js.map +1 -1
- package/dist/router/boundary.d.ts +25 -5
- package/dist/router/boundary.d.ts.map +1 -1
- package/dist/router/boundary.js.map +1 -1
- package/dist/router/controller-registrar.d.ts.map +1 -1
- package/dist/router/controller-registrar.js +56 -1
- package/dist/router/controller-registrar.js.map +1 -1
- package/dist/router/index.d.ts +2 -2
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +2 -1
- package/dist/router/index.js.map +1 -1
- package/dist/router/route-builder.d.ts +70 -36
- package/dist/router/route-builder.d.ts.map +1 -1
- package/dist/router/route-builder.js +13 -15
- package/dist/router/route-builder.js.map +1 -1
- package/dist/router/route-scanner.d.ts +1 -0
- package/dist/router/route-scanner.d.ts.map +1 -1
- package/dist/router/route-scanner.js +217 -10
- package/dist/router/route-scanner.js.map +1 -1
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/router.js +35 -17
- package/dist/router/router.js.map +1 -1
- package/dist/router/types.d.ts +11 -1
- package/dist/router/types.d.ts.map +1 -1
- package/dist/sanitize/index.d.ts +6 -6
- package/dist/sanitize/index.d.ts.map +1 -1
- package/dist/sanitize/index.js +8 -8
- package/dist/sanitize/index.js.map +1 -1
- package/dist/server/app.d.ts +2 -0
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/app.js +14 -0
- package/dist/server/app.js.map +1 -1
- package/dist/server/bootstrapper.d.ts.map +1 -1
- package/dist/server/bootstrapper.js +91 -30
- package/dist/server/bootstrapper.js.map +1 -1
- package/dist/server/cluster.d.ts.map +1 -1
- package/dist/server/cluster.js +13 -2
- package/dist/server/cluster.js.map +1 -1
- package/dist/server/context.d.ts +8 -0
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/context.js +50 -6
- package/dist/server/context.js.map +1 -1
- package/dist/server/engines/node-engine.d.ts.map +1 -1
- package/dist/server/engines/node-engine.js +1 -0
- package/dist/server/engines/node-engine.js.map +1 -1
- package/dist/server/lifecycle.d.ts +22 -0
- package/dist/server/lifecycle.d.ts.map +1 -0
- package/dist/server/lifecycle.js +106 -0
- package/dist/server/lifecycle.js.map +1 -0
- package/dist/server/request-handler.d.ts +1 -0
- package/dist/server/request-handler.d.ts.map +1 -1
- package/dist/server/request-handler.js +42 -18
- package/dist/server/request-handler.js.map +1 -1
- package/dist/server/request.d.ts +6 -0
- package/dist/server/request.d.ts.map +1 -1
- package/dist/server/request.js +112 -33
- package/dist/server/request.js.map +1 -1
- package/dist/server/response.d.ts +13 -2
- package/dist/server/response.d.ts.map +1 -1
- package/dist/server/response.js +250 -10
- package/dist/server/response.js.map +1 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +6 -1
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/mocks.d.ts.map +1 -1
- package/dist/testing/mocks.js +15 -3
- package/dist/testing/mocks.js.map +1 -1
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +184 -1
- package/dist/utils/logger.js.map +1 -1
- package/dist/validator/tex-types.d.ts +2 -1
- package/dist/validator/tex-types.d.ts.map +1 -1
- package/dist/validator/tex-types.js +2 -1
- package/dist/validator/tex-types.js.map +1 -1
- package/dist/validator/tex.d.ts +38 -22
- package/dist/validator/tex.d.ts.map +1 -1
- package/dist/validator/tex.js +490 -40
- package/dist/validator/tex.js.map +1 -1
- package/package.json +10 -10
- package/dist/dev/error-overlay.d.ts +0 -3
- package/dist/dev/error-overlay.d.ts.map +0 -1
- package/dist/dev/error-overlay.js +0 -118
- package/dist/dev/error-overlay.js.map +0 -1
|
@@ -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
|
|
180
|
-
|
|
|
181
|
-
| `(req, res, next)`
|
|
182
|
-
| `(
|
|
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
|
|
197
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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,20 +32,34 @@ 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
|
|
36
|
-
- `tex.email()` - Validates standard email formatting.
|
|
37
|
-
- `tex.
|
|
35
|
+
- `tex.string()` - String validation (supports options like `{ min, max, trim, toLowerCase, toUpperCase, escapeHtml, stripHtml, slugify, mask, default }`).
|
|
36
|
+
- `tex.email()` - Validates standard email formatting (supports `{ trim, toLowerCase, mask, default }`).
|
|
37
|
+
- `tex.uuid()` - Validates UUID strings (supports `{ version: 1 | 4, default }`).
|
|
38
|
+
- `tex.cuid()` - Validates CUID identifiers (supports `{ default }`).
|
|
39
|
+
- `tex.password()` - Validates password complexity (`{ min, max, requireNumbers, requireSymbols, requireUppercase, requireLowercase }`).
|
|
40
|
+
- `tex.creditCard()` - Validates credit card number formats with optional masking (`{ mask: true }`).
|
|
41
|
+
- `tex.number()` - Strict number validation (fails if passed a string unless `coerce: true`).
|
|
42
|
+
- `tex.number({ coerce: true, min: 0, max: 100, default: 10 })` - Automatically coerces incoming string representations (e.g., query params) into native numbers with optional default fallback.
|
|
38
43
|
- `tex.boolean()` - Strict boolean validation.
|
|
39
|
-
- `tex.
|
|
40
|
-
- `tex.
|
|
41
|
-
- `tex.
|
|
44
|
+
- `tex.boolean({ coerce: true, default: false })` - Coerces `"true"`/`"1"` and `"false"`/`"0"` into native booleans.
|
|
45
|
+
- `tex.date()` - Strict Date instance validation.
|
|
46
|
+
- `tex.date({ coerce: true, minDate: '2020-01-01', maxDate: '2030-01-01', default: () => new Date() })` - Automatically parses ISO 8601 strings, UNIX timestamps, or date strings into native JavaScript `Date` objects with optional boundary checks and lazy default factories.
|
|
47
|
+
- `tex.array(schema, { coerce: true, min: 1, max: 10, default: [] })` - 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 `{ ... }`.
|
|
48
|
+
- `tex.enum(['admin', 'user'], { default: 'user' })` - Ensures the value matches one of the provided exact strings.
|
|
49
|
+
- `tex.literal(value)` - Validates against an exact primitive literal (`string | number | boolean`).
|
|
50
|
+
- `tex.record(schema)` - Validates a dictionary/record of key-value pairs matching the value schema.
|
|
51
|
+
- `tex.any()` - Allows any payload value without constraint.
|
|
52
|
+
- `tex.file({ maxSize: 5 * 1024 * 1024, mimeTypes: ['image/png', 'image/jpeg'] })` - Validates uploaded file payloads (size and MIME type).
|
|
42
53
|
- `tex.object({ ... })` - Validates structured JSON objects.
|
|
43
|
-
- `tex.enum(['A', 'B'])` - Ensures the value matches one of the provided exact strings.
|
|
44
54
|
|
|
45
55
|
All primitives accept modifier options or chained helper methods:
|
|
46
56
|
- `.optional()` / `{ optional: true }` - Allows `undefined`.
|
|
47
57
|
- `.nullable()` / `{ nullable: true }` - Allows `null`.
|
|
48
58
|
- `.nullish()` / `{ nullish: true }` - Allows both `null` and `undefined`.
|
|
59
|
+
- `{ default: value | (() => value) }` - Provides a static default or dynamic factory function when the field is omitted or `undefined`. Automatically marks the field as optional in schemas and infers concrete types in TypeScript.
|
|
60
|
+
|
|
61
|
+
> [!TIP]
|
|
62
|
+
> **Unified Defaults without Method Chaining**: ExisJS avoids `.default()` chaining overhead in favor of declarative options objects (e.g. `tex.number({ default: 3000 })` or `tex.date({ default: () => new Date() })`). Dynamic factory functions evaluate per request so timestamps and UUIDs remain fresh!
|
|
49
63
|
|
|
50
64
|
> [!TIP]
|
|
51
65
|
> **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!
|
|
@@ -254,12 +268,75 @@ export const RegisterUserSchema = tex.object({
|
|
|
254
268
|
- **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
269
|
- **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
270
|
|
|
271
|
+
### Query String Array & Date Coercion
|
|
272
|
+
|
|
273
|
+
Query strings in HTTP cannot natively represent arrays or date instances. ExisJS solves this elegantly through `{ coerce: true }`:
|
|
274
|
+
|
|
275
|
+
#### Functional Paradigm
|
|
276
|
+
|
|
277
|
+
```typescript filename="src/http/events/route.ts"
|
|
278
|
+
import { controller, route } from 'exisjs/router'
|
|
279
|
+
import { tex } from 'exisjs/validator'
|
|
280
|
+
|
|
281
|
+
const FilterSchema = tex.object({
|
|
282
|
+
// Coerces ISO 8601 strings (?from=2026-09-01T00:00:00Z) into Date instances
|
|
283
|
+
from: tex.date({ coerce: true, optional: true }),
|
|
284
|
+
to: tex.date({ coerce: true, optional: true }),
|
|
285
|
+
|
|
286
|
+
// Coerces both comma-separated strings (?tags=tech,ai) AND repeated params (?tags=tech&tags=ai)
|
|
287
|
+
// or single values (?tags=tech -> ['tech']) into a typed array of strings
|
|
288
|
+
tags: tex.array(tex.string(), { coerce: true, optional: true }),
|
|
289
|
+
|
|
290
|
+
// Numbers within comma-separated arrays (?ids=1,2,3 -> [1, 2, 3]) are also coerced
|
|
291
|
+
ids: tex.array(tex.number({ coerce: true }), { coerce: true, optional: true })
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
export default controller({
|
|
295
|
+
listEvents: route.get('/events', {
|
|
296
|
+
query: FilterSchema,
|
|
297
|
+
handle({ query, res }) {
|
|
298
|
+
// query.from is Date | undefined
|
|
299
|
+
// query.tags is string[] | undefined
|
|
300
|
+
// query.ids is number[] | undefined
|
|
301
|
+
res.json({ filter: query })
|
|
302
|
+
}
|
|
303
|
+
})
|
|
304
|
+
})
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
#### Class-Based (OOP) Paradigm
|
|
308
|
+
|
|
309
|
+
```typescript filename="src/http/events/route.ts"
|
|
310
|
+
import { Controller, Get, Query } from 'exisjs/decorators'
|
|
311
|
+
import { tex } from 'exisjs/validator'
|
|
312
|
+
import type { ResolveSchema } from 'exisjs/validator'
|
|
313
|
+
|
|
314
|
+
const FilterSchema = tex.object({
|
|
315
|
+
from: tex.date({ coerce: true, optional: true }),
|
|
316
|
+
to: tex.date({ coerce: true, optional: true }),
|
|
317
|
+
tags: tex.array(tex.string(), { coerce: true, optional: true }),
|
|
318
|
+
ids: tex.array(tex.number({ coerce: true }), { coerce: true, optional: true }),
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
type FilterDto = ResolveSchema<typeof FilterSchema>
|
|
322
|
+
|
|
323
|
+
@Controller('/events')
|
|
324
|
+
export default class EventsController {
|
|
325
|
+
@Get('/', { query: FilterSchema })
|
|
326
|
+
listEvents(@Query() query: FilterDto) {
|
|
327
|
+
return { filter: query }
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
257
332
|
---
|
|
258
333
|
|
|
259
334
|
## Pagination Schemas (`tex.pagination`)
|
|
260
335
|
|
|
261
336
|
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
337
|
|
|
338
|
+
#### Functional Paradigm
|
|
339
|
+
|
|
263
340
|
```typescript filename="src/http/items/route.ts"
|
|
264
341
|
import { controller, route } from 'exisjs/router'
|
|
265
342
|
import { tex, paginate, getPaginationSkip } from 'exisjs/validator'
|
|
@@ -283,11 +360,35 @@ export default controller({
|
|
|
283
360
|
})
|
|
284
361
|
```
|
|
285
362
|
|
|
363
|
+
#### Class-Based (OOP) Paradigm
|
|
364
|
+
|
|
365
|
+
```typescript filename="src/http/items/route.ts"
|
|
366
|
+
import { Controller, Get, Query } from 'exisjs/decorators'
|
|
367
|
+
import { tex, paginate, getPaginationSkip } from 'exisjs/validator'
|
|
368
|
+
|
|
369
|
+
const ItemQuerySchema = tex.pagination({
|
|
370
|
+
defaultLimit: 20,
|
|
371
|
+
maxLimit: 100,
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
@Controller('/items')
|
|
375
|
+
export default class ItemsController {
|
|
376
|
+
@Get('/', { query: ItemQuerySchema })
|
|
377
|
+
async list(@Query() query: any) {
|
|
378
|
+
const { skip, limit, page } = getPaginationSkip(query)
|
|
379
|
+
const items = await fetchItems({ skip, limit, sort: query.sort, order: query.order })
|
|
380
|
+
const total = await countItems()
|
|
381
|
+
|
|
382
|
+
return paginate(items, total, { page, limit })
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
286
387
|
---
|
|
287
388
|
|
|
288
389
|
## Error Handling & Developer Diagnostics
|
|
289
390
|
|
|
290
|
-
If a request fails validation, ExisJS intercepts the rejection and returns a standardized `400 Bad Request` containing a
|
|
391
|
+
If a request fails validation, ExisJS intercepts the rejection and returns a standardized `400 Bad Request` containing both a legacy `errors: Record<string, string>` dictionary and a structured `details: ValidationErrorDescriptor[]` array for frontend form field mapping:
|
|
291
392
|
|
|
292
393
|
```json filename="HTTP 400 Bad Request Response"
|
|
293
394
|
{
|
|
@@ -297,16 +398,32 @@ If a request fails validation, ExisJS intercepts the rejection and returns a sta
|
|
|
297
398
|
"errors": {
|
|
298
399
|
"username": "Field must be at least 4 characters",
|
|
299
400
|
"profile.tags": "Expected array of strings"
|
|
300
|
-
}
|
|
401
|
+
},
|
|
402
|
+
"details": [
|
|
403
|
+
{
|
|
404
|
+
"path": "username",
|
|
405
|
+
"message": "Field must be at least 4 characters",
|
|
406
|
+
"expected": "min: 4",
|
|
407
|
+
"received": "abc",
|
|
408
|
+
"code": "VALUE_TOO_SMALL"
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
"path": "profile.tags",
|
|
412
|
+
"message": "Expected array of strings",
|
|
413
|
+
"expected": "Array of string",
|
|
414
|
+
"received": "number",
|
|
415
|
+
"code": "INVALID_TYPE"
|
|
416
|
+
}
|
|
417
|
+
]
|
|
301
418
|
}
|
|
302
419
|
```
|
|
303
420
|
|
|
304
421
|
### Dev Mode Terminal Diagnostics
|
|
305
422
|
|
|
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:
|
|
423
|
+
When validation fails in development mode, ExisJS prints an immediate tabular diagnostic to your terminal displaying the HTTP method, route path, failing HTTP part (`body`, `query`, `params`, `headers`), exact field path, received value, and expected schema constraint:
|
|
307
424
|
|
|
308
425
|
```text
|
|
309
|
-
[VALIDATION FAILED] POST /visits (part: body)
|
|
426
|
+
[VALIDATION FAILED] POST /api/v1/visits (part: body)
|
|
310
427
|
┌─────────────┬───────────────────────────┬──────────────────┐
|
|
311
428
|
│ Field │ Received │ Expected │
|
|
312
429
|
├─────────────┼───────────────────────────┼──────────────────┤
|
|
@@ -315,3 +432,101 @@ When validation fails in development mode, ExisJS prints an immediate tabular di
|
|
|
315
432
|
└─────────────┴───────────────────────────┴──────────────────┘
|
|
316
433
|
```
|
|
317
434
|
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
## Environment Variable Validation (`tex.env`)
|
|
438
|
+
|
|
439
|
+
Because `process.env` always supplies raw string values (e.g. `"4000"`, `"true"`, `"1"`), ExisJS includes a dedicated `tex.env()` schema parser. It automatically injects coercion for numbers, booleans, dates, and arrays, allowing you to validate your environment variables on boot without writing `{ coerce: true }` on every field:
|
|
440
|
+
|
|
441
|
+
```typescript filename="src/config/env.ts"
|
|
442
|
+
import { tex } from 'exisjs/validator'
|
|
443
|
+
|
|
444
|
+
export const env = tex.env({
|
|
445
|
+
PORT: tex.number({ default: 4000 }), // Automatically coerced from "4000" -> 4000
|
|
446
|
+
DEBUG: tex.boolean({ default: false }), // Parsed from "true" | "1" | "false" | "0"
|
|
447
|
+
DATABASE_URL: tex.string(),
|
|
448
|
+
JWT_SECRET: tex.string({ min: 32 }),
|
|
449
|
+
ALLOWED_ORIGINS: tex.array(tex.string()), // Parsed from "http://localhost:3000,https://app.com"
|
|
450
|
+
NODE_ENV: tex.enum(['development', 'production', 'test'] as const, {
|
|
451
|
+
default: 'development',
|
|
452
|
+
}),
|
|
453
|
+
}).parse(process.env)
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
> [!TIP]
|
|
457
|
+
> **Zero-Boilerplate Boot Safety**: `tex.env()` automatically validates `process.env` on server startup. If a required environment variable is missing, it crashes immediately with a clean diagnostic log before the HTTP listener opens.
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
|
|
461
|
+
## Third-Party Validator Interoperability (Zod, Valibot, ArkType)
|
|
462
|
+
|
|
463
|
+
While ExisJS provides the high-performance native `tex` engine out of the box, it is **100% interoperable with external schema libraries like Zod, Valibot, or ArkType** via Duck Typing.
|
|
464
|
+
|
|
465
|
+
Any object with a `.parse()` or `.safeParse()` method can be passed directly as a route schema or used in `src/config/env.ts`.
|
|
466
|
+
|
|
467
|
+
### Using Zod in Routes
|
|
468
|
+
|
|
469
|
+
#### Functional Paradigm
|
|
470
|
+
|
|
471
|
+
```typescript filename="src/http/users/route.ts"
|
|
472
|
+
import { controller, route } from 'exisjs/router'
|
|
473
|
+
import { z } from 'zod'
|
|
474
|
+
|
|
475
|
+
const CreateUserSchema = z.object({
|
|
476
|
+
name: z.string().min(3),
|
|
477
|
+
email: z.string().email(),
|
|
478
|
+
role: z.enum(['admin', 'user']),
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
export default controller({
|
|
482
|
+
create: route.post('/', {
|
|
483
|
+
body: CreateUserSchema, // 👈 Directly pass your Zod schema!
|
|
484
|
+
async handle({ body, res }) {
|
|
485
|
+
return res.status(201).json({ success: true, user: body })
|
|
486
|
+
}
|
|
487
|
+
})
|
|
488
|
+
})
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
#### Class-Based (OOP) Paradigm
|
|
492
|
+
|
|
493
|
+
```typescript filename="src/http/users/route.ts"
|
|
494
|
+
import { Controller, Post, Body, HttpCode } from 'exisjs/decorators'
|
|
495
|
+
import { z } from 'zod'
|
|
496
|
+
|
|
497
|
+
const CreateUserSchema = z.object({
|
|
498
|
+
name: z.string().min(3),
|
|
499
|
+
email: z.string().email(),
|
|
500
|
+
role: z.enum(['admin', 'user']),
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
type CreateUserDto = z.infer<typeof CreateUserSchema>
|
|
504
|
+
|
|
505
|
+
@Controller('/users')
|
|
506
|
+
export default class UsersController {
|
|
507
|
+
@Post('/')
|
|
508
|
+
@HttpCode(201)
|
|
509
|
+
async create(@Body(CreateUserSchema) body: CreateUserDto) {
|
|
510
|
+
return { success: true, user: body }
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
### Using Zod for Environment Validation
|
|
516
|
+
|
|
517
|
+
```typescript filename="src/config/env.ts"
|
|
518
|
+
import { z } from 'zod'
|
|
519
|
+
|
|
520
|
+
const EnvSchema = z.object({
|
|
521
|
+
PORT: z.coerce.number().default(4000),
|
|
522
|
+
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
523
|
+
DATABASE_URL: z.string().url(),
|
|
524
|
+
JWT_SECRET: z.string().min(32),
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
export const env = EnvSchema.parse(process.env)
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
> [!NOTE]
|
|
531
|
+
> **Universal Error Normalization**: Regardless of which library you use (**Zod**, **Valibot**, **Yup**, **Joi**, **ArkType**, or any Standard Schema-compliant library), ExisJS inspects the error structure (`issues`, `errors`, `inner`) and automatically normalizes it into standardized `400 Bad Request` JSON responses with complete field mappings and dev-mode terminal overlays.
|
|
532
|
+
|
|
@@ -166,3 +166,14 @@ export default class UserController {
|
|
|
166
166
|
|
|
167
167
|
> [!TIP]
|
|
168
168
|
> For deep services inside an OOP architecture, you can still seamlessly use `getContext()`, `getRequest()`, or `after()` from `exisjs/router` to access the async context without prop-drilling!
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Context Isolation & Memory Leak Safety
|
|
173
|
+
|
|
174
|
+
In high-concurrency micro-benchmarks or long-lived streaming connections (SSE / WebSockets), holding references in `AsyncLocalStorage` without explicit teardown can lead to memory retention. ExisJS enforces a deterministic context cleanup lifecycle:
|
|
175
|
+
|
|
176
|
+
1. **Deterministic Teardown (`cleanupContext`)**: When a response emits the `'finish'` or `'close'` event, ExisJS automatically executes all remaining `after()` callbacks, clears request-scoped dependency injection instances (`diCache`), purges the `state` dictionary, and unlinks circular references (`req = null!`, `res = null!`, `app = null!`).
|
|
177
|
+
2. **`try / finally` Dispatch Boundary**: The router wraps every handler dispatch in a `try ... finally` boundary, guaranteeing that unhandled exceptions or rejected promises cannot leak state into subsequent async continuations.
|
|
178
|
+
3. **Recycling Pool Memory Scrubbing**: When recycled `ExisRequest` and `ExisResponse` instances return to the internal object pool, their internal dependency injection caches and request metadata are scrubbed clean.
|
|
179
|
+
|
|
@@ -68,6 +68,10 @@ 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
|
+
- `@Cookies(nameOrPipe?)` / `@Cookie(nameOrPipe?)`: Injects incoming parsed cookies or a specific cookie value.
|
|
72
|
+
- `@State(keyOrPipe?)`: Injects request state from execution context or middleware.
|
|
73
|
+
- `@AppCtx()`: Injects the root ExisJS Application instance.
|
|
74
|
+
- `@Fields(nameOrPipe?)`: Injects non-file multipart form fields.
|
|
71
75
|
- `@UploadedFile()`: Injects the uploaded file (`req.file`) into the method argument.
|
|
72
76
|
- `@UploadedFiles()`: Injects the uploaded files array or dictionary (`req.files`) into the method argument.
|
|
73
77
|
- `@HostParam(nameOrPipe?)`: Injects a subdomain parameter.
|
|
@@ -83,8 +87,14 @@ Parameter decorators automatically extract data from the incoming request and in
|
|
|
83
87
|
|
|
84
88
|
- `@Use(...middlewaresOrGuardsOrInterceptorsOrFilters)`: Unified decorator that binds middlewares, guards (`canActivate`), interceptors (`intercept`), or exception filters (`catch`) to a controller class or method.
|
|
85
89
|
- `@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
|
|
87
|
-
- `@Idempotent(options?)`: Caches responses based on the `Idempotency-Key` header using native in-memory off-heap storage.
|
|
90
|
+
- `@Timeout(ms: number | TimeoutOptions)`: Overrides the request timeout for a class or method (e.g. `@Timeout(120000)`). Passing `0` disables timeouts for long-running streaming or exports.
|
|
91
|
+
- `@Idempotent(options?)`: Caches responses based on the `Idempotency-Key` header using native in-memory off-heap storage. Works at class and method level.
|
|
92
|
+
- `@RateLimit(options?)`: Enforces rate limits (e.g. `@RateLimit({ max: 100, windowMs: 60000 })`) at class or method level.
|
|
93
|
+
- `@Cors(options?)`: Enforces Cross-Origin Resource Sharing rules at class or method level.
|
|
94
|
+
- `@IpFilter(options)`: Enforces CIDR and IP allow/deny lists at class or method level.
|
|
95
|
+
- `@Compress()`: Enables automatic Gzip and Brotli compression on responses.
|
|
96
|
+
- `@Dedupe(options)`: Deduplicates concurrent identical requests sharing the same key.
|
|
97
|
+
- `@BlockProbes(options?)`: Intercepts vulnerability scanner probes (`.env`, `/.git`, `/.DS_Store`) and blackholes them without triggering log noise.
|
|
88
98
|
|
|
89
99
|
---
|
|
90
100
|
|
|
@@ -108,8 +118,8 @@ Parameter decorators automatically extract data from the incoming request and in
|
|
|
108
118
|
|
|
109
119
|
- `@Module({ imports?, controllers?, providers?, exports? })`: OOP class decorator equivalent to `defineModule()` for structuring application modules.
|
|
110
120
|
- `@Injectable(options?)`: Marks a service class as an IoC provider managed by the container. Supports `{ scope: 'singleton' | 'request' | 'transient' }`.
|
|
111
|
-
- `@Inject(token)`: Constructor parameter decorator for injecting dependencies by class, string, or symbol token.
|
|
112
|
-
- `@Optional()`: Constructor parameter decorator resolving missing dependencies to `undefined` without throwing.
|
|
121
|
+
- `@Inject(token)`: Constructor parameter decorator from `exisjs/di` for injecting dependencies by class, string, or symbol token.
|
|
122
|
+
- `@Optional()`: Constructor parameter decorator from `exisjs/di` resolving missing dependencies to `undefined` without throwing.
|
|
113
123
|
- `@Boundary(config)`: Marks a class as a folder-scoped Boundary configuring CORS, headers, exclusions, and pipeline steps.
|
|
114
124
|
- `@Server(config)`: Marks a class as a root server definition.
|
|
115
125
|
|
|
@@ -170,3 +170,47 @@ app.use((err, req, res, next) => {
|
|
|
170
170
|
ExisJS natively intercepts and handles Promise rejections at the core routing level.
|
|
171
171
|
|
|
172
172
|
Unlike Express, you do **not** need to wrap your routes in an `asyncHandler`. However, for strict compatibility with older Express middleware ecosystems, `exisjs/error` does export an `asyncHandler` wrapper. Note that using it in ExisJS is entirely redundant and merely adds unnecessary Promise allocations.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Developer Error Formatting & Overlay Utilities
|
|
177
|
+
|
|
178
|
+
`exisjs/error` also exports powerful error diagnostic utilities that you can use in custom error handlers, CLI scripts, background workers, and testing:
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
import {
|
|
182
|
+
formatDevError,
|
|
183
|
+
renderDevErrorToString,
|
|
184
|
+
categorizeError,
|
|
185
|
+
generateErrorHint,
|
|
186
|
+
buildCodeFrame,
|
|
187
|
+
stripInternalStackFrames,
|
|
188
|
+
devErrorResponse,
|
|
189
|
+
type ParsedError,
|
|
190
|
+
} from 'exisjs/error'
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Utility Functions
|
|
194
|
+
|
|
195
|
+
| Function | Signature | Description |
|
|
196
|
+
| :--- | :--- | :--- |
|
|
197
|
+
| `formatDevError` | `(err: Error, optionsOrFile?: string \| { routeFile?: string; title?: string }) => void` | Prints a formatted terminal error envelope with code frames and actionable hints |
|
|
198
|
+
| `renderDevErrorToString` | `(err: Error, optionsOrFile?: ...) => string` | Returns the formatted terminal error envelope string |
|
|
199
|
+
| `categorizeError` | `(err: Error) => string` | Intelligently classifies errors into domain categories (`Validation Schema Error`, `Environment Configuration Error`, etc.) |
|
|
200
|
+
| `generateErrorHint` | `(err: Error) => string \| null` | Generates actionable hints for common coding mistakes |
|
|
201
|
+
| `buildCodeFrame` | `(filePath: string, line: number, col?: number) => string` | Builds a source code snippet visualizer with gutter line numbers and caret indicator |
|
|
202
|
+
| `stripInternalStackFrames` | `(stack?: string) => string[]` | Strips internal Node.js and framework runtime stack frames |
|
|
203
|
+
| `devErrorResponse` | `(err: Error, routeFile?: string) => object` | Generates a structured developer error envelope payload for JSON API responses |
|
|
204
|
+
|
|
205
|
+
### Example: Custom Worker or Script Error Handling
|
|
206
|
+
|
|
207
|
+
```typescript filename="src/workers/import-csv.ts"
|
|
208
|
+
import { formatDevError, categorizeError } from 'exisjs/error'
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
await processCsvImport(filePath)
|
|
212
|
+
} catch (err: any) {
|
|
213
|
+
// Prints a clean code frame with line numbers and actionable hints directly in terminal
|
|
214
|
+
formatDevError(err, { routeFile: filePath, title: categorizeError(err) })
|
|
215
|
+
}
|
|
216
|
+
```
|