@engjts/nexus 0.1.8 → 0.1.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/advanced/playground/playground.js.map +1 -1
- package/dist/advanced/static/generateDirectoryListing.d.ts +1 -1
- package/dist/advanced/static/generateDirectoryListing.d.ts.map +1 -1
- package/dist/advanced/static/generateDirectoryListing.js +12 -6
- package/dist/advanced/static/generateDirectoryListing.js.map +1 -1
- package/dist/advanced/static/index.d.ts +2 -0
- package/dist/advanced/static/index.d.ts.map +1 -1
- package/dist/advanced/static/index.js +4 -1
- package/dist/advanced/static/index.js.map +1 -1
- package/dist/advanced/static/serveStatic.d.ts.map +1 -1
- package/dist/advanced/static/serveStatic.js +7 -1
- package/dist/advanced/static/serveStatic.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/BENCHMARK_REPORT.md +0 -343
- package/documentation/01-getting-started.md +0 -240
- package/documentation/02-context.md +0 -335
- package/documentation/03-routing.md +0 -397
- package/documentation/04-middleware.md +0 -483
- package/documentation/05-validation.md +0 -514
- package/documentation/06-error-handling.md +0 -465
- package/documentation/07-performance.md +0 -364
- package/documentation/08-adapters.md +0 -470
- package/documentation/09-api-reference.md +0 -548
- package/documentation/10-examples.md +0 -582
- package/documentation/11-deployment.md +0 -477
- package/documentation/12-sentry.md +0 -620
- package/documentation/13-sentry-data-storage.md +0 -996
- package/documentation/14-sentry-data-reference.md +0 -457
- package/documentation/15-sentry-summary.md +0 -409
- package/documentation/16-alerts-system.md +0 -745
- package/documentation/17-alert-adapters.md +0 -696
- package/documentation/18-alerts-implementation-summary.md +0 -385
- package/documentation/19-class-based-routing.md +0 -840
- package/documentation/20-websocket-realtime.md +0 -813
- package/documentation/21-cache-system.md +0 -510
- package/documentation/22-job-queue.md +0 -772
- package/documentation/23-sentry-plugin.md +0 -551
- package/documentation/24-testing-utilities.md +0 -1287
- package/documentation/25-api-versioning.md +0 -533
- package/documentation/26-context-store.md +0 -607
- package/documentation/27-dependency-injection.md +0 -329
- package/documentation/28-lifecycle-hooks.md +0 -521
- package/documentation/29-package-structure.md +0 -196
- package/documentation/30-plugin-system.md +0 -414
- package/documentation/31-jwt-authentication.md +0 -597
- package/documentation/32-cli.md +0 -268
- package/documentation/ALERTS-COMPLETE-SUMMARY.md +0 -429
- package/documentation/ALERTS-INDEX.md +0 -330
- package/documentation/ALERTS-QUICK-REFERENCE.md +0 -286
- package/documentation/README.md +0 -178
- package/documentation/index.html +0 -34
- package/modern_framework_paper.md +0 -1870
- package/public/css/style.css +0 -87
- package/public/index.html +0 -34
- package/public/js/app.js +0 -27
- package/src/advanced/cache/InMemoryCacheStore.ts +0 -68
- package/src/advanced/cache/MultiTierCache.ts +0 -194
- package/src/advanced/cache/RedisCacheStore.ts +0 -341
- package/src/advanced/cache/index.ts +0 -5
- package/src/advanced/cache/types.ts +0 -40
- package/src/advanced/graphql/SimpleDataLoader.ts +0 -42
- package/src/advanced/graphql/index.ts +0 -22
- package/src/advanced/graphql/server.ts +0 -252
- package/src/advanced/graphql/types.ts +0 -42
- package/src/advanced/jobs/InMemoryQueueStore.ts +0 -68
- package/src/advanced/jobs/JobQueue.ts +0 -556
- package/src/advanced/jobs/RedisQueueStore.ts +0 -367
- package/src/advanced/jobs/index.ts +0 -5
- package/src/advanced/jobs/types.ts +0 -70
- package/src/advanced/observability/APMManager.ts +0 -163
- package/src/advanced/observability/AlertManager.ts +0 -109
- package/src/advanced/observability/MetricRegistry.ts +0 -151
- package/src/advanced/observability/ObservabilityCenter.ts +0 -304
- package/src/advanced/observability/StructuredLogger.ts +0 -154
- package/src/advanced/observability/TracingManager.ts +0 -117
- package/src/advanced/observability/adapters.ts +0 -304
- package/src/advanced/observability/createObservabilityMiddleware.ts +0 -63
- package/src/advanced/observability/index.ts +0 -11
- package/src/advanced/observability/types.ts +0 -174
- package/src/advanced/playground/extractPathParams.ts +0 -6
- package/src/advanced/playground/generateFieldExample.ts +0 -31
- package/src/advanced/playground/generatePlaygroundHTML.ts +0 -1956
- package/src/advanced/playground/generateSummary.ts +0 -19
- package/src/advanced/playground/getTagFromPath.ts +0 -9
- package/src/advanced/playground/index.ts +0 -8
- package/src/advanced/playground/playground.ts +0 -250
- package/src/advanced/playground/types.ts +0 -49
- package/src/advanced/playground/zodToExample.ts +0 -16
- package/src/advanced/playground/zodToParams.ts +0 -15
- package/src/advanced/postman/buildAuth.ts +0 -31
- package/src/advanced/postman/buildBody.ts +0 -15
- package/src/advanced/postman/buildQueryParams.ts +0 -27
- package/src/advanced/postman/buildRequestItem.ts +0 -36
- package/src/advanced/postman/buildResponses.ts +0 -11
- package/src/advanced/postman/buildUrl.ts +0 -33
- package/src/advanced/postman/capitalize.ts +0 -4
- package/src/advanced/postman/generateCollection.ts +0 -59
- package/src/advanced/postman/generateEnvironment.ts +0 -34
- package/src/advanced/postman/generateExampleFromZod.ts +0 -21
- package/src/advanced/postman/generateFieldExample.ts +0 -45
- package/src/advanced/postman/generateName.ts +0 -20
- package/src/advanced/postman/generateUUID.ts +0 -11
- package/src/advanced/postman/getTagFromPath.ts +0 -10
- package/src/advanced/postman/index.ts +0 -28
- package/src/advanced/postman/postman.ts +0 -156
- package/src/advanced/postman/slugify.ts +0 -7
- package/src/advanced/postman/types.ts +0 -140
- package/src/advanced/realtime/index.ts +0 -18
- package/src/advanced/realtime/websocket.ts +0 -231
- package/src/advanced/sentry/index.ts +0 -1236
- package/src/advanced/sentry/types.ts +0 -355
- package/src/advanced/static/generateDirectoryListing.ts +0 -47
- package/src/advanced/static/generateETag.ts +0 -7
- package/src/advanced/static/getMimeType.ts +0 -9
- package/src/advanced/static/index.ts +0 -32
- package/src/advanced/static/isSafePath.ts +0 -13
- package/src/advanced/static/publicDir.ts +0 -21
- package/src/advanced/static/serveStatic.ts +0 -225
- package/src/advanced/static/spa.ts +0 -24
- package/src/advanced/static/types.ts +0 -159
- package/src/advanced/swagger/SwaggerGenerator.ts +0 -66
- package/src/advanced/swagger/buildOperation.ts +0 -61
- package/src/advanced/swagger/buildParameters.ts +0 -61
- package/src/advanced/swagger/buildRequestBody.ts +0 -21
- package/src/advanced/swagger/buildResponses.ts +0 -54
- package/src/advanced/swagger/capitalize.ts +0 -5
- package/src/advanced/swagger/convertPath.ts +0 -9
- package/src/advanced/swagger/createSwagger.ts +0 -12
- package/src/advanced/swagger/generateOperationId.ts +0 -21
- package/src/advanced/swagger/generateSpec.ts +0 -105
- package/src/advanced/swagger/generateSummary.ts +0 -24
- package/src/advanced/swagger/generateSwaggerUI.ts +0 -70
- package/src/advanced/swagger/generateThemeCss.ts +0 -53
- package/src/advanced/swagger/index.ts +0 -25
- package/src/advanced/swagger/swagger.ts +0 -237
- package/src/advanced/swagger/types.ts +0 -206
- package/src/advanced/swagger/zodFieldToOpenAPI.ts +0 -94
- package/src/advanced/swagger/zodSchemaToOpenAPI.ts +0 -50
- package/src/advanced/swagger/zodToOpenAPI.ts +0 -22
- package/src/advanced/testing/factory.ts +0 -509
- package/src/advanced/testing/harness.ts +0 -612
- package/src/advanced/testing/index.ts +0 -430
- package/src/advanced/testing/load-test.ts +0 -618
- package/src/advanced/testing/mock-server.ts +0 -498
- package/src/advanced/testing/mock.ts +0 -670
- package/src/cli/bin.ts +0 -9
- package/src/cli/cli.ts +0 -158
- package/src/cli/commands/add.ts +0 -178
- package/src/cli/commands/build.ts +0 -73
- package/src/cli/commands/create.ts +0 -166
- package/src/cli/commands/dev.ts +0 -85
- package/src/cli/commands/generate.ts +0 -99
- package/src/cli/commands/help.ts +0 -95
- package/src/cli/commands/init.ts +0 -91
- package/src/cli/commands/version.ts +0 -38
- package/src/cli/index.ts +0 -6
- package/src/cli/templates/generators.ts +0 -359
- package/src/cli/templates/index.ts +0 -680
- package/src/cli/utils/exec.ts +0 -52
- package/src/cli/utils/file-system.ts +0 -78
- package/src/cli/utils/logger.ts +0 -111
- package/src/core/adapter.ts +0 -88
- package/src/core/application.ts +0 -1453
- package/src/core/context-pool.ts +0 -79
- package/src/core/context.ts +0 -856
- package/src/core/index.ts +0 -94
- package/src/core/middleware.ts +0 -272
- package/src/core/performance/buffer-pool.ts +0 -108
- package/src/core/performance/middleware-optimizer.ts +0 -162
- package/src/core/plugin/PluginManager.ts +0 -435
- package/src/core/plugin/builder.ts +0 -358
- package/src/core/plugin/index.ts +0 -50
- package/src/core/plugin/types.ts +0 -214
- package/src/core/router/file-router.ts +0 -623
- package/src/core/router/index.ts +0 -260
- package/src/core/router/radix-tree.ts +0 -242
- package/src/core/serializer.ts +0 -397
- package/src/core/store/index.ts +0 -30
- package/src/core/store/registry.ts +0 -178
- package/src/core/store/request-store.ts +0 -240
- package/src/core/store/types.ts +0 -233
- package/src/core/types.ts +0 -616
- package/src/database/adapter.ts +0 -35
- package/src/database/adapters/index.ts +0 -1
- package/src/database/adapters/mysql.ts +0 -669
- package/src/database/database.ts +0 -70
- package/src/database/dialect.ts +0 -388
- package/src/database/index.ts +0 -12
- package/src/database/migrations.ts +0 -86
- package/src/database/optimizer.ts +0 -125
- package/src/database/query-builder.ts +0 -404
- package/src/database/realtime.ts +0 -53
- package/src/database/schema.ts +0 -71
- package/src/database/transactions.ts +0 -56
- package/src/database/types.ts +0 -87
- package/src/deployment/cluster.ts +0 -471
- package/src/deployment/config.ts +0 -454
- package/src/deployment/docker.ts +0 -599
- package/src/deployment/graceful-shutdown.ts +0 -373
- package/src/deployment/index.ts +0 -56
- package/src/index.ts +0 -281
- package/src/security/adapter.ts +0 -318
- package/src/security/auth/JWTPlugin.ts +0 -234
- package/src/security/auth/JWTProvider.ts +0 -316
- package/src/security/auth/adapter.ts +0 -12
- package/src/security/auth/jwt.ts +0 -234
- package/src/security/auth/middleware.ts +0 -188
- package/src/security/csrf.ts +0 -220
- package/src/security/headers.ts +0 -108
- package/src/security/index.ts +0 -60
- package/src/security/rate-limit/adapter.ts +0 -7
- package/src/security/rate-limit/memory.ts +0 -108
- package/src/security/rate-limit/middleware.ts +0 -181
- package/src/security/sanitization.ts +0 -75
- package/src/security/types.ts +0 -240
- package/src/security/utils.ts +0 -52
- package/tsconfig.json +0 -39
package/src/core/application.ts
DELETED
|
@@ -1,1453 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Main application class
|
|
3
|
-
* Orchestrates all framework components
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { createServer, Server as HTTPServer, IncomingMessage, ServerResponse } from 'http';
|
|
7
|
-
import { Router } from './router';
|
|
8
|
-
import { ContextPool } from './context-pool';
|
|
9
|
-
import { MiddlewareExecutor } from './middleware';
|
|
10
|
-
import {
|
|
11
|
-
Context,
|
|
12
|
-
Handler,
|
|
13
|
-
Middleware,
|
|
14
|
-
Response,
|
|
15
|
-
AppConfig,
|
|
16
|
-
ErrorHandler,
|
|
17
|
-
RouteConfig,
|
|
18
|
-
Plugin,
|
|
19
|
-
HTTPMethod,
|
|
20
|
-
RouteBase,
|
|
21
|
-
Route,
|
|
22
|
-
VersioningConfig,
|
|
23
|
-
VersioningStrategy,
|
|
24
|
-
DependencyContainer,
|
|
25
|
-
InjectedRouteConfig,
|
|
26
|
-
LifecycleHooks
|
|
27
|
-
} from './types';
|
|
28
|
-
import { AdapterRegistry } from './adapter';
|
|
29
|
-
import { PluginManager, NexusPlugin, SimplePlugin } from './plugin';
|
|
30
|
-
import { FileRouter, FileRouterOptions } from './router/file-router';
|
|
31
|
-
import { createObservabilityMiddleware } from '../advanced/observability/createObservabilityMiddleware';
|
|
32
|
-
import { ObservabilityCenter } from '../advanced/observability/ObservabilityCenter';
|
|
33
|
-
import {
|
|
34
|
-
GracefulShutdownManager,
|
|
35
|
-
GracefulShutdownOptions,
|
|
36
|
-
ClusterManager,
|
|
37
|
-
ClusterOptions
|
|
38
|
-
} from '../deployment';
|
|
39
|
-
import {
|
|
40
|
-
WebSocketGateway,
|
|
41
|
-
WebSocketRouteConfig
|
|
42
|
-
} from '../advanced/realtime/websocket';
|
|
43
|
-
import { ObservabilityOptions } from '../advanced/observability/types';
|
|
44
|
-
import { StoreRegistry, StoreConstructor, ContextStore } from './store';
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Default error handler
|
|
48
|
-
*/
|
|
49
|
-
const defaultErrorHandler: ErrorHandler = (error: Error, ctx: Context): Response => {
|
|
50
|
-
// Only log unexpected errors, not intentional ones (returned from handler)
|
|
51
|
-
if (!(error as any)._isIntentional) {
|
|
52
|
-
console.error('Unhandled error:', error);
|
|
53
|
-
console.error('Path:', ctx.path, 'Method:', ctx.method);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return {
|
|
57
|
-
statusCode: 500,
|
|
58
|
-
headers: { 'Content-Type': 'application/json' },
|
|
59
|
-
body: JSON.stringify({
|
|
60
|
-
error: error.message || 'Internal Server Error',
|
|
61
|
-
stack: process.env.NODE_ENV === 'development' && !(error as any)._isIntentional ? error.stack : undefined
|
|
62
|
-
})
|
|
63
|
-
};
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Application class
|
|
68
|
-
*/
|
|
69
|
-
export class Application<TDeps extends DependencyContainer = {}> {
|
|
70
|
-
private router: Router;
|
|
71
|
-
private contextPool: ContextPool;
|
|
72
|
-
private middlewareExecutor: MiddlewareExecutor;
|
|
73
|
-
private globalMiddlewares: Middleware[] = [];
|
|
74
|
-
private errorHandler: ErrorHandler;
|
|
75
|
-
private server?: HTTPServer;
|
|
76
|
-
private config: AppConfig;
|
|
77
|
-
private adapters: AdapterRegistry;
|
|
78
|
-
private shutdownManager?: GracefulShutdownManager;
|
|
79
|
-
private clusterManager?: ClusterManager;
|
|
80
|
-
private fallbackHandler?: Handler;
|
|
81
|
-
private wsGateway?: WebSocketGateway;
|
|
82
|
-
|
|
83
|
-
// Dependency injection container
|
|
84
|
-
private dependencies: TDeps = {} as TDeps;
|
|
85
|
-
|
|
86
|
-
// Versioning properties
|
|
87
|
-
private versioningConfig?: VersioningConfig;
|
|
88
|
-
private registeredVersions: Set<string> = new Set();
|
|
89
|
-
|
|
90
|
-
// Store registry for ContextStore system
|
|
91
|
-
private storeRegistry: StoreRegistry;
|
|
92
|
-
|
|
93
|
-
// Plugin manager for advanced plugins
|
|
94
|
-
private pluginManager: PluginManager;
|
|
95
|
-
|
|
96
|
-
// Lifecycle hooks
|
|
97
|
-
private lifecycleHooks: LifecycleHooks = {};
|
|
98
|
-
|
|
99
|
-
constructor(config: AppConfig = {}) {
|
|
100
|
-
this.config = {
|
|
101
|
-
contextPoolSize: 100,
|
|
102
|
-
enableJIT: true,
|
|
103
|
-
debug: false,
|
|
104
|
-
logRequests: true,
|
|
105
|
-
...config
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
this.router = new Router();
|
|
109
|
-
this.contextPool = new ContextPool(this.config.contextPoolSize);
|
|
110
|
-
this.middlewareExecutor = new MiddlewareExecutor();
|
|
111
|
-
this.errorHandler = config.onError || defaultErrorHandler;
|
|
112
|
-
this.adapters = new AdapterRegistry();
|
|
113
|
-
this.storeRegistry = new StoreRegistry({ debug: config.debug });
|
|
114
|
-
this.pluginManager = new PluginManager(this, { debug: config.debug });
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Register lifecycle hooks for request processing
|
|
119
|
-
* Hooks are called at specific points during request lifecycle
|
|
120
|
-
*
|
|
121
|
-
* @example
|
|
122
|
-
* ```typescript
|
|
123
|
-
* app.hooks({
|
|
124
|
-
* onRequest: async (ctx) => {
|
|
125
|
-
* ctx.requestId = crypto.randomUUID();
|
|
126
|
-
* console.log(`[${ctx.requestId}] ${ctx.method} ${ctx.path}`);
|
|
127
|
-
* },
|
|
128
|
-
*
|
|
129
|
-
* beforeValidation: async (ctx) => {
|
|
130
|
-
* // Transform raw body before validation
|
|
131
|
-
* if (ctx.body?.data) {
|
|
132
|
-
* ctx.body = ctx.body.data;
|
|
133
|
-
* }
|
|
134
|
-
* },
|
|
135
|
-
*
|
|
136
|
-
* afterValidation: async (ctx) => {
|
|
137
|
-
* // Check authorization after body is validated
|
|
138
|
-
* if (!ctx.headers.authorization) {
|
|
139
|
-
* return ctx.response.status(401).json({ error: 'Unauthorized' });
|
|
140
|
-
* }
|
|
141
|
-
* },
|
|
142
|
-
*
|
|
143
|
-
* beforeHandler: async (ctx) => {
|
|
144
|
-
* // Load user from session
|
|
145
|
-
* ctx.user = await getUserFromToken(ctx.headers.authorization);
|
|
146
|
-
* },
|
|
147
|
-
*
|
|
148
|
-
* afterHandler: async (ctx, result) => {
|
|
149
|
-
* // Add metadata to all responses
|
|
150
|
-
* return { ...result, timestamp: Date.now(), requestId: ctx.requestId };
|
|
151
|
-
* },
|
|
152
|
-
*
|
|
153
|
-
* onError: async (ctx, error) => {
|
|
154
|
-
* // Log errors to external service
|
|
155
|
-
* await logToSentry(error, { requestId: ctx.requestId });
|
|
156
|
-
* },
|
|
157
|
-
*
|
|
158
|
-
* onResponse: async (ctx, response) => {
|
|
159
|
-
* console.log(`[${ctx.requestId}] Response: ${response.statusCode}`);
|
|
160
|
-
* }
|
|
161
|
-
* });
|
|
162
|
-
* ```
|
|
163
|
-
*/
|
|
164
|
-
hooks(hooks: LifecycleHooks): this {
|
|
165
|
-
this.lifecycleHooks = { ...this.lifecycleHooks, ...hooks };
|
|
166
|
-
return this;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Set a fallback handler for unmatched routes (e.g., static files)
|
|
171
|
-
* Called before returning 404
|
|
172
|
-
*/
|
|
173
|
-
setFallbackHandler(handler: Handler): this {
|
|
174
|
-
this.fallbackHandler = handler;
|
|
175
|
-
return this;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Provide dependencies for injection into route handlers
|
|
180
|
-
* Dependencies are available via the second parameter in handlers
|
|
181
|
-
*
|
|
182
|
-
* @example
|
|
183
|
-
* ```typescript
|
|
184
|
-
* const db = new Database();
|
|
185
|
-
* const cache = new Redis();
|
|
186
|
-
* const mailer = new Mailer();
|
|
187
|
-
*
|
|
188
|
-
* const app = createApp()
|
|
189
|
-
* .provide({ db, cache, mailer });
|
|
190
|
-
*
|
|
191
|
-
* // Use with inject option
|
|
192
|
-
* app.get('/users', {
|
|
193
|
-
* inject: ['db', 'cache'],
|
|
194
|
-
* handler: async (ctx, { db, cache }) => {
|
|
195
|
-
* // db and cache are fully typed!
|
|
196
|
-
* const users = await db.query('SELECT * FROM users');
|
|
197
|
-
* await cache.set('users', users);
|
|
198
|
-
* return { users };
|
|
199
|
-
* }
|
|
200
|
-
* });
|
|
201
|
-
*
|
|
202
|
-
* // Or access all dependencies
|
|
203
|
-
* app.get('/mail', async (ctx, deps) => {
|
|
204
|
-
* await deps.mailer.send({ to: 'user@example.com', subject: 'Hello' });
|
|
205
|
-
* return { sent: true };
|
|
206
|
-
* });
|
|
207
|
-
* ```
|
|
208
|
-
*/
|
|
209
|
-
provide<T extends DependencyContainer>(deps: T): Application<TDeps & T> {
|
|
210
|
-
this.dependencies = { ...this.dependencies, ...deps } as TDeps & T;
|
|
211
|
-
return this as unknown as Application<TDeps & T>;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Get a specific dependency by name
|
|
216
|
-
*
|
|
217
|
-
* @example
|
|
218
|
-
* ```typescript
|
|
219
|
-
* const db = app.getDep('db');
|
|
220
|
-
* ```
|
|
221
|
-
*/
|
|
222
|
-
getDep<K extends keyof TDeps>(name: K): TDeps[K] {
|
|
223
|
-
return this.dependencies[name];
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Get all registered dependencies
|
|
228
|
-
*/
|
|
229
|
-
getDeps(): TDeps {
|
|
230
|
-
return this.dependencies;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Configure API versioning
|
|
235
|
-
*
|
|
236
|
-
* @example
|
|
237
|
-
* ```typescript
|
|
238
|
-
* app.configVersions({
|
|
239
|
-
* strategies: ['header', 'query'],
|
|
240
|
-
* header: 'api-version',
|
|
241
|
-
* queryParam: 'v',
|
|
242
|
-
* defaultVersion: 'v1',
|
|
243
|
-
* register: ['v1', 'v2']
|
|
244
|
-
* });
|
|
245
|
-
*
|
|
246
|
-
* // Route tanpa prefix → otomatis jadi /{defaultVersion}/login
|
|
247
|
-
* app.post('/login', handler);
|
|
248
|
-
*
|
|
249
|
-
* // Route dengan prefix explicit
|
|
250
|
-
* app.post('/v2/login', handler);
|
|
251
|
-
* ```
|
|
252
|
-
*/
|
|
253
|
-
configVersions(config: VersioningConfig): this {
|
|
254
|
-
this.versioningConfig = {
|
|
255
|
-
header: 'api-version',
|
|
256
|
-
queryParam: 'v',
|
|
257
|
-
...config
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
// Register all versions
|
|
261
|
-
config.register.forEach(v => this.registeredVersions.add(v));
|
|
262
|
-
|
|
263
|
-
return this;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Check if a path starts with a registered version prefix
|
|
268
|
-
*/
|
|
269
|
-
private hasVersionPrefix(path: string): string | null {
|
|
270
|
-
const segment = path.split('/').filter(Boolean)[0];
|
|
271
|
-
if (segment && this.registeredVersions.has(segment)) {
|
|
272
|
-
return segment;
|
|
273
|
-
}
|
|
274
|
-
return null;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Resolve version from request context
|
|
279
|
-
*/
|
|
280
|
-
private resolveVersion(ctx: Context): { version: string; basePath: string; source: VersioningStrategy | 'default' } {
|
|
281
|
-
if (!this.versioningConfig) {
|
|
282
|
-
return { version: '', basePath: ctx.path, source: 'default' };
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const { strategies, header, queryParam, defaultVersion } = this.versioningConfig;
|
|
286
|
-
|
|
287
|
-
// 1. Check path strategy: /v1/login
|
|
288
|
-
if (strategies.includes('path')) {
|
|
289
|
-
const versionPrefix = this.hasVersionPrefix(ctx.path);
|
|
290
|
-
if (versionPrefix) {
|
|
291
|
-
const basePath = ctx.path.replace(new RegExp(`^/${versionPrefix}`), '') || '/';
|
|
292
|
-
return { version: versionPrefix, basePath, source: 'path' };
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// 2. Check header strategy
|
|
297
|
-
if (strategies.includes('header') && header) {
|
|
298
|
-
const headerValue = ctx.headers[header] || ctx.headers[header.toLowerCase()];
|
|
299
|
-
const version = Array.isArray(headerValue) ? headerValue[0] : headerValue;
|
|
300
|
-
if (version && this.registeredVersions.has(version)) {
|
|
301
|
-
return { version, basePath: ctx.path, source: 'header' };
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// 3. Check query strategy
|
|
306
|
-
if (strategies.includes('query') && queryParam) {
|
|
307
|
-
const queryVersion = ctx.query?.[queryParam];
|
|
308
|
-
const version = Array.isArray(queryVersion) ? queryVersion[0] : queryVersion;
|
|
309
|
-
if (version && this.registeredVersions.has(version)) {
|
|
310
|
-
return { version, basePath: ctx.path, source: 'query' };
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// 4. Default version
|
|
315
|
-
return { version: defaultVersion, basePath: ctx.path, source: 'default' };
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* Build versioned path for route registration
|
|
320
|
-
*/
|
|
321
|
-
private buildVersionedPath(path: string): string {
|
|
322
|
-
if (!this.versioningConfig) return path;
|
|
323
|
-
|
|
324
|
-
// If path already has version prefix, return as-is
|
|
325
|
-
if (this.hasVersionPrefix(path)) {
|
|
326
|
-
return path;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Add default version prefix
|
|
330
|
-
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
331
|
-
return `/${this.versioningConfig.defaultVersion}${normalizedPath}`;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Add global middleware or mount a router
|
|
336
|
-
*
|
|
337
|
-
* @example
|
|
338
|
-
* ```typescript
|
|
339
|
-
* // Add middleware
|
|
340
|
-
* app.use(loggerMiddleware);
|
|
341
|
-
* app.use(corsMiddleware);
|
|
342
|
-
*
|
|
343
|
-
* // Mount router
|
|
344
|
-
* const routes = new Router();
|
|
345
|
-
* routes.get('/users', getUsers);
|
|
346
|
-
* app.use(routes);
|
|
347
|
-
*
|
|
348
|
-
* // Mount router with prefix
|
|
349
|
-
* app.use('/api', routes);
|
|
350
|
-
* ```
|
|
351
|
-
*/
|
|
352
|
-
use(middlewareOrRouter: Middleware | Router): this;
|
|
353
|
-
use(prefix: string, router: Router): this;
|
|
354
|
-
use(middlewareOrPrefixOrRouter: Middleware | Router | string, router?: Router): this {
|
|
355
|
-
// app.use('/api', router)
|
|
356
|
-
if (typeof middlewareOrPrefixOrRouter === 'string' && router) {
|
|
357
|
-
this.mountRouter(middlewareOrPrefixOrRouter, router);
|
|
358
|
-
return this;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// app.use(router)
|
|
362
|
-
if (middlewareOrPrefixOrRouter instanceof Router) {
|
|
363
|
-
this.mountRouter('', middlewareOrPrefixOrRouter);
|
|
364
|
-
return this;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// app.use(middleware)
|
|
368
|
-
this.globalMiddlewares.push(middlewareOrPrefixOrRouter as Middleware);
|
|
369
|
-
return this;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* Mount a router's routes into the application
|
|
374
|
-
*/
|
|
375
|
-
private mountRouter(prefix: string, router: Router): void {
|
|
376
|
-
const routes = router.getRawRoutes();
|
|
377
|
-
for (const route of routes) {
|
|
378
|
-
const fullPath = prefix ? `${prefix}${route.path}` : route.path;
|
|
379
|
-
this.router.addRoute({
|
|
380
|
-
method: route.method,
|
|
381
|
-
path: fullPath,
|
|
382
|
-
handler: route.config.handler,
|
|
383
|
-
middlewares: route.config.middlewares,
|
|
384
|
-
schema: route.config.schema,
|
|
385
|
-
meta: route.config.meta
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* Register ContextStore classes
|
|
392
|
-
* Stores are singleton instances accessible via ctx.store(StoreClass)
|
|
393
|
-
*
|
|
394
|
-
* @example
|
|
395
|
-
* ```typescript
|
|
396
|
-
* class UserStore extends ContextStore<UserState> {
|
|
397
|
-
* protected initial() { return { users: [], loading: false }; }
|
|
398
|
-
*
|
|
399
|
-
* async fetchUsers() {
|
|
400
|
-
* this.update({ loading: true });
|
|
401
|
-
* const users = await api.getUsers();
|
|
402
|
-
* this.set({ users, loading: false });
|
|
403
|
-
* }
|
|
404
|
-
* }
|
|
405
|
-
*
|
|
406
|
-
* const app = createApp();
|
|
407
|
-
* app.stores([UserStore, ProductStore]);
|
|
408
|
-
*
|
|
409
|
-
* app.get('/users', async (ctx) => {
|
|
410
|
-
* const userStore = ctx.store(UserStore);
|
|
411
|
-
* return { users: userStore.state.users };
|
|
412
|
-
* });
|
|
413
|
-
* ```
|
|
414
|
-
*/
|
|
415
|
-
stores(storeClasses: StoreConstructor<any>[]): this {
|
|
416
|
-
this.storeRegistry.registerAll(storeClasses);
|
|
417
|
-
return this;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
/**
|
|
421
|
-
* Register a single ContextStore class
|
|
422
|
-
*
|
|
423
|
-
* @example
|
|
424
|
-
* ```typescript
|
|
425
|
-
* app.store(UserStore);
|
|
426
|
-
* ```
|
|
427
|
-
*/
|
|
428
|
-
store<T extends ContextStore<any>>(StoreClass: StoreConstructor<T>): this {
|
|
429
|
-
this.storeRegistry.register(StoreClass);
|
|
430
|
-
return this;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
/**
|
|
434
|
-
* Get a store instance directly from application level
|
|
435
|
-
* Useful for accessing stores outside of request context
|
|
436
|
-
*
|
|
437
|
-
* @example
|
|
438
|
-
* ```typescript
|
|
439
|
-
* const userStore = app.getStore(UserStore);
|
|
440
|
-
* userStore.listen((state) => console.log('Users updated:', state));
|
|
441
|
-
* ```
|
|
442
|
-
*/
|
|
443
|
-
getStore<T extends ContextStore<any>>(StoreClass: StoreConstructor<T>): T {
|
|
444
|
-
return this.storeRegistry.get(StoreClass);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
/**
|
|
448
|
-
* Get the store registry for advanced usage
|
|
449
|
-
*/
|
|
450
|
-
getStoreRegistry(): StoreRegistry {
|
|
451
|
-
return this.storeRegistry;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
/**
|
|
455
|
-
* Register a route
|
|
456
|
-
*/
|
|
457
|
-
route(config: RouteConfig): this {
|
|
458
|
-
this.router.addRoute(config);
|
|
459
|
-
return this;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
/**
|
|
463
|
-
* Convenience: GET route
|
|
464
|
-
* Supports both function-style and class-based routing
|
|
465
|
-
*
|
|
466
|
-
* @example
|
|
467
|
-
* ```typescript
|
|
468
|
-
* // Function style (with dependencies)
|
|
469
|
-
* app.get('/users', async (ctx, { db }) => ({ users: await db.getUsers() }));
|
|
470
|
-
*
|
|
471
|
-
* // Config style with inject
|
|
472
|
-
* app.get('/users', {
|
|
473
|
-
* inject: ['db', 'cache'],
|
|
474
|
-
* handler: async (ctx, { db, cache }) => ({ users: [] }),
|
|
475
|
-
* meta: {...}
|
|
476
|
-
* });
|
|
477
|
-
*
|
|
478
|
-
* // Class-based style
|
|
479
|
-
* app.get(new UserListRoute());
|
|
480
|
-
* ```
|
|
481
|
-
*/
|
|
482
|
-
get<K extends keyof TDeps = keyof TDeps>(
|
|
483
|
-
pathOrRoute: string | Route,
|
|
484
|
-
handlerOrConfig?: Handler<Context, TDeps> | InjectedRouteConfig<Context, TDeps, K>
|
|
485
|
-
): this {
|
|
486
|
-
// Class-based routing
|
|
487
|
-
if (typeof pathOrRoute === 'object' && ('pathName' in pathOrRoute || 'handler' in pathOrRoute)) {
|
|
488
|
-
return this.registerClassRoute('GET', pathOrRoute as Route);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
return this.registerVersionedRoute('GET', pathOrRoute as string, handlerOrConfig);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
/**
|
|
495
|
-
* Convenience: POST route
|
|
496
|
-
* Supports both function-style and class-based routing
|
|
497
|
-
*/
|
|
498
|
-
post<K extends keyof TDeps = keyof TDeps>(
|
|
499
|
-
pathOrRoute: string | Route,
|
|
500
|
-
handlerOrConfig?: Handler<Context, TDeps> | InjectedRouteConfig<Context, TDeps, K>
|
|
501
|
-
): this {
|
|
502
|
-
// Class-based routing
|
|
503
|
-
if (typeof pathOrRoute === 'object' && ('pathName' in pathOrRoute || 'handler' in pathOrRoute)) {
|
|
504
|
-
return this.registerClassRoute('POST', pathOrRoute as Route);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
return this.registerVersionedRoute('POST', pathOrRoute as string, handlerOrConfig);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
/**
|
|
511
|
-
* Convenience: PUT route
|
|
512
|
-
* Supports both function-style and class-based routing
|
|
513
|
-
*/
|
|
514
|
-
put<K extends keyof TDeps = keyof TDeps>(
|
|
515
|
-
pathOrRoute: string | Route,
|
|
516
|
-
handlerOrConfig?: Handler<Context, TDeps> | InjectedRouteConfig<Context, TDeps, K>
|
|
517
|
-
): this {
|
|
518
|
-
// Class-based routing
|
|
519
|
-
if (typeof pathOrRoute === 'object' && ('pathName' in pathOrRoute || 'handler' in pathOrRoute)) {
|
|
520
|
-
return this.registerClassRoute('PUT', pathOrRoute as Route);
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
return this.registerVersionedRoute('PUT', pathOrRoute as string, handlerOrConfig);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/**
|
|
527
|
-
* Convenience: DELETE route
|
|
528
|
-
* Supports both function-style and class-based routing
|
|
529
|
-
*/
|
|
530
|
-
delete<K extends keyof TDeps = keyof TDeps>(
|
|
531
|
-
pathOrRoute: string | Route,
|
|
532
|
-
handlerOrConfig?: Handler<Context, TDeps> | InjectedRouteConfig<Context, TDeps, K>
|
|
533
|
-
): this {
|
|
534
|
-
// Class-based routing
|
|
535
|
-
if (typeof pathOrRoute === 'object' && ('pathName' in pathOrRoute || 'handler' in pathOrRoute)) {
|
|
536
|
-
return this.registerClassRoute('DELETE', pathOrRoute as Route);
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
return this.registerVersionedRoute('DELETE', pathOrRoute as string, handlerOrConfig);
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
/**
|
|
543
|
-
* Convenience: PATCH route
|
|
544
|
-
* Supports both function-style and class-based routing
|
|
545
|
-
*/
|
|
546
|
-
patch<K extends keyof TDeps = keyof TDeps>(
|
|
547
|
-
pathOrRoute: string | Route,
|
|
548
|
-
handlerOrConfig?: Handler<Context, TDeps> | InjectedRouteConfig<Context, TDeps, K>
|
|
549
|
-
): this {
|
|
550
|
-
// Class-based routing
|
|
551
|
-
if (typeof pathOrRoute === 'object' && ('pathName' in pathOrRoute || 'handler' in pathOrRoute)) {
|
|
552
|
-
return this.registerClassRoute('PATCH', pathOrRoute as Route);
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
return this.registerVersionedRoute('PATCH', pathOrRoute as string, handlerOrConfig);
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
/**
|
|
559
|
-
* Register a versioned route (internal)
|
|
560
|
-
*/
|
|
561
|
-
private registerVersionedRoute<K extends keyof TDeps>(
|
|
562
|
-
method: HTTPMethod,
|
|
563
|
-
path: string,
|
|
564
|
-
handlerOrConfig?: Handler<Context, TDeps> | InjectedRouteConfig<Context, TDeps, K>
|
|
565
|
-
): this {
|
|
566
|
-
// Build versioned path if versioning is configured
|
|
567
|
-
const versionedPath = this.buildVersionedPath(path);
|
|
568
|
-
|
|
569
|
-
if (typeof handlerOrConfig === 'function') {
|
|
570
|
-
// Function handler - wrap to inject all dependencies
|
|
571
|
-
const originalHandler = handlerOrConfig;
|
|
572
|
-
const deps = this.dependencies;
|
|
573
|
-
const wrappedHandler: Handler = async (ctx) => originalHandler(ctx, deps);
|
|
574
|
-
this.router.addRoute({ method, path: versionedPath, handler: wrappedHandler });
|
|
575
|
-
} else if (handlerOrConfig) {
|
|
576
|
-
// Config object with possible inject option
|
|
577
|
-
const config = handlerOrConfig as InjectedRouteConfig<Context, TDeps, K>;
|
|
578
|
-
const originalHandler = config.handler;
|
|
579
|
-
const allDeps = this.dependencies;
|
|
580
|
-
const injectKeys = config.inject;
|
|
581
|
-
|
|
582
|
-
// Create wrapped handler that injects dependencies
|
|
583
|
-
const wrappedHandler: Handler = async (ctx) => {
|
|
584
|
-
let injectedDeps: any;
|
|
585
|
-
if (injectKeys && injectKeys.length > 0) {
|
|
586
|
-
// Only inject specified dependencies
|
|
587
|
-
injectedDeps = {} as Pick<TDeps, K>;
|
|
588
|
-
for (const key of injectKeys) {
|
|
589
|
-
(injectedDeps as any)[key] = allDeps[key];
|
|
590
|
-
}
|
|
591
|
-
} else {
|
|
592
|
-
// Inject all dependencies
|
|
593
|
-
injectedDeps = allDeps;
|
|
594
|
-
}
|
|
595
|
-
return originalHandler(ctx, injectedDeps);
|
|
596
|
-
};
|
|
597
|
-
|
|
598
|
-
this.router.addRoute({
|
|
599
|
-
method,
|
|
600
|
-
path: versionedPath,
|
|
601
|
-
handler: wrappedHandler,
|
|
602
|
-
middlewares: config.middlewares,
|
|
603
|
-
schema: config.schema,
|
|
604
|
-
meta: config.meta
|
|
605
|
-
});
|
|
606
|
-
}
|
|
607
|
-
return this;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
/**
|
|
611
|
-
* Register a class-based route with lifecycle hooks support and dependency injection
|
|
612
|
-
*/
|
|
613
|
-
private registerClassRoute(method: HTTPMethod, route: Route): this {
|
|
614
|
-
// ⚠️ ENFORCE: Route class MUST extend Route abstract class
|
|
615
|
-
if (!(route instanceof Route)) {
|
|
616
|
-
const className = (route as any).constructor?.name || 'Unknown';
|
|
617
|
-
throw new Error(
|
|
618
|
-
`Route class "${className}" must extend the Route abstract class.\n` +
|
|
619
|
-
`Example:\n` +
|
|
620
|
-
` import { Route } from 'nexus';\n` +
|
|
621
|
-
` class ${className} extends Route {\n` +
|
|
622
|
-
` pathName = '/your/path';\n` +
|
|
623
|
-
` async handler(ctx) { ... }\n` +
|
|
624
|
-
` }`
|
|
625
|
-
);
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
if (!route.pathName) {
|
|
629
|
-
throw new Error(
|
|
630
|
-
`Route class must have a pathName property when using manual registration. ` +
|
|
631
|
-
`Use app.useFileRoutes() for file-based routing without pathName.`
|
|
632
|
-
);
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
// Determine the handler - either the handler method or the method-specific handler
|
|
636
|
-
let originalHandler = route.handler;
|
|
637
|
-
if (!originalHandler && typeof (route as any)[method] === 'function') {
|
|
638
|
-
originalHandler = (route as any)[method].bind(route);
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
if (!originalHandler) {
|
|
642
|
-
throw new Error(
|
|
643
|
-
`Route class must have a handler method or a ${method} method.`
|
|
644
|
-
);
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
// Wrap handler with lifecycle hooks if route extends Route class
|
|
648
|
-
const routeInstance = route as Route;
|
|
649
|
-
const hasHooks = typeof routeInstance.onBefore === 'function' ||
|
|
650
|
-
typeof routeInstance.onAfter === 'function' ||
|
|
651
|
-
typeof routeInstance.onError === 'function';
|
|
652
|
-
|
|
653
|
-
// Capture dependencies for injection
|
|
654
|
-
const deps = this.dependencies;
|
|
655
|
-
let finalHandler: Handler;
|
|
656
|
-
|
|
657
|
-
if (hasHooks) {
|
|
658
|
-
const boundOriginalHandler = originalHandler.bind(route);
|
|
659
|
-
const onBefore = routeInstance.onBefore?.bind(route);
|
|
660
|
-
const onAfter = routeInstance.onAfter?.bind(route);
|
|
661
|
-
const onError = routeInstance.onError?.bind(route);
|
|
662
|
-
|
|
663
|
-
finalHandler = async (ctx: Context) => {
|
|
664
|
-
try {
|
|
665
|
-
// Run onBefore hook (with deps)
|
|
666
|
-
if (onBefore) {
|
|
667
|
-
const beforeResult = await onBefore(ctx, deps);
|
|
668
|
-
// If onBefore returns a value (not undefined), skip handler
|
|
669
|
-
if (beforeResult !== undefined) {
|
|
670
|
-
return beforeResult;
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
// Run the main handler with dependencies
|
|
675
|
-
let result = await boundOriginalHandler(ctx, deps);
|
|
676
|
-
|
|
677
|
-
// Run onAfter hook (with deps)
|
|
678
|
-
if (onAfter) {
|
|
679
|
-
result = await onAfter(ctx, result, deps);
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
return result;
|
|
683
|
-
} catch (error) {
|
|
684
|
-
// Run onError hook if defined (with deps)
|
|
685
|
-
if (onError) {
|
|
686
|
-
return await onError(ctx, error as Error, deps);
|
|
687
|
-
}
|
|
688
|
-
// Re-throw if no onError handler
|
|
689
|
-
throw error;
|
|
690
|
-
}
|
|
691
|
-
};
|
|
692
|
-
} else {
|
|
693
|
-
// No hooks - just wrap with dependency injection
|
|
694
|
-
const boundHandler = originalHandler.bind(route);
|
|
695
|
-
finalHandler = async (ctx: Context) => boundHandler(ctx, deps);
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
const config: RouteConfig = {
|
|
699
|
-
method,
|
|
700
|
-
path: route.pathName,
|
|
701
|
-
handler: finalHandler,
|
|
702
|
-
schema: route.schema?.(),
|
|
703
|
-
meta: route.meta?.(),
|
|
704
|
-
middlewares: route.middlewares?.()
|
|
705
|
-
};
|
|
706
|
-
this.router.addRoute(config);
|
|
707
|
-
return this;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
/**
|
|
711
|
-
* Register a WebSocket route
|
|
712
|
-
*
|
|
713
|
-
* @example
|
|
714
|
-
* ```typescript
|
|
715
|
-
* app.ws('/ws/chat', {
|
|
716
|
-
* auth: async (ctx) => validateToken(ctx.query.token),
|
|
717
|
-
* onConnect: async (socket, ctx) => {
|
|
718
|
-
* console.log('User connected:', ctx.user);
|
|
719
|
-
* },
|
|
720
|
-
* onMessage: async (socket, message, ctx) => {
|
|
721
|
-
* socket.send(JSON.stringify({ echo: message }));
|
|
722
|
-
* },
|
|
723
|
-
* onClose: async (socket, ctx) => {
|
|
724
|
-
* console.log('User disconnected');
|
|
725
|
-
* }
|
|
726
|
-
* });
|
|
727
|
-
* ```
|
|
728
|
-
*/
|
|
729
|
-
ws(path: string, config: WebSocketRouteConfig): this {
|
|
730
|
-
if (!this.wsGateway) {
|
|
731
|
-
this.wsGateway = new WebSocketGateway();
|
|
732
|
-
}
|
|
733
|
-
this.wsGateway.register(path, config);
|
|
734
|
-
return this;
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
/**
|
|
738
|
-
* Get the WebSocket gateway for advanced usage (rooms, broadcast, etc.)
|
|
739
|
-
*
|
|
740
|
-
* @example
|
|
741
|
-
* ```typescript
|
|
742
|
-
* const ws = app.getWebSocket();
|
|
743
|
-
* ws.broadcast('room-name', { type: 'notification', message: 'Hello!' });
|
|
744
|
-
* ws.joinRoom('room-name', socket);
|
|
745
|
-
* ```
|
|
746
|
-
*/
|
|
747
|
-
getWebSocket(): WebSocketGateway | undefined {
|
|
748
|
-
return this.wsGateway;
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
/**
|
|
752
|
-
* Use file-based routing (Next.js style)
|
|
753
|
-
*
|
|
754
|
-
* Automatically scans a directory and registers routes based on file/folder structure.
|
|
755
|
-
* Supports dynamic parameters [id], catch-all [...slug], and nested routes.
|
|
756
|
-
*
|
|
757
|
-
* @example
|
|
758
|
-
* ```typescript
|
|
759
|
-
* // Folder structure:
|
|
760
|
-
* // routes/
|
|
761
|
-
* // api/
|
|
762
|
-
* // users/
|
|
763
|
-
* // index.ts → GET/POST /api/users
|
|
764
|
-
* // [id]/
|
|
765
|
-
* // index.ts → GET/PUT/DELETE /api/users/:id
|
|
766
|
-
* // posts.ts → GET /api/users/:id/posts
|
|
767
|
-
*
|
|
768
|
-
* const app = createApp();
|
|
769
|
-
*
|
|
770
|
-
* await app.useFileRoutes({
|
|
771
|
-
* dir: './src/routes',
|
|
772
|
-
* prefix: '',
|
|
773
|
-
* debug: true
|
|
774
|
-
* });
|
|
775
|
-
*
|
|
776
|
-
* app.listen(3000);
|
|
777
|
-
* ```
|
|
778
|
-
*
|
|
779
|
-
* Route file format (function-style):
|
|
780
|
-
* ```typescript
|
|
781
|
-
* // routes/api/users/index.ts
|
|
782
|
-
* import { Context } from '@engjts/nexus';
|
|
783
|
-
*
|
|
784
|
-
* export async function GET(ctx: Context) {
|
|
785
|
-
* return ctx.json({ users: [] });
|
|
786
|
-
* }
|
|
787
|
-
*
|
|
788
|
-
* export async function POST(ctx: Context) {
|
|
789
|
-
* const body = ctx.body;
|
|
790
|
-
* return ctx.json({ created: body }, 201);
|
|
791
|
-
* }
|
|
792
|
-
*
|
|
793
|
-
* export const schema = { body: z.object({ name: z.string() }) };
|
|
794
|
-
* export const meta = { summary: 'User endpoints', tags: ['Users'] };
|
|
795
|
-
* ```
|
|
796
|
-
*
|
|
797
|
-
* Route file format (class-style):
|
|
798
|
-
* ```typescript
|
|
799
|
-
* // routes/api/users/[id]/index.ts
|
|
800
|
-
* import { RouteBase, Context } from '@engjts/nexus';
|
|
801
|
-
*
|
|
802
|
-
* export default class UserRoute implements RouteBase {
|
|
803
|
-
* method = ['GET', 'PUT', 'DELETE'] as const;
|
|
804
|
-
*
|
|
805
|
-
* async GET(ctx: Context) {
|
|
806
|
-
* return ctx.json({ user: { id: ctx.params.id } });
|
|
807
|
-
* }
|
|
808
|
-
*
|
|
809
|
-
* async PUT(ctx: Context) {
|
|
810
|
-
* return ctx.json({ updated: true });
|
|
811
|
-
* }
|
|
812
|
-
*
|
|
813
|
-
* async DELETE(ctx: Context) {
|
|
814
|
-
* return ctx.json({ deleted: true });
|
|
815
|
-
* }
|
|
816
|
-
* }
|
|
817
|
-
* ```
|
|
818
|
-
*/
|
|
819
|
-
async useFileRoutes(options: FileRouterOptions): Promise<this> {
|
|
820
|
-
const router = new FileRouter(options);
|
|
821
|
-
await router.register(this);
|
|
822
|
-
return this;
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
/**
|
|
826
|
-
* Set custom error handler
|
|
827
|
-
*/
|
|
828
|
-
onError(handler: ErrorHandler): this {
|
|
829
|
-
this.errorHandler = handler;
|
|
830
|
-
return this;
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
/**
|
|
834
|
-
* Install a plugin (supports both legacy and new plugin formats)
|
|
835
|
-
*
|
|
836
|
-
* @example
|
|
837
|
-
* ```typescript
|
|
838
|
-
* // Legacy plugin
|
|
839
|
-
* app.plugin({ name: 'my-plugin', version: '1.0.0', install: (app) => {} });
|
|
840
|
-
*
|
|
841
|
-
* // New advanced plugin
|
|
842
|
-
* const authPlugin = definePlugin('auth')
|
|
843
|
-
* .version('1.0.0')
|
|
844
|
-
* .config<{ secret: string }>()
|
|
845
|
-
* .register(ctx => ctx.app.use(jwtMiddleware))
|
|
846
|
-
* .build();
|
|
847
|
-
*
|
|
848
|
-
* app.plugin(authPlugin, { secret: 'my-secret' });
|
|
849
|
-
* ```
|
|
850
|
-
*/
|
|
851
|
-
plugin<TConfig = any>(
|
|
852
|
-
plugin: Plugin | NexusPlugin<TConfig> | SimplePlugin,
|
|
853
|
-
config?: TConfig
|
|
854
|
-
): this {
|
|
855
|
-
// Check if it's a legacy Plugin with install method directly on app
|
|
856
|
-
if ('install' in plugin && !('meta' in plugin)) {
|
|
857
|
-
// Legacy format - call install directly
|
|
858
|
-
(plugin as Plugin).install(this);
|
|
859
|
-
} else {
|
|
860
|
-
// New plugin format - use PluginManager
|
|
861
|
-
this.pluginManager.add(plugin as NexusPlugin<TConfig>, config);
|
|
862
|
-
}
|
|
863
|
-
return this;
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
/**
|
|
867
|
-
* Get the plugin manager for advanced plugin operations
|
|
868
|
-
*
|
|
869
|
-
* @example
|
|
870
|
-
* ```typescript
|
|
871
|
-
* const pm = app.getPluginManager();
|
|
872
|
-
*
|
|
873
|
-
* // Check if plugin exists
|
|
874
|
-
* if (pm.has('auth-plugin')) {
|
|
875
|
-
* const authApi = pm.getExports<AuthAPI>('auth-plugin');
|
|
876
|
-
* authApi.verify(token);
|
|
877
|
-
* }
|
|
878
|
-
*
|
|
879
|
-
* // Listen to plugin events
|
|
880
|
-
* pm.on('plugin:ready', (meta) => {
|
|
881
|
-
* console.log(`Plugin ${meta.name} is ready`);
|
|
882
|
-
* });
|
|
883
|
-
* ```
|
|
884
|
-
*/
|
|
885
|
-
getPluginManager(): PluginManager {
|
|
886
|
-
return this.pluginManager;
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
/**
|
|
890
|
-
* Get exports from a specific plugin
|
|
891
|
-
* Shorthand for getPluginManager().getExports()
|
|
892
|
-
*/
|
|
893
|
-
getPluginExports<T = any>(pluginName: string): T | undefined {
|
|
894
|
-
return this.pluginManager.getExports<T>(pluginName);
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
/**
|
|
898
|
-
* Check if a plugin is installed
|
|
899
|
-
*/
|
|
900
|
-
hasPlugin(name: string): boolean {
|
|
901
|
-
return this.pluginManager.has(name);
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
/**
|
|
905
|
-
* Register an adapter
|
|
906
|
-
*/
|
|
907
|
-
adapter<T>(name: string, adapter: T): this {
|
|
908
|
-
this.adapters.register(name, adapter);
|
|
909
|
-
return this;
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
/**
|
|
913
|
-
* Enable built-in observability (metrics, tracing, health checks)
|
|
914
|
-
* with safe, configurable endpoints.
|
|
915
|
-
*/
|
|
916
|
-
observe(options: ObservabilityOptions = {}): this {
|
|
917
|
-
const center = new ObservabilityCenter(options);
|
|
918
|
-
|
|
919
|
-
// Attach middleware for metrics/tracing/logging
|
|
920
|
-
this.use(createObservabilityMiddleware(center, options));
|
|
921
|
-
|
|
922
|
-
const existingRoutes = this.router.getRoutes();
|
|
923
|
-
|
|
924
|
-
// Metrics endpoint (default: /__nexus/metrics)
|
|
925
|
-
const metricsEnabled = options.metrics?.enabled ?? true;
|
|
926
|
-
if (metricsEnabled) {
|
|
927
|
-
const metricsPath = options.metrics?.endpoint ?? '/__nexus/metrics';
|
|
928
|
-
this.ensureNoRouteConflict('GET', metricsPath, existingRoutes);
|
|
929
|
-
this.get(metricsPath, async (ctx) => center.metricsHandler()(ctx));
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
// Health endpoint (default: /__nexus/health)
|
|
933
|
-
if (options.health) {
|
|
934
|
-
const healthPath = options.health.endpoint ?? '/__nexus/health';
|
|
935
|
-
this.ensureNoRouteConflict('GET', healthPath, existingRoutes);
|
|
936
|
-
this.get(healthPath, async (ctx) => center.healthHandler()(ctx));
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
return this;
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
private ensureNoRouteConflict(method: HTTPMethod, path: string, routes: Array<{ method: string; path: string }>) {
|
|
943
|
-
const conflict = routes.some(r => r.method === method && r.path === path);
|
|
944
|
-
if (conflict) {
|
|
945
|
-
throw new Error(
|
|
946
|
-
`Observability endpoint conflict for ${method} ${path}. ` +
|
|
947
|
-
'Silakan override endpoint melalui konfigurasi observability (metrics.endpoint / health.endpoint).'
|
|
948
|
-
);
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
/**
|
|
953
|
-
* Get an adapter
|
|
954
|
-
*/
|
|
955
|
-
getAdapter<T>(name: string): T | undefined {
|
|
956
|
-
return this.adapters.get<T>(name);
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
/**
|
|
960
|
-
* Initialize all plugins (must be called before listen/start)
|
|
961
|
-
* This runs: configure → register → boot lifecycle phases
|
|
962
|
-
*
|
|
963
|
-
* @example
|
|
964
|
-
* ```typescript
|
|
965
|
-
* const app = createApp()
|
|
966
|
-
* .plugin(authPlugin, { secret: 'xxx' })
|
|
967
|
-
* .plugin(dbPlugin, { url: 'postgres://...' });
|
|
968
|
-
*
|
|
969
|
-
* // Initialize all plugins
|
|
970
|
-
* await app.initialize();
|
|
971
|
-
*
|
|
972
|
-
* // Start the server
|
|
973
|
-
* app.listen(3000);
|
|
974
|
-
* ```
|
|
975
|
-
*/
|
|
976
|
-
async initialize(): Promise<this> {
|
|
977
|
-
await this.pluginManager.initialize();
|
|
978
|
-
return this;
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
/**
|
|
982
|
-
* Start the HTTP server
|
|
983
|
-
*/
|
|
984
|
-
listen(port: number | string, callback?: () => void): HTTPServer {
|
|
985
|
-
const portNumber = typeof port === 'string' ? parseInt(port, 10) : port;
|
|
986
|
-
|
|
987
|
-
this.server = createServer(async (req, res) => {
|
|
988
|
-
await this.handleRequest(req, res);
|
|
989
|
-
});
|
|
990
|
-
|
|
991
|
-
// Attach graceful shutdown if enabled
|
|
992
|
-
if (this.shutdownManager) {
|
|
993
|
-
this.shutdownManager.attach(this.server);
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
// Attach WebSocket gateway if any ws routes registered
|
|
997
|
-
if (this.wsGateway) {
|
|
998
|
-
this.wsGateway.attach(this.server);
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
this.server.listen(portNumber, () => {
|
|
1002
|
-
// Notify plugins that server is ready
|
|
1003
|
-
this.pluginManager.notifyReady().catch(err => {
|
|
1004
|
-
console.error('[PluginManager] Error in ready phase:', err);
|
|
1005
|
-
});
|
|
1006
|
-
callback?.();
|
|
1007
|
-
});
|
|
1008
|
-
return this.server;
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
/**
|
|
1012
|
-
* Start the server (alias for listen with modern options)
|
|
1013
|
-
*/
|
|
1014
|
-
start(options: { port: number | string; host?: string; callback?: () => void } | number | string): HTTPServer {
|
|
1015
|
-
if (typeof options === 'number' || typeof options === 'string') {
|
|
1016
|
-
return this.listen(options);
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
const portNumber = typeof options.port === 'string' ? parseInt(options.port, 10) : options.port;
|
|
1020
|
-
|
|
1021
|
-
this.server = createServer(async (req, res) => {
|
|
1022
|
-
await this.handleRequest(req, res);
|
|
1023
|
-
});
|
|
1024
|
-
|
|
1025
|
-
// Attach graceful shutdown if enabled
|
|
1026
|
-
if (this.shutdownManager) {
|
|
1027
|
-
this.shutdownManager.attach(this.server);
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
// Attach WebSocket gateway if any ws routes registered
|
|
1031
|
-
if (this.wsGateway) {
|
|
1032
|
-
this.wsGateway.attach(this.server);
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
const { host = '0.0.0.0', callback } = options;
|
|
1036
|
-
this.server.listen(portNumber, host, () => {
|
|
1037
|
-
// Notify plugins that server is ready
|
|
1038
|
-
this.pluginManager.notifyReady().catch(err => {
|
|
1039
|
-
console.error('[PluginManager] Error in ready phase:', err);
|
|
1040
|
-
});
|
|
1041
|
-
callback?.();
|
|
1042
|
-
});
|
|
1043
|
-
return this.server;
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
/**
|
|
1047
|
-
* Handle incoming HTTP request
|
|
1048
|
-
* Optimized hot path for maximum performance
|
|
1049
|
-
*/
|
|
1050
|
-
private async handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
1051
|
-
// Use non-null assertion - context pool always returns valid context
|
|
1052
|
-
// acquire is now sync for better performance
|
|
1053
|
-
const ctx = this.contextPool.acquire(req, res) as any;
|
|
1054
|
-
|
|
1055
|
-
try {
|
|
1056
|
-
// Direct method calls - ContextImpl always has these methods
|
|
1057
|
-
ctx.setStoreRegistry(this.storeRegistry);
|
|
1058
|
-
ctx.setDebugMode(this.config.debug ?? false);
|
|
1059
|
-
|
|
1060
|
-
// === HOOK: onRequest (skip check if no hooks) ===
|
|
1061
|
-
if (this.lifecycleHooks.onRequest) {
|
|
1062
|
-
const hookResult = await this.lifecycleHooks.onRequest(ctx);
|
|
1063
|
-
if (hookResult && typeof hookResult === 'object' && 'statusCode' in hookResult) {
|
|
1064
|
-
await this.sendResponse(res, hookResult, ctx);
|
|
1065
|
-
this.cleanupRequest(ctx);
|
|
1066
|
-
return;
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
// Fast path: no versioning configured (most common)
|
|
1071
|
-
let matchPath = ctx.path;
|
|
1072
|
-
if (this.versioningConfig) {
|
|
1073
|
-
const { version, basePath, source } = this.resolveVersion(ctx);
|
|
1074
|
-
ctx.version = version;
|
|
1075
|
-
ctx.versionSource = source;
|
|
1076
|
-
|
|
1077
|
-
if (source !== 'path') {
|
|
1078
|
-
matchPath = `/${version}${basePath.startsWith('/') ? basePath : '/' + basePath}`;
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
// Find matching route
|
|
1083
|
-
const match = this.router.match(ctx.method, matchPath);
|
|
1084
|
-
|
|
1085
|
-
if (!match) {
|
|
1086
|
-
// Try fallback handler
|
|
1087
|
-
if (this.fallbackHandler) {
|
|
1088
|
-
const fallbackResponse = await this.fallbackHandler(ctx, this.dependencies);
|
|
1089
|
-
await this.sendResponse(res, fallbackResponse, ctx);
|
|
1090
|
-
this.cleanupRequest(ctx);
|
|
1091
|
-
return;
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
// 404 Not Found - use cached response
|
|
1095
|
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1096
|
-
res.end('{"error":"Not Found"}');
|
|
1097
|
-
this.cleanupRequest(ctx);
|
|
1098
|
-
return;
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
// Set route params directly
|
|
1102
|
-
ctx.params = match.params;
|
|
1103
|
-
|
|
1104
|
-
// Set serializers for fast JSON response if available
|
|
1105
|
-
if (match._serializer) {
|
|
1106
|
-
ctx.setSerializers(match._serializer);
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
// Auto-parse body for POST/PUT/PATCH if body exists
|
|
1110
|
-
// This maintains backwards compatibility while keeping lazy-loading for GET
|
|
1111
|
-
const method = ctx.method;
|
|
1112
|
-
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
|
|
1113
|
-
// Only parse if there's actually a body (Content-Length > 0 or Transfer-Encoding)
|
|
1114
|
-
const contentLength = req.headers['content-length'];
|
|
1115
|
-
const transferEncoding = req.headers['transfer-encoding'];
|
|
1116
|
-
const hasBody = (contentLength && contentLength !== '0') || transferEncoding;
|
|
1117
|
-
|
|
1118
|
-
if (hasBody) {
|
|
1119
|
-
await ctx.getBody();
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
// Fast path: no middleware
|
|
1124
|
-
let response: Response;
|
|
1125
|
-
if (this.globalMiddlewares.length === 0 && match.middlewares.length === 0) {
|
|
1126
|
-
// Direct handler execution (skip middleware executor)
|
|
1127
|
-
response = await this.executeHandlerDirect(ctx, match.handler);
|
|
1128
|
-
} else {
|
|
1129
|
-
// Combine global and route-specific middleware
|
|
1130
|
-
const allMiddlewares = this.globalMiddlewares.length === 0
|
|
1131
|
-
? match.middlewares
|
|
1132
|
-
: match.middlewares.length === 0
|
|
1133
|
-
? this.globalMiddlewares
|
|
1134
|
-
: [...this.globalMiddlewares, ...match.middlewares];
|
|
1135
|
-
|
|
1136
|
-
// Execute middleware chain and handler with hooks
|
|
1137
|
-
response = await this.middlewareExecutor.executeWithHooks(
|
|
1138
|
-
ctx,
|
|
1139
|
-
allMiddlewares,
|
|
1140
|
-
match.handler,
|
|
1141
|
-
this.lifecycleHooks,
|
|
1142
|
-
this.dependencies
|
|
1143
|
-
);
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
// === HOOK: onResponse ===
|
|
1147
|
-
if (this.lifecycleHooks.onResponse) {
|
|
1148
|
-
const hookResult = await this.lifecycleHooks.onResponse(ctx, response);
|
|
1149
|
-
if (hookResult && typeof hookResult === 'object' && 'statusCode' in hookResult) {
|
|
1150
|
-
response = hookResult;
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
// Send response
|
|
1155
|
-
await this.sendResponse(res, response, ctx);
|
|
1156
|
-
this.cleanupRequest(ctx);
|
|
1157
|
-
|
|
1158
|
-
} catch (error) {
|
|
1159
|
-
await this.handleError(error as Error, ctx, res);
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
/**
|
|
1164
|
-
* Execute handler directly without middleware (fast path)
|
|
1165
|
-
*/
|
|
1166
|
-
private async executeHandlerDirect(ctx: any, handler: Handler): Promise<Response> {
|
|
1167
|
-
// === HOOK: beforeHandler ===
|
|
1168
|
-
if (this.lifecycleHooks.beforeHandler) {
|
|
1169
|
-
const hookResult = await this.lifecycleHooks.beforeHandler(ctx);
|
|
1170
|
-
if (hookResult && typeof hookResult === 'object' && 'statusCode' in hookResult) {
|
|
1171
|
-
return hookResult;
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
let result = await handler(ctx, this.dependencies);
|
|
1176
|
-
|
|
1177
|
-
// If handler returns an Error, throw it
|
|
1178
|
-
if (result instanceof Error) {
|
|
1179
|
-
(result as any)._isIntentional = true;
|
|
1180
|
-
throw result;
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
// === HOOK: afterHandler ===
|
|
1184
|
-
if (this.lifecycleHooks.afterHandler) {
|
|
1185
|
-
const transformedResult = await this.lifecycleHooks.afterHandler(ctx, result);
|
|
1186
|
-
if (transformedResult !== undefined) {
|
|
1187
|
-
result = transformedResult;
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
// If result is a Response, return it
|
|
1192
|
-
if (result && typeof result === 'object' && 'statusCode' in result && 'body' in result) {
|
|
1193
|
-
return result as Response;
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
// Otherwise, wrap in JSON response
|
|
1197
|
-
return ctx.json(result);
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
/**
|
|
1201
|
-
* Cleanup request resources
|
|
1202
|
-
*/
|
|
1203
|
-
private cleanupRequest(ctx: any): void {
|
|
1204
|
-
// Dispose request-scoped stores
|
|
1205
|
-
if (ctx.disposeRequestStores) {
|
|
1206
|
-
ctx.disposeRequestStores();
|
|
1207
|
-
}
|
|
1208
|
-
// Release context back to pool
|
|
1209
|
-
this.contextPool.release(ctx);
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
/**
|
|
1213
|
-
* Handle errors
|
|
1214
|
-
*/
|
|
1215
|
-
private async handleError(error: Error, ctx: any, res: ServerResponse): Promise<void> {
|
|
1216
|
-
try {
|
|
1217
|
-
// === HOOK: onError ===
|
|
1218
|
-
if (ctx && this.lifecycleHooks.onError) {
|
|
1219
|
-
try {
|
|
1220
|
-
const hookResult = await this.lifecycleHooks.onError(ctx, error);
|
|
1221
|
-
if (hookResult && typeof hookResult === 'object' && 'statusCode' in hookResult) {
|
|
1222
|
-
await this.sendResponse(res, hookResult, ctx);
|
|
1223
|
-
this.cleanupRequest(ctx);
|
|
1224
|
-
return;
|
|
1225
|
-
}
|
|
1226
|
-
} catch (hookError) {
|
|
1227
|
-
console.error('onError hook failed:', hookError);
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
// Handle errors with default error handler
|
|
1232
|
-
const errorResponse = await this.errorHandler(error, ctx);
|
|
1233
|
-
|
|
1234
|
-
// === HOOK: onResponse (for error responses) ===
|
|
1235
|
-
if (ctx && this.lifecycleHooks.onResponse) {
|
|
1236
|
-
const hookResult = await this.lifecycleHooks.onResponse(ctx, errorResponse);
|
|
1237
|
-
if (hookResult && typeof hookResult === 'object' && 'statusCode' in hookResult) {
|
|
1238
|
-
await this.sendResponse(res, hookResult, ctx);
|
|
1239
|
-
this.cleanupRequest(ctx);
|
|
1240
|
-
return;
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
await this.sendResponse(res, errorResponse, ctx);
|
|
1245
|
-
} catch (handlerError) {
|
|
1246
|
-
// Fallback error response
|
|
1247
|
-
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
1248
|
-
res.end('Internal Server Error');
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
if (ctx) {
|
|
1252
|
-
this.cleanupRequest(ctx);
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
/**
|
|
1257
|
-
* Check if value is a Response object
|
|
1258
|
-
*/
|
|
1259
|
-
private isResponse(value: any): value is Response {
|
|
1260
|
-
return (
|
|
1261
|
-
value &&
|
|
1262
|
-
typeof value === 'object' &&
|
|
1263
|
-
'statusCode' in value &&
|
|
1264
|
-
'headers' in value &&
|
|
1265
|
-
'body' in value
|
|
1266
|
-
);
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
/**
|
|
1270
|
-
* Send HTTP response
|
|
1271
|
-
* Optimized for minimal overhead - key for benchmark performance
|
|
1272
|
-
*/
|
|
1273
|
-
private async sendResponse(res: ServerResponse, response: Response, ctx: Context): Promise<void> {
|
|
1274
|
-
// Handle stream responses separately
|
|
1275
|
-
if (response.stream) {
|
|
1276
|
-
res.statusCode = response.statusCode;
|
|
1277
|
-
const headers = response.headers;
|
|
1278
|
-
for (const key in headers) {
|
|
1279
|
-
const value = headers[key];
|
|
1280
|
-
if (value !== undefined) {
|
|
1281
|
-
res.setHeader(key, value);
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
response.stream.pipe(res);
|
|
1285
|
-
return;
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
const body = response.body;
|
|
1289
|
-
const headers = response.headers;
|
|
1290
|
-
|
|
1291
|
-
// Fast path for JSON responses (most common case)
|
|
1292
|
-
// Use writeHead for single syscall instead of multiple setHeader calls
|
|
1293
|
-
if (headers['Content-Type'] === 'application/json') {
|
|
1294
|
-
// Calculate content length for proper HTTP behavior
|
|
1295
|
-
const contentLength = Buffer.byteLength(body, 'utf8');
|
|
1296
|
-
|
|
1297
|
-
// Check if we need to set cookies
|
|
1298
|
-
let setCookies: string[] | null = null;
|
|
1299
|
-
if ('getSetCookieHeaders' in ctx && typeof ctx.getSetCookieHeaders === 'function') {
|
|
1300
|
-
const cookies = ctx.getSetCookieHeaders();
|
|
1301
|
-
if (cookies.length > 0) {
|
|
1302
|
-
setCookies = cookies;
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
// Single writeHead call for all headers
|
|
1307
|
-
if (setCookies) {
|
|
1308
|
-
res.writeHead(response.statusCode, {
|
|
1309
|
-
'Content-Type': 'application/json',
|
|
1310
|
-
'Content-Length': contentLength,
|
|
1311
|
-
'Set-Cookie': setCookies
|
|
1312
|
-
});
|
|
1313
|
-
} else {
|
|
1314
|
-
res.writeHead(response.statusCode, {
|
|
1315
|
-
'Content-Type': 'application/json',
|
|
1316
|
-
'Content-Length': contentLength
|
|
1317
|
-
});
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
res.end(body);
|
|
1321
|
-
return;
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
// General path for other response types
|
|
1325
|
-
res.statusCode = response.statusCode;
|
|
1326
|
-
|
|
1327
|
-
// Set headers using for-in (faster than Object.entries)
|
|
1328
|
-
for (const key in headers) {
|
|
1329
|
-
const value = headers[key];
|
|
1330
|
-
if (value !== undefined) {
|
|
1331
|
-
res.setHeader(key, value);
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
// Set cookies
|
|
1336
|
-
if ('getSetCookieHeaders' in ctx && typeof ctx.getSetCookieHeaders === 'function') {
|
|
1337
|
-
const setCookies = ctx.getSetCookieHeaders();
|
|
1338
|
-
if (setCookies.length > 0) {
|
|
1339
|
-
res.setHeader('Set-Cookie', setCookies);
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
// Set Content-Length for non-stream responses
|
|
1344
|
-
if (body && typeof body === 'string') {
|
|
1345
|
-
res.setHeader('Content-Length', Buffer.byteLength(body, 'utf8'));
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
res.end(body);
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
/**
|
|
1352
|
-
* Get all registered routes
|
|
1353
|
-
*/
|
|
1354
|
-
getRoutes() {
|
|
1355
|
-
return this.router.getRoutes();
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
/**
|
|
1359
|
-
* Get context pool statistics
|
|
1360
|
-
*/
|
|
1361
|
-
getPoolStats() {
|
|
1362
|
-
return this.contextPool.getStats();
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
/**
|
|
1366
|
-
* Close the server
|
|
1367
|
-
*/
|
|
1368
|
-
close(callback?: (err?: Error) => void): void {
|
|
1369
|
-
if (this.server) {
|
|
1370
|
-
this.server.close(callback);
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
/**
|
|
1375
|
-
* Enable graceful shutdown for zero-downtime deployments
|
|
1376
|
-
*/
|
|
1377
|
-
gracefulShutdown(options: GracefulShutdownOptions = {}): this {
|
|
1378
|
-
this.shutdownManager = new GracefulShutdownManager({
|
|
1379
|
-
verbose: this.config.debug,
|
|
1380
|
-
...options
|
|
1381
|
-
});
|
|
1382
|
-
return this;
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
/**
|
|
1386
|
-
* Add a shutdown hook (e.g., closing database connections)
|
|
1387
|
-
*/
|
|
1388
|
-
onShutdown(name: string, handler: () => Promise<void>, priority?: number): this {
|
|
1389
|
-
if (!this.shutdownManager) {
|
|
1390
|
-
this.gracefulShutdown();
|
|
1391
|
-
}
|
|
1392
|
-
this.shutdownManager!.addHook({ name, handler, priority });
|
|
1393
|
-
return this;
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
/**
|
|
1397
|
-
* Initiate graceful shutdown programmatically
|
|
1398
|
-
*/
|
|
1399
|
-
async shutdown(): Promise<void> {
|
|
1400
|
-
// Shutdown all plugins first
|
|
1401
|
-
await this.pluginManager.shutdown();
|
|
1402
|
-
|
|
1403
|
-
// Dispose all stores
|
|
1404
|
-
this.storeRegistry.dispose();
|
|
1405
|
-
|
|
1406
|
-
if (this.shutdownManager) {
|
|
1407
|
-
await this.shutdownManager.shutdown();
|
|
1408
|
-
} else {
|
|
1409
|
-
this.close();
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
/**
|
|
1414
|
-
* Check if shutdown is in progress
|
|
1415
|
-
*/
|
|
1416
|
-
isShuttingDown(): boolean {
|
|
1417
|
-
return this.shutdownManager?.isInShutdown() ?? false;
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
/**
|
|
1421
|
-
* Get the shutdown manager for advanced usage
|
|
1422
|
-
*/
|
|
1423
|
-
getShutdownManager(): GracefulShutdownManager | undefined {
|
|
1424
|
-
return this.shutdownManager;
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
|
-
/**
|
|
1428
|
-
* Start the application with clustering support
|
|
1429
|
-
* @param options Cluster options
|
|
1430
|
-
* @param startFn Function to start the server (called in each worker)
|
|
1431
|
-
*/
|
|
1432
|
-
cluster(options: ClusterOptions = {}): ClusterManager {
|
|
1433
|
-
this.clusterManager = new ClusterManager({
|
|
1434
|
-
verbose: this.config.debug,
|
|
1435
|
-
...options
|
|
1436
|
-
});
|
|
1437
|
-
return this.clusterManager;
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
/**
|
|
1441
|
-
* Get the cluster manager
|
|
1442
|
-
*/
|
|
1443
|
-
getClusterManager(): ClusterManager | undefined {
|
|
1444
|
-
return this.clusterManager;
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
|
-
/**
|
|
1449
|
-
* Factory function to create an application
|
|
1450
|
-
*/
|
|
1451
|
-
export function createApp(config?: AppConfig): Application {
|
|
1452
|
-
return new Application(config);
|
|
1453
|
-
}
|