exisjs 0.7.5 → 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/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +156 -85
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/start.d.ts.map +1 -1
- package/dist/cli/commands/start.js +54 -16
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/index.js +10 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/manifest.d.ts.map +1 -1
- package/dist/cli/manifest.js +106 -13
- 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/types.d.ts +2 -0
- package/dist/config/types.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/cli/commands.mdx +1 -1
- package/dist/docs/content/controllers.mdx +127 -10
- package/dist/docs/content/decorators.mdx +22 -4
- package/dist/docs/content/introduction.mdx +7 -7
- package/dist/docs/content/middleware.mdx +269 -6
- package/dist/docs/content/requests.mdx +180 -6
- package/dist/docs/content/responses.mdx +283 -32
- package/dist/docs/content/routing.mdx +46 -0
- package/dist/docs/content/validation.mdx +190 -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 +33 -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/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 +17 -9
- 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.map +1 -1
- package/dist/router/route-scanner.js +24 -2
- 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 +8 -0
- package/dist/router/types.d.ts.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 +10 -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/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 +27 -17
- package/dist/server/request-handler.js.map +1 -1
- package/dist/server/request.d.ts.map +1 -1
- package/dist/server/request.js +18 -0
- package/dist/server/request.js.map +1 -1
- package/dist/server/response.d.ts +10 -1
- package/dist/server/response.d.ts.map +1 -1
- package/dist/server/response.js +154 -2
- 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 +31 -15
- package/dist/validator/tex.d.ts.map +1 -1
- package/dist/validator/tex.js +280 -42
- package/dist/validator/tex.js.map +1 -1
- package/package.json +4 -4
- 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
|
@@ -101,13 +101,29 @@ In most cases, you don't need to manually access the entire request object. Inst
|
|
|
101
101
|
<td className="px-4 py-3 font-mono text-xs"><span className="text-primary">@</span>Ip()</td>
|
|
102
102
|
<td className="px-4 py-3 text-muted-foreground">The client's IP address</td>
|
|
103
103
|
</tr>
|
|
104
|
+
<tr className="bg-background hover:bg-muted/30 transition-colors">
|
|
105
|
+
<td className="px-4 py-3 font-mono text-xs"><span className="text-primary">@</span>Cookies(name?) / <span className="text-primary">@</span>Cookie(name?)</td>
|
|
106
|
+
<td className="px-4 py-3 text-muted-foreground">Incoming parsed cookies or specific cookie value</td>
|
|
107
|
+
</tr>
|
|
108
|
+
<tr className="bg-background hover:bg-muted/30 transition-colors">
|
|
109
|
+
<td className="px-4 py-3 font-mono text-xs"><span className="text-primary">@</span>State(key?)</td>
|
|
110
|
+
<td className="px-4 py-3 text-muted-foreground">Request state from execution context or middleware</td>
|
|
111
|
+
</tr>
|
|
112
|
+
<tr className="bg-background hover:bg-muted/30 transition-colors">
|
|
113
|
+
<td className="px-4 py-3 font-mono text-xs"><span className="text-primary">@</span>AppCtx()</td>
|
|
114
|
+
<td className="px-4 py-3 text-muted-foreground">The root ExisJS Application instance</td>
|
|
115
|
+
</tr>
|
|
116
|
+
<tr className="bg-background hover:bg-muted/30 transition-colors">
|
|
117
|
+
<td className="px-4 py-3 font-mono text-xs"><span className="text-primary">@</span>Fields(name?)</td>
|
|
118
|
+
<td className="px-4 py-3 text-muted-foreground">Non-file multipart form fields</td>
|
|
119
|
+
</tr>
|
|
104
120
|
<tr className="bg-background hover:bg-muted/30 transition-colors">
|
|
105
121
|
<td className="px-4 py-3 font-mono text-xs"><span className="text-primary">@</span>UploadedFile()</td>
|
|
106
122
|
<td className="px-4 py-3 text-muted-foreground">A single uploaded file (multipart/form-data)</td>
|
|
107
123
|
</tr>
|
|
108
124
|
<tr className="bg-background hover:bg-muted/30 transition-colors">
|
|
109
125
|
<td className="px-4 py-3 font-mono text-xs"><span className="text-primary">@</span>UploadedFiles()</td>
|
|
110
|
-
<td className="px-4 py-3 text-muted-foreground">Array of uploaded files</td>
|
|
126
|
+
<td className="px-4 py-3 text-muted-foreground">Array or dictionary of uploaded files</td>
|
|
111
127
|
</tr>
|
|
112
128
|
</tbody>
|
|
113
129
|
</table>
|
|
@@ -115,22 +131,123 @@ In most cases, you don't need to manually access the entire request object. Inst
|
|
|
115
131
|
|
|
116
132
|
## Payloads & Validation
|
|
117
133
|
|
|
118
|
-
For POST and
|
|
134
|
+
For POST, PUT, and PATCH endpoints, you can validate and read incoming payloads using either method-level schemas or parameter-level `@Body(Schema)` decorators:
|
|
119
135
|
|
|
120
136
|
```typescript filename="src/http/users/route.ts"
|
|
121
|
-
import { Controller, Post, Body } from 'exisjs/decorators'
|
|
137
|
+
import { Controller, Post, Body, HttpCode } from 'exisjs/decorators'
|
|
138
|
+
import { tex } from 'exisjs/validator'
|
|
139
|
+
import type { ResolveSchema } from 'exisjs/validator'
|
|
140
|
+
|
|
141
|
+
export const CreateUserSchema = tex.object({
|
|
142
|
+
name: tex.string({ min: 2 }),
|
|
143
|
+
email: tex.email(),
|
|
144
|
+
role: tex.enum(['admin', 'user'], { default: 'user' }),
|
|
145
|
+
})
|
|
122
146
|
|
|
123
|
-
export
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
147
|
+
export type CreateUserDto = ResolveSchema<typeof CreateUserSchema>
|
|
148
|
+
|
|
149
|
+
@Controller('users')
|
|
150
|
+
export default class UsersController {
|
|
151
|
+
// 1. Parameter-level validation schema
|
|
152
|
+
@Post('/')
|
|
153
|
+
@HttpCode(201)
|
|
154
|
+
async create(@Body(CreateUserSchema) dto: CreateUserDto) {
|
|
155
|
+
return { id: 3, ...dto }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 2. Or method-level schema object
|
|
159
|
+
@Post('/raw', { body: CreateUserSchema })
|
|
160
|
+
async createWithMethodSchema(@Body() dto: CreateUserDto) {
|
|
161
|
+
return { id: 4, ...dto }
|
|
162
|
+
}
|
|
127
163
|
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Dependency Injection in Controllers
|
|
167
|
+
|
|
168
|
+
Controllers can inject services using either the ergonomic `inject()` helper or standard constructor injection:
|
|
169
|
+
|
|
170
|
+
```typescript filename="src/http/users/route.ts"
|
|
171
|
+
import { Controller, Get, Param } from 'exisjs/decorators'
|
|
172
|
+
import { inject, Inject, Optional } from 'exisjs/di'
|
|
173
|
+
import { UsersService } from './service'
|
|
128
174
|
|
|
129
175
|
@Controller('users')
|
|
130
176
|
export default class UsersController {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
177
|
+
// Approach A: Property-level injection (Recommended)
|
|
178
|
+
private usersService = inject(UsersService)
|
|
179
|
+
|
|
180
|
+
// Approach B: Constructor injection
|
|
181
|
+
constructor(
|
|
182
|
+
@Inject('DATABASE_URL') private dbUrl: string,
|
|
183
|
+
@Optional() @Inject('METRICS') private metrics?: any
|
|
184
|
+
) {}
|
|
185
|
+
|
|
186
|
+
@Get('/:id')
|
|
187
|
+
async getUser(@Param('id') id: string) {
|
|
188
|
+
return this.usersService.findById(id)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## File Uploads
|
|
194
|
+
|
|
195
|
+
You can process file uploads in OOP controllers using `@UseUpload` or `@Upload` decorators alongside `@UploadedFile()` and `@UploadedFiles()`:
|
|
196
|
+
|
|
197
|
+
```typescript filename="src/http/users/route.ts"
|
|
198
|
+
import { Controller, Post, UseUpload, UploadedFile, Body } from 'exisjs/decorators'
|
|
199
|
+
import type { UploadedFile as IUploadedFile } from 'exisjs/router'
|
|
200
|
+
|
|
201
|
+
@Controller('users')
|
|
202
|
+
export default class UsersController {
|
|
203
|
+
@Post('/avatar')
|
|
204
|
+
@UseUpload({
|
|
205
|
+
field: 'avatar',
|
|
206
|
+
maxBytes: 5 * 1024 * 1024,
|
|
207
|
+
mimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
|
|
208
|
+
})
|
|
209
|
+
async uploadAvatar(
|
|
210
|
+
@UploadedFile() file: IUploadedFile,
|
|
211
|
+
@Body() body: { caption?: string }
|
|
212
|
+
) {
|
|
213
|
+
return {
|
|
214
|
+
success: true,
|
|
215
|
+
filename: file.filename,
|
|
216
|
+
size: file.size,
|
|
217
|
+
caption: body.caption,
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Streaming & Downloads
|
|
224
|
+
|
|
225
|
+
OOP controllers support all streaming features including Node.js `Readable` streams, Web `ReadableStream`, async generators, and file downloads:
|
|
226
|
+
|
|
227
|
+
```typescript filename="src/http/files/route.ts"
|
|
228
|
+
import { Controller, Get, Res } from 'exisjs/decorators'
|
|
229
|
+
import type { Response } from 'exisjs/router'
|
|
230
|
+
import fs from 'node:fs'
|
|
231
|
+
|
|
232
|
+
@Controller('files')
|
|
233
|
+
export default class FilesController {
|
|
234
|
+
// 1. Force file download
|
|
235
|
+
@Get('/download')
|
|
236
|
+
download(@Res() res: Response) {
|
|
237
|
+
res.download('./reports/annual.pdf', 'Annual-Report.pdf')
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 2. Stream Node.js Readable or auto-pipe return value
|
|
241
|
+
@Get('/stream')
|
|
242
|
+
streamCsv() {
|
|
243
|
+
return fs.createReadStream('./data/export.csv')
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 3. Server-Sent Events (SSE)
|
|
247
|
+
@Get('/events')
|
|
248
|
+
events(@Res() res: Response) {
|
|
249
|
+
const sse = res.sse()
|
|
250
|
+
sse.send({ event: 'ping', data: { time: Date.now() } })
|
|
134
251
|
}
|
|
135
252
|
}
|
|
136
253
|
```
|
|
@@ -134,6 +134,12 @@ export default class UsersController {
|
|
|
134
134
|
- `@Param(nameOrPipe?)` — Extracts a route path param (e.g. `:id`).
|
|
135
135
|
- `@QueryParam(nameOrPipe?)` / `@Query(nameOrPipe?)` — Extracts a query parameter from the URL query string.
|
|
136
136
|
- `@Headers(nameOrPipe?)` — Extracts a request header by name.
|
|
137
|
+
- `@Cookies(nameOrPipe?)` / `@Cookie(nameOrPipe?)` — Injects incoming parsed cookies or a specific cookie value.
|
|
138
|
+
- `@State(keyOrPipe?)` — Injects request state from execution context or middleware.
|
|
139
|
+
- `@AppCtx()` — Injects the root ExisJS Application instance.
|
|
140
|
+
- `@Fields(nameOrPipe?)` — Injects non-file multipart form fields.
|
|
141
|
+
- `@UploadedFile(nameOrPipe?)` — Injects a single uploaded file.
|
|
142
|
+
- `@UploadedFiles(nameOrPipe?)` — Injects multiple uploaded files.
|
|
137
143
|
- `@HostParam(nameOrPipe?)` — Extracts a host subdomain parameter.
|
|
138
144
|
- `@Req()` — Injects the raw request object.
|
|
139
145
|
- `@Res(options?)` — Injects the response object (e.g. `{ passthrough: true }`).
|
|
@@ -150,16 +156,19 @@ export default class UsersController {
|
|
|
150
156
|
|
|
151
157
|
## 4. Middleware & Pipeline Decorators
|
|
152
158
|
|
|
153
|
-
You can bind middlewares, guards, interceptors, and exception filters to controllers or route methods using
|
|
159
|
+
You can bind middlewares, guards, interceptors, and exception filters to controllers or route methods using first-class decorators:
|
|
154
160
|
|
|
155
161
|
```typescript filename="src/http/checkout/route.ts"
|
|
156
|
-
import { Controller, Post, Use, Idempotent } from 'exisjs/decorators'
|
|
162
|
+
import { Controller, Post, Use, Idempotent, RateLimit, Cors, Timeout } from 'exisjs/decorators'
|
|
157
163
|
import { requireAuth } from '../../middleware/auth'
|
|
158
164
|
|
|
159
165
|
@Controller('/checkout')
|
|
160
166
|
@Use(requireAuth) // Applies to all routes in this controller
|
|
167
|
+
@Cors({ origin: 'https://app.yourdomain.com' })
|
|
168
|
+
@RateLimit({ max: 10, windowMs: 60000 })
|
|
161
169
|
export default class CheckoutController {
|
|
162
170
|
@Post('/')
|
|
171
|
+
@Timeout(30000)
|
|
163
172
|
@Idempotent({ ttlMs: 86400000 }) // In-memory off-heap idempotency protection
|
|
164
173
|
async processPayment() {
|
|
165
174
|
return { success: true }
|
|
@@ -169,7 +178,15 @@ export default class CheckoutController {
|
|
|
169
178
|
|
|
170
179
|
### Available Lifecycle & Middleware Decorators
|
|
171
180
|
- `@Use(...items)` — Unified decorator that attaches middlewares, guards (`canActivate`), interceptors (`intercept`), or exception filters (`catch`) to a class or method.
|
|
172
|
-
- `@
|
|
181
|
+
- `@Timeout(ms: number | TimeoutOptions)` — Overrides the request timeout for a class or method.
|
|
182
|
+
- `@Idempotent(options?)` — Caches responses based on the `Idempotency-Key` header using native in-memory off-heap storage.
|
|
183
|
+
- `@RateLimit(options?)` — Enforces request rate limits on a controller class or route method.
|
|
184
|
+
- `@Cors(options?)` — Configures Cross-Origin Resource Sharing rules on a controller class or route method.
|
|
185
|
+
- `@IpFilter(options)` — Enforces IP/CIDR allowlist and denylist.
|
|
186
|
+
- `@Compress()` — Enables automatic response compression (Gzip/Brotli).
|
|
187
|
+
- `@Dedupe(options)` — Deduplicates concurrent identical requests sharing the same key.
|
|
188
|
+
- `@UseUpload(options)` / `@Upload(options)` — Configures multipart file upload handling.
|
|
189
|
+
- `@BlockProbes(options?)` — Intercepts scanner exploit probes (`.env`, `/.git`, `/.DS_Store`) and blackholes them without log noise.
|
|
173
190
|
|
|
174
191
|
---
|
|
175
192
|
|
|
@@ -229,7 +246,8 @@ export class UserService {
|
|
|
229
246
|
You can inject dependencies using tokens or classes, and mark optional dependencies with `@Optional()`:
|
|
230
247
|
|
|
231
248
|
```typescript filename="src/http/users/route.ts"
|
|
232
|
-
import { Controller, Get, Param
|
|
249
|
+
import { Controller, Get, Param } from 'exisjs/decorators'
|
|
250
|
+
import { Inject, Optional } from 'exisjs/di'
|
|
233
251
|
import { UserService } from './service'
|
|
234
252
|
|
|
235
253
|
@Controller('/users')
|
|
@@ -20,7 +20,7 @@ Exis aims to provide that out-of-the-box application architecture for backends:
|
|
|
20
20
|
The fastest way to start is the Exis CLI, which scaffolds a new project directory with the core files and a conventional base structure already in place. This is the recommended path for first-time users.
|
|
21
21
|
|
|
22
22
|
<div className="my-6 max-w-lg">
|
|
23
|
-
<CopyCommand command="
|
|
23
|
+
<CopyCommand command="npm create exisjs@latest my-backend" />
|
|
24
24
|
</div>
|
|
25
25
|
|
|
26
26
|
<Hint>
|
|
@@ -43,12 +43,12 @@ export default controller({
|
|
|
43
43
|
getUsers: route.get('/', {
|
|
44
44
|
handle(req) {
|
|
45
45
|
// Return a realistic response structure
|
|
46
|
-
return {
|
|
46
|
+
return {
|
|
47
47
|
data: [
|
|
48
48
|
{ id: 1, name: 'Alice' },
|
|
49
|
-
{ id: 2, name: 'Bob' }
|
|
49
|
+
{ id: 2, name: 'Bob' },
|
|
50
50
|
],
|
|
51
|
-
total: 2
|
|
51
|
+
total: 2,
|
|
52
52
|
}
|
|
53
53
|
},
|
|
54
54
|
}),
|
|
@@ -66,12 +66,12 @@ export default class UsersController {
|
|
|
66
66
|
@Get('/')
|
|
67
67
|
getUsers(@Req() req: Request) {
|
|
68
68
|
// Return a realistic response structure
|
|
69
|
-
return {
|
|
69
|
+
return {
|
|
70
70
|
data: [
|
|
71
71
|
{ id: 1, name: 'Alice' },
|
|
72
|
-
{ id: 2, name: 'Bob' }
|
|
72
|
+
{ id: 2, name: 'Bob' },
|
|
73
73
|
],
|
|
74
|
-
total: 2
|
|
74
|
+
total: 2,
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
}
|
|
@@ -32,9 +32,131 @@ export const config = defineBoundary({})
|
|
|
32
32
|
export const logger = requestLogger({ level: 'debug' })
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
### Middleware
|
|
35
|
+
### Middleware Context Type Inference (`defineMiddleware`)
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
When custom middlewares attach data to `req` (such as authenticated user details, session data, or tenant tokens), ExisJS supports **End-to-End Middleware Context Type Inference**.
|
|
38
|
+
|
|
39
|
+
By using `defineMiddleware<TContext>()`, downstream route handlers automatically inherit strongly typed properties on `req` with zero manual type casts (`as any`) or boilerplate.
|
|
40
|
+
|
|
41
|
+
#### Local Route-Level Inference (Recommended)
|
|
42
|
+
|
|
43
|
+
```typescript filename="src/http/route.ts"
|
|
44
|
+
import { controller, route, defineMiddleware } from 'exisjs/router'
|
|
45
|
+
|
|
46
|
+
// 1. Define your custom context shape
|
|
47
|
+
interface AuthContext {
|
|
48
|
+
user: {
|
|
49
|
+
id: string
|
|
50
|
+
role: 'admin' | 'user'
|
|
51
|
+
email: string
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 2. Define the middleware with your context
|
|
56
|
+
export const authMiddleware = defineMiddleware<AuthContext>((req, res, next) => {
|
|
57
|
+
req.user = {
|
|
58
|
+
id: 'usr_123',
|
|
59
|
+
role: 'admin',
|
|
60
|
+
email: 'alex@example.com'
|
|
61
|
+
}
|
|
62
|
+
next()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
export default controller({
|
|
66
|
+
profile: route.get('/profile', {
|
|
67
|
+
middlewares: [authMiddleware],
|
|
68
|
+
handle({ req, res }) {
|
|
69
|
+
// ✨ req.user is automatically inferred directly from middlewares: [authMiddleware]!
|
|
70
|
+
// Full autocomplete and strict type safety:
|
|
71
|
+
return res.json({
|
|
72
|
+
userId: req.user.id,
|
|
73
|
+
role: req.user.role,
|
|
74
|
+
email: req.user.email
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
#### Multi-Middleware Context Composition
|
|
82
|
+
|
|
83
|
+
When passing multiple middlewares, ExisJS automatically merges all injected contexts:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
const authMw = defineMiddleware<{ user: { id: string } }>(...)
|
|
87
|
+
const tenantMw = defineMiddleware<{ tenantId: string }>(...)
|
|
88
|
+
|
|
89
|
+
export default controller({
|
|
90
|
+
data: route.get('/data', {
|
|
91
|
+
middlewares: [authMw, tenantMw],
|
|
92
|
+
handle({ req }) {
|
|
93
|
+
// req has both `req.user` and `req.tenantId` strictly typed!
|
|
94
|
+
return { userId: req.user.id, tenant: req.tenantId }
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
#### Module-Scoped Context (`createRouter<TContext>()`)
|
|
101
|
+
|
|
102
|
+
If an entire file or subsystem shares the same context, use `createRouter<TContext>()`:
|
|
103
|
+
|
|
104
|
+
```typescript filename="src/http/admin/route.ts"
|
|
105
|
+
import { createRouter } from 'exisjs/router'
|
|
106
|
+
|
|
107
|
+
interface AdminContext {
|
|
108
|
+
user: { id: string; role: 'admin' }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const { route, controller } = createRouter<AdminContext>()
|
|
112
|
+
|
|
113
|
+
export default controller({
|
|
114
|
+
settings: route.get('/settings', {
|
|
115
|
+
middlewares: [authMiddleware],
|
|
116
|
+
handle({ req }) {
|
|
117
|
+
return { adminId: req.user.id }
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
#### Boundary-Scoped Context (`defineBoundary<TContext>()`)
|
|
124
|
+
|
|
125
|
+
Propagate context types across all routes in a directory tree:
|
|
126
|
+
|
|
127
|
+
```typescript filename="src/http/admin/boundary.ts"
|
|
128
|
+
import { defineBoundary } from 'exisjs/router'
|
|
129
|
+
|
|
130
|
+
export const config = defineBoundary<AdminContext>({
|
|
131
|
+
beforeHandle: async ({ req }) => {
|
|
132
|
+
console.log(`Admin action by: ${req.user.id}`)
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
#### Class-Based (OOP) Context Usage
|
|
138
|
+
|
|
139
|
+
In OOP controllers, you can attach typed middlewares via `@Use()` and type the injected `@Req()` object:
|
|
140
|
+
|
|
141
|
+
```typescript filename="src/http/admin/route.ts"
|
|
142
|
+
import { Controller, Get, Use, Req } from 'exisjs/decorators'
|
|
143
|
+
import type { Request } from 'exisjs/router'
|
|
144
|
+
import { authMiddleware } from '../../middleware/auth'
|
|
145
|
+
import type { AuthContext } from '../../middleware/auth'
|
|
146
|
+
|
|
147
|
+
@Controller('/admin')
|
|
148
|
+
@Use(authMiddleware)
|
|
149
|
+
export default class AdminController {
|
|
150
|
+
@Get('/profile')
|
|
151
|
+
getProfile(@Req() req: Request & AuthContext) {
|
|
152
|
+
return {
|
|
153
|
+
userId: req.user.id,
|
|
154
|
+
role: req.user.role,
|
|
155
|
+
email: req.user.email,
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
38
160
|
|
|
39
161
|
#### Functional Paradigm
|
|
40
162
|
|
|
@@ -132,15 +254,42 @@ app.use(requestId())
|
|
|
132
254
|
|
|
133
255
|
ExisJS provides high-performance rate limiting powered by Rust native memory:
|
|
134
256
|
|
|
135
|
-
|
|
257
|
+
#### Functional Paradigm
|
|
258
|
+
|
|
259
|
+
```typescript filename="src/http/auth/route.ts"
|
|
260
|
+
import { controller, route } from 'exisjs/router'
|
|
136
261
|
import { rateLimit } from 'exisjs/middleware'
|
|
137
262
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
263
|
+
const authLimiter = rateLimit({ windowMs: 60000, max: 10 })
|
|
264
|
+
|
|
265
|
+
export default controller({
|
|
266
|
+
login: route.post('/login', {
|
|
267
|
+
middlewares: [authLimiter],
|
|
268
|
+
handle({ req }) {
|
|
269
|
+
return { status: 'ok' }
|
|
270
|
+
}
|
|
271
|
+
})
|
|
141
272
|
})
|
|
142
273
|
```
|
|
143
274
|
|
|
275
|
+
#### Class-Based (OOP) Paradigm
|
|
276
|
+
|
|
277
|
+
```typescript filename="src/http/auth/route.ts"
|
|
278
|
+
import { Controller, Post, Use } from 'exisjs/decorators'
|
|
279
|
+
import { rateLimit } from 'exisjs/middleware'
|
|
280
|
+
|
|
281
|
+
const authLimiter = rateLimit({ windowMs: 60000, max: 10 })
|
|
282
|
+
|
|
283
|
+
@Controller('/auth')
|
|
284
|
+
export default class AuthController {
|
|
285
|
+
@Post('/login')
|
|
286
|
+
@Use(authLimiter)
|
|
287
|
+
async login() {
|
|
288
|
+
return { status: 'ok' }
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
144
293
|
> [!NOTE]
|
|
145
294
|
> Core rate limiting runs completely off-heap in-memory. For Redis-backed distributed rate limiting across clusters, use `@exisjs/cache`.
|
|
146
295
|
|
|
@@ -150,6 +299,24 @@ export const limiter = rateLimit({
|
|
|
150
299
|
|
|
151
300
|
Idempotency caches responses based on the `Idempotency-Key` header, preventing duplicate mutations:
|
|
152
301
|
|
|
302
|
+
#### Functional Paradigm
|
|
303
|
+
|
|
304
|
+
```typescript filename="src/http/checkout/route.ts"
|
|
305
|
+
import { controller, route } from 'exisjs/router'
|
|
306
|
+
import { idempotent } from 'exisjs/middleware'
|
|
307
|
+
|
|
308
|
+
export default controller({
|
|
309
|
+
checkout: route.post('/', {
|
|
310
|
+
middlewares: [idempotent({ ttlMs: 86400000 })],
|
|
311
|
+
async handle() {
|
|
312
|
+
return { success: true }
|
|
313
|
+
}
|
|
314
|
+
})
|
|
315
|
+
})
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
#### Class-Based (OOP) Paradigm
|
|
319
|
+
|
|
153
320
|
```typescript filename="src/http/checkout/route.ts"
|
|
154
321
|
import { Controller, Post, Idempotent } from 'exisjs/decorators'
|
|
155
322
|
|
|
@@ -169,6 +336,8 @@ export default class CheckoutController {
|
|
|
169
336
|
|
|
170
337
|
If multiple clients request the exact same resource concurrently, `dedupe` collapses identical in-flight requests into a single operation:
|
|
171
338
|
|
|
339
|
+
#### Functional Paradigm
|
|
340
|
+
|
|
172
341
|
```typescript filename="src/http/stats/route.ts"
|
|
173
342
|
import { controller, route } from 'exisjs/router'
|
|
174
343
|
import { dedupe } from 'exisjs/middleware'
|
|
@@ -183,6 +352,22 @@ export default controller({
|
|
|
183
352
|
})
|
|
184
353
|
```
|
|
185
354
|
|
|
355
|
+
#### Class-Based (OOP) Paradigm
|
|
356
|
+
|
|
357
|
+
```typescript filename="src/http/stats/route.ts"
|
|
358
|
+
import { Controller, Get, Use } from 'exisjs/decorators'
|
|
359
|
+
import { dedupe } from 'exisjs/middleware'
|
|
360
|
+
|
|
361
|
+
@Controller('/stats')
|
|
362
|
+
export default class StatsController {
|
|
363
|
+
@Get('/')
|
|
364
|
+
@Use(dedupe({ ttlMs: 5000 }))
|
|
365
|
+
async getStats() {
|
|
366
|
+
return { activeUsers: 1000 }
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
186
371
|
---
|
|
187
372
|
|
|
188
373
|
## 7. Static File Serving
|
|
@@ -351,3 +536,81 @@ export default controller({
|
|
|
351
536
|
})
|
|
352
537
|
```
|
|
353
538
|
|
|
539
|
+
---
|
|
540
|
+
|
|
541
|
+
## 10. Security Scanner Noise Suppression & Blackhole (`blockSuspiciousProbes`)
|
|
542
|
+
|
|
543
|
+
In public production deployments, automated vulnerability scanners aggressively probe for `.env`, `/.git/HEAD`, `/.env.backup`, `/.DS_Store`, `wp-config.php`, etc.
|
|
544
|
+
|
|
545
|
+
Each probe generates a `404` error log with structured JSON logging (`{"level":40,"statusCode":404}`), flooding log aggregators and triggering false security alerts.
|
|
546
|
+
|
|
547
|
+
ExisJS provides `blockSuspiciousProbes` (alias `blockProbes`) to immediately intercept known exploit patterns, terminate the request with an empty response (blackhole), and suppress logger noise.
|
|
548
|
+
|
|
549
|
+
### Global Usage (`server.ts` / `exis.config.ts`)
|
|
550
|
+
|
|
551
|
+
```typescript filename="src/http/server.ts"
|
|
552
|
+
import { exis } from 'exisjs'
|
|
553
|
+
import { blockSuspiciousProbes } from 'exisjs/middleware'
|
|
554
|
+
|
|
555
|
+
export default exis({
|
|
556
|
+
async onStart(app) {
|
|
557
|
+
// Intercept probes before downstream routes or loggers
|
|
558
|
+
app.use(blockSuspiciousProbes({
|
|
559
|
+
statusCode: 404, // or 403
|
|
560
|
+
silent: true, // Suppresses level-40 warning logs in requestLogger
|
|
561
|
+
}))
|
|
562
|
+
}
|
|
563
|
+
})
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
Or enable directly in `exis.config.ts`:
|
|
567
|
+
|
|
568
|
+
```typescript filename="exis.config.ts"
|
|
569
|
+
import { defineConfig } from 'exisjs/config'
|
|
570
|
+
|
|
571
|
+
export default async () => defineConfig({
|
|
572
|
+
blockProbes: true, // or { statusCode: 404, silent: true }
|
|
573
|
+
})
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
### Folder-Scoped Boundary (`boundary.ts`)
|
|
577
|
+
|
|
578
|
+
```typescript filename="src/http/boundary.ts"
|
|
579
|
+
import { defineBoundary } from 'exisjs/router'
|
|
580
|
+
|
|
581
|
+
export default defineBoundary({
|
|
582
|
+
blockProbes: {
|
|
583
|
+
statusCode: 404,
|
|
584
|
+
silent: true,
|
|
585
|
+
exclude: ['/.well-known/security.txt'],
|
|
586
|
+
}
|
|
587
|
+
})
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
### Class-Based (OOP) Decorator
|
|
591
|
+
|
|
592
|
+
```typescript filename="src/http/api/route.ts"
|
|
593
|
+
import { Controller, Get, BlockProbes } from 'exisjs/decorators'
|
|
594
|
+
|
|
595
|
+
@Controller('/api')
|
|
596
|
+
@BlockProbes({ statusCode: 404 })
|
|
597
|
+
export default class ApiController {
|
|
598
|
+
@Get('/health')
|
|
599
|
+
health() {
|
|
600
|
+
return { status: 'healthy' }
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
### Configuration Options
|
|
606
|
+
|
|
607
|
+
| Option | Type | Default | Description |
|
|
608
|
+
| :--- | :--- | :--- | :--- |
|
|
609
|
+
| `statusCode` | `number` | `404` | HTTP status code to return for blocked probes |
|
|
610
|
+
| `blackhole` | `boolean` | `true` | Immediately terminates connection with empty body without calling downstream middleware |
|
|
611
|
+
| `silent` | `boolean` | `true` | Suppresses `requestLogger` warning logs for blocked probe requests |
|
|
612
|
+
| `patterns` | `(string \| RegExp)[]` | `[]` | Additional custom probe patterns to block |
|
|
613
|
+
| `exclude` | `(string \| RegExp)[]` | `[]` | Whitelist patterns to allow through without blocking |
|
|
614
|
+
| `onProbe` | `(req, res, pattern) => void` | `undefined` | Callback hook when a probe is detected (for SIEM or IP reputation metrics) |
|
|
615
|
+
|
|
616
|
+
|