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
|
@@ -91,112 +91,201 @@ export default class ProcessController {
|
|
|
91
91
|
|
|
92
92
|
---
|
|
93
93
|
|
|
94
|
-
## 3. File Uploads & Form Data
|
|
94
|
+
## 3. File Uploads & Multipart Form Data
|
|
95
95
|
|
|
96
|
-
Handling `multipart/form-data` in Node.js can be difficult. ExisJS provides
|
|
96
|
+
Handling `multipart/form-data` in Node.js can be difficult. ExisJS provides multiple robust, high-performance approaches:
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
### A. Declarative Route Uploads (Recommended)
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
You can declare file upload requirements directly inside your route schema. ExisJS automatically executes the multipart parser, validates MIME types, enforces byte caps, and populates `req.file` or `req.files` along with non-file fields in `req.body`:
|
|
101
|
+
|
|
102
|
+
```typescript filename="src/http/upload/route.ts"
|
|
103
|
+
import { controller, route } from 'exisjs/router'
|
|
104
|
+
import { tex } from 'exisjs/validator'
|
|
105
|
+
|
|
106
|
+
export default controller({
|
|
107
|
+
uploadPhoto: route.post('/', {
|
|
108
|
+
upload: {
|
|
109
|
+
field: 'photo',
|
|
110
|
+
maxBytes: 10 * 1024 * 1024, // 10MB limit
|
|
111
|
+
mimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
|
|
112
|
+
},
|
|
113
|
+
body: tex.object({
|
|
114
|
+
title: tex.string({ min: 1 }),
|
|
115
|
+
}),
|
|
116
|
+
handle({ req }) {
|
|
117
|
+
const file = req.file! // Typed UploadedFile
|
|
118
|
+
console.log(`Uploaded ${file.originalname} (${file.size} bytes)`)
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
success: true,
|
|
122
|
+
filename: file.filename,
|
|
123
|
+
mimetype: file.mimetype,
|
|
124
|
+
title: req.body.title,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### B. First-Party `fileUpload` Middleware
|
|
132
|
+
|
|
133
|
+
ExisJS exports a dedicated `fileUpload` middleware from `exisjs/middleware` supporting single, multiple, and multi-field uploads:
|
|
134
|
+
|
|
135
|
+
```typescript filename="src/http/documents/route.ts"
|
|
136
|
+
import { controller, route } from 'exisjs/router'
|
|
137
|
+
import { fileUpload } from 'exisjs/middleware'
|
|
138
|
+
|
|
139
|
+
export default controller({
|
|
140
|
+
// Single file upload
|
|
141
|
+
single: route.post('/single',
|
|
142
|
+
fileUpload.single('document', {
|
|
143
|
+
maxBytes: 15 * 1024 * 1024, // 15MB
|
|
144
|
+
dest: './uploads/docs',
|
|
145
|
+
}),
|
|
146
|
+
({ req }) => {
|
|
147
|
+
return { file: req.file }
|
|
148
|
+
}
|
|
149
|
+
),
|
|
150
|
+
|
|
151
|
+
// Multiple files with the same field name
|
|
152
|
+
gallery: route.post('/gallery',
|
|
153
|
+
fileUpload.array('photos', 5, {
|
|
154
|
+
mimeTypes: ['image/*'],
|
|
155
|
+
}),
|
|
156
|
+
({ req }) => {
|
|
157
|
+
const files = req.files // UploadedFile[]
|
|
158
|
+
return { count: files?.length }
|
|
159
|
+
}
|
|
160
|
+
),
|
|
161
|
+
|
|
162
|
+
// Multiple distinct fields
|
|
163
|
+
profile: route.post('/profile',
|
|
164
|
+
fileUpload.fields([
|
|
165
|
+
{ name: 'avatar', maxCount: 1 },
|
|
166
|
+
{ name: 'cover', maxCount: 1 },
|
|
167
|
+
]),
|
|
168
|
+
({ req }) => {
|
|
169
|
+
// req.files is a dictionary Record<string, UploadedFile[]>
|
|
170
|
+
return { files: req.files }
|
|
171
|
+
}
|
|
172
|
+
),
|
|
173
|
+
})
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### C. Integrating Express/Connect Middleware (e.g. Multer)
|
|
177
|
+
|
|
178
|
+
If your enterprise project relies on existing Express middleware like **Multer** (for custom storage engines, S3 direct-to-cloud streams, or specialized filters), ExisJS provides the `fromExpress()` bridge:
|
|
179
|
+
|
|
180
|
+
```typescript filename="src/http/legacy/route.ts"
|
|
181
|
+
import { controller, route } from 'exisjs/router'
|
|
182
|
+
import { fromExpress } from 'exisjs/middleware'
|
|
183
|
+
import multer from 'multer'
|
|
184
|
+
|
|
185
|
+
const upload = multer({
|
|
186
|
+
storage: multer.memoryStorage(),
|
|
187
|
+
limits: { fileSize: 10 * 1024 * 1024 },
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
export default controller({
|
|
191
|
+
upload: route.post('/',
|
|
192
|
+
fromExpress(upload.single('file')),
|
|
193
|
+
({ req }) => {
|
|
194
|
+
// Access multer's req.file or req.body seamlessly
|
|
195
|
+
const file = (req.raw as any).file || req.file
|
|
196
|
+
return { success: true, originalname: file?.originalname }
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
})
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### D. Manual `req.formData()` & Streaming Uploads
|
|
203
|
+
|
|
204
|
+
For manual control over incoming multipart streams without route middleware:
|
|
101
205
|
|
|
102
206
|
```typescript filename="src/http/upload/route.ts"
|
|
103
207
|
import { controller, route } from 'exisjs/router'
|
|
104
208
|
|
|
105
209
|
export default controller({
|
|
106
210
|
uploadAvatar: route.post('/avatar', {
|
|
107
|
-
async handle(req
|
|
108
|
-
//
|
|
211
|
+
async handle({ req }) {
|
|
212
|
+
// Parse on-demand
|
|
109
213
|
const { fields, files } = await req.formData()
|
|
110
|
-
|
|
111
|
-
// 2. Access the file (e.g., from an input named "profile_pic")
|
|
112
214
|
const uploadedFile = files['profile_pic']
|
|
113
215
|
|
|
114
216
|
if (uploadedFile) {
|
|
115
|
-
//
|
|
217
|
+
// Save to disk safely with automated collision-free naming
|
|
116
218
|
const savedPath = await uploadedFile.saveToDisk('./uploads/avatars')
|
|
117
219
|
return { success: true, path: savedPath }
|
|
118
220
|
}
|
|
119
221
|
|
|
120
|
-
return { success: false, error:
|
|
222
|
+
return { success: false, error: 'No file uploaded' }
|
|
121
223
|
}
|
|
122
224
|
})
|
|
123
225
|
})
|
|
124
226
|
```
|
|
125
227
|
|
|
126
|
-
|
|
228
|
+
---
|
|
127
229
|
|
|
128
|
-
|
|
129
|
-
import { Controller, Post, UploadedFile } from 'exisjs/decorators'
|
|
230
|
+
## 4. Body Size Limits & HTTP 413
|
|
130
231
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
232
|
+
ExisJS comes pre-configured with a generous **10MB default body limit** (`10485760` bytes) across JSON, form-data, and raw payloads, preventing premature drops during photo or document uploads.
|
|
233
|
+
|
|
234
|
+
If a client sends a payload exceeding the configured threshold, ExisJS halts parsing immediately and responds with `413 Payload Too Large`:
|
|
235
|
+
|
|
236
|
+
```json
|
|
237
|
+
{
|
|
238
|
+
"statusCode": 413,
|
|
239
|
+
"error": "Payload Too Large",
|
|
240
|
+
"message": "Request payload exceeded size limit of 10485760 bytes. Increase server.bodyLimit in exis.config.ts to allow larger payloads."
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
To configure custom limits globally across your app, update `exis.config.ts`:
|
|
245
|
+
|
|
246
|
+
```typescript filename="exis.config.ts"
|
|
247
|
+
export default {
|
|
248
|
+
server: {
|
|
249
|
+
// Set custom maximum payload limit in bytes (e.g. 50MB)
|
|
250
|
+
bodyLimit: 50 * 1024 * 1024,
|
|
142
251
|
}
|
|
143
252
|
}
|
|
144
253
|
```
|
|
145
254
|
|
|
146
|
-
|
|
255
|
+
---
|
|
147
256
|
|
|
148
|
-
|
|
257
|
+
## 5. Dynamic Request Timeouts
|
|
149
258
|
|
|
150
|
-
|
|
259
|
+
ExisJS ships with a **60-second default request timeout** in its `timeout()` middleware, offering ample headroom for slow network clients and third-party cloud operations (Cloudinary, AWS S3, OpenAI, Stripe).
|
|
151
260
|
|
|
152
|
-
|
|
261
|
+
### Dynamic In-Handler Control
|
|
262
|
+
|
|
263
|
+
Handlers can dynamically extend or cancel the active timeout on the fly using `req.setTimeout()` and `req.clearTimeout()`:
|
|
264
|
+
|
|
265
|
+
```typescript filename="src/http/ai/route.ts"
|
|
153
266
|
import { controller, route } from 'exisjs/router'
|
|
154
267
|
|
|
155
268
|
export default controller({
|
|
156
|
-
|
|
157
|
-
async handle(req
|
|
158
|
-
// 1.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
uploadedFiles: files.map(f => f.filename),
|
|
168
|
-
customFields: fields
|
|
269
|
+
generateVideo: route.post('/', {
|
|
270
|
+
async handle({ req }) {
|
|
271
|
+
// 1. Extend timeout for heavy AI pass generation (e.g. 3 minutes)
|
|
272
|
+
req.setTimeout(180000)
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
const result = await longRunningAiTask()
|
|
276
|
+
return { success: true, result }
|
|
277
|
+
} finally {
|
|
278
|
+
// 2. Clear timeout once work completes
|
|
279
|
+
req.clearTimeout()
|
|
169
280
|
}
|
|
170
281
|
}
|
|
171
282
|
})
|
|
172
283
|
})
|
|
173
284
|
```
|
|
174
285
|
|
|
175
|
-
#### Class-Based (OOP) Paradigm
|
|
176
|
-
|
|
177
|
-
```typescript
|
|
178
|
-
import { Controller, Post, Req } from 'exisjs/decorators'
|
|
179
|
-
import { streamUpload } from 'exisjs/storage'
|
|
180
|
-
|
|
181
|
-
@Controller()
|
|
182
|
-
export default class UploadController {
|
|
183
|
-
@Post('/cover')
|
|
184
|
-
async uploadCover(@Req() req: any) {
|
|
185
|
-
// Both req.streamUpload() and the exported helper from exisjs/storage work.
|
|
186
|
-
const { fields, files } = await streamUpload(req, { dest: './uploads/covers' })
|
|
187
|
-
|
|
188
|
-
return {
|
|
189
|
-
success: true,
|
|
190
|
-
uploadedFiles: files.map(f => f.filename),
|
|
191
|
-
customFields: fields
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
```
|
|
196
|
-
|
|
197
286
|
---
|
|
198
287
|
|
|
199
|
-
##
|
|
288
|
+
## 6. HTTP Headers & Content Negotiation
|
|
200
289
|
|
|
201
290
|
```typescript
|
|
202
291
|
// Get a specific header (case-insensitive)
|
|
@@ -132,7 +132,62 @@ export default class UsersController {
|
|
|
132
132
|
|
|
133
133
|
---
|
|
134
134
|
|
|
135
|
-
## 3.
|
|
135
|
+
## 3. Standardized Pagination Envelopes (`paginate`)
|
|
136
|
+
|
|
137
|
+
To eliminate pagination inconsistency across different endpoints, ExisJS exports standard `paginate()` and `getPaginationSkip()` utilities directly from `exisjs/validator` (as well as `exisjs/response`).
|
|
138
|
+
|
|
139
|
+
```typescript filename="src/http/products/route.ts"
|
|
140
|
+
import { controller, route } from 'exisjs/router'
|
|
141
|
+
import { tex, paginate, getPaginationSkip } from 'exisjs/validator'
|
|
142
|
+
|
|
143
|
+
export default controller({
|
|
144
|
+
listProducts: route.get('/', {
|
|
145
|
+
query: tex.pagination({ defaultLimit: 15, maxLimit: 50 }),
|
|
146
|
+
async handle({ query }) {
|
|
147
|
+
// 1. Automatically extracts { skip, limit, page } with fallback defaults
|
|
148
|
+
const { skip, limit, page } = getPaginationSkip(query)
|
|
149
|
+
|
|
150
|
+
// 2. Fetch items and count total from your database
|
|
151
|
+
const [items, total] = await Promise.all([
|
|
152
|
+
db.product.findMany({ skip, take: limit, orderBy: { createdAt: 'desc' } }),
|
|
153
|
+
db.product.count(),
|
|
154
|
+
])
|
|
155
|
+
|
|
156
|
+
// 3. Return standardized pagination envelope
|
|
157
|
+
return paginate(items, total, { page, limit })
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### The Standardized Pagination Envelope
|
|
164
|
+
|
|
165
|
+
Calling `paginate(items, total, { page, limit })` produces a uniform envelope:
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"data": [
|
|
170
|
+
{ "id": 1, "name": "Mechanical Keyboard" },
|
|
171
|
+
{ "id": 2, "name": "Ergonomic Mouse" }
|
|
172
|
+
],
|
|
173
|
+
"pagination": {
|
|
174
|
+
"page": 1,
|
|
175
|
+
"limit": 15,
|
|
176
|
+
"total": 42,
|
|
177
|
+
"totalPages": 3,
|
|
178
|
+
"hasNext": true,
|
|
179
|
+
"hasPrev": false,
|
|
180
|
+
"hasNextPage": true,
|
|
181
|
+
"hasPrevPage": false
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Both `hasNext`/`hasPrev` and `hasNextPage`/`hasPrevPage` are provided out-of-the-box, ensuring full compatibility with both cursor-style and page-number frontend UI components.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## 4. Setting Headers & Status
|
|
136
191
|
|
|
137
192
|
The `ExisResponse` object implements a chainable API for HTTP headers and status codes.
|
|
138
193
|
|
|
@@ -269,3 +324,109 @@ ExisJS features native, automatic ETag generation.
|
|
|
269
324
|
If the client sends an `If-None-Match` header matching the ETag of the current response, ExisJS will immediately abort serialization and send a `304 Not Modified` header with an empty body, saving bandwidth and CPU cycles.
|
|
270
325
|
|
|
271
326
|
*Note: ETag generation is disabled by default, but can be enabled globally in your `exis()` config, or manually per-route via `res.etagEnabled = true`.*
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## 7. Server-Sent Events (SSE) & AI Streaming
|
|
331
|
+
|
|
332
|
+
ExisJS provides a first-class `res.sse()` method for real-time live events and AI chat completions (OpenAI, Anthropic, Ollama, LangChain).
|
|
333
|
+
|
|
334
|
+
When called, `res.sse()` automatically configures and flushes the required HTTP headers:
|
|
335
|
+
- `Content-Type: text/event-stream; charset=utf-8`
|
|
336
|
+
- `Cache-Control: no-cache, no-transform`
|
|
337
|
+
- `Connection: keep-alive`
|
|
338
|
+
- `X-Accel-Buffering: no` (disables NGINX proxy buffering)
|
|
339
|
+
|
|
340
|
+
You can import `SSEStream`, `formatSSEEvent`, `SSEMessage`, and `SSEOptions` from `exisjs/router`.
|
|
341
|
+
|
|
342
|
+
### Basic SSE Stream
|
|
343
|
+
|
|
344
|
+
#### Functional Paradigm
|
|
345
|
+
|
|
346
|
+
```typescript filename="src/http/events/route.ts"
|
|
347
|
+
import { controller, route } from 'exisjs/router'
|
|
348
|
+
|
|
349
|
+
export default controller({
|
|
350
|
+
liveEvents: route.get('/', {
|
|
351
|
+
handle(req, res) {
|
|
352
|
+
// Initialize the stream (heartbeat ping every 15s by default)
|
|
353
|
+
const sse = res.sse()
|
|
354
|
+
|
|
355
|
+
sse.send({ event: 'connected', data: { timestamp: Date.now() } })
|
|
356
|
+
|
|
357
|
+
const interval = setInterval(() => {
|
|
358
|
+
sse.send({ event: 'price-update', data: { symbol: 'BTC', price: 95000 } })
|
|
359
|
+
}, 1000)
|
|
360
|
+
|
|
361
|
+
// Automatically clean up intervals when the client disconnects or closes the tab
|
|
362
|
+
sse.onClose(() => {
|
|
363
|
+
clearInterval(interval)
|
|
364
|
+
})
|
|
365
|
+
}
|
|
366
|
+
})
|
|
367
|
+
})
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
#### Class-Based (OOP) Paradigm
|
|
371
|
+
|
|
372
|
+
```typescript filename="src/http/events/route.ts"
|
|
373
|
+
import { Controller, Get, Res } from 'exisjs/decorators'
|
|
374
|
+
import type { Response } from 'exisjs/router'
|
|
375
|
+
|
|
376
|
+
@Controller()
|
|
377
|
+
export default class EventsController {
|
|
378
|
+
@Get('/events')
|
|
379
|
+
streamEvents(@Res() res: Response) {
|
|
380
|
+
const sse = res.sse()
|
|
381
|
+
|
|
382
|
+
sse.send({ event: 'connected', data: { status: 'ok' } })
|
|
383
|
+
|
|
384
|
+
sse.onClose(() => {
|
|
385
|
+
console.log('Client disconnected from SSE stream')
|
|
386
|
+
})
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### AI / LLM Streaming with `sse.pipeFrom()`
|
|
392
|
+
|
|
393
|
+
You can directly stream chunks from any `AsyncIterable` (such as the OpenAI SDK or custom generators) using `sse.pipeFrom()`:
|
|
394
|
+
|
|
395
|
+
```typescript filename="src/http/ai/route.ts"
|
|
396
|
+
import { controller, route } from 'exisjs/router'
|
|
397
|
+
|
|
398
|
+
export default controller({
|
|
399
|
+
chat: route.post('/', {
|
|
400
|
+
handle(req, res) {
|
|
401
|
+
res.sse(async (sse) => {
|
|
402
|
+
// Stream directly from an async generator or AI SDK
|
|
403
|
+
async function* generateTokens() {
|
|
404
|
+
yield 'Hello'
|
|
405
|
+
yield ' from'
|
|
406
|
+
yield ' ExisJS'
|
|
407
|
+
yield ' AI streaming!'
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
await sse.pipeFrom(generateTokens(), (token) => ({
|
|
411
|
+
event: 'delta',
|
|
412
|
+
data: { text: token }
|
|
413
|
+
}))
|
|
414
|
+
|
|
415
|
+
sse.close()
|
|
416
|
+
})
|
|
417
|
+
}
|
|
418
|
+
})
|
|
419
|
+
})
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### SSE Options & Methods
|
|
423
|
+
|
|
424
|
+
- `res.sse(options?)`: Options include:
|
|
425
|
+
- `heartbeat`: Heartbeat interval in milliseconds (default: `15000`). Pass `false` to disable.
|
|
426
|
+
- `heartbeatComment`: Custom comment string for keep-alive pings (default: `": ping\n\n"`).
|
|
427
|
+
- `headers`: Additional custom HTTP headers.
|
|
428
|
+
- `sse.send(message)`: Sends an event. Accepts string, number, object (auto JSON-stringified), or `{ event, data, id, retry }`.
|
|
429
|
+
- `sse.comment(text)`: Sends a raw SSE comment line (e.g. `: ping\n\n`) without triggering client-side event listeners.
|
|
430
|
+
- `sse.onClose(callback)`: Registers a callback fired when the client disconnects.
|
|
431
|
+
- `sse.close()`: Gracefully terminates the stream and closes the HTTP response.
|
|
432
|
+
|
|
@@ -15,11 +15,11 @@ ExisJS natively supports both a **Functional Paradigm** and a **Class-Based OOP
|
|
|
15
15
|
|
|
16
16
|
ExisJS uses **folder-based file-system routing** instead of programmatic route declaration in a single entry file.
|
|
17
17
|
|
|
18
|
-
For a folder to become an accessible API endpoint, it must contain a `route.ts` file that exports a route definition.
|
|
18
|
+
For a folder to become an accessible API endpoint, it must contain a `route.ts` or `<feature>.route.ts` (e.g. `user.route.ts`) file that exports a route definition.
|
|
19
19
|
|
|
20
20
|
In addition to `route.ts`, the ExisJS router utilizes two other special gatekeeper files:
|
|
21
21
|
- **`server.ts`**: Placed at the root of your `http` directory, it is the entry point that boots your application, registers global plugins, and connects to databases.
|
|
22
|
-
- **`
|
|
22
|
+
- **`boundary.ts`** (or `*.boundary.ts`): Can be placed in any folder. It acts as a gatekeeper and request pipeline, automatically configuring middleware, CORS, headers, and dependencies for *all* routes within its directory and subdirectories.
|
|
23
23
|
|
|
24
24
|
### Routing Conventions
|
|
25
25
|
|
|
@@ -38,9 +38,9 @@ The `server.ts` file acts as the root configuration for your application. It pro
|
|
|
38
38
|
import { exis } from 'exisjs'
|
|
39
39
|
|
|
40
40
|
export default exis({
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
providers: [
|
|
42
|
+
['LoggerService', { useValue: console }]
|
|
43
|
+
]
|
|
44
44
|
})
|
|
45
45
|
```
|
|
46
46
|
|
|
@@ -59,27 +59,27 @@ export default class RootServer {
|
|
|
59
59
|
}
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
### Folder
|
|
62
|
+
### Folder Boundaries (`boundary.ts`)
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
Boundaries apply configurations, security policies, and request pipelines to all routes in their directory and subdirectories.
|
|
65
65
|
|
|
66
66
|
#### Functional Paradigm
|
|
67
|
-
```typescript filename="src/http/admin/
|
|
68
|
-
import {
|
|
67
|
+
```typescript filename="src/http/admin/boundary.ts"
|
|
68
|
+
import { defineBoundary } from 'exisjs/router'
|
|
69
69
|
|
|
70
|
-
export
|
|
70
|
+
export const config = defineBoundary({
|
|
71
71
|
headers: { 'X-Admin-Area': 'true' }
|
|
72
72
|
})
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
#### Class-Based (OOP) Paradigm
|
|
76
|
-
```typescript filename="src/http/admin/
|
|
77
|
-
import {
|
|
76
|
+
```typescript filename="src/http/admin/boundary.ts"
|
|
77
|
+
import { Boundary } from 'exisjs/decorators'
|
|
78
78
|
|
|
79
|
-
@
|
|
79
|
+
@Boundary({
|
|
80
80
|
headers: { 'X-Admin-Area': 'true' }
|
|
81
81
|
})
|
|
82
|
-
export default class
|
|
82
|
+
export default class AdminBoundary {}
|
|
83
83
|
```
|
|
84
84
|
|
|
85
85
|
### Route Definitions (`route.ts`)
|
|
@@ -264,13 +264,18 @@ export default controller({
|
|
|
264
264
|
}),
|
|
265
265
|
getSingle: route.get('/:id', {
|
|
266
266
|
handle({ params }) {
|
|
267
|
+
// params.id is automatically inferred as string with full autocomplete!
|
|
267
268
|
const { id } = params
|
|
268
|
-
return { message:
|
|
269
|
+
return { message: `Fetching user ${id}` }
|
|
269
270
|
}
|
|
270
271
|
})
|
|
271
272
|
})
|
|
272
273
|
```
|
|
273
274
|
|
|
275
|
+
<Info title="Automatic Route Parameter Type Inference">
|
|
276
|
+
In ExisJS v0.7+, paths with named parameters like `route.get('/users/:userId/posts/:postId', ...)` automatically parse the path string literal to infer `params: { userId: string; postId: string }` on the `handle` context. No manual casting or boilerplate interfaces required.
|
|
277
|
+
</Info>
|
|
278
|
+
|
|
274
279
|
|
|
275
280
|
#### Class-Based (OOP) Paradigm
|
|
276
281
|
|
|
@@ -447,6 +452,123 @@ export default class SearchController {
|
|
|
447
452
|
|
|
448
453
|
|
|
449
454
|
|
|
455
|
+
## Declarative Route Options (Uploads, Timeouts & Validation)
|
|
456
|
+
|
|
457
|
+
ExisJS allows configuring input validation schemas, multipart file uploads, and per-route timeouts directly inside the route definition without manual middleware plumbing.
|
|
458
|
+
|
|
459
|
+
### Declarative File Uploads
|
|
460
|
+
|
|
461
|
+
Specify `upload` in your route definition to automatically handle `multipart/form-data`. Uploaded files are parsed and attached to `req.file` or `req.files`, while non-file form fields are populated in `req.body`.
|
|
462
|
+
|
|
463
|
+
#### Functional Paradigm
|
|
464
|
+
```typescript filename="src/http/avatar/route.ts"
|
|
465
|
+
import { controller, route } from 'exisjs/router'
|
|
466
|
+
import { tex } from 'exisjs/validator'
|
|
467
|
+
|
|
468
|
+
export default controller({
|
|
469
|
+
uploadAvatar: route.post('/', {
|
|
470
|
+
upload: {
|
|
471
|
+
field: 'avatar',
|
|
472
|
+
maxBytes: 5 * 1024 * 1024, // 5MB limit
|
|
473
|
+
mimeTypes: ['image/png', 'image/jpeg', 'image/webp'],
|
|
474
|
+
},
|
|
475
|
+
body: tex.object({
|
|
476
|
+
caption: tex.string({ optional: true }),
|
|
477
|
+
}),
|
|
478
|
+
handle({ req }) {
|
|
479
|
+
const file = req.file! // UploadedFile { filename, buffer, size, mimetype }
|
|
480
|
+
const caption = req.body?.caption
|
|
481
|
+
|
|
482
|
+
return {
|
|
483
|
+
success: true,
|
|
484
|
+
filename: file.filename,
|
|
485
|
+
size: file.size,
|
|
486
|
+
caption,
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
})
|
|
490
|
+
})
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
#### Class-Based (OOP) Paradigm
|
|
494
|
+
```typescript filename="src/http/avatar/route.ts"
|
|
495
|
+
import { Controller, Post, Body } from 'exisjs/decorators'
|
|
496
|
+
import { Upload, UploadedFile } from 'exisjs/decorators'
|
|
497
|
+
import type { UploadedFile as UploadedFileType } from 'exisjs/middleware'
|
|
498
|
+
|
|
499
|
+
@Controller('/avatar')
|
|
500
|
+
export default class AvatarController {
|
|
501
|
+
@Post('/')
|
|
502
|
+
@Upload('avatar', {
|
|
503
|
+
maxBytes: 5 * 1024 * 1024,
|
|
504
|
+
mimeTypes: ['image/png', 'image/jpeg', 'image/webp'],
|
|
505
|
+
})
|
|
506
|
+
upload(
|
|
507
|
+
@UploadedFile() file: UploadedFileType,
|
|
508
|
+
@Body('caption') caption?: string
|
|
509
|
+
) {
|
|
510
|
+
return {
|
|
511
|
+
success: true,
|
|
512
|
+
filename: file.filename,
|
|
513
|
+
size: file.size,
|
|
514
|
+
caption,
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
### Route-Level Timeout Overrides
|
|
521
|
+
|
|
522
|
+
While ExisJS provides a generous 60-second default global timeout, long-running batch jobs, PDF generation, or third-party AI integrations can override the timeout at the individual route level:
|
|
523
|
+
|
|
524
|
+
#### Functional Paradigm
|
|
525
|
+
```typescript filename="src/http/reports/route.ts"
|
|
526
|
+
import { controller, route } from 'exisjs/router'
|
|
527
|
+
|
|
528
|
+
export default controller({
|
|
529
|
+
generateAnnualReport: route.post('/annual', {
|
|
530
|
+
timeout: 120000, // 2 minutes specifically for this route
|
|
531
|
+
async handle() {
|
|
532
|
+
// Intensive document rendering or AI workflow
|
|
533
|
+
const reportUrl = await buildReport()
|
|
534
|
+
return { success: true, reportUrl }
|
|
535
|
+
}
|
|
536
|
+
})
|
|
537
|
+
})
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### Query Pagination Schemas
|
|
541
|
+
|
|
542
|
+
ExisJS provides the `tex.pagination()` primitive for declarative pagination, sorting, and ordering query parameters. String query parameters are automatically coerced into numbers:
|
|
543
|
+
|
|
544
|
+
```typescript filename="src/http/products/route.ts"
|
|
545
|
+
import { controller, route } from 'exisjs/router'
|
|
546
|
+
import { tex, paginate, getPaginationSkip } from 'exisjs/validator'
|
|
547
|
+
|
|
548
|
+
const ProductQuerySchema = tex.pagination({
|
|
549
|
+
defaultLimit: 20,
|
|
550
|
+
maxLimit: 100,
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
export default controller({
|
|
554
|
+
list: route.get('/', {
|
|
555
|
+
query: ProductQuerySchema,
|
|
556
|
+
async handle({ query }) {
|
|
557
|
+
// query.page and query.limit are guaranteed positive numbers
|
|
558
|
+
const { skip, limit, page } = getPaginationSkip(query)
|
|
559
|
+
|
|
560
|
+
// Fetch data using your preferred database query
|
|
561
|
+
const [items, total] = await Promise.all([
|
|
562
|
+
db.products.findMany({ skip, take: limit }),
|
|
563
|
+
db.products.count()
|
|
564
|
+
])
|
|
565
|
+
|
|
566
|
+
return paginate(items, total, { page, limit })
|
|
567
|
+
}
|
|
568
|
+
})
|
|
569
|
+
})
|
|
570
|
+
```
|
|
571
|
+
|
|
450
572
|
## Dependency Injection in Functional Controllers
|
|
451
573
|
|
|
452
574
|
ExisJS natively supports **Request-Scoped Dependency Injection** directly inside functional routes. You don't need to use classes to get the benefits of DI!
|
|
@@ -16,18 +16,23 @@ The following security middlewares are available from `exisjs/middleware` and ca
|
|
|
16
16
|
- `helmet(options?)`: Sets 14+ secure HTTP headers (Strict-Transport-Security, X-Content-Type-Options: nosniff, X-Frame-Options: DENY, and CSP).
|
|
17
17
|
- `csrf(options)`: Enforces the Signed Double Submit Cookie pattern to protect state-changing requests (POST, PUT, DELETE) from CSRF attacks.
|
|
18
18
|
- `hpp()`: Protects against HTTP Parameter Pollution attacks by ensuring array query parameters/form fields are safely handled.
|
|
19
|
+
- `mongoSanitize()`: Strips MongoDB operator keys (keys starting with `$` or containing `.`) from `req.body`, `req.query`, and `req.params`.
|
|
19
20
|
- `dbSanitize(options?)`: Protects against NoSQL query operator injection (`$where` and other malicious keys) before reaching database layers.
|
|
20
21
|
- `timeout(ms)`: Automatically terminates requests that exceed the specified timeout limit, returning a 503 response.
|
|
21
22
|
|
|
23
|
+
> [!TIP]
|
|
24
|
+
> Security middlewares like `hpp()`, `mongoSanitize()`, and `dbSanitize()` automatically synchronize with the request body parser. If registered globally before route handlers, they safely await the body and sanitize it deterministically without runtime crashes.
|
|
25
|
+
|
|
22
26
|
```typescript filename="src/http/server.ts"
|
|
23
27
|
import { exis } from 'exisjs'
|
|
24
|
-
import { helmet, csrf, timeout, hpp } from 'exisjs/middleware'
|
|
28
|
+
import { helmet, csrf, timeout, hpp, mongoSanitize } from 'exisjs/middleware'
|
|
25
29
|
|
|
26
30
|
export default exis({
|
|
27
31
|
async onStart(app) {
|
|
28
32
|
app.use(helmet())
|
|
29
33
|
app.use(timeout(10000))
|
|
30
34
|
app.use(hpp())
|
|
35
|
+
app.use(mongoSanitize())
|
|
31
36
|
}
|
|
32
37
|
})
|
|
33
38
|
```
|