exisjs 0.7.3 → 0.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/build.d.ts +4 -2
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +80 -22
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +121 -27
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/doctor.js +5 -2
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/generate.d.ts +5 -0
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +252 -14
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/start.d.ts.map +1 -1
- package/dist/cli/commands/start.js +17 -5
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/index.js +16 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/manifest.d.ts.map +1 -1
- package/dist/cli/manifest.js +12 -4
- package/dist/cli/manifest.js.map +1 -1
- package/dist/config/config.js +1 -1
- package/dist/config/config.js.map +1 -1
- package/dist/config/env.d.ts.map +1 -1
- package/dist/config/env.js +8 -0
- package/dist/config/env.js.map +1 -1
- package/dist/database/index.d.ts +4 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +20 -0
- package/dist/database/index.js.map +1 -0
- package/dist/database/lifecycle.d.ts +52 -0
- package/dist/database/lifecycle.d.ts.map +1 -0
- package/dist/database/lifecycle.js +122 -0
- package/dist/database/lifecycle.js.map +1 -0
- package/dist/database/mongoose.d.ts +47 -0
- package/dist/database/mongoose.d.ts.map +1 -0
- package/dist/database/mongoose.js +44 -0
- package/dist/database/mongoose.js.map +1 -0
- package/dist/database/transaction.d.ts +39 -0
- package/dist/database/transaction.d.ts.map +1 -0
- package/dist/database/transaction.js +99 -0
- package/dist/database/transaction.js.map +1 -0
- package/dist/decorators/constants.d.ts +2 -0
- package/dist/decorators/constants.d.ts.map +1 -1
- package/dist/decorators/index.d.ts +1 -0
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/decorators/index.js +1 -0
- package/dist/decorators/index.js.map +1 -1
- package/dist/decorators/middleware.d.ts +10 -0
- package/dist/decorators/middleware.d.ts.map +1 -1
- package/dist/decorators/middleware.js +27 -0
- package/dist/decorators/middleware.js.map +1 -1
- package/dist/decorators/upload.d.ts +37 -0
- package/dist/decorators/upload.d.ts.map +1 -0
- package/dist/decorators/upload.js +78 -0
- package/dist/decorators/upload.js.map +1 -0
- package/dist/docs/content/boundaries.mdx +33 -3
- package/dist/docs/content/cli/commands.mdx +4 -3
- package/dist/docs/content/config.mdx +27 -1
- package/dist/docs/content/errors.mdx +81 -11
- package/dist/docs/content/middleware.mdx +138 -0
- package/dist/docs/content/providers.mdx +89 -0
- package/dist/docs/content/requests.mdx +191 -69
- package/dist/docs/content/responses.mdx +99 -6
- package/dist/docs/content/routing.mdx +117 -0
- package/dist/docs/content/security.mdx +17 -4
- package/dist/docs/content/structure.mdx +91 -12
- package/dist/docs/content/validation.mdx +131 -10
- package/dist/docs/reference/database.mdx +152 -0
- package/dist/docs/reference/decorators.mdx +4 -0
- package/dist/docs/reference/error.mdx +3 -1
- package/dist/docs/reference/middleware.mdx +84 -2
- package/dist/docs/reference/router.mdx +4 -2
- package/dist/docs/reference/validator.mdx +43 -5
- package/dist/error/errors.d.ts +10 -0
- package/dist/error/errors.d.ts.map +1 -1
- package/dist/error/errors.js +156 -25
- package/dist/error/errors.js.map +1 -1
- package/dist/error/index.d.ts +1 -1
- package/dist/error/index.d.ts.map +1 -1
- package/dist/error/index.js +3 -1
- package/dist/error/index.js.map +1 -1
- package/dist/lib/start-server.js +16 -0
- package/dist/lib/start-server.js.map +1 -1
- package/dist/middleware/index.d.ts +2 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +18 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/middleware.d.ts +13 -0
- package/dist/middleware/middleware.d.ts.map +1 -1
- package/dist/middleware/middleware.js +22 -2
- package/dist/middleware/middleware.js.map +1 -1
- package/dist/middleware/rate-limit.d.ts.map +1 -1
- package/dist/middleware/rate-limit.js +9 -0
- package/dist/middleware/rate-limit.js.map +1 -1
- package/dist/middleware/security.d.ts +13 -3
- package/dist/middleware/security.d.ts.map +1 -1
- package/dist/middleware/security.js +98 -14
- package/dist/middleware/security.js.map +1 -1
- package/dist/middleware/upload.d.ts +82 -0
- package/dist/middleware/upload.d.ts.map +1 -0
- package/dist/middleware/upload.js +387 -0
- package/dist/middleware/upload.js.map +1 -0
- package/dist/router/boundary.d.ts +12 -0
- package/dist/router/boundary.d.ts.map +1 -1
- package/dist/router/boundary.js.map +1 -1
- package/dist/router/index.d.ts +3 -1
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +6 -1
- package/dist/router/index.js.map +1 -1
- package/dist/router/route-builder.d.ts +11 -2
- package/dist/router/route-builder.d.ts.map +1 -1
- package/dist/router/route-builder.js.map +1 -1
- package/dist/router/route-scanner.d.ts +1 -0
- package/dist/router/route-scanner.d.ts.map +1 -1
- package/dist/router/route-scanner.js +219 -8
- package/dist/router/route-scanner.js.map +1 -1
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/router.js +58 -2
- package/dist/router/router.js.map +1 -1
- package/dist/router/types.d.ts +33 -1
- package/dist/router/types.d.ts.map +1 -1
- package/dist/sanitize/index.d.ts +6 -6
- package/dist/sanitize/index.d.ts.map +1 -1
- package/dist/sanitize/index.js +8 -8
- package/dist/sanitize/index.js.map +1 -1
- package/dist/server/app.d.ts +1 -0
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/app.js +4 -0
- package/dist/server/app.js.map +1 -1
- package/dist/server/engines/node-engine.d.ts.map +1 -1
- package/dist/server/engines/node-engine.js +1 -0
- package/dist/server/engines/node-engine.js.map +1 -1
- package/dist/server/request-handler.d.ts.map +1 -1
- package/dist/server/request-handler.js +15 -1
- package/dist/server/request-handler.js.map +1 -1
- package/dist/server/request.d.ts +11 -0
- package/dist/server/request.d.ts.map +1 -1
- package/dist/server/request.js +125 -36
- package/dist/server/request.js.map +1 -1
- package/dist/server/response.d.ts +3 -1
- package/dist/server/response.d.ts.map +1 -1
- package/dist/server/response.js +98 -10
- package/dist/server/response.js.map +1 -1
- package/dist/testing/mocks.d.ts +3 -1
- package/dist/testing/mocks.d.ts.map +1 -1
- package/dist/testing/mocks.js +7 -1
- package/dist/testing/mocks.js.map +1 -1
- package/dist/validator/index.d.ts +1 -1
- package/dist/validator/index.d.ts.map +1 -1
- package/dist/validator/index.js +3 -1
- package/dist/validator/index.js.map +1 -1
- package/dist/validator/tex-types.d.ts +5 -1
- package/dist/validator/tex-types.d.ts.map +1 -1
- package/dist/validator/tex-types.js +4 -0
- package/dist/validator/tex-types.js.map +1 -1
- package/dist/validator/tex.d.ts +45 -7
- package/dist/validator/tex.d.ts.map +1 -1
- package/dist/validator/tex.js +341 -20
- package/dist/validator/tex.js.map +1 -1
- package/package.json +12 -8
|
@@ -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,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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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(
|
|
66
|
-
Automatically times out requests that exceed the specified
|
|
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 `
|
|
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.
|