exisjs 0.7.8 → 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/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +14 -4
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/resolve-aliases.js +1 -1
- package/dist/cli/resolve-aliases.js.map +1 -1
- package/dist/config/config.js +1 -1
- package/dist/config/config.js.map +1 -1
- package/dist/decorators/caching.d.ts +53 -0
- package/dist/decorators/caching.d.ts.map +1 -0
- package/dist/decorators/caching.js +154 -0
- package/dist/decorators/caching.js.map +1 -0
- package/dist/decorators/constants.d.ts +6 -0
- package/dist/decorators/constants.d.ts.map +1 -1
- package/dist/decorators/constants.js +7 -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 +29 -0
- package/dist/decorators/filters.d.ts.map +1 -0
- package/dist/decorators/filters.js +30 -0
- package/dist/decorators/filters.js.map +1 -0
- package/dist/decorators/index.d.ts +7 -0
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/decorators/index.js +7 -0
- package/dist/decorators/index.js.map +1 -1
- package/dist/decorators/lifecycle.d.ts +66 -0
- package/dist/decorators/lifecycle.d.ts.map +1 -0
- package/dist/decorators/lifecycle.js +3 -0
- package/dist/decorators/lifecycle.js.map +1 -0
- package/dist/decorators/middleware.d.ts +24 -3
- package/dist/decorators/middleware.d.ts.map +1 -1
- package/dist/decorators/middleware.js +24 -3
- package/dist/decorators/middleware.js.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 +1 -7
- 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/decorators/profile.d.ts +32 -0
- package/dist/decorators/profile.d.ts.map +1 -0
- package/dist/decorators/profile.js +61 -0
- package/dist/decorators/profile.js.map +1 -0
- package/dist/decorators/response.d.ts +0 -1
- package/dist/decorators/response.d.ts.map +1 -1
- package/dist/decorators/response.js +1 -18
- package/dist/decorators/response.js.map +1 -1
- package/dist/decorators/routing.d.ts +22 -12
- package/dist/decorators/routing.d.ts.map +1 -1
- package/dist/decorators/routing.js +49 -3
- package/dist/decorators/routing.js.map +1 -1
- package/dist/decorators/security.d.ts +42 -0
- package/dist/decorators/security.d.ts.map +1 -0
- package/dist/decorators/security.js +110 -0
- package/dist/decorators/security.js.map +1 -0
- package/dist/decorators/transaction.d.ts +29 -0
- package/dist/decorators/transaction.d.ts.map +1 -0
- package/dist/decorators/transaction.js +91 -0
- package/dist/decorators/transaction.js.map +1 -0
- package/dist/di/container.d.ts +8 -3
- package/dist/di/container.d.ts.map +1 -1
- package/dist/di/container.js +214 -66
- 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/decorators.mdx +89 -4
- package/dist/docs/content/errors.mdx +44 -21
- package/dist/docs/content/modules.mdx +23 -4
- package/dist/docs/content/providers.mdx +90 -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 +42 -8
- package/dist/docs/reference/di.mdx +45 -12
- package/dist/docs/reference/router.mdx +75 -0
- package/dist/docs/reference/validator.mdx +2 -2
- package/dist/error/errors.d.ts +24 -12
- package/dist/error/errors.d.ts.map +1 -1
- package/dist/error/errors.js +48 -12
- package/dist/error/errors.js.map +1 -1
- package/dist/router/controller-registrar.d.ts.map +1 -1
- package/dist/router/controller-registrar.js +217 -94
- 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 +40 -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 +16 -12
- package/dist/testing/client.d.ts.map +1 -1
- package/dist/testing/client.js +51 -15
- package/dist/testing/client.js.map +1 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +10 -0
- package/dist/testing/index.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
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.forwardRef = forwardRef;
|
|
4
|
+
exports.isForwardRef = isForwardRef;
|
|
5
|
+
/**
|
|
6
|
+
* Creates a lazy forward reference for resolving circular dependencies between providers or modules.
|
|
7
|
+
*
|
|
8
|
+
* Example:
|
|
9
|
+
* ```ts
|
|
10
|
+
* @Injectable()
|
|
11
|
+
* export class OrderService {
|
|
12
|
+
* constructor(
|
|
13
|
+
* @Inject(forwardRef(() => InventoryService))
|
|
14
|
+
* private inventoryService: InventoryService
|
|
15
|
+
* ) {}
|
|
16
|
+
* }
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
function forwardRef(fn) {
|
|
20
|
+
return { forwardRef: fn };
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Checks if a value is a ForwardReference wrapper.
|
|
24
|
+
*/
|
|
25
|
+
function isForwardRef(val) {
|
|
26
|
+
return (val !== null &&
|
|
27
|
+
typeof val === 'object' &&
|
|
28
|
+
typeof val.forwardRef === 'function');
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=forward-ref.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"forward-ref.js","sourceRoot":"","sources":["../../src/di/forward-ref.ts"],"names":[],"mappings":";;AAkBA,gCAEC;AAKD,oCAMC;AA3BD;;;;;;;;;;;;;GAaG;AACH,SAAgB,UAAU,CAAU,EAAW;IAC7C,OAAO,EAAE,UAAU,EAAE,EAAE,EAAE,CAAA;AAC3B,CAAC;AAED;;GAEG;AACH,SAAgB,YAAY,CAAC,GAAQ;IACnC,OAAO,CACL,GAAG,KAAK,IAAI;QACZ,OAAO,GAAG,KAAK,QAAQ;QACvB,OAAO,GAAG,CAAC,UAAU,KAAK,UAAU,CACrC,CAAA;AACH,CAAC"}
|
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
|
+
|
|
@@ -178,6 +178,10 @@ export default class CheckoutController {
|
|
|
178
178
|
|
|
179
179
|
### Available Lifecycle & Middleware Decorators
|
|
180
180
|
- `@Use(...items)` — Unified decorator that attaches middlewares, guards (`canActivate`), interceptors (`intercept`), or exception filters (`catch`) to a class or method.
|
|
181
|
+
- `@UseGuards(...guards)` — Explicit alias for `@Use` dedicated to route guards.
|
|
182
|
+
- `@UseInterceptors(...interceptors)` — Explicit alias for `@Use` dedicated to interceptors.
|
|
183
|
+
- `@UseFilters(...filters)` — Explicit alias for `@Use` dedicated to exception filters.
|
|
184
|
+
- `@Catch(...exceptions)` — Declares exception classes handled by an Exception Filter.
|
|
181
185
|
- `@Timeout(ms: number | TimeoutOptions)` — Overrides the request timeout for a class or method.
|
|
182
186
|
- `@Idempotent(options?)` — Caches responses based on the `Idempotency-Key` header using native in-memory off-heap storage.
|
|
183
187
|
- `@RateLimit(options?)` — Enforces request rate limits on a controller class or route method.
|
|
@@ -190,7 +194,88 @@ export default class CheckoutController {
|
|
|
190
194
|
|
|
191
195
|
---
|
|
192
196
|
|
|
193
|
-
## 5.
|
|
197
|
+
## 5. Security & Authorization Decorators
|
|
198
|
+
|
|
199
|
+
Declare access control rules directly on controller classes and route methods:
|
|
200
|
+
|
|
201
|
+
```typescript filename="src/http/roles/route.ts"
|
|
202
|
+
import { Controller, Get, Post, Body, Use, Permissions, Roles, Public } from 'exisjs/decorators'
|
|
203
|
+
import { AuthGuard } from '@/common/guards/auth.guard'
|
|
204
|
+
|
|
205
|
+
@Controller('/roles')
|
|
206
|
+
@Use(AuthGuard) // Enforced on all routes in this controller
|
|
207
|
+
export default class RolesController {
|
|
208
|
+
@Get('/public')
|
|
209
|
+
@Public() // Bypasses auth guard for public inspection
|
|
210
|
+
async getPublicInfo() {
|
|
211
|
+
return { status: 'public' }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
@Get('/')
|
|
215
|
+
@Roles('admin', 'manager') // Requires user to have admin or manager role
|
|
216
|
+
async getRoles() {
|
|
217
|
+
return { roles: [] }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
@Post('/')
|
|
221
|
+
@Permissions('role:create', 'role:manage') // Granular permission check
|
|
222
|
+
async createRole(@Body() body: any) {
|
|
223
|
+
return { success: true }
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Available Security Decorators
|
|
229
|
+
- `@Permissions(...permissions: string[])` — Declares required permissions (auto-bypassed when `req.user.isSuperAdmin` is true).
|
|
230
|
+
- `@Roles(...roles: string[])` — Declares required role names.
|
|
231
|
+
- `@Public()` / `@IsPublic()` / `@AllowAnonymous()` — Marks an endpoint or controller as public, instructing global auth guards to skip credential verification.
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## 6. Service Enhancements: Caching, Transactions & Profiling
|
|
236
|
+
|
|
237
|
+
ExisJS provides high-performance method decorators for `@Injectable()` services:
|
|
238
|
+
|
|
239
|
+
```typescript filename="src/http/orders/service.ts"
|
|
240
|
+
import { Injectable, Cacheable, CacheEvict, Transactional, Profile, type OnInit, type OnDestroy } from 'exisjs/decorators'
|
|
241
|
+
|
|
242
|
+
@Injectable()
|
|
243
|
+
export class OrderService implements OnInit, OnDestroy {
|
|
244
|
+
async onInit() {
|
|
245
|
+
console.log('OrderService initialized')
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async onDestroy() {
|
|
249
|
+
console.log('OrderService destroyed')
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Caches result in off-heap Rust cache for 300 seconds
|
|
253
|
+
@Cacheable({ ttl: 300, key: (id: string) => `order:${id}` })
|
|
254
|
+
@Profile({ thresholdMs: 50, warnSlow: true })
|
|
255
|
+
async getOrder(id: string) {
|
|
256
|
+
return this.db.query('SELECT * FROM orders WHERE id = $1', [id])
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Automatically runs in a database transaction and evicts cached entries
|
|
260
|
+
@Transactional()
|
|
261
|
+
@CacheEvict({ key: (id: string) => `order:${id}` })
|
|
262
|
+
async updateOrder(id: string, data: any) {
|
|
263
|
+
return this.db.update('orders', id, data)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Available Service Decorators & Lifecycle Interfaces
|
|
269
|
+
- `@Cacheable({ ttl?: number, key?: string | ((...args) => string) })` — Caches method return values in off-heap memory.
|
|
270
|
+
- `@CacheEvict({ key: string | ((...args) => string) })` — Evicts cached keys on mutation.
|
|
271
|
+
- `@Transactional(options?)` — Wraps service operations in an atomic database transaction.
|
|
272
|
+
- `@Profile({ thresholdMs?: number, warnSlow?: boolean, log?: Function })` — Measures method execution time with sub-millisecond precision.
|
|
273
|
+
- `OnInit` (`onInit()`) & `OnDestroy` (`onDestroy()`) — Service startup and shutdown lifecycle hooks.
|
|
274
|
+
- `OnModuleInit`, `OnModuleDestroy`, `OnApplicationBootstrap` — Module lifecycle hooks.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## 7. Response & Metadata Modifiers
|
|
194
279
|
|
|
195
280
|
Modify HTTP response status codes, redirect clients, or set headers declaratively:
|
|
196
281
|
|
|
@@ -219,14 +304,13 @@ export default class ApiController {
|
|
|
219
304
|
- `@Header(name: string, value: string)` — Injects a response header.
|
|
220
305
|
- `@Returns(schema)` — Attaches response schema for OpenAPI/Swagger generation.
|
|
221
306
|
- `@Redirect(url: string, statusCode?: number)` — Redirects the client.
|
|
222
|
-
- `@Permissions(...permissions: string[])` — Attaches route permissions metadata.
|
|
223
307
|
- `@Hosts(...hosts: string[])` — Restricts route/controller to specific Host headers.
|
|
224
308
|
- `@SetMetadata(key, value)` — Attaches custom metadata to a class or method.
|
|
225
|
-
- `Reflector` — Static utility to inspect metadata attached via `@SetMetadata` across classes and methods
|
|
309
|
+
- `Reflector` — Static utility to inspect metadata attached via `@SetMetadata` across classes and methods.
|
|
226
310
|
|
|
227
311
|
---
|
|
228
312
|
|
|
229
|
-
##
|
|
313
|
+
## 8. Dependency Injection & Modules
|
|
230
314
|
|
|
231
315
|
To mark a service class as an injectable provider for the IoC container, use the `@Injectable()` decorator. ExisJS supports `singleton` (default), `request`, and `transient` scopes:
|
|
232
316
|
|
|
@@ -281,3 +365,4 @@ import { UserService } from './service'
|
|
|
281
365
|
})
|
|
282
366
|
export class UsersModule {}
|
|
283
367
|
```
|
|
368
|
+
|
|
@@ -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
|
|
|
@@ -335,26 +342,42 @@ export default controller({
|
|
|
335
342
|
|
|
336
343
|
### Class-Based (OOP) Paradigm
|
|
337
344
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
345
|
+
You can use function handlers or structured class-based `ExceptionFilter` implementations with `@Catch()`:
|
|
346
|
+
|
|
347
|
+
```typescript filename="src/http/filters/not-found.filter.ts"
|
|
348
|
+
import { Catch, type ExceptionFilter, type ArgumentsHost } from 'exisjs/decorators'
|
|
349
|
+
import { NotFoundException } from 'exisjs/error'
|
|
341
350
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
351
|
+
@Catch(NotFoundException)
|
|
352
|
+
export class NotFoundExceptionFilter implements ExceptionFilter<NotFoundException> {
|
|
353
|
+
catch(exception: NotFoundException, host: ArgumentsHost) {
|
|
354
|
+
const { res } = host
|
|
355
|
+
res.status(404).json({
|
|
356
|
+
statusCode: 404,
|
|
357
|
+
error: 'Not Found',
|
|
358
|
+
message: exception.message,
|
|
359
|
+
timestamp: new Date().toISOString(),
|
|
360
|
+
})
|
|
361
|
+
}
|
|
345
362
|
}
|
|
363
|
+
```
|
|
346
364
|
|
|
347
|
-
|
|
348
|
-
|
|
365
|
+
```typescript filename="src/http/users/route.ts"
|
|
366
|
+
import { Controller, Get, UseFilters, Use } from 'exisjs/decorators'
|
|
367
|
+
import { NotFoundException } from 'exisjs/error'
|
|
368
|
+
import { NotFoundExceptionFilter } from '../filters/not-found.filter'
|
|
369
|
+
|
|
370
|
+
@Controller('/users')
|
|
371
|
+
@Use(NotFoundExceptionFilter) // or @UseFilters(NotFoundExceptionFilter)
|
|
349
372
|
export default class UsersController {
|
|
350
|
-
@Get('
|
|
351
|
-
async
|
|
352
|
-
throw new
|
|
373
|
+
@Get('/:id')
|
|
374
|
+
async getUser() {
|
|
375
|
+
throw new NotFoundException('User')
|
|
353
376
|
}
|
|
354
377
|
}
|
|
355
378
|
```
|
|
356
379
|
|
|
357
|
-
By combining Exception Filters and Local Error Hooks, ExisJS gives you control over your error boundaries.
|
|
380
|
+
By combining Exception Filters and Local Error Hooks, ExisJS gives you complete control over your error boundaries.
|
|
358
381
|
|
|
359
382
|
## Observability (BYOM/BYOT)
|
|
360
383
|
|
|
@@ -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`)
|