exisjs 0.7.9 → 0.7.10
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/config/config.js +1 -1
- package/dist/config/config.js.map +1 -1
- package/dist/decorators/constants.d.ts +2 -0
- package/dist/decorators/constants.d.ts.map +1 -1
- package/dist/decorators/constants.js +3 -1
- package/dist/decorators/constants.js.map +1 -1
- package/dist/decorators/core.d.ts +14 -0
- package/dist/decorators/core.d.ts.map +1 -1
- package/dist/decorators/core.js +156 -41
- package/dist/decorators/core.js.map +1 -1
- package/dist/decorators/filters.d.ts +2 -10
- package/dist/decorators/filters.d.ts.map +1 -1
- package/dist/decorators/filters.js.map +1 -1
- package/dist/decorators/index.d.ts +1 -0
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/decorators/index.js +1 -0
- package/dist/decorators/index.js.map +1 -1
- package/dist/decorators/lifecycle.d.ts +29 -0
- package/dist/decorators/lifecycle.d.ts.map +1 -1
- package/dist/decorators/params.d.ts +3 -7
- package/dist/decorators/params.d.ts.map +1 -1
- package/dist/decorators/params.js.map +1 -1
- package/dist/decorators/pipes.d.ts +91 -0
- package/dist/decorators/pipes.d.ts.map +1 -0
- package/dist/decorators/pipes.js +283 -0
- package/dist/decorators/pipes.js.map +1 -0
- package/dist/di/container.d.ts +6 -3
- package/dist/di/container.d.ts.map +1 -1
- package/dist/di/container.js +186 -68
- package/dist/di/container.js.map +1 -1
- package/dist/di/decorators.d.ts +23 -5
- package/dist/di/decorators.d.ts.map +1 -1
- package/dist/di/decorators.js +83 -20
- package/dist/di/decorators.js.map +1 -1
- package/dist/di/forward-ref.d.ts +23 -0
- package/dist/di/forward-ref.d.ts.map +1 -0
- package/dist/di/forward-ref.js +30 -0
- package/dist/di/forward-ref.js.map +1 -0
- package/dist/di/index.d.ts +2 -1
- package/dist/di/index.d.ts.map +1 -1
- package/dist/di/index.js +8 -1
- package/dist/di/index.js.map +1 -1
- package/dist/docs/content/controllers.mdx +230 -0
- package/dist/docs/content/errors.mdx +16 -9
- package/dist/docs/content/modules.mdx +23 -4
- package/dist/docs/content/providers.mdx +42 -6
- package/dist/docs/content/requests.mdx +9 -3
- package/dist/docs/content/responses.mdx +19 -5
- package/dist/docs/reference/config.mdx +3 -2
- package/dist/docs/reference/decorators.mdx +15 -4
- package/dist/docs/reference/di.mdx +23 -16
- package/dist/docs/reference/router.mdx +75 -0
- package/dist/docs/reference/validator.mdx +2 -2
- package/dist/error/errors.d.ts.map +1 -1
- package/dist/error/errors.js +12 -0
- package/dist/error/errors.js.map +1 -1
- package/dist/router/controller-registrar.d.ts.map +1 -1
- package/dist/router/controller-registrar.js +142 -87
- package/dist/router/controller-registrar.js.map +1 -1
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js.map +1 -1
- package/dist/router/route-builder.d.ts +160 -29
- package/dist/router/route-builder.d.ts.map +1 -1
- package/dist/router/route-builder.js +105 -31
- package/dist/router/route-builder.js.map +1 -1
- package/dist/router/route-scanner.d.ts +3 -2
- package/dist/router/route-scanner.d.ts.map +1 -1
- package/dist/router/route-scanner.js.map +1 -1
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/router.js +44 -0
- package/dist/router/router.js.map +1 -1
- package/dist/router/scanner/functional-compiler.d.ts +1 -1
- package/dist/router/scanner/functional-compiler.d.ts.map +1 -1
- package/dist/router/scanner/functional-compiler.js +403 -64
- package/dist/router/scanner/functional-compiler.js.map +1 -1
- package/dist/router/types.d.ts +4 -2
- package/dist/router/types.d.ts.map +1 -1
- package/dist/server/app.d.ts +2 -0
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/app.js +38 -0
- package/dist/server/app.js.map +1 -1
- package/dist/server/helpers/etag.d.ts +1 -0
- package/dist/server/helpers/etag.d.ts.map +1 -1
- package/dist/server/helpers/etag.js +121 -10
- package/dist/server/helpers/etag.js.map +1 -1
- package/dist/server/helpers/ip.d.ts.map +1 -1
- package/dist/server/helpers/ip.js +75 -17
- package/dist/server/helpers/ip.js.map +1 -1
- package/dist/server/response.d.ts.map +1 -1
- package/dist/server/response.js +24 -9
- package/dist/server/response.js.map +1 -1
- package/dist/swagger/generator.d.ts.map +1 -1
- package/dist/swagger/generator.js +66 -22
- package/dist/swagger/generator.js.map +1 -1
- package/dist/testing/client.d.ts +15 -11
- package/dist/testing/client.d.ts.map +1 -1
- package/dist/testing/client.js +32 -11
- package/dist/testing/client.js.map +1 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +2 -0
- package/dist/utils/logger.js.map +1 -1
- package/dist/validator/engine.d.ts.map +1 -1
- package/dist/validator/engine.js +45 -0
- package/dist/validator/engine.js.map +1 -1
- package/package.json +1 -1
package/dist/di/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { inject } from './inject';
|
|
2
|
-
export { Container } from './container';
|
|
2
|
+
export { Container, INJECT_METADATA, PROPERTY_INJECT_METADATA, OPTIONAL_METADATA, SCOPE_METADATA, } from './container';
|
|
3
3
|
export { Inject, Optional } from './decorators';
|
|
4
4
|
export type { ProviderToken, ProviderDefinition, ValueProvider, FactoryProvider, ClassProvider, } from './container';
|
|
5
|
+
export { forwardRef, isForwardRef, type ForwardReference } from './forward-ref';
|
|
5
6
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/di/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/di/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACjC,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/di/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACjC,OAAO,EACL,SAAS,EACT,eAAe,EACf,wBAAwB,EACxB,iBAAiB,EACjB,cAAc,GACf,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAC/C,YAAY,EACV,aAAa,EACb,kBAAkB,EAClB,aAAa,EACb,eAAe,EACf,aAAa,GACd,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAA"}
|
package/dist/di/index.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Optional = exports.Inject = exports.Container = exports.inject = void 0;
|
|
3
|
+
exports.isForwardRef = exports.forwardRef = exports.Optional = exports.Inject = exports.SCOPE_METADATA = exports.OPTIONAL_METADATA = exports.PROPERTY_INJECT_METADATA = exports.INJECT_METADATA = exports.Container = exports.inject = void 0;
|
|
4
4
|
var inject_1 = require("./inject");
|
|
5
5
|
Object.defineProperty(exports, "inject", { enumerable: true, get: function () { return inject_1.inject; } });
|
|
6
6
|
var container_1 = require("./container");
|
|
7
7
|
Object.defineProperty(exports, "Container", { enumerable: true, get: function () { return container_1.Container; } });
|
|
8
|
+
Object.defineProperty(exports, "INJECT_METADATA", { enumerable: true, get: function () { return container_1.INJECT_METADATA; } });
|
|
9
|
+
Object.defineProperty(exports, "PROPERTY_INJECT_METADATA", { enumerable: true, get: function () { return container_1.PROPERTY_INJECT_METADATA; } });
|
|
10
|
+
Object.defineProperty(exports, "OPTIONAL_METADATA", { enumerable: true, get: function () { return container_1.OPTIONAL_METADATA; } });
|
|
11
|
+
Object.defineProperty(exports, "SCOPE_METADATA", { enumerable: true, get: function () { return container_1.SCOPE_METADATA; } });
|
|
8
12
|
var decorators_1 = require("./decorators");
|
|
9
13
|
Object.defineProperty(exports, "Inject", { enumerable: true, get: function () { return decorators_1.Inject; } });
|
|
10
14
|
Object.defineProperty(exports, "Optional", { enumerable: true, get: function () { return decorators_1.Optional; } });
|
|
15
|
+
var forward_ref_1 = require("./forward-ref");
|
|
16
|
+
Object.defineProperty(exports, "forwardRef", { enumerable: true, get: function () { return forward_ref_1.forwardRef; } });
|
|
17
|
+
Object.defineProperty(exports, "isForwardRef", { enumerable: true, get: function () { return forward_ref_1.isForwardRef; } });
|
|
11
18
|
//# sourceMappingURL=index.js.map
|
package/dist/di/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/di/index.ts"],"names":[],"mappings":";;;AAAA,mCAAiC;AAAxB,gGAAA,MAAM,OAAA;AACf,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/di/index.ts"],"names":[],"mappings":";;;AAAA,mCAAiC;AAAxB,gGAAA,MAAM,OAAA;AACf,yCAMoB;AALlB,sGAAA,SAAS,OAAA;AACT,4GAAA,eAAe,OAAA;AACf,qHAAA,wBAAwB,OAAA;AACxB,8GAAA,iBAAiB,OAAA;AACjB,2GAAA,cAAc,OAAA;AAEhB,2CAA+C;AAAtC,oGAAA,MAAM,OAAA;AAAE,sGAAA,QAAQ,OAAA;AAQzB,6CAA+E;AAAtE,yGAAA,UAAU,OAAA;AAAE,2GAAA,YAAY,OAAA"}
|
|
@@ -353,6 +353,236 @@ export default class UsersController {
|
|
|
353
353
|
}
|
|
354
354
|
```
|
|
355
355
|
|
|
356
|
+
## Controller Inheritance (Base & Abstract Controllers)
|
|
357
|
+
|
|
358
|
+
ExisJS supports full OOP inheritance for controllers. You can create generic base or abstract CRUD controllers and inherit all route handlers, parameter decorators, middlewares, guards, interceptors, and filters down to subclasses:
|
|
359
|
+
|
|
360
|
+
```typescript filename="src/http/base-crud.controller.ts"
|
|
361
|
+
import { Get, Post, Delete, Param, Body } from 'exisjs/decorators'
|
|
362
|
+
|
|
363
|
+
export abstract class BaseCrudController<T> {
|
|
364
|
+
protected abstract resourceName: string
|
|
365
|
+
|
|
366
|
+
@Get('/')
|
|
367
|
+
async findAll(): Promise<T[]> {
|
|
368
|
+
return []
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
@Get('/:id')
|
|
372
|
+
async findById(@Param('id') id: string): Promise<T> {
|
|
373
|
+
return { id, resource: this.resourceName } as any
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
@Delete('/:id')
|
|
377
|
+
async remove(@Param('id') id: string): Promise<{ deleted: boolean }> {
|
|
378
|
+
return { deleted: true }
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Subclasses inherit all parent endpoints and can add new routes or selectively override base methods:
|
|
384
|
+
|
|
385
|
+
```typescript filename="src/http/users/route.ts"
|
|
386
|
+
import { Controller, Post, Body, Param } from 'exisjs/decorators'
|
|
387
|
+
import { BaseCrudController } from '../base-crud.controller'
|
|
388
|
+
|
|
389
|
+
export interface User {
|
|
390
|
+
id: string
|
|
391
|
+
name: string
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
@Controller('/users')
|
|
395
|
+
export default class UsersController extends BaseCrudController<User> {
|
|
396
|
+
protected resourceName = 'users'
|
|
397
|
+
|
|
398
|
+
// Subclass defines additional route
|
|
399
|
+
@Post('/')
|
|
400
|
+
async create(@Body() user: User) {
|
|
401
|
+
return { created: true, user }
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Subclass can override inherited remove handler
|
|
405
|
+
override async remove(@Param('id') id: string) {
|
|
406
|
+
return { deleted: true, id, customAudit: true }
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### Functional Controller Inheritance (`controller.extend`)
|
|
412
|
+
|
|
413
|
+
In the functional paradigm, inheritance is achieved via `controller.extend(base, overrides)`. This inherits all route handlers, lifecycle hooks, and middlewares from a base controller factory and allows overriding endpoints or adding new routes with full type inference:
|
|
414
|
+
|
|
415
|
+
```typescript filename="src/http/products/route.ts"
|
|
416
|
+
import { controller, route } from 'exisjs/router'
|
|
417
|
+
|
|
418
|
+
// 1. Generic functional base CRUD controller
|
|
419
|
+
function createBaseCrud(resourceName: string) {
|
|
420
|
+
return controller({
|
|
421
|
+
list: route.get('/', {
|
|
422
|
+
async handle() {
|
|
423
|
+
return { resource: resourceName, items: [] }
|
|
424
|
+
}
|
|
425
|
+
}),
|
|
426
|
+
getById: route.get('/:id', {
|
|
427
|
+
async handle({ params }) {
|
|
428
|
+
return { resource: resourceName, id: params.id }
|
|
429
|
+
}
|
|
430
|
+
}),
|
|
431
|
+
remove: route.delete('/:id', {
|
|
432
|
+
async handle({ params }) {
|
|
433
|
+
return { deleted: true, id: params.id }
|
|
434
|
+
}
|
|
435
|
+
})
|
|
436
|
+
})
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// 2. Extend base controller with overrides and additional routes
|
|
440
|
+
export default controller.extend(createBaseCrud('products'), {
|
|
441
|
+
// Additional specialized route
|
|
442
|
+
featured: route.get('/featured', {
|
|
443
|
+
async handle() {
|
|
444
|
+
return { featured: ['Product 1', 'Product 2'] }
|
|
445
|
+
}
|
|
446
|
+
}),
|
|
447
|
+
|
|
448
|
+
// Override base endpoint
|
|
449
|
+
getById: route.get('/:id', {
|
|
450
|
+
async handle({ params }) {
|
|
451
|
+
return { customProduct: true, id: params.id }
|
|
452
|
+
}
|
|
453
|
+
})
|
|
454
|
+
})
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
## Functional Controller Composition (`controller.merge`)
|
|
458
|
+
|
|
459
|
+
When a single `route.ts` manages a feature area with multiple concerns (for example, auth flows containing credentials, sessions, and OAuth), `controller.merge(...controllers)` allows splitting logic into modular controller objects and merging them into a single default export:
|
|
460
|
+
|
|
461
|
+
```typescript filename="src/http/auth/route.ts"
|
|
462
|
+
import { controller, route } from 'exisjs/router'
|
|
463
|
+
|
|
464
|
+
// 1. Modular controller for password authentication
|
|
465
|
+
const passwordAuth = controller({
|
|
466
|
+
login: route.post('/login', {
|
|
467
|
+
async handle({ body }) {
|
|
468
|
+
return { token: 'jwt-auth-token' }
|
|
469
|
+
}
|
|
470
|
+
}),
|
|
471
|
+
logout: route.post('/logout', {
|
|
472
|
+
async handle() {
|
|
473
|
+
return { success: true }
|
|
474
|
+
}
|
|
475
|
+
})
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
// 2. Modular controller for OAuth providers
|
|
479
|
+
const oauthProviders = controller({
|
|
480
|
+
google: route.get('/google', {
|
|
481
|
+
async handle() {
|
|
482
|
+
return { redirectUrl: 'https://accounts.google.com/o/oauth2/auth' }
|
|
483
|
+
}
|
|
484
|
+
}),
|
|
485
|
+
github: route.get('/github', {
|
|
486
|
+
async handle() {
|
|
487
|
+
return { redirectUrl: 'https://github.com/login/oauth/authorize' }
|
|
488
|
+
}
|
|
489
|
+
})
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
// 3. Merge both into the single exported controller for this folder
|
|
493
|
+
export default controller.merge(passwordAuth, oauthProviders)
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
## Built-in Transformation Pipes & `@UsePipes`
|
|
497
|
+
|
|
498
|
+
Pipes transform and validate incoming request data before it reaches the route handler. ExisJS provides built-in pipes for common primitives:
|
|
499
|
+
|
|
500
|
+
<div className="overflow-x-auto mb-6">
|
|
501
|
+
<table className="w-full text-sm text-left border border-border/50 rounded-lg overflow-hidden">
|
|
502
|
+
<thead className="bg-muted/50 text-foreground">
|
|
503
|
+
<tr>
|
|
504
|
+
<th className="px-4 py-3 border-b border-border/50 font-semibold w-1/3">Pipe</th>
|
|
505
|
+
<th className="px-4 py-3 border-b border-border/50 font-semibold">Description</th>
|
|
506
|
+
</tr>
|
|
507
|
+
</thead>
|
|
508
|
+
<tbody className="divide-y divide-border/50">
|
|
509
|
+
<tr className="bg-background hover:bg-muted/30 transition-colors">
|
|
510
|
+
<td className="px-4 py-3 font-mono text-xs">ParseIntPipe</td>
|
|
511
|
+
<td className="px-4 py-3 text-muted-foreground">Parses string/number parameter into integer or throws 400 Bad Request</td>
|
|
512
|
+
</tr>
|
|
513
|
+
<tr className="bg-background hover:bg-muted/30 transition-colors">
|
|
514
|
+
<td className="px-4 py-3 font-mono text-xs">ParseFloatPipe</td>
|
|
515
|
+
<td className="px-4 py-3 text-muted-foreground">Parses parameter into floating-point number</td>
|
|
516
|
+
</tr>
|
|
517
|
+
<tr className="bg-background hover:bg-muted/30 transition-colors">
|
|
518
|
+
<td className="px-4 py-3 font-mono text-xs">ParseBoolPipe</td>
|
|
519
|
+
<td className="px-4 py-3 text-muted-foreground">Parses boolean strings (<code>'true'</code>, <code>'false'</code>, <code>1</code>, <code>0</code>) into native booleans</td>
|
|
520
|
+
</tr>
|
|
521
|
+
<tr className="bg-background hover:bg-muted/30 transition-colors">
|
|
522
|
+
<td className="px-4 py-3 font-mono text-xs">ParseUUIDPipe</td>
|
|
523
|
+
<td className="px-4 py-3 text-muted-foreground">Validates UUID format (v3, v4, v5, or all)</td>
|
|
524
|
+
</tr>
|
|
525
|
+
<tr className="bg-background hover:bg-muted/30 transition-colors">
|
|
526
|
+
<td className="px-4 py-3 font-mono text-xs">ParseArrayPipe</td>
|
|
527
|
+
<td className="px-4 py-3 text-muted-foreground">Splits comma-separated strings into arrays and applies item pipes</td>
|
|
528
|
+
</tr>
|
|
529
|
+
<tr className="bg-background hover:bg-muted/30 transition-colors">
|
|
530
|
+
<td className="px-4 py-3 font-mono text-xs">ValidationPipe</td>
|
|
531
|
+
<td className="px-4 py-3 text-muted-foreground">Validates and transforms DTO classes or schema metatypes</td>
|
|
532
|
+
</tr>
|
|
533
|
+
</tbody>
|
|
534
|
+
</table>
|
|
535
|
+
</div>
|
|
536
|
+
|
|
537
|
+
```typescript filename="src/http/items/route.ts"
|
|
538
|
+
import {
|
|
539
|
+
Controller,
|
|
540
|
+
Get,
|
|
541
|
+
Post,
|
|
542
|
+
Param,
|
|
543
|
+
Query,
|
|
544
|
+
Body,
|
|
545
|
+
UsePipes,
|
|
546
|
+
ParseIntPipe,
|
|
547
|
+
ParseBoolPipe,
|
|
548
|
+
ParseUUIDPipe,
|
|
549
|
+
ParseArrayPipe,
|
|
550
|
+
ValidationPipe,
|
|
551
|
+
} from 'exisjs/decorators'
|
|
552
|
+
|
|
553
|
+
@Controller('/items')
|
|
554
|
+
export default class ItemsController {
|
|
555
|
+
// 1. Parameter-level pipe
|
|
556
|
+
@Get('/:id')
|
|
557
|
+
async getItem(@Param('id', ParseIntPipe) id: number) {
|
|
558
|
+
return { id, isNumber: typeof id === 'number' }
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// 2. Query boolean & array pipes
|
|
562
|
+
@Get('/')
|
|
563
|
+
async listItems(
|
|
564
|
+
@Query('active', ParseBoolPipe) active: boolean,
|
|
565
|
+
@Query('ids', new ParseArrayPipe({ items: ParseIntPipe })) ids: number[]
|
|
566
|
+
) {
|
|
567
|
+
return { active, ids }
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// 3. UUID validation pipe
|
|
571
|
+
@Get('/by-uuid/:uuid')
|
|
572
|
+
async getByUUID(@Param('uuid', ParseUUIDPipe) uuid: string) {
|
|
573
|
+
return { uuid }
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// 4. Method-level or Class-level @UsePipes
|
|
577
|
+
@Post('/')
|
|
578
|
+
@UsePipes(new ValidationPipe({ transform: true }))
|
|
579
|
+
async createItem(@Body() body: any) {
|
|
580
|
+
return { created: true, body }
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
```
|
|
584
|
+
|
|
356
585
|
<Info title="Tip: Safely Returning with @Res">
|
|
357
586
|
In some frameworks, injecting the `@Res()` object disables auto-serialization, causing the request to hang if you forget to call `res.send()`. ExisJS handles this differently: if you return a value from your handler, ExisJS will still safely auto-serialize it to JSON even if you injected `@Res()` to set a custom header.
|
|
358
587
|
</Info>
|
|
588
|
+
|
|
@@ -98,33 +98,40 @@ export const uploadMedia = (req, res) => {
|
|
|
98
98
|
}
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
-
### The Standardized Response
|
|
101
|
+
### The Standardized Error Response Envelope
|
|
102
102
|
|
|
103
|
-
When an `HttpError` is thrown, the client
|
|
103
|
+
When an `HttpError` is thrown or a validation failure occurs, the client automatically receives a standardized JSON payload with status codes, error descriptions, and ISO timestamps:
|
|
104
104
|
|
|
105
|
-
```json filename="Response"
|
|
105
|
+
```json filename="HTTP Error Response Envelope"
|
|
106
106
|
{
|
|
107
107
|
"success": false,
|
|
108
|
-
"
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
108
|
+
"statusCode": 404,
|
|
109
|
+
"error": "Not Found",
|
|
110
|
+
"message": "User not found",
|
|
111
|
+
"validationErrors": null,
|
|
112
|
+
"timestamp": "2026-09-27T10:11:42.000Z"
|
|
112
113
|
}
|
|
113
114
|
```
|
|
114
115
|
|
|
115
116
|
### Validation Error Responses & Diagnostic Visibility
|
|
116
117
|
|
|
117
|
-
When schema validation fails (via `tex`, Zod, or Yup), ExisJS produces a typed `400 Bad Request`
|
|
118
|
+
When schema validation fails (via `tex`, Zod, or Yup), ExisJS produces a typed `400 Bad Request` containing both a concise `validationErrors` (and `errors`) map for seamless frontend form field binding:
|
|
118
119
|
|
|
119
120
|
```json filename="HTTP 400 Bad Request Response"
|
|
120
121
|
{
|
|
122
|
+
"success": false,
|
|
121
123
|
"statusCode": 400,
|
|
122
124
|
"error": "Bad Request",
|
|
123
125
|
"message": "Validation Error: email: Invalid email format, age: Field must be >= 18",
|
|
126
|
+
"validationErrors": {
|
|
127
|
+
"email": "Invalid email format",
|
|
128
|
+
"age": "Field must be >= 18"
|
|
129
|
+
},
|
|
124
130
|
"errors": {
|
|
125
131
|
"email": "Invalid email format",
|
|
126
132
|
"age": "Field must be >= 18"
|
|
127
|
-
}
|
|
133
|
+
},
|
|
134
|
+
"timestamp": "2026-09-27T10:11:42.000Z"
|
|
128
135
|
}
|
|
129
136
|
```
|
|
130
137
|
|
|
@@ -76,7 +76,7 @@ export default exis({
|
|
|
76
76
|
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
#### Class-Based (OOP) Paradigm (`@Module`)
|
|
79
|
+
#### Class-Based (OOP) Paradigm (`@Module` & `@Global`)
|
|
80
80
|
|
|
81
81
|
For applications following the OOP paradigm, you can define modules using the `@Module` class decorator:
|
|
82
82
|
|
|
@@ -95,23 +95,42 @@ import { DatabaseModule } from './database.module'
|
|
|
95
95
|
export class AuthModule {}
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
#### Global Modules (`@Global`)
|
|
99
|
+
|
|
100
|
+
If you have utility services, audit loggers, or database connections that should be globally available across all modules without having to list the module in every `imports: [...]` array, decorate the module with `@Global()`:
|
|
101
|
+
|
|
102
|
+
```typescript filename="src/modules/core.module.ts"
|
|
103
|
+
import { Global, Module } from 'exisjs/decorators'
|
|
104
|
+
import { AuditService } from './audit.service'
|
|
105
|
+
import { LoggerService } from './logger.service'
|
|
106
|
+
|
|
107
|
+
@Global()
|
|
108
|
+
@Module({
|
|
109
|
+
providers: [AuditService, LoggerService],
|
|
110
|
+
exports: [AuditService, LoggerService],
|
|
111
|
+
})
|
|
112
|
+
export class CoreModule {}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
You can then register it into your server or root module once:
|
|
99
116
|
|
|
100
117
|
```typescript filename="src/http/server.ts"
|
|
101
118
|
import { Server } from 'exisjs/decorators'
|
|
102
119
|
import { AuthModule } from '../modules/auth.module'
|
|
120
|
+
import { CoreModule } from '../modules/core.module'
|
|
103
121
|
import type { App } from 'exisjs'
|
|
104
122
|
|
|
105
123
|
@Server({
|
|
106
|
-
plugins: [AuthModule]
|
|
124
|
+
plugins: [CoreModule, AuthModule]
|
|
107
125
|
})
|
|
108
126
|
export default class RootServer {
|
|
109
127
|
async onStart(app: App) {
|
|
110
|
-
console.log("Server with AuthModule booted")
|
|
128
|
+
console.log("Server with Global CoreModule and AuthModule booted")
|
|
111
129
|
}
|
|
112
130
|
}
|
|
113
131
|
```
|
|
114
132
|
|
|
133
|
+
|
|
115
134
|
---
|
|
116
135
|
|
|
117
136
|
## 2. Framework Plugins (`definePlugin`)
|
|
@@ -64,7 +64,9 @@ export default class UsersController {
|
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
<Info title="Note on Dependency Injection">
|
|
67
|
-
ExisJS
|
|
67
|
+
ExisJS provides two elegant ways to inject dependencies:
|
|
68
|
+
1. **Property-Level / Functional Injection**: Use `inject(UsersService)` for concise, boilerplate-free injection.
|
|
69
|
+
2. **Constructor Injection**: In OOP controllers and services, ExisJS inspects TypeScript's emitted `design:paramtypes` and `INJECTABLE_REGISTRY` to automatically inject service instances into constructor parameters (`constructor(private usersService: UsersService)`) without requiring explicit `@Inject()` decorators on every parameter!
|
|
68
70
|
</Info>
|
|
69
71
|
|
|
70
72
|
### Extended: Manual Resolution
|
|
@@ -182,20 +184,31 @@ import { UsersService } from './users/service'
|
|
|
182
184
|
export default class RootServer {}
|
|
183
185
|
```
|
|
184
186
|
|
|
185
|
-
### Constructor Injection (`@Inject
|
|
187
|
+
### Constructor & Property Injection (`@Inject`, `@Optional`, and `forwardRef`)
|
|
186
188
|
|
|
187
|
-
In addition to property-level `inject()`, ExisJS provides full OOP constructor
|
|
189
|
+
In addition to property-level `inject()`, ExisJS provides full OOP constructor and property field injection with `@Inject(token)`, `@Optional()`, and `forwardRef()`:
|
|
188
190
|
|
|
189
191
|
```typescript filename="src/http/users/route.ts"
|
|
190
|
-
import { Controller, Get, Param
|
|
192
|
+
import { Controller, Get, Param } from 'exisjs/decorators'
|
|
193
|
+
import { Inject, Optional, forwardRef } from 'exisjs/di'
|
|
191
194
|
import { UsersService } from './service'
|
|
195
|
+
import { ConfigService } from '../config/service'
|
|
192
196
|
|
|
193
197
|
@Controller('/users')
|
|
194
198
|
export default class UsersController {
|
|
199
|
+
// 1. Property-level decorator injection
|
|
200
|
+
@Inject(ConfigService)
|
|
201
|
+
private config!: ConfigService
|
|
202
|
+
|
|
203
|
+
// 2. Optional token injection
|
|
204
|
+
@Inject('METRICS')
|
|
205
|
+
@Optional()
|
|
206
|
+
private metrics?: any
|
|
207
|
+
|
|
208
|
+
// 3. Constructor injection with forwardRef for circular dependencies
|
|
195
209
|
constructor(
|
|
196
210
|
private usersService: UsersService,
|
|
197
|
-
@Inject(
|
|
198
|
-
@Optional() @Inject('METRICS') private metrics?: any
|
|
211
|
+
@Inject(forwardRef(() => UsersService)) private circularRef: UsersService
|
|
199
212
|
) {}
|
|
200
213
|
|
|
201
214
|
@Get('/:id')
|
|
@@ -205,6 +218,29 @@ export default class UsersController {
|
|
|
205
218
|
}
|
|
206
219
|
```
|
|
207
220
|
|
|
221
|
+
### Forward References (`forwardRef`) & Circular Dependencies
|
|
222
|
+
|
|
223
|
+
When two services mutually depend on each other (`ServiceA` requires `ServiceB` and `ServiceB` requires `ServiceA`), circular references normally lead to `ReferenceError` during module evaluation. ExisJS solves this using `forwardRef(() => Service)` alongside lazy ES6 proxy resolution:
|
|
224
|
+
|
|
225
|
+
```typescript filename="src/services/order.service.ts"
|
|
226
|
+
import { Injectable } from 'exisjs/decorators'
|
|
227
|
+
import { Inject, forwardRef } from 'exisjs/di'
|
|
228
|
+
import { InventoryService } from './inventory.service'
|
|
229
|
+
|
|
230
|
+
@Injectable()
|
|
231
|
+
export class OrderService {
|
|
232
|
+
constructor(
|
|
233
|
+
@Inject(forwardRef(() => InventoryService))
|
|
234
|
+
private inventory: InventoryService
|
|
235
|
+
) {}
|
|
236
|
+
|
|
237
|
+
processOrder(id: string) {
|
|
238
|
+
return this.inventory.reserveStock(id)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
|
|
208
244
|
## Injection Scopes
|
|
209
245
|
|
|
210
246
|
For people coming from different programming language backgrounds, it might be unexpected to learn that in ExisJS, almost everything is shared across incoming requests. We have a connection pool to the database, singleton services with global state, etc. This is because Node.js doesn't follow the request/response Multi-Threaded Stateless Model.
|
|
@@ -451,11 +451,17 @@ const language = req.acceptsLanguages('en', 'es', 'fr')
|
|
|
451
451
|
|
|
452
452
|
---
|
|
453
453
|
|
|
454
|
-
## 7. Network Information &
|
|
454
|
+
## 7. Network Information & Reverse Proxy IP Resolution
|
|
455
455
|
|
|
456
|
-
ExisJS handles
|
|
456
|
+
ExisJS handles reverse proxies, CDNs, and dual-stack IP networks automatically. When deployed behind Cloudflare, AWS ALB, NGINX, Render, or GCP, set `trustProxy: true` in your `exis.config.ts` or define `TRUST_PROXY=true` in your environment variables.
|
|
457
457
|
|
|
458
|
-
|
|
458
|
+
When proxy trust is enabled, ExisJS evaluates industry-standard proxy and CDN headers in priority order:
|
|
459
|
+
1. `CF-Connecting-IP` (Cloudflare)
|
|
460
|
+
2. `True-Client-IP` (Akamai / Cloudflare Enterprise)
|
|
461
|
+
3. `X-Real-IP` / `X-Client-IP` (NGINX / Reverse Proxies)
|
|
462
|
+
4. `X-Forwarded-For` (Standard multi-hop proxy chains)
|
|
463
|
+
|
|
464
|
+
- **`req.ip`**: The real client's IP address. Automatically strips proxy hops, normalizes IPv6-mapped IPv4 dual-stack addresses (e.g., `::ffff:192.168.1.50` $\to$ `192.168.1.50`), and converts IPv6 loopback `::1` $\to$ `127.0.0.1` for consistent rate limiting and geo-IP lookups.
|
|
459
465
|
- **`req.ips`**: An array of IPs if multiple proxies were traversed, each normalized cleanly.
|
|
460
466
|
- **`req.hostname`**: The resolved hostname (from `Host` or `X-Forwarded-Host`).
|
|
461
467
|
- **`req.protocol`**: Returns `'http'` or `'https'`.
|
|
@@ -575,17 +575,31 @@ export default class AuthController {
|
|
|
575
575
|
|
|
576
576
|
---
|
|
577
577
|
|
|
578
|
-
## 6.
|
|
578
|
+
## 6. Safe Response Serialization (BSON, Dates & Prototype Auto-Unwrapping)
|
|
579
579
|
|
|
580
|
-
ExisJS
|
|
580
|
+
ExisJS includes a robust, zero-crash serialization engine that automatically prepares complex objects before sending them down the wire:
|
|
581
581
|
|
|
582
|
-
|
|
582
|
+
- **Mongoose / ORM Documents**: Automatically unwraps documents by invoking `.toObject({ virtuals: true, getters: true })` or `.toJSON()`. You no longer need to call `.lean()` or sanitize documents manually.
|
|
583
|
+
- **MongoDB BSON Types**: Automatically converts `ObjectId`, `Decimal128`, `Long`, and `Binary` buffers to their clean string / base64 representations.
|
|
584
|
+
- **BigInt & Dates**: Converts native `BigInt` values to strings and `Date` instances to ISO 8601 strings.
|
|
585
|
+
- **Maps & Sets**: Automatically converts `Map` to plain objects and `Set` to arrays.
|
|
586
|
+
- **Circular Reference Guard**: Circular object references are safely detected and replaced with `'[Circular]'` rather than throwing fatal V8 JSON serialization exceptions.
|
|
587
|
+
- **Enhanced Diagnostics**: If an unhandled serialization issue occurs, ExisJS logs the exact property path (e.g. `'recentTransactions[0].accountId'`) for immediate debugging clarity.
|
|
583
588
|
|
|
584
|
-
|
|
589
|
+
---
|
|
590
|
+
|
|
591
|
+
## 7. ETags & Conditional Requests (304 Not Modified)
|
|
592
|
+
|
|
593
|
+
ExisJS features native ETag generation.
|
|
594
|
+
|
|
595
|
+
By default, ETags are disabled (`etag: false`) on dynamic REST API endpoints to minimize hashing CPU overhead and provide transparent logging timings.
|
|
596
|
+
|
|
597
|
+
When enabled globally (`etag: true` in `exis.config.ts`) or on a per-route basis (`res.etagEnabled = true`):
|
|
598
|
+
- If the client sends an `If-None-Match` header matching the response ETag, ExisJS immediately sends `304 Not Modified` with an empty body, saving bandwidth.
|
|
585
599
|
|
|
586
600
|
---
|
|
587
601
|
|
|
588
|
-
##
|
|
602
|
+
## 8. Server-Sent Events (SSE) & AI Streaming
|
|
589
603
|
|
|
590
604
|
ExisJS provides a first-class `res.sse()` method for real-time live events and AI chat completions (OpenAI, Anthropic, Ollama, LangChain).
|
|
591
605
|
|
|
@@ -103,14 +103,15 @@ Here is the full list of options supported natively by the ExisJS configuration
|
|
|
103
103
|
- **`host`** *(string)*: The host interface to bind to (default: `'0.0.0.0'`).
|
|
104
104
|
- **`server`** *(`'node' | 'uws' | 'auto'`)*: ExisJS supports running on standard Node `http`, or incredibly fast C++ `uWebSockets.js` (`uws`). Default is `'auto'` (uses `uws` if installed, falls back to `node`).
|
|
105
105
|
- **`workers`** *(number | `'safe'` | `'max'`)*: Number of Node.js Cluster workers to spawn. `'max'` uses all CPU cores. `'safe'` uses all cores minus 1 (to leave room for the OS).
|
|
106
|
-
- **`trustProxy`** *(boolean)*: Enable if running behind
|
|
106
|
+
- **`trustProxy`** *(boolean | number)*: Enable if running behind reverse proxies or CDNs (Cloudflare, AWS ALB, NGINX, Render) to evaluate `CF-Connecting-IP`, `True-Client-IP`, `X-Real-IP`, and `X-Forwarded-For`. (Can also be enabled via `TRUST_PROXY=true`).
|
|
107
107
|
|
|
108
108
|
### Security & Middlewares
|
|
109
109
|
- **`cors`** *(boolean | object)*: Enable and configure Cross-Origin Resource Sharing globally.
|
|
110
110
|
- **`helmet`** *(boolean | object)*: Enable strict security headers. Enabled by default.
|
|
111
111
|
- **`logger`** *(boolean | object)*: Configure the efficient Pino request logger.
|
|
112
112
|
- **`compression`** *(boolean | object)*: Enable gzip/brotli response compression.
|
|
113
|
-
- **`
|
|
113
|
+
- **`etag`** *(boolean)*: Automatic ETag generation for HTTP conditional requests (default: `false` for dynamic API routes).
|
|
114
|
+
- **`bodyLimit`** *(number)*: Maximum JSON/URL-encoded payload size in bytes (default: `10MB`).
|
|
114
115
|
|
|
115
116
|
### Extended Integrations
|
|
116
117
|
- **`queue`**: Define your background queue driver (`memory` or `redis`) and configure `maxConcurrent` processing limits.
|
|
@@ -83,8 +83,16 @@ Parameter decorators automatically extract data from the incoming request and in
|
|
|
83
83
|
|
|
84
84
|
---
|
|
85
85
|
|
|
86
|
-
## 3.
|
|
87
|
-
|
|
86
|
+
## 3. Transformation Pipes & Middleware Decorators
|
|
87
|
+
|
|
88
|
+
- `@UsePipes(...pipes)`: Binds transformation and validation pipes to a controller class or route method.
|
|
89
|
+
- **Built-in Pipes**:
|
|
90
|
+
- `ParseIntPipe`: Parses numeric strings to integers (`options?: { errorHttpStatusCode, exceptionFactory }`).
|
|
91
|
+
- `ParseFloatPipe`: Parses numeric float strings (`options?: { errorHttpStatusCode, exceptionFactory }`).
|
|
92
|
+
- `ParseBoolPipe`: Parses boolean strings (`'true'`, `'false'`, `1`, `0`).
|
|
93
|
+
- `ParseUUIDPipe`: Validates UUID format (`options?: { version: '3' | '4' | '5' | 'all' }`).
|
|
94
|
+
- `ParseArrayPipe`: Splits comma-separated strings and applies item-level transformation pipes.
|
|
95
|
+
- `ValidationPipe`: Validates DTO classes and schemas (`options?: { transform: boolean }`).
|
|
88
96
|
- `@Use(...middlewaresOrGuardsOrInterceptorsOrFilters)`: Unified decorator that binds middlewares, guards (`canActivate`), interceptors (`intercept`), or exception filters (`catch`) to a controller class or method.
|
|
89
97
|
- `@UseGuards(...guards)`: Dedicated decorator to apply guards to a class or method.
|
|
90
98
|
- `@UseInterceptors(...interceptors)`: Dedicated decorator to apply interceptors to a class or method.
|
|
@@ -139,10 +147,12 @@ Parameter decorators automatically extract data from the incoming request and in
|
|
|
139
147
|
|
|
140
148
|
## 7. Dependency Injection & Modules
|
|
141
149
|
|
|
150
|
+
- `@Global()`: Marks a `@Module()` as global, automatically making its exported providers available across the entire application without explicit re-importing.
|
|
142
151
|
- `@Module({ imports?, controllers?, providers?, exports? })`: OOP class decorator equivalent to `defineModule()` for structuring application modules.
|
|
143
152
|
- `@Injectable(options?)`: Marks a service class as an IoC provider managed by the container. Supports `{ scope: 'singleton' | 'request' | 'transient' }`.
|
|
144
|
-
- `@Inject(token)`: Constructor parameter decorator from `exisjs/di` for injecting dependencies by class, string, or symbol token.
|
|
145
|
-
- `@Optional()`:
|
|
153
|
+
- `@Inject(token)`: Constructor parameter and class property decorator from `exisjs/di` for injecting dependencies by class, string, or symbol token.
|
|
154
|
+
- `@Optional()`: Parameter and property decorator from `exisjs/di` resolving missing dependencies to `undefined` without throwing.
|
|
155
|
+
- `forwardRef(() => Target)`: Creates a lazy forward reference for resolving circular dependencies in constructor or property injection.
|
|
146
156
|
- `@Boundary(config)`: Marks a class as a folder-scoped Boundary configuring CORS, headers, exclusions, and pipeline steps.
|
|
147
157
|
- `@Server(config)`: Marks a class as a root server definition.
|
|
148
158
|
|
|
@@ -154,3 +164,4 @@ Parameter decorators automatically extract data from the incoming request and in
|
|
|
154
164
|
- `@Interval(ms: number, options?: IntervalOptions)`: Schedules a class method to execute repeatedly every `ms` milliseconds.
|
|
155
165
|
- `@TimeoutTask(ms: number, options?: TimeoutOptions)` (alias `@ScheduledTimeout`): Schedules a class method to execute once after `ms` milliseconds following application bootstrap.
|
|
156
166
|
|
|
167
|
+
|
|
@@ -109,20 +109,39 @@ export default controller({
|
|
|
109
109
|
> [!NOTE]
|
|
110
110
|
> `inject()` relies on the active execution context. If you call `inject()` outside of an active HTTP request or application lifecycle hook (e.g., in a background `setInterval` without context), it will throw an error.
|
|
111
111
|
|
|
112
|
-
### 2. Constructor Injection in Classes (OOP)
|
|
112
|
+
### 2. Constructor & Property Injection in Classes (OOP)
|
|
113
113
|
|
|
114
|
-
ExisJS supports full OOP constructor parameter
|
|
114
|
+
ExisJS supports full OOP constructor parameter and property field injection with **automatic parameter type reflection**:
|
|
115
|
+
- **Automatic Type Reflection**: If `emitDecoratorMetadata: true` is enabled in `tsconfig.json`, ExisJS inspects `design:paramtypes` and automatically maps constructor parameter classes directly to dependencies registered in `INJECTABLE_REGISTRY`. You do not need `@Inject()` on standard service classes!
|
|
116
|
+
- **Property-Level `@Inject()`**: Inject dependencies directly onto class properties (`@Inject(Token) public service!: Service`) with automatic `design:type` reflection fallback.
|
|
117
|
+
- **Token Injection (`@Inject(token)`)**: Use `@Inject('TOKEN')` when injecting custom strings, config objects, or symbols.
|
|
118
|
+
- **Optional Dependencies (`@Optional()`)**: Mark dependencies that can safely resolve to `undefined` if not registered.
|
|
119
|
+
- **Forward References (`forwardRef(() => Service)`)**: Resolve circular dependencies between services using lazy forward references and ES6 Proxy delegates.
|
|
115
120
|
|
|
116
121
|
```typescript filename="src/http/users/route.ts"
|
|
117
|
-
import { Controller, Get, Param
|
|
122
|
+
import { Controller, Get, Param } from 'exisjs/decorators'
|
|
123
|
+
import { Inject, Optional, forwardRef } from 'exisjs/di'
|
|
118
124
|
import { UserService } from './service'
|
|
125
|
+
import { ConfigService } from '../config/service'
|
|
119
126
|
|
|
120
127
|
@Controller('/users')
|
|
121
128
|
export default class UserController {
|
|
129
|
+
// 1. Property-level injection
|
|
130
|
+
@Inject(ConfigService)
|
|
131
|
+
private config!: ConfigService
|
|
132
|
+
|
|
133
|
+
// 2. Optional property injection
|
|
134
|
+
@Inject('ANALYTICS')
|
|
135
|
+
@Optional()
|
|
136
|
+
private analytics?: any
|
|
137
|
+
|
|
122
138
|
constructor(
|
|
139
|
+
// 3. Automatically resolved from INJECTABLE_REGISTRY via type reflection!
|
|
123
140
|
private userService: UserService,
|
|
141
|
+
// 4. Explicit token injection
|
|
124
142
|
@Inject('DATABASE_URL') private dbUrl: string,
|
|
125
|
-
|
|
143
|
+
// 5. Circular dependency resolution via forwardRef
|
|
144
|
+
@Inject(forwardRef(() => UserService)) private circularRef: UserService
|
|
126
145
|
) {}
|
|
127
146
|
|
|
128
147
|
@Get('/:id')
|
|
@@ -132,18 +151,6 @@ export default class UserController {
|
|
|
132
151
|
}
|
|
133
152
|
```
|
|
134
153
|
|
|
135
|
-
You can also use property-level `inject()` inside your Controller methods:
|
|
136
|
-
|
|
137
|
-
```typescript
|
|
138
|
-
@Controller('/users')
|
|
139
|
-
export default class UserController {
|
|
140
|
-
@Get('/')
|
|
141
|
-
async getUsers() {
|
|
142
|
-
const db = inject(Database)
|
|
143
|
-
return db.query('SELECT * FROM users')
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
```
|
|
147
154
|
|
|
148
155
|
---
|
|
149
156
|
|