@webpieces/http-server 0.3.234 → 0.3.236
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/package.json +2 -2
- package/src/InProcessApiClientFactory.d.ts +34 -0
- package/src/InProcessApiClientFactory.js +77 -0
- package/src/InProcessApiClientFactory.js.map +1 -0
- package/src/WebpiecesRouteCreator.d.ts +90 -0
- package/src/WebpiecesRouteCreator.js +157 -0
- package/src/WebpiecesRouteCreator.js.map +1 -0
- package/src/WebpiecesServerImpl.d.ts +2 -10
- package/src/WebpiecesServerImpl.js +12 -84
- package/src/WebpiecesServerImpl.js.map +1 -1
- package/src/filters/ContextFilter.d.ts +3 -3
- package/src/filters/ContextFilter.js +9 -15
- package/src/filters/ContextFilter.js.map +1 -1
- package/src/filters/LogApiFilter.d.ts +2 -2
- package/src/filters/LogApiFilter.js +8 -10
- package/src/filters/LogApiFilter.js.map +1 -1
- package/src/filters/RecordingFilter.d.ts +30 -0
- package/src/filters/RecordingFilter.js +91 -0
- package/src/filters/RecordingFilter.js.map +1 -0
- package/src/headers/WebpiecesCoreHeaders.d.ts +4 -37
- package/src/headers/WebpiecesCoreHeaders.js +5 -48
- package/src/headers/WebpiecesCoreHeaders.js.map +1 -1
- package/src/index.d.ts +8 -1
- package/src/index.js +18 -1
- package/src/index.js.map +1 -1
- package/src/modules/WebpiecesModule.js +9 -0
- package/src/modules/WebpiecesModule.js.map +1 -1
- package/src/recorder/SpecGenerator.d.ts +16 -0
- package/src/recorder/SpecGenerator.js +70 -0
- package/src/recorder/SpecGenerator.js.map +1 -0
- package/src/recorder/TestCaseRecorderImpl.d.ts +33 -0
- package/src/recorder/TestCaseRecorderImpl.js +75 -0
- package/src/recorder/TestCaseRecorderImpl.js.map +1 -0
- package/src/recorder/recordable.d.ts +16 -0
- package/src/recorder/recordable.js +52 -0
- package/src/recorder/recordable.js.map +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/http-server",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.236",
|
|
4
4
|
"description": "WebPieces server with filter chain and dependency injection",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"access": "public"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@webpieces/http-routing": "0.3.
|
|
25
|
+
"@webpieces/http-routing": "0.3.236",
|
|
26
26
|
"cors": "2.8.5"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { RouteBuilderImpl } from '@webpieces/http-routing';
|
|
2
|
+
/**
|
|
3
|
+
* InProcessApiClientFactory - Creates API client proxies that invoke routes
|
|
4
|
+
* in-process (full filter chain + controller) WITHOUT any HTTP overhead.
|
|
5
|
+
*
|
|
6
|
+
* Extracted from WebpiecesServerImpl so both the full server
|
|
7
|
+
* (WebpiecesServer.createApiClient) and the embeddable adapter
|
|
8
|
+
* (WebpiecesRouteCreator.createApiClient) share one code path.
|
|
9
|
+
*
|
|
10
|
+
* The client uses the ApiPrototype class to discover routes via decorators,
|
|
11
|
+
* then creates pre-configured invoker functions for each API method.
|
|
12
|
+
*
|
|
13
|
+
* IMPORTANT: This loops over the API methods (from decorators), NOT all routes.
|
|
14
|
+
* For each API method, it sets up the filter chain ONCE during proxy creation,
|
|
15
|
+
* so subsequent calls reuse the same filter chain (efficient!).
|
|
16
|
+
*/
|
|
17
|
+
export declare class InProcessApiClientFactory {
|
|
18
|
+
private routeBuilder;
|
|
19
|
+
constructor(routeBuilder: RouteBuilderImpl);
|
|
20
|
+
/**
|
|
21
|
+
* Create an API client proxy for testing.
|
|
22
|
+
*
|
|
23
|
+
* @param apiPrototype - The API prototype class with routing decorators (can be abstract)
|
|
24
|
+
* @returns A proxy that implements the API interface
|
|
25
|
+
*
|
|
26
|
+
* Example:
|
|
27
|
+
* ```typescript
|
|
28
|
+
* const saveApi = factory.createApiClient<SaveApi>(SaveApi);
|
|
29
|
+
* const response = await saveApi.save(request);
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
createApiClient<T>(apiPrototype: abstract new (...args: any[]) => T): T;
|
|
33
|
+
private runMethod;
|
|
34
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InProcessApiClientFactory = void 0;
|
|
4
|
+
const http_routing_1 = require("@webpieces/http-routing");
|
|
5
|
+
const core_context_1 = require("@webpieces/core-context");
|
|
6
|
+
/**
|
|
7
|
+
* InProcessApiClientFactory - Creates API client proxies that invoke routes
|
|
8
|
+
* in-process (full filter chain + controller) WITHOUT any HTTP overhead.
|
|
9
|
+
*
|
|
10
|
+
* Extracted from WebpiecesServerImpl so both the full server
|
|
11
|
+
* (WebpiecesServer.createApiClient) and the embeddable adapter
|
|
12
|
+
* (WebpiecesRouteCreator.createApiClient) share one code path.
|
|
13
|
+
*
|
|
14
|
+
* The client uses the ApiPrototype class to discover routes via decorators,
|
|
15
|
+
* then creates pre-configured invoker functions for each API method.
|
|
16
|
+
*
|
|
17
|
+
* IMPORTANT: This loops over the API methods (from decorators), NOT all routes.
|
|
18
|
+
* For each API method, it sets up the filter chain ONCE during proxy creation,
|
|
19
|
+
* so subsequent calls reuse the same filter chain (efficient!).
|
|
20
|
+
*/
|
|
21
|
+
class InProcessApiClientFactory {
|
|
22
|
+
routeBuilder;
|
|
23
|
+
constructor(routeBuilder) {
|
|
24
|
+
this.routeBuilder = routeBuilder;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Create an API client proxy for testing.
|
|
28
|
+
*
|
|
29
|
+
* @param apiPrototype - The API prototype class with routing decorators (can be abstract)
|
|
30
|
+
* @returns A proxy that implements the API interface
|
|
31
|
+
*
|
|
32
|
+
* Example:
|
|
33
|
+
* ```typescript
|
|
34
|
+
* const saveApi = factory.createApiClient<SaveApi>(SaveApi);
|
|
35
|
+
* const response = await saveApi.save(request);
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
// webpieces-disable no-any-unknown -- abstract constructor signature requires any[] args
|
|
39
|
+
createApiClient(apiPrototype) {
|
|
40
|
+
// Get endpoints from the API prototype using @ApiPath/@Endpoint decorators
|
|
41
|
+
const basePath = (0, http_routing_1.getApiPath)(apiPrototype) || '';
|
|
42
|
+
const endpoints = (0, http_routing_1.getEndpoints)(apiPrototype) || {};
|
|
43
|
+
// Create proxy object
|
|
44
|
+
// webpieces-disable no-any-unknown -- proxy holds methods of arbitrary API shapes
|
|
45
|
+
const proxy = {};
|
|
46
|
+
// Loop over API endpoints and create proxy functions
|
|
47
|
+
for (const [methodName, endpointPath] of Object.entries(endpoints)) {
|
|
48
|
+
const httpMethod = 'POST';
|
|
49
|
+
const path = basePath + endpointPath;
|
|
50
|
+
const authMeta = (0, http_routing_1.getAuthMeta)(apiPrototype, methodName);
|
|
51
|
+
const routeMeta = new http_routing_1.RouteMetadata(httpMethod, path, methodName, apiPrototype.name, authMeta);
|
|
52
|
+
// Create invoker service ONCE (sets up filter chain once, not on every call!)
|
|
53
|
+
const service = this.routeBuilder.createRouteInvoker(httpMethod, path);
|
|
54
|
+
// Proxy method creates MethodMeta and calls the pre-configured service
|
|
55
|
+
// webpieces-disable no-any-unknown -- request/response DTO types are erased at proxy level
|
|
56
|
+
proxy[methodName] = async (requestDto) => {
|
|
57
|
+
// Auto-activate a RequestContext if the test did not wrap the call itself
|
|
58
|
+
if (!core_context_1.RequestContext.isActive()) {
|
|
59
|
+
return core_context_1.RequestContext.run(async () => {
|
|
60
|
+
return await this.runMethod(routeMeta, requestDto, service);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return await this.runMethod(routeMeta, requestDto, service);
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return proxy;
|
|
67
|
+
}
|
|
68
|
+
// webpieces-disable no-any-unknown -- DTO types are erased at the routing layer
|
|
69
|
+
async runMethod(routeMeta, requestDto, service) {
|
|
70
|
+
// Create MethodMeta without headers (in-process mode - no HTTP involved)
|
|
71
|
+
const meta = new http_routing_1.MethodMeta(routeMeta, undefined, requestDto);
|
|
72
|
+
const responseWrapper = await service.invoke(meta);
|
|
73
|
+
return responseWrapper.response;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
exports.InProcessApiClientFactory = InProcessApiClientFactory;
|
|
77
|
+
//# sourceMappingURL=InProcessApiClientFactory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"InProcessApiClientFactory.js","sourceRoot":"","sources":["../../../../../packages/http/http-server/src/InProcessApiClientFactory.ts"],"names":[],"mappings":";;;AAAA,0DAOiC;AACjC,0DAAyD;AAGzD;;;;;;;;;;;;;;GAcG;AACH,MAAa,yBAAyB;IACd;IAApB,YAAoB,YAA8B;QAA9B,iBAAY,GAAZ,YAAY,CAAkB;IAAG,CAAC;IAEtD;;;;;;;;;;;OAWG;IACH,yFAAyF;IACzF,eAAe,CAAI,YAAgD;QAC/D,2EAA2E;QAC3E,MAAM,QAAQ,GAAG,IAAA,yBAAU,EAAC,YAAY,CAAC,IAAI,EAAE,CAAC;QAChD,MAAM,SAAS,GAAG,IAAA,2BAAY,EAAC,YAAY,CAAC,IAAI,EAAE,CAAC;QAEnD,sBAAsB;QACtB,kFAAkF;QAClF,MAAM,KAAK,GAA4B,EAAE,CAAC;QAE1C,qDAAqD;QACrD,KAAK,MAAM,CAAC,UAAU,EAAE,YAAY,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;YACjE,MAAM,UAAU,GAAG,MAAM,CAAC;YAC1B,MAAM,IAAI,GAAG,QAAQ,GAAG,YAAY,CAAC;YAErC,MAAM,QAAQ,GAAG,IAAA,0BAAW,EAAC,YAAY,EAAE,UAAU,CAAC,CAAC;YACvD,MAAM,SAAS,GAAG,IAAI,4BAAa,CAAC,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,YAAY,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;YAE/F,8EAA8E;YAC9E,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,kBAAkB,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;YAEvE,uEAAuE;YACvE,2FAA2F;YAC3F,KAAK,CAAC,UAAU,CAAC,GAAG,KAAK,EAAE,UAAmB,EAAoB,EAAE;gBAChE,0EAA0E;gBAC1E,IAAI,CAAC,6BAAc,CAAC,QAAQ,EAAE,EAAE,CAAC;oBAC7B,OAAO,6BAAc,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE;wBACjC,OAAO,MAAM,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC;oBAChE,CAAC,CAAC,CAAC;gBACP,CAAC;gBACD,OAAO,MAAM,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC;YAChE,CAAC,CAAC;QACN,CAAC;QAED,OAAO,KAAU,CAAC;IACtB,CAAC;IAED,gFAAgF;IACxE,KAAK,CAAC,SAAS,CAAC,SAAwB,EAAE,UAAmB,EAAE,OAAiD;QACpH,yEAAyE;QACzE,MAAM,IAAI,GAAG,IAAI,yBAAU,CAAC,SAAS,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC;QAC9D,MAAM,eAAe,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACnD,OAAO,eAAe,CAAC,QAAQ,CAAC;IACpC,CAAC;CACJ;AA3DD,8DA2DC","sourcesContent":["import {\n getApiPath,\n getAuthMeta,\n getEndpoints,\n MethodMeta,\n RouteBuilderImpl,\n RouteMetadata,\n} from '@webpieces/http-routing';\nimport { RequestContext } from '@webpieces/core-context';\nimport { Service, WpResponse } from '@webpieces/http-filters';\n\n/**\n * InProcessApiClientFactory - Creates API client proxies that invoke routes\n * in-process (full filter chain + controller) WITHOUT any HTTP overhead.\n *\n * Extracted from WebpiecesServerImpl so both the full server\n * (WebpiecesServer.createApiClient) and the embeddable adapter\n * (WebpiecesRouteCreator.createApiClient) share one code path.\n *\n * The client uses the ApiPrototype class to discover routes via decorators,\n * then creates pre-configured invoker functions for each API method.\n *\n * IMPORTANT: This loops over the API methods (from decorators), NOT all routes.\n * For each API method, it sets up the filter chain ONCE during proxy creation,\n * so subsequent calls reuse the same filter chain (efficient!).\n */\nexport class InProcessApiClientFactory {\n constructor(private routeBuilder: RouteBuilderImpl) {}\n\n /**\n * Create an API client proxy for testing.\n *\n * @param apiPrototype - The API prototype class with routing decorators (can be abstract)\n * @returns A proxy that implements the API interface\n *\n * Example:\n * ```typescript\n * const saveApi = factory.createApiClient<SaveApi>(SaveApi);\n * const response = await saveApi.save(request);\n * ```\n */\n // webpieces-disable no-any-unknown -- abstract constructor signature requires any[] args\n createApiClient<T>(apiPrototype: abstract new (...args: any[]) => T): T {\n // Get endpoints from the API prototype using @ApiPath/@Endpoint decorators\n const basePath = getApiPath(apiPrototype) || '';\n const endpoints = getEndpoints(apiPrototype) || {};\n\n // Create proxy object\n // webpieces-disable no-any-unknown -- proxy holds methods of arbitrary API shapes\n const proxy: Record<string, unknown> = {};\n\n // Loop over API endpoints and create proxy functions\n for (const [methodName, endpointPath] of Object.entries(endpoints)) {\n const httpMethod = 'POST';\n const path = basePath + endpointPath;\n\n const authMeta = getAuthMeta(apiPrototype, methodName);\n const routeMeta = new RouteMetadata(httpMethod, path, methodName, apiPrototype.name, authMeta);\n\n // Create invoker service ONCE (sets up filter chain once, not on every call!)\n const service = this.routeBuilder.createRouteInvoker(httpMethod, path);\n\n // Proxy method creates MethodMeta and calls the pre-configured service\n // webpieces-disable no-any-unknown -- request/response DTO types are erased at proxy level\n proxy[methodName] = async (requestDto: unknown): Promise<unknown> => {\n // Auto-activate a RequestContext if the test did not wrap the call itself\n if (!RequestContext.isActive()) {\n return RequestContext.run(async () => {\n return await this.runMethod(routeMeta, requestDto, service);\n });\n }\n return await this.runMethod(routeMeta, requestDto, service);\n };\n }\n\n return proxy as T;\n }\n\n // webpieces-disable no-any-unknown -- DTO types are erased at the routing layer\n private async runMethod(routeMeta: RouteMetadata, requestDto: unknown, service: Service<MethodMeta, WpResponse<unknown>>): Promise<unknown> {\n // Create MethodMeta without headers (in-process mode - no HTTP involved)\n const meta = new MethodMeta(routeMeta, undefined, requestDto);\n const responseWrapper = await service.invoke(meta);\n return responseWrapper.response;\n }\n}\n"]}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Express } from 'express';
|
|
2
|
+
import { Container } from 'inversify';
|
|
3
|
+
import { ClassType, FilterDefinition, RouteBuilderImpl } from '@webpieces/http-routing';
|
|
4
|
+
import { WebpiecesMiddleware } from './WebpiecesMiddleware';
|
|
5
|
+
/**
|
|
6
|
+
* WebpiecesRouteCreator - Embeddable adapter that mounts the webpieces
|
|
7
|
+
* api -> filters -> controller pipeline onto ANY existing Express app.
|
|
8
|
+
*
|
|
9
|
+
* Legacy Express apps can adopt webpieces incrementally: existing routes and
|
|
10
|
+
* middleware keep working untouched; each wired webpieces route is fully
|
|
11
|
+
* self-contained (own body parsing, own RequestContext, own error->JSON mapping).
|
|
12
|
+
* This class never calls app.use() - it only registers per-route handlers.
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* ```typescript
|
|
16
|
+
* const app = express(); // your existing legacy app
|
|
17
|
+
* const container = new Container();
|
|
18
|
+
* await container.load(buildProviderModule()); // picks up @provideSingleton classes
|
|
19
|
+
* await container.load(WebpiecesModule); // required if you use ContextFilter
|
|
20
|
+
*
|
|
21
|
+
* const creator = new WebpiecesRouteCreator(app, container);
|
|
22
|
+
* creator.wireFilters(
|
|
23
|
+
* new FilterDefinition(2000, ContextFilter, '*'),
|
|
24
|
+
* new FilterDefinition(1900, AuthFilter, 'src/controllers/admin/**'),
|
|
25
|
+
* );
|
|
26
|
+
* creator.wireApi(SaveApi, SaveController); // controller resolved from container
|
|
27
|
+
* creator.wireApi(PublicApi, PublicController);
|
|
28
|
+
* app.listen(8080);
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* Notes:
|
|
32
|
+
* - ALL wireFilters() calls must come BEFORE the first wireApi() call. Filter
|
|
33
|
+
* chains are composed per-route at wireApi time, so late filters would be
|
|
34
|
+
* silently ignored - we throw instead.
|
|
35
|
+
* - Scoped filter glob patterns match the controller filepath from the
|
|
36
|
+
* @SourceFile decorator, falling back to the pattern `**\/{ClassName}.ts`.
|
|
37
|
+
* - Want webpieces' localhost CORS? Opt in yourself:
|
|
38
|
+
* `app.use(new WebpiecesMiddleware().corsForLocalhost())`.
|
|
39
|
+
*
|
|
40
|
+
* This same class is used internally by WebpiecesServerImpl.start(), so the
|
|
41
|
+
* full server and the embeddable adapter share one code path.
|
|
42
|
+
*/
|
|
43
|
+
export declare class WebpiecesRouteCreator {
|
|
44
|
+
private app;
|
|
45
|
+
private routeBuilder;
|
|
46
|
+
private middleware;
|
|
47
|
+
private clientFactory;
|
|
48
|
+
/** Locks wireFilters() once the first wireApi() has composed a filter chain. */
|
|
49
|
+
private apisWired;
|
|
50
|
+
/**
|
|
51
|
+
* @param app - The Express app to mount routes on (yours - never taken over)
|
|
52
|
+
* @param container - Inversify container used to resolve controllers and filters
|
|
53
|
+
* @param routeBuilder - Internal: WebpiecesServerImpl passes its DI singleton; standalone users omit
|
|
54
|
+
* @param middleware - Internal: WebpiecesServerImpl passes its DI singleton; standalone users omit
|
|
55
|
+
*/
|
|
56
|
+
constructor(app: Express, container: Container, routeBuilder?: RouteBuilderImpl, middleware?: WebpiecesMiddleware);
|
|
57
|
+
/**
|
|
58
|
+
* Register filters that wrap every matching route (glob pattern vs controller filepath).
|
|
59
|
+
* Must be called before the first wireApi() - filter chains are composed per-route.
|
|
60
|
+
*/
|
|
61
|
+
wireFilters(...defs: FilterDefinition[]): void;
|
|
62
|
+
/**
|
|
63
|
+
* Wire an API prototype class (with @ApiPath/@Endpoint decorators) to its
|
|
64
|
+
* controller, mounting one Express route per endpoint with the full filter
|
|
65
|
+
* chain. The controller is resolved from the Inversify container.
|
|
66
|
+
*/
|
|
67
|
+
wireApi<TApi, TController extends TApi>(apiPrototype: ClassType<TApi>, controllerClass: ClassType<TController>): void;
|
|
68
|
+
/**
|
|
69
|
+
* Mount every route currently registered on the RouteBuilder.
|
|
70
|
+
* Used by WebpiecesServerImpl.start() where routes were registered up front
|
|
71
|
+
* from WebAppMeta.getRoutes().
|
|
72
|
+
*
|
|
73
|
+
* @returns Number of routes mounted
|
|
74
|
+
*/
|
|
75
|
+
mountRegisteredRoutes(): number;
|
|
76
|
+
/**
|
|
77
|
+
* Create an in-process API client (full filter chain + controller, no HTTP).
|
|
78
|
+
* Same testing story as WebpiecesServer.createApiClient().
|
|
79
|
+
*/
|
|
80
|
+
createApiClient<T>(apiPrototype: abstract new (...args: any[]) => T): T;
|
|
81
|
+
/**
|
|
82
|
+
* Escape hatch for advanced wiring (e.g. addRoute with a hand-built RouteDefinition).
|
|
83
|
+
*/
|
|
84
|
+
getRouteBuilder(): RouteBuilderImpl;
|
|
85
|
+
/**
|
|
86
|
+
* Compose the filter chain for one route and register it on the Express app.
|
|
87
|
+
*/
|
|
88
|
+
private mountRoute;
|
|
89
|
+
private registerHandler;
|
|
90
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WebpiecesRouteCreator = void 0;
|
|
4
|
+
const http_routing_1 = require("@webpieces/http-routing");
|
|
5
|
+
const WebpiecesMiddleware_1 = require("./WebpiecesMiddleware");
|
|
6
|
+
const InProcessApiClientFactory_1 = require("./InProcessApiClientFactory");
|
|
7
|
+
/**
|
|
8
|
+
* WebpiecesRouteCreator - Embeddable adapter that mounts the webpieces
|
|
9
|
+
* api -> filters -> controller pipeline onto ANY existing Express app.
|
|
10
|
+
*
|
|
11
|
+
* Legacy Express apps can adopt webpieces incrementally: existing routes and
|
|
12
|
+
* middleware keep working untouched; each wired webpieces route is fully
|
|
13
|
+
* self-contained (own body parsing, own RequestContext, own error->JSON mapping).
|
|
14
|
+
* This class never calls app.use() - it only registers per-route handlers.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const app = express(); // your existing legacy app
|
|
19
|
+
* const container = new Container();
|
|
20
|
+
* await container.load(buildProviderModule()); // picks up @provideSingleton classes
|
|
21
|
+
* await container.load(WebpiecesModule); // required if you use ContextFilter
|
|
22
|
+
*
|
|
23
|
+
* const creator = new WebpiecesRouteCreator(app, container);
|
|
24
|
+
* creator.wireFilters(
|
|
25
|
+
* new FilterDefinition(2000, ContextFilter, '*'),
|
|
26
|
+
* new FilterDefinition(1900, AuthFilter, 'src/controllers/admin/**'),
|
|
27
|
+
* );
|
|
28
|
+
* creator.wireApi(SaveApi, SaveController); // controller resolved from container
|
|
29
|
+
* creator.wireApi(PublicApi, PublicController);
|
|
30
|
+
* app.listen(8080);
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* Notes:
|
|
34
|
+
* - ALL wireFilters() calls must come BEFORE the first wireApi() call. Filter
|
|
35
|
+
* chains are composed per-route at wireApi time, so late filters would be
|
|
36
|
+
* silently ignored - we throw instead.
|
|
37
|
+
* - Scoped filter glob patterns match the controller filepath from the
|
|
38
|
+
* @SourceFile decorator, falling back to the pattern `**\/{ClassName}.ts`.
|
|
39
|
+
* - Want webpieces' localhost CORS? Opt in yourself:
|
|
40
|
+
* `app.use(new WebpiecesMiddleware().corsForLocalhost())`.
|
|
41
|
+
*
|
|
42
|
+
* This same class is used internally by WebpiecesServerImpl.start(), so the
|
|
43
|
+
* full server and the embeddable adapter share one code path.
|
|
44
|
+
*/
|
|
45
|
+
class WebpiecesRouteCreator {
|
|
46
|
+
app;
|
|
47
|
+
routeBuilder;
|
|
48
|
+
middleware;
|
|
49
|
+
clientFactory;
|
|
50
|
+
/** Locks wireFilters() once the first wireApi() has composed a filter chain. */
|
|
51
|
+
apisWired = false;
|
|
52
|
+
/**
|
|
53
|
+
* @param app - The Express app to mount routes on (yours - never taken over)
|
|
54
|
+
* @param container - Inversify container used to resolve controllers and filters
|
|
55
|
+
* @param routeBuilder - Internal: WebpiecesServerImpl passes its DI singleton; standalone users omit
|
|
56
|
+
* @param middleware - Internal: WebpiecesServerImpl passes its DI singleton; standalone users omit
|
|
57
|
+
*/
|
|
58
|
+
constructor(app, container, routeBuilder, middleware) {
|
|
59
|
+
this.app = app;
|
|
60
|
+
this.routeBuilder = routeBuilder ?? new http_routing_1.RouteBuilderImpl();
|
|
61
|
+
this.routeBuilder.setContainer(container);
|
|
62
|
+
this.middleware = middleware ?? new WebpiecesMiddleware_1.WebpiecesMiddleware();
|
|
63
|
+
this.clientFactory = new InProcessApiClientFactory_1.InProcessApiClientFactory(this.routeBuilder);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Register filters that wrap every matching route (glob pattern vs controller filepath).
|
|
67
|
+
* Must be called before the first wireApi() - filter chains are composed per-route.
|
|
68
|
+
*/
|
|
69
|
+
wireFilters(...defs) {
|
|
70
|
+
if (this.apisWired) {
|
|
71
|
+
throw new Error('wireFilters() must be called before wireApi() - filter chains are composed per-route at wireApi time, so filters added later would never run.');
|
|
72
|
+
}
|
|
73
|
+
for (const def of defs) {
|
|
74
|
+
this.routeBuilder.addFilter(def);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Wire an API prototype class (with @ApiPath/@Endpoint decorators) to its
|
|
79
|
+
* controller, mounting one Express route per endpoint with the full filter
|
|
80
|
+
* chain. The controller is resolved from the Inversify container.
|
|
81
|
+
*/
|
|
82
|
+
wireApi(apiPrototype, controllerClass) {
|
|
83
|
+
this.apisWired = true;
|
|
84
|
+
// Reuses all existing validation: @ApiPath present, controller extends
|
|
85
|
+
// api prototype, every endpoint implemented + has @Authentication.
|
|
86
|
+
const factory = new http_routing_1.ApiRoutingFactory(apiPrototype, controllerClass);
|
|
87
|
+
// Mount only the routes added by THIS call
|
|
88
|
+
const routesBefore = this.routeBuilder.getRoutes().length;
|
|
89
|
+
factory.configure(this.routeBuilder);
|
|
90
|
+
const routes = this.routeBuilder.getRoutes();
|
|
91
|
+
for (let i = routesBefore; i < routes.length; i++) {
|
|
92
|
+
this.mountRoute(routes[i]);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Mount every route currently registered on the RouteBuilder.
|
|
97
|
+
* Used by WebpiecesServerImpl.start() where routes were registered up front
|
|
98
|
+
* from WebAppMeta.getRoutes().
|
|
99
|
+
*
|
|
100
|
+
* @returns Number of routes mounted
|
|
101
|
+
*/
|
|
102
|
+
mountRegisteredRoutes() {
|
|
103
|
+
const routes = this.routeBuilder.getRoutes();
|
|
104
|
+
for (const routeWithMeta of routes) {
|
|
105
|
+
this.mountRoute(routeWithMeta);
|
|
106
|
+
}
|
|
107
|
+
return routes.length;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Create an in-process API client (full filter chain + controller, no HTTP).
|
|
111
|
+
* Same testing story as WebpiecesServer.createApiClient().
|
|
112
|
+
*/
|
|
113
|
+
// webpieces-disable no-any-unknown -- abstract constructor signature requires any[] args
|
|
114
|
+
createApiClient(apiPrototype) {
|
|
115
|
+
return this.clientFactory.createApiClient(apiPrototype);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Escape hatch for advanced wiring (e.g. addRoute with a hand-built RouteDefinition).
|
|
119
|
+
*/
|
|
120
|
+
getRouteBuilder() {
|
|
121
|
+
return this.routeBuilder;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Compose the filter chain for one route and register it on the Express app.
|
|
125
|
+
*/
|
|
126
|
+
mountRoute(routeWithMeta) {
|
|
127
|
+
const service = this.routeBuilder.createRouteHandler(routeWithMeta);
|
|
128
|
+
const routeMeta = routeWithMeta.definition.routeMeta;
|
|
129
|
+
// ExpressWrapper handles the full request/response cycle per route:
|
|
130
|
+
// RequestContext.run, header read, manual JSON body parse, error->ProtocolError
|
|
131
|
+
const wrapper = this.middleware.createExpressWrapper(service, routeMeta);
|
|
132
|
+
this.registerHandler(routeMeta.httpMethod, routeMeta.path, wrapper.execute.bind(wrapper));
|
|
133
|
+
}
|
|
134
|
+
registerHandler(httpMethod, path, expressHandler) {
|
|
135
|
+
switch (httpMethod.toLowerCase()) {
|
|
136
|
+
case 'get':
|
|
137
|
+
this.app.get(path, expressHandler);
|
|
138
|
+
break;
|
|
139
|
+
case 'post':
|
|
140
|
+
this.app.post(path, expressHandler);
|
|
141
|
+
break;
|
|
142
|
+
case 'put':
|
|
143
|
+
this.app.put(path, expressHandler);
|
|
144
|
+
break;
|
|
145
|
+
case 'delete':
|
|
146
|
+
this.app.delete(path, expressHandler);
|
|
147
|
+
break;
|
|
148
|
+
case 'patch':
|
|
149
|
+
this.app.patch(path, expressHandler);
|
|
150
|
+
break;
|
|
151
|
+
default:
|
|
152
|
+
console.warn(`[WebpiecesRouteCreator] Unknown HTTP method: ${httpMethod}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
exports.WebpiecesRouteCreator = WebpiecesRouteCreator;
|
|
157
|
+
//# sourceMappingURL=WebpiecesRouteCreator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WebpiecesRouteCreator.js","sourceRoot":"","sources":["../../../../../packages/http/http-server/src/WebpiecesRouteCreator.ts"],"names":[],"mappings":"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","sourcesContent":["import { Express } from 'express';\nimport { Container } from 'inversify';\nimport {\n ApiRoutingFactory,\n ClassType,\n ExpressRouteHandler,\n FilterDefinition,\n RouteBuilderImpl,\n RouteHandlerWithMeta,\n} from '@webpieces/http-routing';\nimport { WebpiecesMiddleware } from './WebpiecesMiddleware';\nimport { InProcessApiClientFactory } from './InProcessApiClientFactory';\n\n/**\n * WebpiecesRouteCreator - Embeddable adapter that mounts the webpieces\n * api -> filters -> controller pipeline onto ANY existing Express app.\n *\n * Legacy Express apps can adopt webpieces incrementally: existing routes and\n * middleware keep working untouched; each wired webpieces route is fully\n * self-contained (own body parsing, own RequestContext, own error->JSON mapping).\n * This class never calls app.use() - it only registers per-route handlers.\n *\n * Usage:\n * ```typescript\n * const app = express(); // your existing legacy app\n * const container = new Container();\n * await container.load(buildProviderModule()); // picks up @provideSingleton classes\n * await container.load(WebpiecesModule); // required if you use ContextFilter\n *\n * const creator = new WebpiecesRouteCreator(app, container);\n * creator.wireFilters(\n * new FilterDefinition(2000, ContextFilter, '*'),\n * new FilterDefinition(1900, AuthFilter, 'src/controllers/admin/**'),\n * );\n * creator.wireApi(SaveApi, SaveController); // controller resolved from container\n * creator.wireApi(PublicApi, PublicController);\n * app.listen(8080);\n * ```\n *\n * Notes:\n * - ALL wireFilters() calls must come BEFORE the first wireApi() call. Filter\n * chains are composed per-route at wireApi time, so late filters would be\n * silently ignored - we throw instead.\n * - Scoped filter glob patterns match the controller filepath from the\n * @SourceFile decorator, falling back to the pattern `**\\/{ClassName}.ts`.\n * - Want webpieces' localhost CORS? Opt in yourself:\n * `app.use(new WebpiecesMiddleware().corsForLocalhost())`.\n *\n * This same class is used internally by WebpiecesServerImpl.start(), so the\n * full server and the embeddable adapter share one code path.\n */\nexport class WebpiecesRouteCreator {\n private routeBuilder: RouteBuilderImpl;\n private middleware: WebpiecesMiddleware;\n private clientFactory: InProcessApiClientFactory;\n\n /** Locks wireFilters() once the first wireApi() has composed a filter chain. */\n private apisWired = false;\n\n /**\n * @param app - The Express app to mount routes on (yours - never taken over)\n * @param container - Inversify container used to resolve controllers and filters\n * @param routeBuilder - Internal: WebpiecesServerImpl passes its DI singleton; standalone users omit\n * @param middleware - Internal: WebpiecesServerImpl passes its DI singleton; standalone users omit\n */\n constructor(\n private app: Express,\n container: Container,\n routeBuilder?: RouteBuilderImpl,\n middleware?: WebpiecesMiddleware,\n ) {\n this.routeBuilder = routeBuilder ?? new RouteBuilderImpl();\n this.routeBuilder.setContainer(container);\n this.middleware = middleware ?? new WebpiecesMiddleware();\n this.clientFactory = new InProcessApiClientFactory(this.routeBuilder);\n }\n\n /**\n * Register filters that wrap every matching route (glob pattern vs controller filepath).\n * Must be called before the first wireApi() - filter chains are composed per-route.\n */\n wireFilters(...defs: FilterDefinition[]): void {\n if (this.apisWired) {\n throw new Error(\n 'wireFilters() must be called before wireApi() - filter chains are composed per-route at wireApi time, so filters added later would never run.',\n );\n }\n for (const def of defs) {\n this.routeBuilder.addFilter(def);\n }\n }\n\n /**\n * Wire an API prototype class (with @ApiPath/@Endpoint decorators) to its\n * controller, mounting one Express route per endpoint with the full filter\n * chain. The controller is resolved from the Inversify container.\n */\n wireApi<TApi, TController extends TApi>(\n apiPrototype: ClassType<TApi>,\n controllerClass: ClassType<TController>,\n ): void {\n this.apisWired = true;\n\n // Reuses all existing validation: @ApiPath present, controller extends\n // api prototype, every endpoint implemented + has @Authentication.\n const factory = new ApiRoutingFactory(apiPrototype, controllerClass);\n\n // Mount only the routes added by THIS call\n const routesBefore = this.routeBuilder.getRoutes().length;\n factory.configure(this.routeBuilder);\n const routes = this.routeBuilder.getRoutes();\n\n for (let i = routesBefore; i < routes.length; i++) {\n this.mountRoute(routes[i]);\n }\n }\n\n /**\n * Mount every route currently registered on the RouteBuilder.\n * Used by WebpiecesServerImpl.start() where routes were registered up front\n * from WebAppMeta.getRoutes().\n *\n * @returns Number of routes mounted\n */\n mountRegisteredRoutes(): number {\n const routes = this.routeBuilder.getRoutes();\n for (const routeWithMeta of routes) {\n this.mountRoute(routeWithMeta);\n }\n return routes.length;\n }\n\n /**\n * Create an in-process API client (full filter chain + controller, no HTTP).\n * Same testing story as WebpiecesServer.createApiClient().\n */\n // webpieces-disable no-any-unknown -- abstract constructor signature requires any[] args\n createApiClient<T>(apiPrototype: abstract new (...args: any[]) => T): T {\n return this.clientFactory.createApiClient(apiPrototype);\n }\n\n /**\n * Escape hatch for advanced wiring (e.g. addRoute with a hand-built RouteDefinition).\n */\n getRouteBuilder(): RouteBuilderImpl {\n return this.routeBuilder;\n }\n\n /**\n * Compose the filter chain for one route and register it on the Express app.\n */\n private mountRoute(routeWithMeta: RouteHandlerWithMeta): void {\n const service = this.routeBuilder.createRouteHandler(routeWithMeta);\n const routeMeta = routeWithMeta.definition.routeMeta;\n\n // ExpressWrapper handles the full request/response cycle per route:\n // RequestContext.run, header read, manual JSON body parse, error->ProtocolError\n const wrapper = this.middleware.createExpressWrapper(service, routeMeta);\n\n this.registerHandler(\n routeMeta.httpMethod,\n routeMeta.path,\n wrapper.execute.bind(wrapper),\n );\n }\n\n private registerHandler(httpMethod: string, path: string, expressHandler: ExpressRouteHandler): void {\n switch (httpMethod.toLowerCase()) {\n case 'get':\n this.app.get(path, expressHandler);\n break;\n case 'post':\n this.app.post(path, expressHandler);\n break;\n case 'put':\n this.app.put(path, expressHandler);\n break;\n case 'delete':\n this.app.delete(path, expressHandler);\n break;\n case 'patch':\n this.app.patch(path, expressHandler);\n break;\n default:\n console.warn(`[WebpiecesRouteCreator] Unknown HTTP method: ${httpMethod}`);\n }\n }\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Container, ContainerModule } from 'inversify';
|
|
2
|
-
import {
|
|
2
|
+
import { RouteBuilderImpl, WebAppMeta } from '@webpieces/http-routing';
|
|
3
3
|
import { WebpiecesServer } from './WebpiecesServer';
|
|
4
4
|
import { WebpiecesMiddleware } from './WebpiecesMiddleware';
|
|
5
5
|
/**
|
|
@@ -84,14 +84,6 @@ export declare class WebpiecesServerImpl implements WebpiecesServer {
|
|
|
84
84
|
* @returns Promise that resolves when server is ready
|
|
85
85
|
*/
|
|
86
86
|
start(port?: number, testMode?: boolean): Promise<void>;
|
|
87
|
-
/**
|
|
88
|
-
* Register Express routes - the SINGLE loop over routes.
|
|
89
|
-
* For each route: createHandler (sets up filter chain) → wrapExpress → registerHandler.
|
|
90
|
-
*
|
|
91
|
-
* @returns Number of routes registered
|
|
92
|
-
*/
|
|
93
|
-
private registerExpressRoutes;
|
|
94
|
-
registerHandler(httpMethod: string, path: string, expressHandler: ExpressRouteHandler): void;
|
|
95
87
|
/**
|
|
96
88
|
* Stop the HTTP server.
|
|
97
89
|
* Returns a Promise that resolves when the server is stopped,
|
|
@@ -131,5 +123,5 @@ export declare class WebpiecesServerImpl implements WebpiecesServer {
|
|
|
131
123
|
* ```
|
|
132
124
|
*/
|
|
133
125
|
createApiClient<T>(apiPrototype: abstract new (...args: any[]) => T): T;
|
|
134
|
-
private
|
|
126
|
+
private clientFactory?;
|
|
135
127
|
}
|
|
@@ -7,7 +7,8 @@ const inversify_1 = require("inversify");
|
|
|
7
7
|
const binding_decorators_1 = require("@inversifyjs/binding-decorators");
|
|
8
8
|
const http_routing_1 = require("@webpieces/http-routing");
|
|
9
9
|
const WebpiecesMiddleware_1 = require("./WebpiecesMiddleware");
|
|
10
|
-
const
|
|
10
|
+
const WebpiecesRouteCreator_1 = require("./WebpiecesRouteCreator");
|
|
11
|
+
const InProcessApiClientFactory_1 = require("./InProcessApiClientFactory");
|
|
11
12
|
/**
|
|
12
13
|
* WebpiecesServerImpl - Internal server implementation.
|
|
13
14
|
*
|
|
@@ -145,8 +146,10 @@ let WebpiecesServerImpl = class WebpiecesServerImpl {
|
|
|
145
146
|
this.app.use(this.middleware.corsForLocalhost());
|
|
146
147
|
// Layer 3: Request/Response Logging
|
|
147
148
|
this.app.use(this.middleware.logNextLayer.bind(this.middleware));
|
|
148
|
-
// Register routes
|
|
149
|
-
|
|
149
|
+
// Register routes via the shared adapter (same code path as the
|
|
150
|
+
// embeddable WebpiecesRouteCreator used by legacy Express apps)
|
|
151
|
+
const routeCreator = new WebpiecesRouteCreator_1.WebpiecesRouteCreator(this.app, this.appContainer, this.routeBuilder, this.middleware);
|
|
152
|
+
const routeCount = routeCreator.mountRegisteredRoutes();
|
|
150
153
|
// Start listening - wrap in Promise
|
|
151
154
|
const promise = new Promise((resolve, reject) => {
|
|
152
155
|
this.server = this.app.listen(this.port, (error) => {
|
|
@@ -162,52 +165,6 @@ let WebpiecesServerImpl = class WebpiecesServerImpl {
|
|
|
162
165
|
});
|
|
163
166
|
await promise;
|
|
164
167
|
}
|
|
165
|
-
/**
|
|
166
|
-
* Register Express routes - the SINGLE loop over routes.
|
|
167
|
-
* For each route: createHandler (sets up filter chain) → wrapExpress → registerHandler.
|
|
168
|
-
*
|
|
169
|
-
* @returns Number of routes registered
|
|
170
|
-
*/
|
|
171
|
-
registerExpressRoutes() {
|
|
172
|
-
if (!this.app) {
|
|
173
|
-
throw new Error('Express app not initialized');
|
|
174
|
-
}
|
|
175
|
-
const routes = this.routeBuilder.getRoutes();
|
|
176
|
-
let count = 0;
|
|
177
|
-
for (const routeWithMeta of routes) {
|
|
178
|
-
const service = this.routeBuilder.createRouteHandler(routeWithMeta);
|
|
179
|
-
const routeMeta = routeWithMeta.definition.routeMeta;
|
|
180
|
-
// Create ExpressWrapper directly (handles full request/response cycle)
|
|
181
|
-
const wrapper = this.middleware.createExpressWrapper(service, routeMeta);
|
|
182
|
-
this.registerHandler(routeMeta.httpMethod, routeMeta.path, wrapper.execute.bind(wrapper));
|
|
183
|
-
count++;
|
|
184
|
-
}
|
|
185
|
-
return count;
|
|
186
|
-
}
|
|
187
|
-
registerHandler(httpMethod, path, expressHandler) {
|
|
188
|
-
if (!this.app) {
|
|
189
|
-
throw new Error('Express app not initialized');
|
|
190
|
-
}
|
|
191
|
-
switch (httpMethod.toLowerCase()) {
|
|
192
|
-
case 'get':
|
|
193
|
-
this.app.get(path, expressHandler);
|
|
194
|
-
break;
|
|
195
|
-
case 'post':
|
|
196
|
-
this.app.post(path, expressHandler);
|
|
197
|
-
break;
|
|
198
|
-
case 'put':
|
|
199
|
-
this.app.put(path, expressHandler);
|
|
200
|
-
break;
|
|
201
|
-
case 'delete':
|
|
202
|
-
this.app.delete(path, expressHandler);
|
|
203
|
-
break;
|
|
204
|
-
case 'patch':
|
|
205
|
-
this.app.patch(path, expressHandler);
|
|
206
|
-
break;
|
|
207
|
-
default:
|
|
208
|
-
console.warn(`[WebpiecesServer] Unknown HTTP method: ${httpMethod}`);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
168
|
/**
|
|
212
169
|
* Stop the HTTP server.
|
|
213
170
|
* Returns a Promise that resolves when the server is stopped,
|
|
@@ -263,47 +220,18 @@ let WebpiecesServerImpl = class WebpiecesServerImpl {
|
|
|
263
220
|
* const response = await saveApi.save(request);
|
|
264
221
|
* ```
|
|
265
222
|
*/
|
|
223
|
+
// webpieces-disable no-any-unknown -- abstract constructor signature requires any[] args
|
|
266
224
|
createApiClient(apiPrototype) {
|
|
267
225
|
if (!this.initialized) {
|
|
268
226
|
throw new Error('Server not initialized. Call initialize() before createApiClient().');
|
|
269
227
|
}
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
// Create proxy object
|
|
274
|
-
const proxy = {};
|
|
275
|
-
// Loop over API endpoints and create proxy functions
|
|
276
|
-
for (const [methodName, endpointPath] of Object.entries(endpoints)) {
|
|
277
|
-
const httpMethod = 'POST';
|
|
278
|
-
const path = basePath + endpointPath;
|
|
279
|
-
const authMeta = (0, http_routing_1.getAuthMeta)(apiPrototype, methodName);
|
|
280
|
-
const routeMeta = new http_routing_1.RouteMetadata(httpMethod, path, methodName, apiPrototype.name, authMeta);
|
|
281
|
-
// Create invoker service ONCE (sets up filter chain once, not on every call!)
|
|
282
|
-
const service = this.routeBuilder.createRouteInvoker(httpMethod, path);
|
|
283
|
-
// Proxy method creates MethodMeta and calls the pre-configured service
|
|
284
|
-
// IMPORTANT: Tests MUST wrap calls in RequestContext.run() themselves
|
|
285
|
-
// This forces explicit context setup in tests, matching production behavior
|
|
286
|
-
proxy[methodName] = async (requestDto) => {
|
|
287
|
-
// Verify we're inside an active RequestContext
|
|
288
|
-
// This helps test authors know they need to wrap their test in RequestContext.run()
|
|
289
|
-
if (!core_context_1.RequestContext.isActive()) {
|
|
290
|
-
//Many devs may not activate headers
|
|
291
|
-
return core_context_1.RequestContext.run(async () => {
|
|
292
|
-
return await this.runMethod(routeMeta, requestDto, service);
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
return await this.runMethod(routeMeta, requestDto, service);
|
|
296
|
-
};
|
|
228
|
+
// Delegates to the shared factory (same code path as WebpiecesRouteCreator.createApiClient)
|
|
229
|
+
if (!this.clientFactory) {
|
|
230
|
+
this.clientFactory = new InProcessApiClientFactory_1.InProcessApiClientFactory(this.routeBuilder);
|
|
297
231
|
}
|
|
298
|
-
return
|
|
299
|
-
}
|
|
300
|
-
async runMethod(routeMeta, requestDto, service) {
|
|
301
|
-
// Create MethodMeta without headers (test mode - no HTTP involved)
|
|
302
|
-
// requestHeaders is optional, so we can omit it
|
|
303
|
-
const meta = new http_routing_1.MethodMeta(routeMeta, undefined, requestDto);
|
|
304
|
-
const responseWrapper = await service.invoke(meta);
|
|
305
|
-
return responseWrapper.response;
|
|
232
|
+
return this.clientFactory.createApiClient(apiPrototype);
|
|
306
233
|
}
|
|
234
|
+
clientFactory;
|
|
307
235
|
};
|
|
308
236
|
exports.WebpiecesServerImpl = WebpiecesServerImpl;
|
|
309
237
|
exports.WebpiecesServerImpl = WebpiecesServerImpl = tslib_1.__decorate([
|