exisjs 0.7.2 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +3 -0
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +258 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/generate.d.ts +11 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +244 -24
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/index.js +23 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/manifest.d.ts.map +1 -1
- package/dist/cli/manifest.js +3 -1
- package/dist/cli/manifest.js.map +1 -1
- package/dist/cli/resolve-aliases.d.ts.map +1 -1
- package/dist/cli/resolve-aliases.js +37 -31
- package/dist/cli/resolve-aliases.js.map +1 -1
- package/dist/cli/routes.d.ts +8 -1
- package/dist/cli/routes.d.ts.map +1 -1
- package/dist/cli/routes.js +96 -47
- package/dist/cli/routes.js.map +1 -1
- package/dist/config/config.d.ts +2 -1
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +2 -1
- package/dist/config/config.js.map +1 -1
- package/dist/config/index.d.ts +1 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +6 -1
- package/dist/config/index.js.map +1 -1
- package/dist/config/types.d.ts +4 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/database/index.d.ts +4 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +20 -0
- package/dist/database/index.js.map +1 -0
- package/dist/database/lifecycle.d.ts +52 -0
- package/dist/database/lifecycle.d.ts.map +1 -0
- package/dist/database/lifecycle.js +122 -0
- package/dist/database/lifecycle.js.map +1 -0
- package/dist/database/mongoose.d.ts +47 -0
- package/dist/database/mongoose.d.ts.map +1 -0
- package/dist/database/mongoose.js +44 -0
- package/dist/database/mongoose.js.map +1 -0
- package/dist/database/transaction.d.ts +39 -0
- package/dist/database/transaction.d.ts.map +1 -0
- package/dist/database/transaction.js +99 -0
- package/dist/database/transaction.js.map +1 -0
- package/dist/decorators/index.d.ts +1 -0
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/decorators/index.js +1 -0
- package/dist/decorators/index.js.map +1 -1
- package/dist/decorators/middleware.d.ts +10 -0
- package/dist/decorators/middleware.d.ts.map +1 -1
- package/dist/decorators/middleware.js +27 -0
- package/dist/decorators/middleware.js.map +1 -1
- package/dist/decorators/upload.d.ts +37 -0
- package/dist/decorators/upload.d.ts.map +1 -0
- package/dist/decorators/upload.js +78 -0
- package/dist/decorators/upload.js.map +1 -0
- package/dist/docs/content/cli/commands.mdx +39 -5
- package/dist/docs/content/cli/overview.mdx +1 -0
- package/dist/docs/content/config.mdx +9 -2
- package/dist/docs/content/errors.mdx +57 -23
- package/dist/docs/content/middleware.mdx +138 -0
- package/dist/docs/content/providers.mdx +89 -0
- package/dist/docs/content/requests.mdx +153 -64
- package/dist/docs/content/responses.mdx +162 -1
- package/dist/docs/content/routing.mdx +137 -15
- package/dist/docs/content/security.mdx +6 -1
- package/dist/docs/content/structure.mdx +12 -6
- package/dist/docs/content/validation.mdx +92 -7
- package/dist/docs/reference/database.mdx +152 -0
- package/dist/docs/reference/decorators.mdx +4 -0
- package/dist/docs/reference/error.mdx +3 -1
- package/dist/docs/reference/middleware.mdx +84 -2
- package/dist/docs/reference/router.mdx +4 -2
- package/dist/docs/reference/validator.mdx +43 -5
- package/dist/error/errors.d.ts +10 -0
- package/dist/error/errors.d.ts.map +1 -1
- package/dist/error/errors.js +156 -25
- package/dist/error/errors.js.map +1 -1
- package/dist/error/index.d.ts +1 -1
- package/dist/error/index.d.ts.map +1 -1
- package/dist/error/index.js +3 -1
- package/dist/error/index.js.map +1 -1
- package/dist/middleware/compression.d.ts.map +1 -1
- package/dist/middleware/compression.js +20 -5
- package/dist/middleware/compression.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 +16 -0
- package/dist/middleware/middleware.d.ts.map +1 -1
- package/dist/middleware/middleware.js +134 -48
- package/dist/middleware/middleware.js.map +1 -1
- package/dist/middleware/security.d.ts +13 -3
- package/dist/middleware/security.d.ts.map +1 -1
- package/dist/middleware/security.js +154 -25
- package/dist/middleware/security.js.map +1 -1
- package/dist/middleware/upload.d.ts +82 -0
- package/dist/middleware/upload.d.ts.map +1 -0
- package/dist/middleware/upload.js +387 -0
- package/dist/middleware/upload.js.map +1 -0
- package/dist/router/index.d.ts +5 -1
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +10 -1
- package/dist/router/index.js.map +1 -1
- package/dist/router/route-builder.d.ts +11 -2
- package/dist/router/route-builder.d.ts.map +1 -1
- package/dist/router/route-builder.js.map +1 -1
- package/dist/router/route-scanner.d.ts.map +1 -1
- package/dist/router/route-scanner.js +42 -3
- package/dist/router/route-scanner.js.map +1 -1
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/router.js +67 -4
- package/dist/router/router.js.map +1 -1
- package/dist/router/types.d.ts +30 -0
- package/dist/router/types.d.ts.map +1 -1
- package/dist/sanitize/index.d.ts +9 -9
- package/dist/sanitize/index.d.ts.map +1 -1
- package/dist/sanitize/index.js +19 -10
- package/dist/sanitize/index.js.map +1 -1
- package/dist/sanitize/sanitize.d.ts +11 -11
- package/dist/sanitize/sanitize.d.ts.map +1 -1
- package/dist/sanitize/sanitize.js +22 -13
- package/dist/sanitize/sanitize.js.map +1 -1
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/app.js +18 -0
- package/dist/server/app.js.map +1 -1
- package/dist/server/request.d.ts +5 -0
- package/dist/server/request.d.ts.map +1 -1
- package/dist/server/request.js +32 -4
- package/dist/server/request.js.map +1 -1
- package/dist/server/response.d.ts +27 -0
- package/dist/server/response.d.ts.map +1 -1
- package/dist/server/response.js +109 -4
- package/dist/server/response.js.map +1 -1
- package/dist/server/sse.d.ts +82 -0
- package/dist/server/sse.d.ts.map +1 -0
- package/dist/server/sse.js +216 -0
- package/dist/server/sse.js.map +1 -0
- package/dist/testing/client.d.ts.map +1 -1
- package/dist/testing/client.js +33 -5
- package/dist/testing/client.js.map +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +2 -1
- package/dist/testing/index.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/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -1
- package/dist/validator/index.d.ts +3 -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 +7 -3
- package/dist/validator/tex-types.d.ts.map +1 -1
- package/dist/validator/tex-types.js +4 -0
- package/dist/validator/tex-types.js.map +1 -1
- package/dist/validator/tex.d.ts +38 -0
- package/dist/validator/tex.d.ts.map +1 -1
- package/dist/validator/tex.js +126 -16
- package/dist/validator/tex.js.map +1 -1
- package/package.json +5 -1
|
@@ -85,16 +85,18 @@ $ npx exisjs g <type> <name> [options]
|
|
|
85
85
|
|
|
86
86
|
| Option | Description |
|
|
87
87
|
| :--- | :--- |
|
|
88
|
+
| `-n, --named` | Names generated files using the feature prefix (e.g. `<name>.route.ts`, `<name>.schema.ts`, `<name>.service.ts`). |
|
|
88
89
|
| `--oop` | Generates using Class-Based (OOP) decorators instead of Functional syntax. |
|
|
89
90
|
|
|
90
91
|
**Supported Schematics**
|
|
91
92
|
|
|
92
93
|
| Schematic | Alias | Description | Output Path |
|
|
93
94
|
| :--- | :--- | :--- | :--- |
|
|
94
|
-
| `
|
|
95
|
+
| `crud` | `resource` | Full 4-file CRUD slice (`schema.ts`, `service.ts`, `route.ts`, and `model.ts` entity), 100% database-agnostic | `src/http/<name>/` & `src/models/` |
|
|
96
|
+
| `model` | | Standalone database-agnostic TypeScript entity and input types | `src/models/<Name>.ts` |
|
|
95
97
|
| `route` | `r`, `c`, `controller` | HTTP route file | `src/http/<name>/route.ts` |
|
|
96
|
-
| `service` | `s` | Injectable service / business logic | `src/http/<name>/service.ts` |
|
|
97
|
-
| `schema` | `sc` | Validation schemas powered by `tex` | `src/http/<name>/schema.ts` |
|
|
98
|
+
| `service` | `s` | Injectable service / business logic with pagination helpers | `src/http/<name>/service.ts` |
|
|
99
|
+
| `schema` | `sc` | Validation schemas powered by `tex` (includes pagination) | `src/http/<name>/schema.ts` |
|
|
98
100
|
| `boundary` | `b` | Folder-scoped configuration and request pipeline | `src/http/<name>/boundary.ts` |
|
|
99
101
|
| `plugin` | `p` | Distributable plugin module | `src/plugins/<name>.ts` |
|
|
100
102
|
| `middleware` | `m` | Standalone middleware handler | `src/middleware/<name>.ts` |
|
|
@@ -104,7 +106,7 @@ $ npx exisjs g <type> <name> [options]
|
|
|
104
106
|
|
|
105
107
|
### `exisjs routes`
|
|
106
108
|
|
|
107
|
-
Analyzes and prints the application routing table to the terminal.
|
|
109
|
+
Analyzes and prints the application routing table to the terminal in an interactive, formatted grid with method badges and dynamic parameter highlighting.
|
|
108
110
|
|
|
109
111
|
```bash
|
|
110
112
|
$ npx exisjs routes [options]
|
|
@@ -114,7 +116,17 @@ $ npx exisjs routes [options]
|
|
|
114
116
|
|
|
115
117
|
| Option | Description |
|
|
116
118
|
| :--- | :--- |
|
|
117
|
-
| `-e, --entry <file>` | Custom entry file path to bootstrap the app from. |
|
|
119
|
+
| `-e, --entry <file>` | Custom entry file path to bootstrap the app from (default: `src/http/server.ts`). |
|
|
120
|
+
| `-m, --method <method>` | Filter endpoints by HTTP method (e.g. `GET`, `POST`, `PUT`, `DELETE`). |
|
|
121
|
+
| `-f, --filter <keyword>` | Filter endpoints matching path, method, or source file keyword. |
|
|
122
|
+
| `--json` | Output the parsed routes and middleware metadata directly as structured JSON. |
|
|
123
|
+
|
|
124
|
+
**Features**
|
|
125
|
+
- **Method Badges**: Visually styled badges (`GET`, `POST`, `PUT`, `PATCH`, `DEL`, `ALL`).
|
|
126
|
+
- **Parameter Highlighting**: Dynamically distinguishes path parameters (`:id`, `*slug`).
|
|
127
|
+
- **Middleware Counting**: Displays total attached middleware / boundary steps.
|
|
128
|
+
- **Source Resolution**: Displays relative file source path (e.g. `src/http/users/user.route.ts`).
|
|
129
|
+
- **Summary Metrics**: Displays total endpoint counts and method breakdown.
|
|
118
130
|
|
|
119
131
|
---
|
|
120
132
|
|
|
@@ -155,6 +167,12 @@ $ npx exisjs repl [options]
|
|
|
155
167
|
$ npx exisjs console [options]
|
|
156
168
|
```
|
|
157
169
|
|
|
170
|
+
**Options**
|
|
171
|
+
|
|
172
|
+
| Option | Description |
|
|
173
|
+
| :--- | :--- |
|
|
174
|
+
| `-e, --entry <file>` | Custom entry file path to bootstrap the REPL context. |
|
|
175
|
+
|
|
158
176
|
---
|
|
159
177
|
|
|
160
178
|
### `exisjs exports`
|
|
@@ -174,3 +192,19 @@ Displays runtime environment, Node.js version, platform, architecture, and ExisJ
|
|
|
174
192
|
```bash
|
|
175
193
|
$ npx exisjs info
|
|
176
194
|
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
### `exisjs doctor`
|
|
199
|
+
|
|
200
|
+
Performs an end-to-end diagnostic audit of the local project environment, verifying Node.js version compatibility, native Rust hardware acceleration (`@exisjs/rs`), `tsconfig.json` strictness, entry-point resolution, route naming patterns, and file-system route counts.
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
$ npx exisjs doctor
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Diagnostic Checks**
|
|
207
|
+
- **Runtime**: Validates Node.js engine compatibility (v18+ recommended for N-API and worker thread pools).
|
|
208
|
+
- **Native Engine**: Verifies `@exisjs/rs` compiled native binaries are active for hardware-accelerated radix tree traversal, off-heap caching, and native JSON serialization.
|
|
209
|
+
- **Project Configuration**: Checks `package.json` root and `tsconfig.json` strict mode.
|
|
210
|
+
- **Routes & Pipeline**: Confirms application entry point (`src/http/server.ts`), discovers active route slices, and flags non-standard or orphan file naming conventions.
|
|
@@ -79,4 +79,5 @@ $ npx exisjs g r users --oop
|
|
|
79
79
|
| `repl` | `console` | Starts an interactive Node.js REPL loaded with the application context. |
|
|
80
80
|
| `exports` | | Displays all available `@exisjs` and `exisjs/*` subpath exports. |
|
|
81
81
|
| `info` | | Displays system, Node.js, and ExisJS runtime information. |
|
|
82
|
+
| `doctor` | | Performs system, native Rust acceleration, TypeScript, and routing diagnostics. |
|
|
82
83
|
| `init` | | Initializes a new ExisJS application in the current directory. |
|
|
@@ -140,7 +140,14 @@ Here is a full breakdown of the configuration schema:
|
|
|
140
140
|
|
|
141
141
|
### Global Middleware
|
|
142
142
|
|
|
143
|
-
- `cors` _(object | boolean)_: Global CORS policy configuration
|
|
143
|
+
- `cors` _(object | boolean)_: Global CORS policy configuration. Supports:
|
|
144
|
+
- Exact string origins (e.g. `'https://example.com'` or `'*'`).
|
|
145
|
+
- Regular expressions (e.g. `/localhost:\d+/`).
|
|
146
|
+
- Arrays mixing strings and regexes (`['https://app.example.com', /https:\/\/.*\.domain\.com/']`).
|
|
147
|
+
- Dynamic callbacks: `(origin, cb) => ...` or async functions: `async (origin) => boolean` for multi-tenant setups.
|
|
148
|
+
- `transformResponse` _(boolean | function)_: Automatically wraps all successful handler responses into a standardized envelope:
|
|
149
|
+
- When `true`: `{ success: true, data: ..., timestamp: ... }` (matching `HttpError`'s `{ success: false, error: ... }`).
|
|
150
|
+
- When a function: `(data, req, res) => ({ ... })` to provide your own custom envelope.
|
|
144
151
|
- `helmet` _(object | boolean)_: Automatically secures your app with standard HTTP headers. (Enabled by default).
|
|
145
152
|
- `compression` _(object | boolean)_: Enables Gzip/Brotli compression for all HTTP responses.
|
|
146
153
|
- `logger` _(object | boolean)_: Configures the internal Pino-based logger (`level`, `pretty`).
|
|
@@ -148,7 +155,7 @@ Here is a full breakdown of the configuration schema:
|
|
|
148
155
|
|
|
149
156
|
### Performance & Security
|
|
150
157
|
|
|
151
|
-
- `bodyLimit` _(number)_: The maximum allowed size (in bytes) for incoming request bodies. (Default: `
|
|
158
|
+
- `bodyLimit` _(number)_: The maximum allowed size (in bytes) for incoming request bodies. (Default: `10MB` / `10485760` bytes, raising HTTP 413 `PayloadTooLargeError` when exceeded)
|
|
152
159
|
- `trustProxy` _(boolean | number)_: If true, respects `X-Forwarded-For` and `X-Forwarded-Host` headers (essential for apps deployed behind NGINX, Cloudflare, or AWS ELB). If a number, trusts that many hops.
|
|
153
160
|
- `keepAlive` _(object | boolean)_: Tunes the Keep-Alive timeout and Header timeout limits.
|
|
154
161
|
- `workers` _(number | 'safe' | 'max')_: Number of CPU workers for cluster mode. Defaults to 1. 'safe' caps to 2. 'max' uses all available cores.
|
|
@@ -74,7 +74,9 @@ The `HttpError` class provides static factory methods for standard HTTP status c
|
|
|
74
74
|
- `HttpError.forbidden('Message')` - 403
|
|
75
75
|
- `HttpError.notFound('Resource')` - 404
|
|
76
76
|
- `HttpError.conflict('Message')` - 409
|
|
77
|
+
- `HttpError.payloadTooLarge('Message')` - 413
|
|
77
78
|
- `HttpError.unprocessable('Message', details)` - 422
|
|
79
|
+
- `HttpError.rateLimit('Message')` - 429
|
|
78
80
|
- `HttpError.internal('Message')` - 500
|
|
79
81
|
|
|
80
82
|
### Direct Error Classes & Exceptions
|
|
@@ -82,15 +84,17 @@ The `HttpError` class provides static factory methods for standard HTTP status c
|
|
|
82
84
|
If you prefer a class-based approach, ExisJS exports every standard error as its own class. We also provide `Exception` aliases for every error class. You can import and throw them directly:
|
|
83
85
|
|
|
84
86
|
```typescript filename="example.ts"
|
|
85
|
-
import {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
import {
|
|
88
|
+
NotFoundException,
|
|
89
|
+
UnauthorizedException,
|
|
90
|
+
BadRequestError,
|
|
91
|
+
PayloadTooLargeError
|
|
92
|
+
} from 'exisjs/error'
|
|
93
|
+
|
|
94
|
+
export const uploadMedia = (req, res) => {
|
|
95
|
+
if (req.headers['content-length'] > 10 * 1024 * 1024) {
|
|
96
|
+
throw new PayloadTooLargeError('File exceeds 10MB limit')
|
|
97
|
+
}
|
|
94
98
|
}
|
|
95
99
|
```
|
|
96
100
|
|
|
@@ -108,6 +112,36 @@ When an `HttpError` is thrown, the client will automatically receive a structure
|
|
|
108
112
|
}
|
|
109
113
|
```
|
|
110
114
|
|
|
115
|
+
### Validation Error Responses & Diagnostic Visibility
|
|
116
|
+
|
|
117
|
+
When schema validation fails (via `tex`, Zod, or Yup), ExisJS produces a typed `400 Bad Request` featuring a clean `errors: Record<string, string>` map for seamless frontend form field binding:
|
|
118
|
+
|
|
119
|
+
```json filename="HTTP 400 Bad Request Response"
|
|
120
|
+
{
|
|
121
|
+
"statusCode": 400,
|
|
122
|
+
"error": "Bad Request",
|
|
123
|
+
"message": "Validation Error: email: Invalid email format, age: Field must be >= 18",
|
|
124
|
+
"errors": {
|
|
125
|
+
"email": "Invalid email format",
|
|
126
|
+
"age": "Field must be >= 18"
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
#### Development Terminal Diagnostics
|
|
132
|
+
|
|
133
|
+
In development mode, ExisJS automatically outputs formatted tabular diagnostics to the terminal displaying the exact HTTP part, field, received value, and expected requirement:
|
|
134
|
+
|
|
135
|
+
```text
|
|
136
|
+
[VALIDATION FAILED] POST /auth/register (part: body)
|
|
137
|
+
┌─────────────┬───────────────────────────┬──────────────────┐
|
|
138
|
+
│ Field │ Received │ Expected │
|
|
139
|
+
├─────────────┼───────────────────────────┼──────────────────┤
|
|
140
|
+
│ email │ "alice" │ Valid email │
|
|
141
|
+
│ age │ "16" │ min: 18 │
|
|
142
|
+
└─────────────┴───────────────────────────┴──────────────────┘
|
|
143
|
+
```
|
|
144
|
+
|
|
111
145
|
If you throw an unhandled exception (like a database connection failure or a `TypeError`), ExisJS catches it and securely returns a `500 Internal Server Error`, hiding the stack trace from the client in production mode while logging the full trace to your internal logger.
|
|
112
146
|
|
|
113
147
|
## Error Handling Decision Tree
|
|
@@ -247,8 +281,8 @@ You can use the `metrics` middleware to track request durations and counts.
|
|
|
247
281
|
|
|
248
282
|
### Functional Paradigm
|
|
249
283
|
|
|
250
|
-
```typescript filename="src/http/
|
|
251
|
-
import {
|
|
284
|
+
```typescript filename="src/http/boundary.ts"
|
|
285
|
+
import { defineBoundary } from 'exisjs/router'
|
|
252
286
|
import { metrics } from 'exisjs/observability'
|
|
253
287
|
|
|
254
288
|
const myPrometheusAdapter = {
|
|
@@ -261,15 +295,15 @@ const myPrometheusAdapter = {
|
|
|
261
295
|
}
|
|
262
296
|
}
|
|
263
297
|
|
|
264
|
-
export
|
|
298
|
+
export const config = defineBoundary({
|
|
265
299
|
middleware: [metrics(myPrometheusAdapter)]
|
|
266
300
|
})
|
|
267
301
|
```
|
|
268
302
|
|
|
269
303
|
### Class-Based (OOP) Paradigm
|
|
270
304
|
|
|
271
|
-
```typescript filename="src/http/
|
|
272
|
-
import {
|
|
305
|
+
```typescript filename="src/http/boundary.ts"
|
|
306
|
+
import { Boundary } from 'exisjs/decorators'
|
|
273
307
|
import { metrics } from 'exisjs/observability'
|
|
274
308
|
|
|
275
309
|
const myPrometheusAdapter = {
|
|
@@ -280,10 +314,10 @@ const myPrometheusAdapter = {
|
|
|
280
314
|
}
|
|
281
315
|
}
|
|
282
316
|
|
|
283
|
-
@
|
|
317
|
+
@Boundary({
|
|
284
318
|
middleware: [metrics(myPrometheusAdapter)]
|
|
285
319
|
})
|
|
286
|
-
export default class
|
|
320
|
+
export default class RootBoundary {}
|
|
287
321
|
```
|
|
288
322
|
|
|
289
323
|
*Note: ExisJS normalizes dynamic paths (e.g., turning `/users/123` into `/users/:id`) to prevent metrics cardinality issues.*
|
|
@@ -294,8 +328,8 @@ ExisJS seamlessly integrates with OpenTelemetry (OTel) via the `tracing` middlew
|
|
|
294
328
|
|
|
295
329
|
### Functional Paradigm
|
|
296
330
|
|
|
297
|
-
```typescript filename="src/http/
|
|
298
|
-
import {
|
|
331
|
+
```typescript filename="src/http/boundary.ts"
|
|
332
|
+
import { defineBoundary } from 'exisjs/router'
|
|
299
333
|
import { tracing } from 'exisjs/observability'
|
|
300
334
|
|
|
301
335
|
const myOtelAdapter = {
|
|
@@ -312,15 +346,15 @@ const myOtelAdapter = {
|
|
|
312
346
|
}
|
|
313
347
|
}
|
|
314
348
|
|
|
315
|
-
export
|
|
349
|
+
export const config = defineBoundary({
|
|
316
350
|
middleware: [tracing(myOtelAdapter)]
|
|
317
351
|
})
|
|
318
352
|
```
|
|
319
353
|
|
|
320
354
|
### Class-Based (OOP) Paradigm
|
|
321
355
|
|
|
322
|
-
```typescript filename="src/http/
|
|
323
|
-
import {
|
|
356
|
+
```typescript filename="src/http/boundary.ts"
|
|
357
|
+
import { Boundary } from 'exisjs/decorators'
|
|
324
358
|
import { tracing } from 'exisjs/observability'
|
|
325
359
|
|
|
326
360
|
const myOtelAdapter = {
|
|
@@ -335,10 +369,10 @@ const myOtelAdapter = {
|
|
|
335
369
|
}
|
|
336
370
|
}
|
|
337
371
|
|
|
338
|
-
@
|
|
372
|
+
@Boundary({
|
|
339
373
|
middleware: [tracing(myOtelAdapter)]
|
|
340
374
|
})
|
|
341
|
-
export default class
|
|
375
|
+
export default class RootBoundary {}
|
|
342
376
|
```
|
|
343
377
|
|
|
344
378
|
By providing these standard interfaces natively, your application metrics and traces are bound to the ExisJS router lifecycle.
|
|
@@ -213,3 +213,141 @@ const config: ExisConfig = {
|
|
|
213
213
|
|
|
214
214
|
export default config
|
|
215
215
|
```
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## 9. Request Timeout (`timeout`)
|
|
220
|
+
|
|
221
|
+
The `timeout()` middleware terminates runaway requests and returns `503 Service Unavailable` (or a custom status code).
|
|
222
|
+
|
|
223
|
+
ExisJS defaults to **60 seconds (60000ms)** to provide generous headroom for slow network connections and third-party cloud operations (Cloudinary, AWS S3, OpenAI, Stripe).
|
|
224
|
+
|
|
225
|
+
```typescript filename="src/http/server.ts"
|
|
226
|
+
import { exis } from 'exisjs'
|
|
227
|
+
import { timeout } from 'exisjs/middleware'
|
|
228
|
+
|
|
229
|
+
export default exis({
|
|
230
|
+
async onStart(app) {
|
|
231
|
+
app.use(timeout({
|
|
232
|
+
ms: 60000,
|
|
233
|
+
statusCode: 504,
|
|
234
|
+
message: 'Gateway Timeout connecting to upstream cloud service',
|
|
235
|
+
// Bypass webhooks or long-lived streaming endpoints
|
|
236
|
+
exclude: ['/webhooks/*', '/ai/stream'],
|
|
237
|
+
}))
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Handlers can also dynamically adjust timeouts using `req.setTimeout(ms)` and `req.clearTimeout()`.
|
|
243
|
+
|
|
244
|
+
### Route-Level Timeout Overrides
|
|
245
|
+
|
|
246
|
+
Long-running routes (file uploads, PDF exports, AI inferences, data migrations) can declare custom timeouts directly in the route definition, overriding any global application timeout:
|
|
247
|
+
|
|
248
|
+
```typescript filename="src/http/ai/route.ts"
|
|
249
|
+
import { controller, route } from 'exisjs/router'
|
|
250
|
+
|
|
251
|
+
export default controller({
|
|
252
|
+
heavyTask: route.post('/heavy-task', {
|
|
253
|
+
timeoutMs: 120000, // 2 minutes for this endpoint only
|
|
254
|
+
async handle({ body }) {
|
|
255
|
+
return await generateLargeReport(body)
|
|
256
|
+
}
|
|
257
|
+
}),
|
|
258
|
+
|
|
259
|
+
exportStream: route.get('/export', {
|
|
260
|
+
timeoutMs: 0, // 0 disables timeout completely for infinite SSE / streaming
|
|
261
|
+
async handle({ res }) {
|
|
262
|
+
return streamDataToClient(res)
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
})
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
#### Class-Based (OOP) Paradigm
|
|
269
|
+
|
|
270
|
+
Use the `@Timeout(ms)` decorator on any controller method:
|
|
271
|
+
|
|
272
|
+
```typescript filename="src/http/ai/controller.ts"
|
|
273
|
+
import { Controller, Post, Timeout } from 'exisjs/decorators'
|
|
274
|
+
|
|
275
|
+
@Controller('/ai')
|
|
276
|
+
export default class AiController {
|
|
277
|
+
@Post('/inference')
|
|
278
|
+
@Timeout(120000)
|
|
279
|
+
async runInference() {
|
|
280
|
+
return await computeModel()
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
### Declarative Route Uploads
|
|
288
|
+
|
|
289
|
+
You can configure file uploads directly within functional route declarations:
|
|
290
|
+
|
|
291
|
+
```typescript filename="src/http/media/route.ts"
|
|
292
|
+
import { controller, route } from 'exisjs/router'
|
|
293
|
+
|
|
294
|
+
export default controller({
|
|
295
|
+
upload: route.post('/photo', {
|
|
296
|
+
upload: {
|
|
297
|
+
field: 'file',
|
|
298
|
+
maxSize: '10MB',
|
|
299
|
+
allowedMimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
|
|
300
|
+
destination: 'memory', // 'memory' | 'disk' | 'stream'
|
|
301
|
+
},
|
|
302
|
+
async handle({ file, fields, req }) {
|
|
303
|
+
// file.buffer, file.mimetype, file.filename are immediately accessible
|
|
304
|
+
return { success: true, filename: file.filename }
|
|
305
|
+
}
|
|
306
|
+
})
|
|
307
|
+
})
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Class-Based (OOP) Upload Decorators
|
|
311
|
+
|
|
312
|
+
Use `@UseUpload` or `@Upload` to configure file parsing on controller actions:
|
|
313
|
+
|
|
314
|
+
```typescript filename="src/http/media/controller.ts"
|
|
315
|
+
import { Controller, Post, UseUpload, UploadedFile, Body } from 'exisjs/decorators'
|
|
316
|
+
|
|
317
|
+
@Controller('/media')
|
|
318
|
+
export default class MediaController {
|
|
319
|
+
@Post('/photo')
|
|
320
|
+
@UseUpload({
|
|
321
|
+
field: 'file',
|
|
322
|
+
maxSize: '10MB',
|
|
323
|
+
allowedMimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
|
|
324
|
+
destination: 'memory',
|
|
325
|
+
})
|
|
326
|
+
async uploadPhoto(@UploadedFile() file: any, @Body() fields: any) {
|
|
327
|
+
return { filename: file.filename, size: file.size }
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Express / Connect Multer Bridge (`fromExpress`)
|
|
333
|
+
|
|
334
|
+
Seamlessly run existing Express/Connect middleware such as **Multer** directly in ExisJS without manual Promise wrapping:
|
|
335
|
+
|
|
336
|
+
```typescript filename="src/http/legacy/route.ts"
|
|
337
|
+
import { controller, route } from 'exisjs/router'
|
|
338
|
+
import { fromExpress } from 'exisjs/middleware'
|
|
339
|
+
import multer from 'multer'
|
|
340
|
+
|
|
341
|
+
const upload = multer({ storage: multer.memoryStorage() })
|
|
342
|
+
|
|
343
|
+
export default controller({
|
|
344
|
+
upload: route.post('/',
|
|
345
|
+
fromExpress(upload.single('file')),
|
|
346
|
+
({ req }) => {
|
|
347
|
+
const file = (req.raw as any).file || req.file
|
|
348
|
+
return { success: true, originalname: file?.originalname }
|
|
349
|
+
}
|
|
350
|
+
)
|
|
351
|
+
})
|
|
352
|
+
```
|
|
353
|
+
|
|
@@ -177,3 +177,92 @@ export class OperationContext {
|
|
|
177
177
|
> [!WARNING]
|
|
178
178
|
> **Request Scope Constraints**: A provider marked as `scope: 'request'` **must** be resolved within the lifecycle of an active HTTP request. If you try to `inject()` or resolve a request-scoped provider during application startup, inside a background queue worker, or from a cron job, the container will throw a runtime error.
|
|
179
179
|
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Database Providers & Lifecycle Interop (`exisjs/database`)
|
|
183
|
+
|
|
184
|
+
ExisJS provides first-party lifecycle management and transaction coordination for databases (PostgreSQL, SQLite, MySQL, MongoDB, Prisma, Drizzle, Mongoose).
|
|
185
|
+
|
|
186
|
+
### 1. Registering Database Lifecycle (`registerDatabase`)
|
|
187
|
+
|
|
188
|
+
Registering your database with `registerDatabase` ensures connections are automatically connected on boot and safely closed with zero leaked sockets when the server receives `SIGTERM` / `SIGINT`:
|
|
189
|
+
|
|
190
|
+
```typescript filename="src/http/server.ts"
|
|
191
|
+
import { exis } from 'exisjs'
|
|
192
|
+
import { registerDatabase } from 'exisjs/database'
|
|
193
|
+
import mongoose from 'mongoose'
|
|
194
|
+
|
|
195
|
+
export default exis({
|
|
196
|
+
async onStart(app) {
|
|
197
|
+
// Automatically handles startup and graceful shutdown
|
|
198
|
+
registerDatabase(app, {
|
|
199
|
+
name: 'mongodb',
|
|
200
|
+
connect: async () => {
|
|
201
|
+
await mongoose.connect(process.env.MONGODB_URI!)
|
|
202
|
+
},
|
|
203
|
+
disconnect: async () => {
|
|
204
|
+
await mongoose.disconnect()
|
|
205
|
+
},
|
|
206
|
+
healthCheck: async () => {
|
|
207
|
+
return mongoose.connection.readyState === 1
|
|
208
|
+
},
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### 2. Universal Transaction Runner (`withTransaction`)
|
|
215
|
+
|
|
216
|
+
`withTransaction` provides a duck-typed transaction runner that automatically coordinates transaction lifecycles, commits, aborts, and session cleanups across different ORMs:
|
|
217
|
+
|
|
218
|
+
```typescript filename="src/http/orders/service.ts"
|
|
219
|
+
import { withTransaction } from 'exisjs/database'
|
|
220
|
+
import mongoose from 'mongoose'
|
|
221
|
+
import { Order } from '@/models/Order'
|
|
222
|
+
import { Inventory } from '@/models/Inventory'
|
|
223
|
+
|
|
224
|
+
export async function placeOrder(customerId: string, items: { sku: string; qty: number }[]) {
|
|
225
|
+
return withTransaction(mongoose, async (session) => {
|
|
226
|
+
// Automatically attaches session to queries and commits/aborts on error
|
|
227
|
+
const order = await Order.create([{ customerId, items }], { session })
|
|
228
|
+
|
|
229
|
+
for (const item of items) {
|
|
230
|
+
const updated = await Inventory.findOneAndUpdate(
|
|
231
|
+
{ sku: item.sku, stock: { $gte: item.qty } },
|
|
232
|
+
{ $inc: { stock: -item.qty } },
|
|
233
|
+
{ session, returnDocument: 'after' }
|
|
234
|
+
)
|
|
235
|
+
if (!updated) {
|
|
236
|
+
throw new Error(`Insufficient stock for SKU: ${item.sku}`)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return order[0]
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Works identically with Prisma (`prisma.$transaction`) and Drizzle (`db.transaction`).
|
|
246
|
+
|
|
247
|
+
### 3. Modern Mongoose 8/9 Interop & Connection Pooling
|
|
248
|
+
|
|
249
|
+
ExisJS exports helpers to prevent deprecation warnings in modern Mongoose:
|
|
250
|
+
|
|
251
|
+
```typescript filename="src/models/user.ts"
|
|
252
|
+
import { modernUpdateOptions, mongoPoolOptions } from 'exisjs/database'
|
|
253
|
+
import mongoose from 'mongoose'
|
|
254
|
+
|
|
255
|
+
// Cloud-hardened pool options (maxPoolSize, serverSelectionTimeoutMS, etc.)
|
|
256
|
+
export const poolConfig = mongoPoolOptions({ maxPoolSize: 50 })
|
|
257
|
+
|
|
258
|
+
// Automatically maps legacy { new: true } to { returnDocument: 'after' }
|
|
259
|
+
export async function updateUsername(id: string, name: string) {
|
|
260
|
+
return User.findByIdAndUpdate(
|
|
261
|
+
id,
|
|
262
|
+
{ $set: { name } },
|
|
263
|
+
modernUpdateOptions({ returnDocument: 'after' })
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
|