exisjs 0.7.9 → 0.7.11
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 +8 -1
- package/dist/cli/commands/dev.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 +1 -1
- package/dist/config/config.js.map +1 -1
- package/dist/config/types.d.ts +7 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/database/lifecycle.d.ts +2 -1
- package/dist/database/lifecycle.d.ts.map +1 -1
- package/dist/database/lifecycle.js +6 -2
- package/dist/database/lifecycle.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 +8 -4
- 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 +3 -2
- 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/BENCHMARKS.md +5 -5
- package/dist/docs/content/config.mdx +16 -6
- 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/content/security.mdx +48 -35
- package/dist/docs/content/structure.mdx +119 -93
- package/dist/docs/reference/config.mdx +29 -18
- 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/middleware/security.d.ts +18 -0
- package/dist/middleware/security.d.ts.map +1 -1
- package/dist/middleware/security.js +32 -0
- package/dist/middleware/security.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/radix.d.ts +1 -0
- package/dist/router/radix.d.ts.map +1 -1
- package/dist/router/radix.js +21 -0
- package/dist/router/radix.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/adapters/uws-adapter.d.ts +17 -0
- package/dist/server/adapters/uws-adapter.d.ts.map +1 -1
- package/dist/server/adapters/uws-adapter.js +101 -7
- package/dist/server/adapters/uws-adapter.js.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 +168 -9
- 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/request-handler.d.ts.map +1 -1
- package/dist/server/request-handler.js +35 -19
- package/dist/server/request-handler.js.map +1 -1
- package/dist/server/request.d.ts +4 -3
- package/dist/server/request.d.ts.map +1 -1
- package/dist/server/request.js +26 -8
- package/dist/server/request.js.map +1 -1
- package/dist/server/response.d.ts +1 -0
- package/dist/server/response.d.ts.map +1 -1
- package/dist/server/response.js +48 -22
- 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
|
@@ -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
|
-
export type { ProviderToken, ProviderDefinition, ValueProvider, FactoryProvider, ClassProvider, } from './container';
|
|
4
|
+
export type { ProviderToken, ProviderDefinition, ValueProvider, FactoryProvider, ClassProvider, ClassConstructor, } 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,EACb,gBAAgB,GACjB,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;AASzB,6CAA+E;AAAtE,yGAAA,UAAU,OAAA;AAAE,2GAAA,YAAY,OAAA"}
|
package/dist/docs/BENCHMARKS.md
CHANGED
|
@@ -12,16 +12,16 @@
|
|
|
12
12
|
- **Trials:** 3 per framework, order randomized each trial, median reported
|
|
13
13
|
- **Warmup:** 2s untimed warmup run before each timed trial
|
|
14
14
|
- **Cores:** single-core, no clustering, for all four frameworks
|
|
15
|
-
- **Framework versions:** Express ^5.2.1, Fastify ^5.12.
|
|
15
|
+
- **Framework versions:** Express ^5.2.1, Fastify ^5.12.5, Hono ^4.13.8, ExisJS 0.7.3
|
|
16
16
|
|
|
17
17
|
### Performance Metrics
|
|
18
18
|
|
|
19
19
|
| Framework | Req/Sec (median) | Latency avg (ms) | Latency p50 (ms) | Latency p99 (ms) | Errors | Server backend |
|
|
20
20
|
|---|---|---|---|---|---|---|
|
|
21
|
-
| Express |
|
|
22
|
-
| Fastify |
|
|
23
|
-
| Hono |
|
|
24
|
-
| Exis JS |
|
|
21
|
+
| Express | 24778.91 | 3.41 | 3.00 | 5.00 | 0 | n/a |
|
|
22
|
+
| Fastify | 41802.19 | 2.04 | 2.00 | 3.00 | 0 | n/a |
|
|
23
|
+
| Hono | 43174.40 | 2.04 | 2.00 | 3.00 | 0 | n/a |
|
|
24
|
+
| Exis JS | 51430.40 | 1.26 | 1.00 | 3.00 | 0 | uws |
|
|
25
25
|
|
|
26
26
|
### Handler Parity Note
|
|
27
27
|
|
|
@@ -75,11 +75,13 @@ We highly recommend using `v.env()` from `exisjs/validator` to strongly type and
|
|
|
75
75
|
```typescript filename="env.ts"
|
|
76
76
|
import { tex } from 'exisjs/validator'
|
|
77
77
|
|
|
78
|
-
export const env = tex
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
export const env = tex
|
|
79
|
+
.object({
|
|
80
|
+
PORT: tex.number({ coerce: true, optional: true }),
|
|
81
|
+
NODE_ENV: tex.string({ optional: true }),
|
|
82
|
+
MONGODB_URI: tex.string(),
|
|
83
|
+
})
|
|
84
|
+
.parse(process.env)
|
|
83
85
|
```
|
|
84
86
|
|
|
85
87
|
```typescript filename="exis.config.ts"
|
|
@@ -167,6 +169,14 @@ Here is a full breakdown of the configuration schema:
|
|
|
167
169
|
- `debugRouting` _(boolean)_: Enables detailed logging of the resolved route file and applied gateways for every incoming request.
|
|
168
170
|
- `test` _(object)_: Configuration for the native test runner (`include`, `exclude`, `setupFiles`, `concurrency`, `coverage`).
|
|
169
171
|
|
|
172
|
+
### Development Server & File Watcher (`dev`)
|
|
173
|
+
|
|
174
|
+
- `dev.watch` _(string[])_: Custom paths or glob patterns to watch during development (defaults to `[cwd]`).
|
|
175
|
+
- `dev.ignored` _((string | RegExp)[])_: Additional files or patterns to ignore during watch restarts.
|
|
176
|
+
- `dev.usePolling` _(boolean)_: Enables filesystem polling for cross-platform/Windows NTFS reliability (defaults to `true` on Windows).
|
|
177
|
+
- `dev.interval` _(number)_: Polling interval in milliseconds (default: `100`).
|
|
178
|
+
- `dev.binaryInterval` _(number)_: Polling binary interval in milliseconds (default: `300`).
|
|
179
|
+
|
|
170
180
|
### Queues & Plugins
|
|
171
181
|
|
|
172
182
|
- `queue` _(object)_: Configures the Global Job Queue driver (`memory` or `redis`) and backpressure settings.
|
|
@@ -242,6 +252,6 @@ export default exis({
|
|
|
242
252
|
async onStop(app) {
|
|
243
253
|
// Cleanly close database pools when SIGTERM or SIGINT is received
|
|
244
254
|
await db.disconnect()
|
|
245
|
-
}
|
|
255
|
+
},
|
|
246
256
|
})
|
|
247
257
|
```
|
|
@@ -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
|
|