@webpieces/http-server 0.2.12 → 0.2.13

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.
@@ -0,0 +1,104 @@
1
+ import { Container } from 'inversify';
2
+ import { WebAppMeta } from '@webpieces/core-meta';
3
+ import { RouteBuilderImpl } from './RouteBuilderImpl';
4
+ /**
5
+ * WebpiecesCoreServer - Core server implementation with DI.
6
+ *
7
+ * This class uses a two-container pattern similar to Java WebPieces:
8
+ * 1. webpiecesContainer: Core WebPieces framework bindings
9
+ * 2. appContainer: User's application bindings (child of webpiecesContainer)
10
+ *
11
+ * This separation allows:
12
+ * - Clean separation of concerns
13
+ * - Better testability
14
+ * - Ability to override framework bindings in tests
15
+ *
16
+ * The server:
17
+ * 1. Initializes both DI containers from WebAppMeta.getDIModules()
18
+ * 2. Registers routes using explicit RouteBuilderImpl
19
+ * 3. Creates filter chains
20
+ * 4. Supports both HTTP server mode and testing mode (no HTTP)
21
+ *
22
+ * DI Pattern: This class is registered in webpiecesContainer via @provideSingleton()
23
+ * and resolved by WebpiecesServer. It receives RouteBuilder via constructor injection.
24
+ */
25
+ export declare class WebpiecesCoreServer {
26
+ private routeBuilder;
27
+ private meta;
28
+ private webpiecesContainer;
29
+ /**
30
+ * Application container: User's application bindings.
31
+ * This is a child container of webpiecesContainer, so it can access
32
+ * framework bindings while keeping app bindings separate.
33
+ */
34
+ private appContainer;
35
+ private initialized;
36
+ private app?;
37
+ private server?;
38
+ private port;
39
+ constructor(routeBuilder: RouteBuilderImpl);
40
+ /**
41
+ * Initialize the server (DI container, routes, filters).
42
+ * This is called by WebpiecesServer after resolving this class from DI.
43
+ *
44
+ * @param webpiecesContainer - The framework container
45
+ * @param meta - User-provided WebAppMeta with DI modules and routes
46
+ */
47
+ initialize(webpiecesContainer: Container, meta: WebAppMeta): void;
48
+ /**
49
+ * Load DI modules from WebAppMeta.
50
+ *
51
+ * Currently, all user modules are loaded into the application container.
52
+ * In the future, we could separate:
53
+ * - WebPieces framework modules -> webpiecesContainer
54
+ * - Application modules -> appContainer
55
+ *
56
+ * For now, everything goes into appContainer which has access to webpiecesContainer.
57
+ */
58
+ private loadDIModules;
59
+ /**
60
+ * Register routes from WebAppMeta.
61
+ *
62
+ * Creates an explicit RouteBuilderImpl instead of an anonymous object.
63
+ * This improves:
64
+ * - Traceability: Can Cmd+Click on addRoute to see implementation
65
+ * - Debugging: Explicit class shows up in stack traces
66
+ * - Understanding: Clear class name vs anonymous object
67
+ */
68
+ private registerRoutes;
69
+ /**
70
+ * Global error handler middleware - catches ALL unhandled errors.
71
+ * Returns HTML 500 error page for any errors that escape the filter chain.
72
+ *
73
+ * This is the outermost safety net - JsonFilter catches JSON API errors,
74
+ * this catches everything else.
75
+ */
76
+ private globalErrorHandler;
77
+ /**
78
+ * Logging middleware - logs request/response flow.
79
+ * Demonstrates middleware execution order.
80
+ */
81
+ private logNextLayer;
82
+ /**
83
+ * Start the HTTP server with Express.
84
+ * Assumes initialize() has already been called by WebpiecesServer.
85
+ */
86
+ start(port?: number): void;
87
+ /**
88
+ * Register all routes with Express.
89
+ */
90
+ private registerExpressRoutes;
91
+ /**
92
+ * Setup a single route with Express.
93
+ * Finds matching filters, creates handler, and registers with Express.
94
+ *
95
+ * @param key - Route key (method:path)
96
+ * @param routeWithMeta - The route handler paired with its definition
97
+ * @param filtersWithMeta - All filters with their definitions
98
+ */
99
+ private setupRoute;
100
+ /**
101
+ * Stop the HTTP server.
102
+ */
103
+ stop(): void;
104
+ }
@@ -0,0 +1,276 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WebpiecesCoreServer = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const express_1 = tslib_1.__importDefault(require("express"));
6
+ const inversify_1 = require("inversify");
7
+ const binding_decorators_1 = require("@inversifyjs/binding-decorators");
8
+ const core_meta_1 = require("@webpieces/core-meta");
9
+ const http_filters_1 = require("@webpieces/http-filters");
10
+ const http_routing_1 = require("@webpieces/http-routing");
11
+ const RouteBuilderImpl_1 = require("./RouteBuilderImpl");
12
+ const FilterMatcher_1 = require("./FilterMatcher");
13
+ const core_util_1 = require("@webpieces/core-util");
14
+ const MethodMeta_1 = require("./MethodMeta");
15
+ /**
16
+ * WebpiecesCoreServer - Core server implementation with DI.
17
+ *
18
+ * This class uses a two-container pattern similar to Java WebPieces:
19
+ * 1. webpiecesContainer: Core WebPieces framework bindings
20
+ * 2. appContainer: User's application bindings (child of webpiecesContainer)
21
+ *
22
+ * This separation allows:
23
+ * - Clean separation of concerns
24
+ * - Better testability
25
+ * - Ability to override framework bindings in tests
26
+ *
27
+ * The server:
28
+ * 1. Initializes both DI containers from WebAppMeta.getDIModules()
29
+ * 2. Registers routes using explicit RouteBuilderImpl
30
+ * 3. Creates filter chains
31
+ * 4. Supports both HTTP server mode and testing mode (no HTTP)
32
+ *
33
+ * DI Pattern: This class is registered in webpiecesContainer via @provideSingleton()
34
+ * and resolved by WebpiecesServer. It receives RouteBuilder via constructor injection.
35
+ */
36
+ let WebpiecesCoreServer = class WebpiecesCoreServer {
37
+ constructor(routeBuilder) {
38
+ this.routeBuilder = routeBuilder;
39
+ this.initialized = false;
40
+ this.port = 8080;
41
+ }
42
+ /**
43
+ * Initialize the server (DI container, routes, filters).
44
+ * This is called by WebpiecesServer after resolving this class from DI.
45
+ *
46
+ * @param webpiecesContainer - The framework container
47
+ * @param meta - User-provided WebAppMeta with DI modules and routes
48
+ */
49
+ initialize(webpiecesContainer, meta) {
50
+ if (this.initialized) {
51
+ return;
52
+ }
53
+ this.webpiecesContainer = webpiecesContainer;
54
+ this.meta = meta;
55
+ // Create application container as child of framework container
56
+ this.appContainer = new inversify_1.Container({ parent: this.webpiecesContainer });
57
+ // Set container on RouteBuilder (late binding - appContainer didn't exist in constructor)
58
+ this.routeBuilder.setContainer(this.appContainer);
59
+ // 1. Load DI modules
60
+ this.loadDIModules();
61
+ // 2. Register routes and filters
62
+ this.registerRoutes();
63
+ this.initialized = true;
64
+ }
65
+ /**
66
+ * Load DI modules from WebAppMeta.
67
+ *
68
+ * Currently, all user modules are loaded into the application container.
69
+ * In the future, we could separate:
70
+ * - WebPieces framework modules -> webpiecesContainer
71
+ * - Application modules -> appContainer
72
+ *
73
+ * For now, everything goes into appContainer which has access to webpiecesContainer.
74
+ */
75
+ loadDIModules() {
76
+ const modules = this.meta.getDIModules();
77
+ // Load buildProviderModule to auto-scan for @provideSingleton decorators
78
+ this.appContainer.load((0, binding_decorators_1.buildProviderModule)());
79
+ // Load all modules into application container
80
+ // (webpiecesContainer is currently empty, reserved for future framework bindings)
81
+ for (const module of modules) {
82
+ this.appContainer.load(module);
83
+ }
84
+ }
85
+ /**
86
+ * Register routes from WebAppMeta.
87
+ *
88
+ * Creates an explicit RouteBuilderImpl instead of an anonymous object.
89
+ * This improves:
90
+ * - Traceability: Can Cmd+Click on addRoute to see implementation
91
+ * - Debugging: Explicit class shows up in stack traces
92
+ * - Understanding: Clear class name vs anonymous object
93
+ */
94
+ registerRoutes() {
95
+ const routeConfigs = this.meta.getRoutes();
96
+ // Configure routes using the explicit RouteBuilder
97
+ for (const routeConfig of routeConfigs) {
98
+ routeConfig.configure(this.routeBuilder);
99
+ }
100
+ }
101
+ /**
102
+ * Global error handler middleware - catches ALL unhandled errors.
103
+ * Returns HTML 500 error page for any errors that escape the filter chain.
104
+ *
105
+ * This is the outermost safety net - JsonFilter catches JSON API errors,
106
+ * this catches everything else.
107
+ */
108
+ async globalErrorHandler(req, res, next) {
109
+ console.log('🔴 [Layer 1: GlobalErrorHandler] Request START:', req.method, req.path);
110
+ try {
111
+ // await next() catches BOTH:
112
+ // 1. Synchronous throws from next() itself
113
+ // 2. Rejected promises from downstream async middleware
114
+ await next();
115
+ console.log('🔴 [Layer 1: GlobalErrorHandler] Request END (success):', req.method, req.path);
116
+ }
117
+ catch (err) {
118
+ const error = (0, core_util_1.toError)(err);
119
+ console.error('🔴 [Layer 1: GlobalErrorHandler] Caught unhandled error:', error);
120
+ if (!res.headersSent) {
121
+ // Return HTML error page (not JSON - JsonFilter handles JSON errors)
122
+ res.status(500).send(`
123
+ <!DOCTYPE html>
124
+ <html>
125
+ <head><title>Server Error</title></head>
126
+ <body>
127
+ <h1>You hit a server error</h1>
128
+ <p>An unexpected error occurred while processing your request.</p>
129
+ <pre>${error.message}</pre>
130
+ </body>
131
+ </html>
132
+ `);
133
+ }
134
+ console.log('🔴 [Layer 1: GlobalErrorHandler] Request END (error):', req.method, req.path);
135
+ }
136
+ }
137
+ /**
138
+ * Logging middleware - logs request/response flow.
139
+ * Demonstrates middleware execution order.
140
+ */
141
+ logNextLayer(req, res, next) {
142
+ console.log('🟡 [Layer 2: LogNextLayer] Before next() -', req.method, req.path);
143
+ next();
144
+ console.log('🟡 [Layer 2: LogNextLayer] After next() -', req.method, req.path);
145
+ }
146
+ /**
147
+ * Start the HTTP server with Express.
148
+ * Assumes initialize() has already been called by WebpiecesServer.
149
+ */
150
+ start(port = 8080) {
151
+ if (!this.initialized) {
152
+ throw new Error('Server not initialized. Call initialize() before start().');
153
+ }
154
+ this.port = port;
155
+ // Create Express app
156
+ this.app = (0, express_1.default)();
157
+ // Parse JSON request bodies
158
+ this.app.use(express_1.default.json());
159
+ // Layer 1: Global Error Handler (OUTERMOST - runs FIRST)
160
+ // Catches all unhandled errors and returns HTML 500 page
161
+ this.app.use(this.globalErrorHandler.bind(this));
162
+ // Layer 2: Request/Response Logging
163
+ this.app.use(this.logNextLayer.bind(this));
164
+ // Register routes (these become the innermost handlers)
165
+ this.registerExpressRoutes();
166
+ const routes = this.routeBuilder.getRoutes();
167
+ // Start listening
168
+ this.server = this.app.listen(this.port, () => {
169
+ console.log(`[WebpiecesServer] Server listening on http://localhost:${this.port}`);
170
+ console.log(`[WebpiecesServer] Registered ${routes.size} routes`);
171
+ });
172
+ }
173
+ /**
174
+ * Register all routes with Express.
175
+ */
176
+ registerExpressRoutes() {
177
+ if (!this.app) {
178
+ throw new Error('Express app not initialized');
179
+ }
180
+ const routes = this.routeBuilder.getRoutes();
181
+ const sortedFilters = this.routeBuilder.getSortedFilters();
182
+ for (const [key, routeWithMeta] of routes.entries()) {
183
+ this.setupRoute(key, routeWithMeta, sortedFilters);
184
+ }
185
+ }
186
+ /**
187
+ * Setup a single route with Express.
188
+ * Finds matching filters, creates handler, and registers with Express.
189
+ *
190
+ * @param key - Route key (method:path)
191
+ * @param routeWithMeta - The route handler paired with its definition
192
+ * @param filtersWithMeta - All filters with their definitions
193
+ */
194
+ setupRoute(key, routeWithMeta, filtersWithMeta) {
195
+ if (!this.app) {
196
+ throw new Error('Express app not initialized');
197
+ }
198
+ const route = routeWithMeta.definition;
199
+ const routeMeta = route.routeMeta;
200
+ const method = routeMeta.httpMethod.toLowerCase();
201
+ const path = routeMeta.path;
202
+ console.log(`[WebpiecesServer] Registering route: ${method.toUpperCase()} ${path}`);
203
+ // Find matching filters for this route - FilterMatcher returns Filter[] not FilterWithMeta[]
204
+ // So we need to convert our FilterWithMeta[] to what FilterMatcher expects
205
+ const filterDefinitions = filtersWithMeta.map(fwm => {
206
+ // Set the filter instance on the definition for FilterMatcher
207
+ const def = fwm.definition;
208
+ def.filter = fwm.filter;
209
+ return def;
210
+ });
211
+ const matchingFilters = FilterMatcher_1.FilterMatcher.findMatchingFilters(route.controllerFilepath, filterDefinitions);
212
+ // Create service that wraps the controller execution
213
+ const controllerService = {
214
+ invoke: async (meta) => {
215
+ // Invoke the controller method via route handler
216
+ const result = await routeWithMeta.handler.execute(meta);
217
+ const responseWrapper = new http_filters_1.WpResponse(result);
218
+ return responseWrapper;
219
+ }
220
+ };
221
+ // Chain filters with the controller service (reverse order for correct execution)
222
+ // IMPORTANT: MUST USE Filter.chain(filter) and Filter.chainService(svc);
223
+ let filterChain = matchingFilters[matchingFilters.length - 1];
224
+ for (let i = matchingFilters.length - 2; i >= 0; i--) {
225
+ filterChain = filterChain.chain(matchingFilters[i]);
226
+ }
227
+ const svc = filterChain.chainService(controllerService);
228
+ // Create Express route handler - delegates to filter chain
229
+ const handler = async (req, res, next) => {
230
+ // Create RouteRequest with Express Request/Response
231
+ const routeRequest = new core_meta_1.RouteRequest(req, res);
232
+ // Create MethodMeta with route info and Express Request/Response
233
+ const meta = new MethodMeta_1.MethodMeta(routeMeta, routeRequest);
234
+ // Response is written by JsonFilter - we just await completion
235
+ await svc.invoke(meta);
236
+ };
237
+ // Register with Express
238
+ switch (method) {
239
+ case 'get':
240
+ this.app.get(path, handler);
241
+ break;
242
+ case 'post':
243
+ this.app.post(path, handler);
244
+ break;
245
+ case 'put':
246
+ this.app.put(path, handler);
247
+ break;
248
+ case 'delete':
249
+ this.app.delete(path, handler);
250
+ break;
251
+ case 'patch':
252
+ this.app.patch(path, handler);
253
+ break;
254
+ default:
255
+ console.warn(`[WebpiecesServer] Unknown HTTP method: ${method}`);
256
+ }
257
+ }
258
+ /**
259
+ * Stop the HTTP server.
260
+ */
261
+ stop() {
262
+ if (this.server) {
263
+ this.server.close(() => {
264
+ console.log('[WebpiecesServer] Server stopped');
265
+ });
266
+ }
267
+ }
268
+ };
269
+ exports.WebpiecesCoreServer = WebpiecesCoreServer;
270
+ exports.WebpiecesCoreServer = WebpiecesCoreServer = tslib_1.__decorate([
271
+ (0, http_routing_1.provideSingleton)(),
272
+ (0, inversify_1.injectable)(),
273
+ tslib_1.__param(0, (0, inversify_1.inject)(RouteBuilderImpl_1.RouteBuilderImpl)),
274
+ tslib_1.__metadata("design:paramtypes", [RouteBuilderImpl_1.RouteBuilderImpl])
275
+ ], WebpiecesCoreServer);
276
+ //# sourceMappingURL=WebpiecesCoreServer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WebpiecesCoreServer.js","sourceRoot":"","sources":["../../../../../packages/http/http-server/src/WebpiecesCoreServer.ts"],"names":[],"mappings":"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","sourcesContent":["import express, {Express, NextFunction, Request, Response} from 'express';\nimport {Container, inject, injectable} from 'inversify';\nimport {buildProviderModule} from '@inversifyjs/binding-decorators';\nimport {RouteRequest, WebAppMeta} from '@webpieces/core-meta';\nimport {WpResponse, Service} from '@webpieces/http-filters';\nimport {provideSingleton} from '@webpieces/http-routing';\nimport {RouteBuilderImpl, RouteHandlerWithMeta, FilterWithMeta} from './RouteBuilderImpl';\nimport {FilterMatcher} from './FilterMatcher';\nimport {toError} from '@webpieces/core-util';\nimport {MethodMeta} from './MethodMeta';\n\n/**\n * WebpiecesCoreServer - Core server implementation with DI.\n *\n * This class uses a two-container pattern similar to Java WebPieces:\n * 1. webpiecesContainer: Core WebPieces framework bindings\n * 2. appContainer: User's application bindings (child of webpiecesContainer)\n *\n * This separation allows:\n * - Clean separation of concerns\n * - Better testability\n * - Ability to override framework bindings in tests\n *\n * The server:\n * 1. Initializes both DI containers from WebAppMeta.getDIModules()\n * 2. Registers routes using explicit RouteBuilderImpl\n * 3. Creates filter chains\n * 4. Supports both HTTP server mode and testing mode (no HTTP)\n *\n * DI Pattern: This class is registered in webpiecesContainer via @provideSingleton()\n * and resolved by WebpiecesServer. It receives RouteBuilder via constructor injection.\n */\n@provideSingleton()\n@injectable()\nexport class WebpiecesCoreServer {\n private meta!: WebAppMeta;\n private webpiecesContainer!: Container;\n\n /**\n * Application container: User's application bindings.\n * This is a child container of webpiecesContainer, so it can access\n * framework bindings while keeping app bindings separate.\n */\n private appContainer!: Container;\n\n private initialized = false;\n private app?: Express;\n private server?: ReturnType<Express['listen']>;\n private port: number = 8080;\n\n constructor(\n @inject(RouteBuilderImpl) private routeBuilder: RouteBuilderImpl\n ) {\n }\n\n /**\n * Initialize the server (DI container, routes, filters).\n * This is called by WebpiecesServer after resolving this class from DI.\n *\n * @param webpiecesContainer - The framework container\n * @param meta - User-provided WebAppMeta with DI modules and routes\n */\n initialize(webpiecesContainer: Container, meta: WebAppMeta): void {\n if (this.initialized) {\n return;\n }\n\n this.webpiecesContainer = webpiecesContainer;\n this.meta = meta;\n\n // Create application container as child of framework container\n this.appContainer = new Container({ parent: this.webpiecesContainer });\n\n // Set container on RouteBuilder (late binding - appContainer didn't exist in constructor)\n this.routeBuilder.setContainer(this.appContainer);\n\n // 1. Load DI modules\n this.loadDIModules();\n\n // 2. Register routes and filters\n this.registerRoutes();\n\n this.initialized = true;\n }\n\n /**\n * Load DI modules from WebAppMeta.\n *\n * Currently, all user modules are loaded into the application container.\n * In the future, we could separate:\n * - WebPieces framework modules -> webpiecesContainer\n * - Application modules -> appContainer\n *\n * For now, everything goes into appContainer which has access to webpiecesContainer.\n */\n private loadDIModules(): void {\n const modules = this.meta.getDIModules();\n\n // Load buildProviderModule to auto-scan for @provideSingleton decorators\n this.appContainer.load(buildProviderModule());\n\n // Load all modules into application container\n // (webpiecesContainer is currently empty, reserved for future framework bindings)\n for (const module of modules) {\n this.appContainer.load(module);\n }\n }\n\n /**\n * Register routes from WebAppMeta.\n *\n * Creates an explicit RouteBuilderImpl instead of an anonymous object.\n * This improves:\n * - Traceability: Can Cmd+Click on addRoute to see implementation\n * - Debugging: Explicit class shows up in stack traces\n * - Understanding: Clear class name vs anonymous object\n */\n private registerRoutes(): void {\n const routeConfigs = this.meta.getRoutes();\n\n // Configure routes using the explicit RouteBuilder\n for (const routeConfig of routeConfigs) {\n routeConfig.configure(this.routeBuilder);\n }\n }\n\n /**\n * Global error handler middleware - catches ALL unhandled errors.\n * Returns HTML 500 error page for any errors that escape the filter chain.\n *\n * This is the outermost safety net - JsonFilter catches JSON API errors,\n * this catches everything else.\n */\n private async globalErrorHandler(\n req: Request,\n res: Response,\n next: NextFunction\n ): Promise<void> {\n console.log('🔴 [Layer 1: GlobalErrorHandler] Request START:', req.method, req.path);\n\n try {\n // await next() catches BOTH:\n // 1. Synchronous throws from next() itself\n // 2. Rejected promises from downstream async middleware\n await next();\n console.log('🔴 [Layer 1: GlobalErrorHandler] Request END (success):', req.method, req.path);\n } catch (err: unknown) {\n const error = toError(err);\n console.error('🔴 [Layer 1: GlobalErrorHandler] Caught unhandled error:', error);\n if (!res.headersSent) {\n // Return HTML error page (not JSON - JsonFilter handles JSON errors)\n res.status(500).send(`\n <!DOCTYPE html>\n <html>\n <head><title>Server Error</title></head>\n <body>\n <h1>You hit a server error</h1>\n <p>An unexpected error occurred while processing your request.</p>\n <pre>${error.message}</pre>\n </body>\n </html>\n `);\n }\n console.log('🔴 [Layer 1: GlobalErrorHandler] Request END (error):', req.method, req.path);\n }\n }\n\n /**\n * Logging middleware - logs request/response flow.\n * Demonstrates middleware execution order.\n */\n private logNextLayer(req: Request, res: Response, next: NextFunction): void {\n console.log('🟡 [Layer 2: LogNextLayer] Before next() -', req.method, req.path);\n next();\n console.log('🟡 [Layer 2: LogNextLayer] After next() -', req.method, req.path);\n }\n\n /**\n * Start the HTTP server with Express.\n * Assumes initialize() has already been called by WebpiecesServer.\n */\n start(port: number = 8080): void {\n if (!this.initialized) {\n throw new Error('Server not initialized. Call initialize() before start().');\n }\n\n this.port = port;\n\n // Create Express app\n this.app = express();\n\n // Parse JSON request bodies\n this.app.use(express.json());\n\n // Layer 1: Global Error Handler (OUTERMOST - runs FIRST)\n // Catches all unhandled errors and returns HTML 500 page\n this.app.use(this.globalErrorHandler.bind(this));\n\n // Layer 2: Request/Response Logging\n this.app.use(this.logNextLayer.bind(this));\n\n // Register routes (these become the innermost handlers)\n this.registerExpressRoutes();\n\n const routes = this.routeBuilder.getRoutes();\n\n // Start listening\n this.server = this.app.listen(this.port, () => {\n console.log(`[WebpiecesServer] Server listening on http://localhost:${this.port}`);\n console.log(`[WebpiecesServer] Registered ${routes.size} routes`);\n });\n }\n\n /**\n * Register all routes with Express.\n */\n private registerExpressRoutes(): void {\n if (!this.app) {\n throw new Error('Express app not initialized');\n }\n\n const routes = this.routeBuilder.getRoutes();\n const sortedFilters = this.routeBuilder.getSortedFilters();\n for (const [key, routeWithMeta] of routes.entries()) {\n this.setupRoute(key, routeWithMeta, sortedFilters);\n }\n }\n\n /**\n * Setup a single route with Express.\n * Finds matching filters, creates handler, and registers with Express.\n *\n * @param key - Route key (method:path)\n * @param routeWithMeta - The route handler paired with its definition\n * @param filtersWithMeta - All filters with their definitions\n */\n private setupRoute(key: string, routeWithMeta: RouteHandlerWithMeta, filtersWithMeta: Array<FilterWithMeta>): void {\n if (!this.app) {\n throw new Error('Express app not initialized');\n }\n\n const route = routeWithMeta.definition;\n const routeMeta = route.routeMeta;\n const method = routeMeta.httpMethod.toLowerCase();\n const path = routeMeta.path;\n\n console.log(`[WebpiecesServer] Registering route: ${method.toUpperCase()} ${path}`);\n\n // Find matching filters for this route - FilterMatcher returns Filter[] not FilterWithMeta[]\n // So we need to convert our FilterWithMeta[] to what FilterMatcher expects\n const filterDefinitions = filtersWithMeta.map(fwm => {\n // Set the filter instance on the definition for FilterMatcher\n const def = fwm.definition;\n def.filter = fwm.filter;\n return def;\n });\n\n const matchingFilters = FilterMatcher.findMatchingFilters(\n route.controllerFilepath,\n filterDefinitions\n );\n\n // Create service that wraps the controller execution\n const controllerService: Service<MethodMeta, WpResponse> = {\n invoke: async (meta: MethodMeta): Promise<WpResponse> => {\n // Invoke the controller method via route handler\n const result = await routeWithMeta.handler.execute(meta);\n const responseWrapper = new WpResponse(result);\n return responseWrapper;\n }\n };\n\n // Chain filters with the controller service (reverse order for correct execution)\n // IMPORTANT: MUST USE Filter.chain(filter) and Filter.chainService(svc);\n let filterChain = matchingFilters[matchingFilters.length - 1];\n for (let i = matchingFilters.length - 2; i >= 0; i--) {\n filterChain = filterChain.chain(matchingFilters[i]);\n }\n const svc = filterChain.chainService(controllerService);\n\n // Create Express route handler - delegates to filter chain\n const handler = async (req: Request, res: Response, next: NextFunction) => {\n // Create RouteRequest with Express Request/Response\n const routeRequest = new RouteRequest(req, res);\n\n // Create MethodMeta with route info and Express Request/Response\n const meta = new MethodMeta(\n routeMeta,\n routeRequest\n );\n\n // Response is written by JsonFilter - we just await completion\n await svc.invoke(meta);\n };\n\n // Register with Express\n switch (method) {\n case 'get':\n this.app.get(path, handler);\n break;\n case 'post':\n this.app.post(path, handler);\n break;\n case 'put':\n this.app.put(path, handler);\n break;\n case 'delete':\n this.app.delete(path, handler);\n break;\n case 'patch':\n this.app.patch(path, handler);\n break;\n default:\n console.warn(`[WebpiecesServer] Unknown HTTP method: ${method}`);\n }\n }\n\n /**\n * Stop the HTTP server.\n */\n stop(): void {\n if (this.server) {\n this.server.close(() => {\n console.log('[WebpiecesServer] Server stopped');\n });\n }\n }\n}\n"]}
@@ -1,4 +1,3 @@
1
- import { Container } from 'inversify';
2
1
  import { WebAppMeta } from '@webpieces/core-meta';
3
2
  /**
4
3
  * WebpiecesServer - Main bootstrap class for WebPieces applications.
@@ -40,112 +39,19 @@ export declare class WebpiecesServer {
40
39
  * logging, metrics, etc. Similar to Java WebPieces platform container.
41
40
  */
42
41
  private webpiecesContainer;
43
- /**
44
- * Application container: User's application bindings.
45
- * This is a child container of webpiecesContainer, so it can access
46
- * framework bindings while keeping app bindings separate.
47
- */
48
- private appContainer;
49
- /**
50
- * Routes registry: Maps "METHOD:path" -> RegisteredRoute
51
- * Example: "POST:/search/item" -> { method: "POST", path: "/search/item", handler: ... }
52
- *
53
- * We use unknown instead of any for type safety - each route has its own return type,
54
- * but we can't have different generic types in the same Map.
55
- */
56
- private routes;
57
- /**
58
- * Registered filters with their definitions.
59
- * Used by FilterMatcher to match filters to routes based on filepath patterns.
60
- */
61
- private filterRegistry;
62
- private initialized;
63
- private app?;
64
- private server?;
65
- private port;
42
+ private coreService;
66
43
  constructor(meta: WebAppMeta);
67
44
  /**
68
45
  * Initialize the server (DI container, routes, filters).
69
46
  * This is called automatically by start() or can be called manually for testing.
70
47
  */
71
48
  initialize(): void;
72
- /**
73
- * Load DI modules from WebAppMeta.
74
- *
75
- * Currently, all user modules are loaded into the application container.
76
- * In the future, we could separate:
77
- * - WebPieces framework modules -> webpiecesContainer
78
- * - Application modules -> appContainer
79
- *
80
- * For now, everything goes into appContainer which has access to webpiecesContainer.
81
- */
82
- private loadDIModules;
83
- /**
84
- * Register routes from WebAppMeta.
85
- *
86
- * Creates an explicit RouteBuilderImpl instead of an anonymous object.
87
- * This improves:
88
- * - Traceability: Can Cmd+Click on addRoute to see implementation
89
- * - Debugging: Explicit class shows up in stack traces
90
- * - Understanding: Clear class name vs anonymous object
91
- */
92
- private registerRoutes;
93
49
  /**
94
50
  * Start the HTTP server with Express.
95
51
  */
96
52
  start(port?: number): void;
97
- /**
98
- * Handle an incoming HTTP request through the filter chain and controller.
99
- * This is the main request processing logic.
100
- *
101
- * NO try-catch here - errors are handled by:
102
- * 1. JsonFilter - catches and returns JSON error responses
103
- * 2. GlobalErrorHandler middleware - catches any unhandled errors and returns HTML 500
104
- *
105
- * @param req - Express request
106
- * @param res - Express response
107
- * @param route - The registered route to execute
108
- * @param matchingFilters - Filters that apply to this route
109
- * @param key - Route key (method:path)
110
- */
111
- private handleRequest;
112
- /**
113
- * Register all routes with Express.
114
- */
115
- private registerExpressRoutes;
116
- /**
117
- * Setup a single route with Express.
118
- * Finds matching filters, creates handler, and registers with Express.
119
- *
120
- * @param key - Route key (method:path)
121
- * @param route - The registered route definition
122
- */
123
- private setupRoute;
124
53
  /**
125
54
  * Stop the HTTP server.
126
55
  */
127
56
  stop(): void;
128
- /**
129
- * Create an API client proxy for testing (no HTTP).
130
- *
131
- * This creates a proxy object that implements the API interface
132
- * and routes method calls through the full filter chain to the controller.
133
- *
134
- * @param apiMetaClass - The API interface class with decorators
135
- * @returns Proxy object implementing the API interface
136
- */
137
- createApiClient<T>(apiMetaClass: any): T;
138
- /**
139
- * Invoke a route through the filter chain.
140
- */
141
- private invokeRoute;
142
- /**
143
- * Get the application DI container (for testing).
144
- * Returns appContainer which has access to both app and framework bindings.
145
- */
146
- getContainer(): Container;
147
- /**
148
- * Get the WebPieces framework container (for advanced testing/debugging).
149
- */
150
- getWebpiecesContainer(): Container;
151
57
  }