@webpieces/http-routing 0.3.297 → 0.3.299
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 +3 -3
- package/src/ApiClient.d.ts +15 -12
- package/src/ApiClient.js +11 -13
- package/src/ApiClient.js.map +1 -1
- package/src/ApiClientFactory.d.ts +33 -0
- package/src/ApiClientFactory.js +103 -0
- package/src/ApiClientFactory.js.map +1 -0
- package/src/ApiRoutingFactory.js +1 -1
- package/src/ApiRoutingFactory.js.map +1 -1
- package/src/RouteBuilderImpl.d.ts +0 -8
- package/src/RouteBuilderImpl.js +0 -10
- package/src/RouteBuilderImpl.js.map +1 -1
- package/src/WebpiecesRouter.d.ts +6 -4
- package/src/WebpiecesRouter.js +12 -8
- package/src/WebpiecesRouter.js.map +1 -1
- package/src/index.d.ts +1 -1
- package/src/index.js.map +1 -1
- package/src/InProcessApiClientFactory.d.ts +0 -35
- package/src/InProcessApiClientFactory.js +0 -90
- package/src/InProcessApiClientFactory.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/http-routing",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.299",
|
|
4
4
|
"description": "Decorator-based routing with auto-wiring for WebPieces",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@inversifyjs/binding-decorators": "1.1.5",
|
|
25
|
-
"@webpieces/core-context": "0.3.
|
|
26
|
-
"@webpieces/core-util": "0.3.
|
|
25
|
+
"@webpieces/core-context": "0.3.299",
|
|
26
|
+
"@webpieces/core-util": "0.3.299",
|
|
27
27
|
"inversify": "7.10.4",
|
|
28
28
|
"minimatch": "10.0.1"
|
|
29
29
|
}
|
package/src/ApiClient.d.ts
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
|
-
import { RouteMetadata } from '@webpieces/core-util';
|
|
2
1
|
import { ClassType } from './ApiRoutingFactory';
|
|
3
|
-
import { MethodMeta } from './MethodMeta';
|
|
4
|
-
import { Service, WpResponse } from './Filter';
|
|
5
2
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
3
|
+
* ApiClientProxy - the in-process client createApiClient(api) returns: a map of method name →
|
|
4
|
+
* invoker(dto) that runs the filter chain → controller. (Cast to the API interface T for callers.)
|
|
5
|
+
*/
|
|
6
|
+
export type ApiClientProxy = Record<string, (requestDto: unknown) => Promise<unknown>>;
|
|
7
|
+
/**
|
|
8
|
+
* ApiClient - one registered API surface, reified by {@link ApiClientFactory}:
|
|
9
|
+
* - `api` : the contract class passed to addRoutes,
|
|
10
|
+
* - `client` : exactly what createApiClient(api) returns — a proxy whose methods run the
|
|
11
|
+
* filter chain → controller.
|
|
9
12
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
+
* That is ALL a transport needs: the express layer reads the api's @ApiPath/@Endpoint decorators
|
|
14
|
+
* to bind each method's HTTP route to the matching `client` method — one-to-one with a test call
|
|
15
|
+
* `client.method(dto)`. The proxy forms the RouteMetadata and drives it through the filters, so
|
|
16
|
+
* the internal RouteBuilder never leaks out. Data-only structure (a class, per the guidelines).
|
|
13
17
|
*/
|
|
14
18
|
export declare class ApiClient {
|
|
15
19
|
readonly api: ClassType;
|
|
16
|
-
readonly
|
|
17
|
-
|
|
18
|
-
constructor(api: ClassType, routeMeta: RouteMetadata, impl: Service<MethodMeta, WpResponse<unknown>>);
|
|
20
|
+
readonly client: ApiClientProxy;
|
|
21
|
+
constructor(api: ClassType, client: ApiClientProxy);
|
|
19
22
|
}
|
package/src/ApiClient.js
CHANGED
|
@@ -2,24 +2,22 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ApiClient = void 0;
|
|
4
4
|
/**
|
|
5
|
-
* ApiClient - one
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* ApiClient - one registered API surface, reified by {@link ApiClientFactory}:
|
|
6
|
+
* - `api` : the contract class passed to addRoutes,
|
|
7
|
+
* - `client` : exactly what createApiClient(api) returns — a proxy whose methods run the
|
|
8
|
+
* filter chain → controller.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* That is ALL a transport needs: the express layer reads the api's @ApiPath/@Endpoint decorators
|
|
11
|
+
* to bind each method's HTTP route to the matching `client` method — one-to-one with a test call
|
|
12
|
+
* `client.method(dto)`. The proxy forms the RouteMetadata and drives it through the filters, so
|
|
13
|
+
* the internal RouteBuilder never leaks out. Data-only structure (a class, per the guidelines).
|
|
12
14
|
*/
|
|
13
15
|
class ApiClient {
|
|
14
16
|
api;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
constructor(api, routeMeta,
|
|
18
|
-
// webpieces-disable no-any-unknown -- WpResponse<unknown>: the composed impl is response-type-erased at the filter boundary
|
|
19
|
-
impl) {
|
|
17
|
+
client;
|
|
18
|
+
constructor(api, client) {
|
|
20
19
|
this.api = api;
|
|
21
|
-
this.
|
|
22
|
-
this.impl = impl;
|
|
20
|
+
this.client = client;
|
|
23
21
|
}
|
|
24
22
|
}
|
|
25
23
|
exports.ApiClient = ApiClient;
|
package/src/ApiClient.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ApiClient.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/ApiClient.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"ApiClient.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/ApiClient.ts"],"names":[],"mappings":";;;AASA;;;;;;;;;;GAUG;AACH,MAAa,SAAS;IAEE;IACA;IAFpB,YACoB,GAAc,EACd,MAAsB;QADtB,QAAG,GAAH,GAAG,CAAW;QACd,WAAM,GAAN,MAAM,CAAgB;IACvC,CAAC;CACP;AALD,8BAKC","sourcesContent":["import { ClassType } from './ApiRoutingFactory';\n\n/**\n * ApiClientProxy - the in-process client createApiClient(api) returns: a map of method name →\n * invoker(dto) that runs the filter chain → controller. (Cast to the API interface T for callers.)\n */\n// webpieces-disable no-any-unknown -- proxy holds methods of arbitrary API shapes; DTOs are erased\nexport type ApiClientProxy = Record<string, (requestDto: unknown) => Promise<unknown>>;\n\n/**\n * ApiClient - one registered API surface, reified by {@link ApiClientFactory}:\n * - `api` : the contract class passed to addRoutes,\n * - `client` : exactly what createApiClient(api) returns — a proxy whose methods run the\n * filter chain → controller.\n *\n * That is ALL a transport needs: the express layer reads the api's @ApiPath/@Endpoint decorators\n * to bind each method's HTTP route to the matching `client` method — one-to-one with a test call\n * `client.method(dto)`. The proxy forms the RouteMetadata and drives it through the filters, so\n * the internal RouteBuilder never leaks out. Data-only structure (a class, per the guidelines).\n */\nexport class ApiClient {\n constructor(\n public readonly api: ClassType,\n public readonly client: ApiClientProxy,\n ) {}\n}\n"]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { RouteBuilderImpl } from './RouteBuilderImpl';
|
|
2
|
+
import { ApiClient } from './ApiClient';
|
|
3
|
+
/**
|
|
4
|
+
* ApiClientFactory - THE piece that wires api → Proxy → filters → controller.
|
|
5
|
+
*
|
|
6
|
+
* For an API prototype (its @ApiPath/@Endpoint decorators) it builds a proxy whose methods
|
|
7
|
+
* invoke the composed filter chain (via RouteBuilder.createRouteInvoker) — that proxy IS what
|
|
8
|
+
* createApiClient() returns. {@link apiClients} reuses the SAME proxy per registered api, so the
|
|
9
|
+
* express layer binds each method through it. There is no express dependency here, so the proxy
|
|
10
|
+
* is the single invocation path for BOTH in-process (tests) and HTTP (the express adapter drives
|
|
11
|
+
* the same proxy after publishing the request).
|
|
12
|
+
*
|
|
13
|
+
* @provideFrameworkSingleton so WebpiecesRouter can inject it (it shares the one RouteBuilder).
|
|
14
|
+
*/
|
|
15
|
+
export declare class ApiClientFactory {
|
|
16
|
+
private readonly routeBuilder;
|
|
17
|
+
private readonly contextMgr;
|
|
18
|
+
constructor(routeBuilder: RouteBuilderImpl);
|
|
19
|
+
/**
|
|
20
|
+
* Create an API client proxy (cast to the API interface T). The proxy's methods run the full
|
|
21
|
+
* filter chain + controller; used by tests in-process AND driven by the express adapter.
|
|
22
|
+
*/
|
|
23
|
+
createApiClient<T>(apiPrototype: abstract new (...args: any[]) => T): T;
|
|
24
|
+
/**
|
|
25
|
+
* Reify every registered API as an {@link ApiClient} — the contract + its proxy (the
|
|
26
|
+
* createApiClient object). The transport reads the api's decorators to bind each endpoint to
|
|
27
|
+
* the proxy's matching method, so no route metadata needs to leave here.
|
|
28
|
+
*/
|
|
29
|
+
apiClients(): ApiClient[];
|
|
30
|
+
/** Build the proxy record (method name → invoker) from the API prototype's decorators. */
|
|
31
|
+
private buildProxy;
|
|
32
|
+
private runMethod;
|
|
33
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ApiClientFactory = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const inversify_1 = require("inversify");
|
|
6
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
7
|
+
const core_context_1 = require("@webpieces/core-context");
|
|
8
|
+
const MethodMeta_1 = require("./MethodMeta");
|
|
9
|
+
const RouteBuilderImpl_1 = require("./RouteBuilderImpl");
|
|
10
|
+
const ApiClient_1 = require("./ApiClient");
|
|
11
|
+
const fillContext_1 = require("./fillContext");
|
|
12
|
+
/**
|
|
13
|
+
* ApiClientFactory - THE piece that wires api → Proxy → filters → controller.
|
|
14
|
+
*
|
|
15
|
+
* For an API prototype (its @ApiPath/@Endpoint decorators) it builds a proxy whose methods
|
|
16
|
+
* invoke the composed filter chain (via RouteBuilder.createRouteInvoker) — that proxy IS what
|
|
17
|
+
* createApiClient() returns. {@link apiClients} reuses the SAME proxy per registered api, so the
|
|
18
|
+
* express layer binds each method through it. There is no express dependency here, so the proxy
|
|
19
|
+
* is the single invocation path for BOTH in-process (tests) and HTTP (the express adapter drives
|
|
20
|
+
* the same proxy after publishing the request).
|
|
21
|
+
*
|
|
22
|
+
* @provideFrameworkSingleton so WebpiecesRouter can inject it (it shares the one RouteBuilder).
|
|
23
|
+
*/
|
|
24
|
+
let ApiClientFactory = class ApiClientFactory {
|
|
25
|
+
routeBuilder;
|
|
26
|
+
// Builds request headers the SAME way the real HTTP client does — from the ambient
|
|
27
|
+
// RequestContext — so a credential a test put in context travels as a real request header.
|
|
28
|
+
contextMgr = new core_util_1.ContextMgr(new core_context_1.RequestContextReader());
|
|
29
|
+
constructor(routeBuilder) {
|
|
30
|
+
this.routeBuilder = routeBuilder;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Create an API client proxy (cast to the API interface T). The proxy's methods run the full
|
|
34
|
+
* filter chain + controller; used by tests in-process AND driven by the express adapter.
|
|
35
|
+
*/
|
|
36
|
+
// webpieces-disable no-any-unknown -- abstract constructor signature requires any[] args
|
|
37
|
+
createApiClient(apiPrototype) {
|
|
38
|
+
return this.buildProxy(apiPrototype);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Reify every registered API as an {@link ApiClient} — the contract + its proxy (the
|
|
42
|
+
* createApiClient object). The transport reads the api's decorators to bind each endpoint to
|
|
43
|
+
* the proxy's matching method, so no route metadata needs to leave here.
|
|
44
|
+
*/
|
|
45
|
+
apiClients() {
|
|
46
|
+
const apis = new Set();
|
|
47
|
+
for (const route of this.routeBuilder.getRoutes()) {
|
|
48
|
+
apis.add(route.definition.apiClass);
|
|
49
|
+
}
|
|
50
|
+
return [...apis].map((api) => new ApiClient_1.ApiClient(api, this.buildProxy(api)));
|
|
51
|
+
}
|
|
52
|
+
/** Build the proxy record (method name → invoker) from the API prototype's decorators. */
|
|
53
|
+
// webpieces-disable no-any-unknown -- accepts any ClassType / abstract-constructor API prototype
|
|
54
|
+
buildProxy(apiPrototype) {
|
|
55
|
+
const basePath = (0, core_util_1.getApiPath)(apiPrototype) || '';
|
|
56
|
+
const endpoints = (0, core_util_1.getEndpoints)(apiPrototype) || {};
|
|
57
|
+
const proxy = {};
|
|
58
|
+
for (const [methodName, endpointPath] of Object.entries(endpoints)) {
|
|
59
|
+
const httpMethod = 'POST';
|
|
60
|
+
const path = basePath + endpointPath;
|
|
61
|
+
// Use the REGISTERED route's metadata — it carries the real controller name AND api
|
|
62
|
+
// name (so logging/recording read the right one); createRouteInvoker composes its chain.
|
|
63
|
+
const routeMeta = this.routeBuilder.getRouteMeta(httpMethod, path);
|
|
64
|
+
if (!routeMeta) {
|
|
65
|
+
throw new Error(`No registered route for ${apiPrototype.name}.${methodName} (${httpMethod} ${path}) — call addRoutes(api, controller) first.`);
|
|
66
|
+
}
|
|
67
|
+
const service = this.routeBuilder.createRouteInvoker(httpMethod, path);
|
|
68
|
+
// webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary
|
|
69
|
+
proxy[methodName] = async (requestDto) => {
|
|
70
|
+
// Auto-activate a RequestContext if the caller (a pure in-process test) did not.
|
|
71
|
+
if (!core_context_1.RequestContext.isActive()) {
|
|
72
|
+
return core_context_1.RequestContext.run(async () => this.runMethod(routeMeta, requestDto, service));
|
|
73
|
+
}
|
|
74
|
+
return this.runMethod(routeMeta, requestDto, service);
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return proxy;
|
|
78
|
+
}
|
|
79
|
+
// webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary
|
|
80
|
+
async runMethod(routeMeta, requestDto, service) {
|
|
81
|
+
// Only synthesize the request when NONE was published by a transport. The express adapter
|
|
82
|
+
// publishes the HttpRequest from `req` before calling the proxy, so its request wins; a
|
|
83
|
+
// pure in-process call synthesizes one from the ambient context (client-like).
|
|
84
|
+
if (!core_context_1.RequestContext.getRequest()) {
|
|
85
|
+
const headers = new Map();
|
|
86
|
+
this.contextMgr.buildOutboundHeaders().forEach((value, name) => {
|
|
87
|
+
headers.set(name.toLowerCase(), [value]);
|
|
88
|
+
});
|
|
89
|
+
core_context_1.RequestContext.setRequest(new core_context_1.HttpRequest(routeMeta.httpMethod, routeMeta.path, headers));
|
|
90
|
+
(0, fillContext_1.fillContext)();
|
|
91
|
+
}
|
|
92
|
+
const responseWrapper = await service.invoke(new MethodMeta_1.MethodMeta(routeMeta, requestDto));
|
|
93
|
+
return responseWrapper.response;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
exports.ApiClientFactory = ApiClientFactory;
|
|
97
|
+
exports.ApiClientFactory = ApiClientFactory = tslib_1.__decorate([
|
|
98
|
+
(0, core_context_1.provideFrameworkSingleton)(),
|
|
99
|
+
(0, inversify_1.injectable)(),
|
|
100
|
+
tslib_1.__param(0, (0, inversify_1.inject)(RouteBuilderImpl_1.RouteBuilderImpl)),
|
|
101
|
+
tslib_1.__metadata("design:paramtypes", [RouteBuilderImpl_1.RouteBuilderImpl])
|
|
102
|
+
], ApiClientFactory);
|
|
103
|
+
//# sourceMappingURL=ApiClientFactory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ApiClientFactory.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/ApiClientFactory.ts"],"names":[],"mappings":"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","sourcesContent":["import { inject, injectable } from 'inversify';\nimport {\n getApiPath,\n getEndpoints,\n RouteMetadata,\n ContextMgr,\n} from '@webpieces/core-util';\nimport { provideFrameworkSingleton, RequestContext, HttpRequest, RequestContextReader } from '@webpieces/core-context';\nimport { MethodMeta } from './MethodMeta';\nimport { Service, WpResponse } from './Filter';\nimport { RouteBuilderImpl } from './RouteBuilderImpl';\nimport { ApiClient, ApiClientProxy } from './ApiClient';\nimport { ClassType } from './ApiRoutingFactory';\nimport { fillContext } from './fillContext';\n\n/**\n * ApiClientFactory - THE piece that wires api → Proxy → filters → controller.\n *\n * For an API prototype (its @ApiPath/@Endpoint decorators) it builds a proxy whose methods\n * invoke the composed filter chain (via RouteBuilder.createRouteInvoker) — that proxy IS what\n * createApiClient() returns. {@link apiClients} reuses the SAME proxy per registered api, so the\n * express layer binds each method through it. There is no express dependency here, so the proxy\n * is the single invocation path for BOTH in-process (tests) and HTTP (the express adapter drives\n * the same proxy after publishing the request).\n *\n * @provideFrameworkSingleton so WebpiecesRouter can inject it (it shares the one RouteBuilder).\n */\n@provideFrameworkSingleton()\n@injectable()\nexport class ApiClientFactory {\n // Builds request headers the SAME way the real HTTP client does — from the ambient\n // RequestContext — so a credential a test put in context travels as a real request header.\n private readonly contextMgr = new ContextMgr(new RequestContextReader());\n\n constructor(@inject(RouteBuilderImpl) private readonly routeBuilder: RouteBuilderImpl) {}\n\n /**\n * Create an API client proxy (cast to the API interface T). The proxy's methods run the full\n * filter chain + controller; used by tests in-process AND driven by the express adapter.\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.buildProxy(apiPrototype) as T;\n }\n\n /**\n * Reify every registered API as an {@link ApiClient} — the contract + its proxy (the\n * createApiClient object). The transport reads the api's decorators to bind each endpoint to\n * the proxy's matching method, so no route metadata needs to leave here.\n */\n apiClients(): ApiClient[] {\n const apis = new Set<ClassType>();\n for (const route of this.routeBuilder.getRoutes()) {\n apis.add(route.definition.apiClass as ClassType);\n }\n return [...apis].map((api: ClassType) => new ApiClient(api, this.buildProxy(api)));\n }\n\n /** Build the proxy record (method name → invoker) from the API prototype's decorators. */\n // webpieces-disable no-any-unknown -- accepts any ClassType / abstract-constructor API prototype\n private buildProxy(apiPrototype: any): ApiClientProxy {\n const basePath = getApiPath(apiPrototype) || '';\n const endpoints = getEndpoints(apiPrototype) || {};\n const proxy: ApiClientProxy = {};\n\n for (const [methodName, endpointPath] of Object.entries(endpoints)) {\n const httpMethod = 'POST';\n const path = basePath + endpointPath;\n\n // Use the REGISTERED route's metadata — it carries the real controller name AND api\n // name (so logging/recording read the right one); createRouteInvoker composes its chain.\n const routeMeta = this.routeBuilder.getRouteMeta(httpMethod, path);\n if (!routeMeta) {\n throw new Error(\n `No registered route for ${apiPrototype.name}.${methodName} (${httpMethod} ${path}) — call addRoutes(api, controller) first.`,\n );\n }\n const service = this.routeBuilder.createRouteInvoker(httpMethod, path);\n\n // webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary\n proxy[methodName] = async (requestDto: unknown): Promise<unknown> => {\n // Auto-activate a RequestContext if the caller (a pure in-process test) did not.\n if (!RequestContext.isActive()) {\n return RequestContext.run(async () => this.runMethod(routeMeta, requestDto, service));\n }\n return this.runMethod(routeMeta, requestDto, service);\n };\n }\n\n return proxy;\n }\n\n // webpieces-disable no-any-unknown -- request/response DTOs are erased at the routing boundary\n private async runMethod(routeMeta: RouteMetadata, requestDto: unknown, service: Service<MethodMeta, WpResponse<unknown>>): Promise<unknown> {\n // Only synthesize the request when NONE was published by a transport. The express adapter\n // publishes the HttpRequest from `req` before calling the proxy, so its request wins; a\n // pure in-process call synthesizes one from the ambient context (client-like).\n if (!RequestContext.getRequest()) {\n const headers = new Map<string, string[]>();\n this.contextMgr.buildOutboundHeaders().forEach((value: string, name: string) => {\n headers.set(name.toLowerCase(), [value]);\n });\n RequestContext.setRequest(new HttpRequest(routeMeta.httpMethod, routeMeta.path, headers));\n fillContext();\n }\n\n const responseWrapper = await service.invoke(new MethodMeta(routeMeta, requestDto));\n return responseWrapper.response;\n }\n}\n"]}
|
package/src/ApiRoutingFactory.js
CHANGED
|
@@ -73,7 +73,7 @@ class ApiRoutingFactory {
|
|
|
73
73
|
`Add @Authentication(new AuthenticationConfig(...)) to the class or method.`);
|
|
74
74
|
}
|
|
75
75
|
const fullPath = basePath + endpointPath;
|
|
76
|
-
const routeMeta = new core_util_1.RouteMetadata('POST', fullPath, methodName, controllerName, authMeta);
|
|
76
|
+
const routeMeta = new core_util_1.RouteMetadata('POST', fullPath, methodName, controllerName, authMeta, apiName);
|
|
77
77
|
routeBuilder.addRoute(new WebAppMeta_1.RouteDefinition(routeMeta, this.controllerClass, controllerFilepath, this.apiMetaClass));
|
|
78
78
|
}
|
|
79
79
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ApiRoutingFactory.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/ApiRoutingFactory.ts"],"names":[],"mappings":";;;AAAA,6CAAqE;AACrE,oDAAiH;AACjH,4BAA0B;AAC1B,6CAAqD;AAQrD;;;;;;;;;;;;;;;;GAgBG;AACH,+FAA+F;AAC/F,MAAa,iBAAiB;IAClB,YAAY,CAAkB;IAC9B,eAAe,CAAyB;IAEhD;;;OAGG;IACH,YAAY,YAA6B,EAAE,eAAuC;QAC9E,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC;QAEvC,qDAAqD;QACrD,IAAI,CAAC,IAAA,qBAAS,EAAC,YAAY,CAAC,EAAE,CAAC;YAC3B,MAAM,SAAS,GAAG,YAAY,CAAC,IAAI,IAAI,SAAS,CAAC;YACjD,MAAM,IAAI,KAAK,CAAC,SAAS,SAAS,oCAAoC,CAAC,CAAC;QAC5E,CAAC;QAED,+DAA+D;QAC/D,mFAAmF;QACnF,kFAAkF;QAClF,mFAAmF;QACnF,+CAA+C;QAC/C,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,IAAI,SAAS,CAAC;QAC/C,MAAM,cAAc,GAAG,eAAe,CAAC,IAAI,IAAI,SAAS,CAAC;QACzD,IAAI,CAAE,YAAY,CAAC,SAAoB,CAAC,aAAa,CAAC,eAAe,CAAC,SAAmB,CAAC,EAAE,CAAC;YACzF,MAAM,IAAI,KAAK,CACX,cAAc,cAAc,gBAAgB,OAAO,IAAI;gBACvD,mCAAmC;gBACnC,iBAAiB,cAAc,YAAY,OAAO,WAAW,CAChE,CAAC;QACN,CAAC;IAEL,CAAC;IAED;;;OAGG;IACH,SAAS,CAAC,YAA0B;QAChC,MAAM,QAAQ,GAAG,IAAA,sBAAU,EAAC,IAAI,CAAC,YAAY,CAAE,CAAC;QAChD,MAAM,SAAS,GAAG,IAAA,wBAAY,EAAC,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC;QACxD,MAAM,kBAAkB,GAAG,IAAI,CAAC,qBAAqB,EAAE,CAAC;QACxD,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,IAAI,SAAS,CAAC;QACpD,MAAM,cAAc,GAAG,IAAI,CAAC,eAAe,CAAC,IAAI,IAAI,SAAS,CAAC;QAE9D,KAAK,MAAM,CAAC,UAAU,EAAE,YAAY,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;YACjE,6CAA6C;YAC7C,IAAI,OAAO,IAAI,CAAC,eAAe,CAAC,SAAS,CAAC,UAAU,CAAC,KAAK,UAAU,EAAE,CAAC;gBACnE,MAAM,IAAI,KAAK,CACX,cAAc,cAAc,0BAA0B,UAAU,aAAa,OAAO,EAAE,CACzF,CAAC;YACN,CAAC;YAED,+DAA+D;YAC/D,MAAM,QAAQ,GAAG,IAAA,uBAAW,EAAC,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;YAC5D,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACZ,MAAM,IAAI,KAAK,CACX,aAAa,UAAU,QAAQ,OAAO,qCAAqC;oBAC3E,4EAA4E,CAC/E,CAAC;YACN,CAAC;YAED,MAAM,QAAQ,GAAG,QAAQ,GAAG,YAAY,CAAC;YACzC,MAAM,SAAS,GAAG,IAAI,yBAAa,CAC/B,MAAM,EACN,QAAQ,EACR,UAAU,EACV,cAAc,EACd,QAAQ,
|
|
1
|
+
{"version":3,"file":"ApiRoutingFactory.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/ApiRoutingFactory.ts"],"names":[],"mappings":"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","sourcesContent":["import { Routes, RouteBuilder, RouteDefinition } from './WebAppMeta';\nimport { isApiPath, getApiPath, getEndpoints, getAuthMeta, RouteMetadata, AuthMeta } from '@webpieces/core-util';\nimport 'reflect-metadata';\nimport { ROUTING_METADATA_KEYS } from './decorators';\n\n/**\n * Type representing a class constructor (abstract or concrete).\n */\n// webpieces-disable no-any-unknown -- generic type alias requires unconstrained default\nexport type ClassType<T = unknown> = Function & { prototype: T };\n\n/**\n * ApiRoutingFactory - Automatically wire API interfaces to controllers.\n * Reads @ApiPath/@Endpoint decorators from an API prototype class and\n * registers POST routes for each endpoint.\n *\n * Replaces the old RESTApiRoutes class.\n *\n * Usage:\n * ```typescript\n * // In your ServerMeta:\n * getRoutes(): Routes[] {\n * return [\n * new ApiRoutingFactory(SaveApi, SaveController),\n * ];\n * }\n * ```\n */\n// webpieces-disable no-any-unknown -- generic class requires unconstrained default type params\nexport class ApiRoutingFactory<TApi = unknown, TController extends TApi = TApi> implements Routes {\n private apiMetaClass: ClassType<TApi>;\n private controllerClass: ClassType<TController>;\n\n /**\n * @param apiMetaClass - The API prototype class with @ApiPath/@Endpoint decorators\n * @param controllerClass - The controller class that implements the API\n */\n constructor(apiMetaClass: ClassType<TApi>, controllerClass: ClassType<TController>) {\n this.apiMetaClass = apiMetaClass;\n this.controllerClass = controllerClass;\n\n // Validate that apiMetaClass is marked with @ApiPath\n if (!isApiPath(apiMetaClass)) {\n const className = apiMetaClass.name || 'Unknown';\n throw new Error(`Class ${className} must be decorated with @ApiPath()`);\n }\n\n // Validate that controllerClass actually extends apiMetaClass.\n // TypeScript's structural typing won't catch a missing `extends` here, so we check\n // the runtime prototype chain. Without this, a controller can silently drift from\n // the API contract (wrong method names, wrong signatures) and only fail later as a\n // confusing routing or method-not-found error.\n const apiName = apiMetaClass.name || 'Unknown';\n const controllerName = controllerClass.name || 'Unknown';\n if (!(apiMetaClass.prototype as object).isPrototypeOf(controllerClass.prototype as object)) {\n throw new Error(\n `Controller ${controllerName} must extend ${apiName}. ` +\n `Change the class declaration to: ` +\n `'export class ${controllerName} extends ${apiName} { ... }'`,\n );\n }\n\n }\n\n /**\n * Configure routes by reading @ApiPath + @Endpoint metadata.\n * Validates controller methods and auth decorators in single loop.\n */\n configure(routeBuilder: RouteBuilder): void {\n const basePath = getApiPath(this.apiMetaClass)!;\n const endpoints = getEndpoints(this.apiMetaClass) || {};\n const controllerFilepath = this.getControllerFilepath();\n const apiName = this.apiMetaClass.name || 'Unknown';\n const controllerName = this.controllerClass.name || 'Unknown';\n\n for (const [methodName, endpointPath] of Object.entries(endpoints)) {\n // Validate controller implements this method\n if (typeof this.controllerClass.prototype[methodName] !== 'function') {\n throw new Error(\n `Controller ${controllerName} must implement method ${methodName} from API ${apiName}`,\n );\n }\n\n // Validate auth decorator exists (class-level or method-level)\n const authMeta = getAuthMeta(this.apiMetaClass, methodName);\n if (!authMeta) {\n throw new Error(\n `Endpoint '${methodName}' in ${apiName} has no @Authentication decorator. ` +\n `Add @Authentication(new AuthenticationConfig(...)) to the class or method.`,\n );\n }\n\n const fullPath = basePath + endpointPath;\n const routeMeta = new RouteMetadata(\n 'POST',\n fullPath,\n methodName,\n controllerName,\n authMeta,\n apiName,\n );\n\n routeBuilder.addRoute(\n new RouteDefinition(routeMeta, this.controllerClass, controllerFilepath, this.apiMetaClass),\n );\n }\n }\n\n /**\n * Get the filepath of the controller source file.\n * Uses a heuristic based on the controller class name.\n */\n private getControllerFilepath(): string | undefined {\n // Check for explicit @SourceFile decorator metadata\n const filepath = Reflect.getMetadata(\n ROUTING_METADATA_KEYS.SOURCE_FILEPATH,\n this.controllerClass,\n );\n if (filepath) {\n return filepath;\n }\n\n // Fallback to class name pattern\n const className = this.controllerClass.name;\n return className ? `**/${className}.ts` : undefined;\n }\n\n /**\n * Get auth metadata for a specific method, falling back to class-level.\n */\n getAuthMetaForMethod(methodName: string): AuthMeta | undefined {\n return getAuthMeta(this.apiMetaClass, methodName);\n }\n\n /**\n * Get the API interface class.\n */\n getApiClass(): ClassType<TApi> {\n return this.apiMetaClass;\n }\n\n /**\n * Get the controller class.\n */\n getControllerClass(): ClassType<TController> {\n return this.controllerClass;\n }\n}\n"]}
|
|
@@ -5,7 +5,6 @@ import { MethodMeta } from './MethodMeta';
|
|
|
5
5
|
import { RouteMetadata } from '@webpieces/core-util';
|
|
6
6
|
import { WpResponse, Service } from './Filter';
|
|
7
7
|
import { HttpFilter } from './FilterMatcher';
|
|
8
|
-
import { ApiClient } from './ApiClient';
|
|
9
8
|
/**
|
|
10
9
|
* FilterWithMeta - Pairs a resolved filter instance with its definition.
|
|
11
10
|
* Stores both the DI-resolved filter and the metadata needed for matching.
|
|
@@ -112,13 +111,6 @@ export declare class RouteBuilderImpl implements RouteBuilder {
|
|
|
112
111
|
* @returns Map of routes with handlers and definitions, keyed by "METHOD:path"
|
|
113
112
|
*/
|
|
114
113
|
getRoutes(): RouteHandlerWithMeta[];
|
|
115
|
-
/**
|
|
116
|
-
* Reify every registered route as an {@link ApiClient}: its API contract + routeMeta +
|
|
117
|
-
* the composed express-tier impl (filter chain → controller). This is what
|
|
118
|
-
* {@link ApiFactory.apiClients} returns; the express layer binds each ApiClient's impl
|
|
119
|
-
* WITHOUT ever seeing this RouteBuilder.
|
|
120
|
-
*/
|
|
121
|
-
apiClients(): ApiClient[];
|
|
122
114
|
/**
|
|
123
115
|
* Get all filters sorted by priority (highest priority first).
|
|
124
116
|
*
|
package/src/RouteBuilderImpl.js
CHANGED
|
@@ -6,7 +6,6 @@ const inversify_1 = require("inversify");
|
|
|
6
6
|
const core_context_1 = require("@webpieces/core-context");
|
|
7
7
|
const Filter_1 = require("./Filter");
|
|
8
8
|
const FilterMatcher_1 = require("./FilterMatcher");
|
|
9
|
-
const ApiClient_1 = require("./ApiClient");
|
|
10
9
|
const core_util_1 = require("@webpieces/core-util");
|
|
11
10
|
const log = core_util_1.LogManager.getLogger('RouteBuilder');
|
|
12
11
|
/**
|
|
@@ -169,15 +168,6 @@ let RouteBuilderImpl = class RouteBuilderImpl {
|
|
|
169
168
|
getRoutes() {
|
|
170
169
|
return this.routes;
|
|
171
170
|
}
|
|
172
|
-
/**
|
|
173
|
-
* Reify every registered route as an {@link ApiClient}: its API contract + routeMeta +
|
|
174
|
-
* the composed express-tier impl (filter chain → controller). This is what
|
|
175
|
-
* {@link ApiFactory.apiClients} returns; the express layer binds each ApiClient's impl
|
|
176
|
-
* WITHOUT ever seeing this RouteBuilder.
|
|
177
|
-
*/
|
|
178
|
-
apiClients() {
|
|
179
|
-
return this.routes.map((routeWithMeta) => new ApiClient_1.ApiClient(routeWithMeta.definition.apiClass, routeWithMeta.definition.routeMeta, this.createRouteHandler(routeWithMeta)));
|
|
180
|
-
}
|
|
181
171
|
/**
|
|
182
172
|
* Get all filters sorted by priority (highest priority first).
|
|
183
173
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RouteBuilderImpl.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/RouteBuilderImpl.ts"],"names":[],"mappings":";;;;AAAA,yCAAkD;AAElD,0DAAoE;AAIpE,qCAA+C;AAC/C,mDAA4D;AAC5D,2CAAwC;AAExC,oDAAkD;AAElD,MAAM,GAAG,GAAG,sBAAU,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;AAEjD;;;GAGG;AACH,MAAa,cAAc;IAEZ;IACA;IAFX,YACW,MAAkB,EAClB,UAA4B;QAD5B,WAAM,GAAN,MAAM,CAAY;QAClB,eAAU,GAAV,UAAU,CAAkB;IACpC,CAAC;CACP;AALD,wCAKC;AAED;;;GAGG;AACH,MAAa,gBAAgB;IAEb;IACA;IAFZ,YACY,UAAmC,EACnC,MAAiE;QADjE,eAAU,GAAV,UAAU,CAAyB;QACnC,WAAM,GAAN,MAAM,CAA2D;IAC1E,CAAC;IAEJ,KAAK,CAAC,OAAO,CAAC,IAAgB;QAC1B,8CAA8C;QAC9C,sEAAsE;QACtE,MAAM,MAAM,GAAY,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;QACjF,OAAO,MAAM,CAAC;IAClB,CAAC;CACJ;AAZD,4CAYC;AACD;;;;;;GAMG;AACH,MAAa,oBAAoB;IAElB;IACA;IAFX,YACW,uBAA8C,EAC9C,UAA2B;QAD3B,4BAAuB,GAAvB,uBAAuB,CAAuB;QAC9C,eAAU,GAAV,UAAU,CAAiB;IACnC,CAAC;CACP;AALD,oDAKC;AAED;;;;;;;;;;;;;;;GAeG;AAGI,IAAM,gBAAgB,GAAtB,MAAM,gBAAgB;IACjB,MAAM,GAA2B,EAAE,CAAC;IACpC,cAAc,GAA0B,EAAE,CAAC;IAC3C,SAAS,CAAa;IAE9B;;;OAGG;IACK,QAAQ,GAAsC,IAAI,GAAG,EAAE,CAAC;IAEhE;;;OAGG;IACK,cAAc,CAAC,MAAc,EAAE,IAAY;QAC/C,OAAO,GAAG,MAAM,CAAC,WAAW,EAAE,IAAI,IAAI,EAAE,CAAC;IAC7C,CAAC;IAED;;;;;OAKG;IACH,YAAY,CAAC,SAAoB;QAC7B,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC/B,CAAC;IAED;;;;;;;OAOG;IACH,QAAQ,CAAC,KAAsB;QAC3B,MAAM,aAAa,GAAG,IAAI,CAAC,0BAA0B,CAAC,KAAK,CAAC,CAAC;QAC7D,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QAEhC,iDAAiD;QACjD,MAAM,GAAG,GAAG,IAAI,CAAC,cAAc,CAC3B,KAAK,CAAC,SAAS,CAAC,UAAU,EAC1B,KAAK,CAAC,SAAS,CAAC,IAAI,CACvB,CAAC;QACF,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC;IAC1C,CAAC;IAED;;;;;;;;;;;;OAYG;IACK,0BAA0B,CAC9B,KAAsB;QAEtB,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YAClB,MAAM,IAAI,KAAK,CAAC,mEAAmE,CAAC,CAAC;QACzF,CAAC;QAED,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC;QAElC,6EAA6E;QAC7E,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,eAAe,CAA4B,CAAC;QAExF,4BAA4B;QAC5B,MAAM,MAAM,GAAG,UAAU,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;QAChD,IAAI,OAAO,MAAM,KAAK,UAAU,EAAE,CAAC;YAC/B,MAAM,cAAc,GAAI,KAAK,CAAC,eAAqC,CAAC,IAAI,IAAI,SAAS,CAAC;YACtF,MAAM,IAAI,KAAK,CACX,UAAU,SAAS,CAAC,UAAU,4BAA4B,cAAc,EAAE,CAC7E,CAAC;QACN,CAAC;QAED,MAAM,OAAO,GAAG,IAAI,gBAAgB,CAChC,UAAU,EACV,MAAmE,CACtE,CAAC;QAEF,uCAAuC;QACvC,OAAO,IAAI,oBAAoB,CAC3B,OAAgC,EAChC,KAAK,CACR,CAAC;IACN,CAAC;IAED;;;;;;;OAOG;IACH,SAAS,CAAC,SAA2B;QACjC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YAClB,MAAM,IAAI,KAAK,CAAC,oEAAoE,CAAC,CAAC;QAC1F,CAAC;QAED,4CAA4C;QAC5C,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAa,SAAS,CAAC,WAAW,CAAC,CAAC;QAErE,mCAAmC;QACnC,MAAM,cAAc,GAAG,IAAI,cAAc,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;QAC7D,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IAC7C,CAAC;IAED;;;;OAIG;IACH,SAAS;QACL,OAAO,IAAI,CAAC,MAAM,CAAC;IACvB,CAAC;IAED;;;;;OAKG;IACH,UAAU;QACN,OAAO,IAAI,CAAC,MAAM,CAAC,GAAG,CAClB,CAAC,aAAmC,EAAE,EAAE,CACpC,IAAI,qBAAS,CACT,aAAa,CAAC,UAAU,CAAC,QAAqB,EAC9C,aAAa,CAAC,UAAU,CAAC,SAAS,EAClC,IAAI,CAAC,kBAAkB,CAAC,aAAa,CAAC,CACzC,CACR,CAAC;IACN,CAAC;IAED;;;;OAIG;IACH,gBAAgB;QACZ,OAAO,CAAC,GAAG,IAAI,CAAC,cAAc,CAAC,CAAC,IAAI,CAChC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,QAAQ,GAAG,CAAC,CAAC,UAAU,CAAC,QAAQ,CAC1D,CAAC;IACN,CAAC;IAED;;OAEG;IACK,uBAAuB,CAAsB;IAErD;;OAEG;IACK,oBAAoB;QACxB,IAAI,CAAC,IAAI,CAAC,uBAAuB,EAAE,CAAC;YAChC,MAAM,aAAa,GAAG,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAC9C,IAAI,CAAC,uBAAuB,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;gBACrD,MAAM,GAAG,GAAG,GAAG,CAAC,UAAU,CAAC;gBAC3B,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC;gBACxB,OAAO,GAAG,CAAC;YACf,CAAC,CAAC,CAAC;QACP,CAAC;QACD,OAAO,IAAI,CAAC,uBAAuB,CAAC;IACxC,CAAC;IAED;;;;;;;;;;OAUG;IACI,kBAAkB,CACrB,aAAmC;QAEnC,MAAM,KAAK,GAAG,aAAa,CAAC,UAAU,CAAC;QACvC,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC;QAElC,GAAG,CAAC,IAAI,CAAC,oCAAoC,SAAS,CAAC,UAAU,IAAI,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC;QAEvF,kFAAkF;QAClF,yFAAyF;QACzF,gDAAgD;QAChD,MAAM,iBAAiB,GAAG,IAAI,CAAC,oBAAoB,EAAE,CAAC;QAEtD,uCAAuC;QACvC,MAAM,eAAe,GAAG,6BAAa,CAAC,mBAAmB,CACrD,KAAK,CAAC,kBAAkB,EACxB,iBAAiB,CACpB,CAAC;QAEF,qDAAqD;QACrD,MAAM,iBAAiB,GAA6C;YAChE,MAAM,EAAE,KAAK,EAAE,IAAgB,EAAgC,EAAE;gBAC7D,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,uBAAuB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;gBACzE,8EAA8E;gBAC9E,yEAAyE;gBACzE,+EAA+E;gBAC/E,OAAO,IAAI,mBAAU,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;YACxC,CAAC;SACJ,CAAC;QAEF,IAAI,eAAe,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC/B,MAAM,IAAI,KAAK,CAAC,2HAA2H,CAAC,CAAC;QACjJ,CAAC;QAED,mFAAmF;QACnF,yEAAyE;QACzE,0EAA0E;QAC1E,IAAI,OAAO,GAA6C,iBAAiB,CAAC;QAC1E,KAAK,IAAI,CAAC,GAAG,eAAe,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YACnD,OAAO,GAAG,eAAe,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QACvD,CAAC;QAED,OAAO,OAAO,CAAC;IACnB,CAAC;IAED;;;;;;;;;;;;OAYG;IACH,kBAAkB,CAAC,MAAc,EAAE,IAAY;QAC3C,oDAAoD;QACpD,MAAM,GAAG,GAAG,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAC9C,MAAM,aAAa,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAE7C,IAAI,CAAC,aAAa,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CAAC,oBAAoB,MAAM,IAAI,IAAI,EAAE,CAAC,CAAC;QAC1D,CAAC;QAED,0FAA0F;QAC1F,OAAO,IAAI,CAAC,kBAAkB,CAAC,aAAa,CAAC,CAAC;IAClD,CAAC;IAED;;;;;;OAMG;IACH,YAAY,CAAC,MAAc,EAAE,IAAY;QACrC,MAAM,GAAG,GAAG,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAC9C,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,UAAU,CAAC,SAAS,CAAC;IACxD,CAAC;CACJ,CAAA;AA1QY,4CAAgB;2BAAhB,gBAAgB;IAF5B,IAAA,wCAAyB,GAAE;IAC3B,IAAA,sBAAU,GAAE;GACA,gBAAgB,CA0Q5B","sourcesContent":["import { Container, injectable } from 'inversify';\nimport { RouteBuilder, RouteDefinition, FilterDefinition } from './WebAppMeta';\nimport { provideFrameworkSingleton } from '@webpieces/core-context';\nimport { RouteHandler } from './RouteHandler';\nimport { MethodMeta } from './MethodMeta';\nimport { RouteMetadata, DocumentDesign } from '@webpieces/core-util';\nimport { WpResponse, Service } from './Filter';\nimport { FilterMatcher, HttpFilter } from './FilterMatcher';\nimport { ApiClient } from './ApiClient';\nimport { ClassType } from './ApiRoutingFactory';\nimport { LogManager } from '@webpieces/core-util';\n\nconst log = LogManager.getLogger('RouteBuilder');\n\n/**\n * FilterWithMeta - Pairs a resolved filter instance with its definition.\n * Stores both the DI-resolved filter and the metadata needed for matching.\n */\nexport class FilterWithMeta {\n constructor(\n public filter: HttpFilter,\n public definition: FilterDefinition,\n ) {}\n}\n\n/**\n * RouteHandlerImpl - Concrete implementation of RouteHandler.\n * Wraps a resolved controller and method to invoke on each request.\n */\nexport class RouteHandlerImpl<TResult> implements RouteHandler<TResult> {\n constructor(\n private controller: Record<string, unknown>,\n private method: (this: unknown, requestDto?: unknown) => Promise<TResult>,\n ) {}\n\n async execute(meta: MethodMeta): Promise<TResult> {\n // Invoke the method with requestDto from meta\n // The controller is already resolved - no DI lookup on every request!\n const result: TResult = await this.method.call(this.controller, meta.requestDto);\n return result;\n }\n}\n/**\n * RouteHandlerWithMeta - Pairs a route handler with its definition.\n * Stores both the handler (which wraps the DI-resolved controller) and the route metadata.\n *\n * We use unknown for the generic type since we store different TResult types in the same Map.\n * Type safety is maintained through the generic on RouteDefinition at registration time.\n */\nexport class RouteHandlerWithMeta {\n constructor(\n public invokeControllerHandler: RouteHandler<unknown>,\n public definition: RouteDefinition,\n ) {}\n}\n\n/**\n * RouteBuilderImpl - Concrete implementation of RouteBuilder interface.\n *\n * Similar to Java WebPieces RouteBuilder, this class is responsible for:\n * 1. Registering routes with their handlers\n * 2. Registering filters with priority\n *\n * This class is explicit (not anonymous) to:\n * - Improve traceability and debugging\n * - Make the code easier to understand\n * - Enable better IDE navigation (Cmd+Click on addRoute works!)\n *\n * DI Pattern: This class is registered in webpiecesContainer via @provideFrameworkSingleton()\n * but needs appContainer to resolve filters/controllers. The container is set via\n * setContainer() after appContainer is created (late binding pattern).\n */\n@provideFrameworkSingleton()\n@injectable()\nexport class RouteBuilderImpl implements RouteBuilder {\n private routes: RouteHandlerWithMeta[] = [];\n private filterRegistry: Array<FilterWithMeta> = [];\n private container?: Container;\n\n /**\n * Map for O(1) route lookup by method:path.\n * Used by both addRoute() and createRouteInvoker() for fast route access.\n */\n private routeMap: Map<string, RouteHandlerWithMeta> = new Map();\n\n /**\n * Create route key for consistent lookup.\n * Key format: \"${METHOD}:${path}\" (e.g., \"POST:/search/item\")\n */\n private createRouteKey(method: string, path: string): string {\n return `${method.toUpperCase()}:${path}`;\n }\n\n /**\n * Set the DI container used for resolving filters and controllers.\n * Called by WebpiecesCoreServer after appContainer is created.\n *\n * @param container - The application DI container (appContainer)\n */\n setContainer(container: Container): void {\n this.container = container;\n }\n\n /**\n * Register a route with the router.\n *\n * Uses createRouteHandlerWithMeta() to create the handler, then stores it\n * in both the routes array and the routeMap for O(1) lookup.\n *\n * @param route - Route definition with controller class and method name\n */\n addRoute(route: RouteDefinition): void {\n const routeWithMeta = this.createRouteHandlerWithMeta(route);\n this.routes.push(routeWithMeta);\n\n // Also add to map for O(1) lookup by method:path\n const key = this.createRouteKey(\n route.routeMeta.httpMethod,\n route.routeMeta.path\n );\n this.routeMap.set(key, routeWithMeta);\n }\n\n /**\n * Create RouteHandlerWithMeta from a RouteDefinition.\n *\n * Resolves controller from DI container ONCE and creates a handler that\n * invokes the controller method with the request DTO.\n *\n * This method is used by:\n * - addRoute() for production route registration\n * - createRouteInvoker() for test clients (via createApiClient)\n *\n * @param route - Route definition with controller class and method name\n * @returns RouteHandlerWithMeta containing the handler and route definition\n */\n private createRouteHandlerWithMeta<TResult = unknown>(\n route: RouteDefinition,\n ): RouteHandlerWithMeta {\n if (!this.container) {\n throw new Error('Container not set. Call setContainer() before registering routes.');\n }\n\n const routeMeta = route.routeMeta;\n\n // Resolve controller instance from DI container ONCE (not on every request!)\n const controller = this.container.get(route.controllerClass) as Record<string, unknown>;\n\n // Get the controller method\n const method = controller[routeMeta.methodName];\n if (typeof method !== 'function') {\n const controllerName = (route.controllerClass as { name?: string }).name || 'Unknown';\n throw new Error(\n `Method ${routeMeta.methodName} not found on controller ${controllerName}`,\n );\n }\n\n const handler = new RouteHandlerImpl<TResult>(\n controller,\n method as (this: unknown, requestDto?: unknown) => Promise<TResult>\n );\n\n // Return handler with route definition\n return new RouteHandlerWithMeta(\n handler as RouteHandler<unknown>,\n route,\n );\n }\n\n /**\n * Register a filter with the filter chain.\n *\n * Resolves the filter from DI container and pairs it with the filter definition.\n * The definition includes pattern information used for route-specific filtering.\n *\n * @param filterDef - Filter definition with priority, filter class, and optional filepath pattern\n */\n addFilter(filterDef: FilterDefinition): void {\n if (!this.container) {\n throw new Error('Container not set. Call setContainer() before registering filters.');\n }\n\n // Resolve filter instance from DI container\n const filter = this.container.get<HttpFilter>(filterDef.filterClass);\n\n // Store filter with its definition\n const filterWithMeta = new FilterWithMeta(filter, filterDef);\n this.filterRegistry.push(filterWithMeta);\n }\n\n /**\n * Get all registered routes.\n *\n * @returns Map of routes with handlers and definitions, keyed by \"METHOD:path\"\n */\n getRoutes(): RouteHandlerWithMeta[] {\n return this.routes;\n }\n\n /**\n * Reify every registered route as an {@link ApiClient}: its API contract + routeMeta +\n * the composed express-tier impl (filter chain → controller). This is what\n * {@link ApiFactory.apiClients} returns; the express layer binds each ApiClient's impl\n * WITHOUT ever seeing this RouteBuilder.\n */\n apiClients(): ApiClient[] {\n return this.routes.map(\n (routeWithMeta: RouteHandlerWithMeta) =>\n new ApiClient(\n routeWithMeta.definition.apiClass as ClassType,\n routeWithMeta.definition.routeMeta,\n this.createRouteHandler(routeWithMeta),\n ),\n );\n }\n\n /**\n * Get all filters sorted by priority (highest priority first).\n *\n * @returns Array of FilterWithMeta sorted by priority\n */\n getSortedFilters(): Array<FilterWithMeta> {\n return [...this.filterRegistry].sort(\n (a, b) => b.definition.priority - a.definition.priority,\n );\n }\n\n /**\n * Cached filter definitions for lazy route setup.\n */\n private cachedFilterDefinitions?: FilterDefinition[];\n\n /**\n * Get filter definitions, computing once and caching.\n */\n private getFilterDefinitions(): FilterDefinition[] {\n if (!this.cachedFilterDefinitions) {\n const sortedFilters = this.getSortedFilters();\n this.cachedFilterDefinitions = sortedFilters.map((fwm) => {\n const def = fwm.definition;\n def.filter = fwm.filter;\n return def;\n });\n }\n return this.cachedFilterDefinitions;\n }\n\n /**\n * Setup a single route by creating its filter chain.\n * This is called lazily by createHandler() and getRouteService().\n *\n * Creates a Service that wraps the filter chain and controller invocation.\n * The service is DTO-only and has no Express dependency.\n *\n * @param key - Route key in format \"METHOD:path\"\n * @param routeWithMeta - Route handler with metadata\n * @returns The service for this route\n */\n public createRouteHandler(\n routeWithMeta: RouteHandlerWithMeta,\n ): Service<MethodMeta, WpResponse<unknown>> {\n const route = routeWithMeta.definition;\n const routeMeta = route.routeMeta;\n\n log.info(`[RouteBuilder] Setting up route: ${routeMeta.httpMethod} ${routeMeta.path}`);\n\n // ONE chain for both HTTP and in-process — no transport tier. The fixed framework\n // filters (ErrorLogFilter, AuthFilter) are auto-installed and read the transport-neutral\n // HttpRequest, so they run identically in both.\n const filterDefinitions = this.getFilterDefinitions();\n\n // Find matching filters for this route\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<unknown>> = {\n invoke: async (meta: MethodMeta): Promise<WpResponse<unknown>> => {\n const result = await routeWithMeta.invokeControllerHandler.execute(meta);\n // A void endpoint (e.g. a @PubSub cloud-task handler returning Promise<void>)\n // yields undefined; coerce to {} so the response is a non-null JSON body\n // (downstream LogApiCall/serialization require one), mirroring `result ?? {}`.\n return new WpResponse(result ?? {});\n },\n };\n\n if (matchingFilters.length === 0) {\n throw new Error(\"No filters found for route — the framework auto-installs ErrorLogFilter + AuthFilter, so this indicates a wiring problem.\");\n }\n\n // Chain filters: highest priority (first in array) should run first (be outermost)\n // Build from innermost (lowest priority) to outermost (highest priority)\n // Start with controller, then wrap with filters in reverse priority order\n let service: Service<MethodMeta, WpResponse<unknown>> = controllerService;\n for (let i = matchingFilters.length - 1; i >= 0; i--) {\n service = matchingFilters[i].chainService(service);\n }\n\n return service;\n }\n\n /**\n * Create an invoker function for a route (for testing via createApiClient).\n * Uses routeMap for O(1) lookup, sets up the filter chain ONCE,\n * and returns a Service that can be called multiple times without\n * recreating the filter chain.\n *\n * This method is called by WebpiecesServer.createApiClient() during proxy setup.\n * The returned Service is stored as the proxy method and invoked on each call.\n *\n * @param method - HTTP method (GET, POST, etc.)\n * @param path - URL path\n * @returns A Service that invokes the route\n */\n createRouteInvoker(method: string, path: string): Service<MethodMeta, WpResponse<unknown>> {\n // Use routeMap for O(1) lookup (not linear search!)\n const key = this.createRouteKey(method, path);\n const routeWithMeta = this.routeMap.get(key);\n\n if (!routeWithMeta) {\n throw new Error(`Route not found: ${method} ${path}`);\n }\n\n // Setup filter chain ONCE (not on every invocation!). Same chain as HTTP — auth included.\n return this.createRouteHandler(routeWithMeta);\n }\n\n /**\n * Look up the RouteMetadata (incl. authMeta) for a registered route by method+path.\n * Used to build a MethodMeta for an in-process dispatch (e.g. a delivered cloud\n * task) so the filter chain sees the same routeMeta production HTTP would.\n *\n * @returns the route's RouteMetadata, or undefined if no route is registered.\n */\n getRouteMeta(method: string, path: string): RouteMetadata | undefined {\n const key = this.createRouteKey(method, path);\n return this.routeMap.get(key)?.definition.routeMeta;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"RouteBuilderImpl.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/RouteBuilderImpl.ts"],"names":[],"mappings":"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","sourcesContent":["import { Container, injectable } from 'inversify';\nimport { RouteBuilder, RouteDefinition, FilterDefinition } from './WebAppMeta';\nimport { provideFrameworkSingleton } from '@webpieces/core-context';\nimport { RouteHandler } from './RouteHandler';\nimport { MethodMeta } from './MethodMeta';\nimport { RouteMetadata, DocumentDesign } from '@webpieces/core-util';\nimport { WpResponse, Service } from './Filter';\nimport { FilterMatcher, HttpFilter } from './FilterMatcher';\nimport { LogManager } from '@webpieces/core-util';\n\nconst log = LogManager.getLogger('RouteBuilder');\n\n/**\n * FilterWithMeta - Pairs a resolved filter instance with its definition.\n * Stores both the DI-resolved filter and the metadata needed for matching.\n */\nexport class FilterWithMeta {\n constructor(\n public filter: HttpFilter,\n public definition: FilterDefinition,\n ) {}\n}\n\n/**\n * RouteHandlerImpl - Concrete implementation of RouteHandler.\n * Wraps a resolved controller and method to invoke on each request.\n */\nexport class RouteHandlerImpl<TResult> implements RouteHandler<TResult> {\n constructor(\n private controller: Record<string, unknown>,\n private method: (this: unknown, requestDto?: unknown) => Promise<TResult>,\n ) {}\n\n async execute(meta: MethodMeta): Promise<TResult> {\n // Invoke the method with requestDto from meta\n // The controller is already resolved - no DI lookup on every request!\n const result: TResult = await this.method.call(this.controller, meta.requestDto);\n return result;\n }\n}\n/**\n * RouteHandlerWithMeta - Pairs a route handler with its definition.\n * Stores both the handler (which wraps the DI-resolved controller) and the route metadata.\n *\n * We use unknown for the generic type since we store different TResult types in the same Map.\n * Type safety is maintained through the generic on RouteDefinition at registration time.\n */\nexport class RouteHandlerWithMeta {\n constructor(\n public invokeControllerHandler: RouteHandler<unknown>,\n public definition: RouteDefinition,\n ) {}\n}\n\n/**\n * RouteBuilderImpl - Concrete implementation of RouteBuilder interface.\n *\n * Similar to Java WebPieces RouteBuilder, this class is responsible for:\n * 1. Registering routes with their handlers\n * 2. Registering filters with priority\n *\n * This class is explicit (not anonymous) to:\n * - Improve traceability and debugging\n * - Make the code easier to understand\n * - Enable better IDE navigation (Cmd+Click on addRoute works!)\n *\n * DI Pattern: This class is registered in webpiecesContainer via @provideFrameworkSingleton()\n * but needs appContainer to resolve filters/controllers. The container is set via\n * setContainer() after appContainer is created (late binding pattern).\n */\n@provideFrameworkSingleton()\n@injectable()\nexport class RouteBuilderImpl implements RouteBuilder {\n private routes: RouteHandlerWithMeta[] = [];\n private filterRegistry: Array<FilterWithMeta> = [];\n private container?: Container;\n\n /**\n * Map for O(1) route lookup by method:path.\n * Used by both addRoute() and createRouteInvoker() for fast route access.\n */\n private routeMap: Map<string, RouteHandlerWithMeta> = new Map();\n\n /**\n * Create route key for consistent lookup.\n * Key format: \"${METHOD}:${path}\" (e.g., \"POST:/search/item\")\n */\n private createRouteKey(method: string, path: string): string {\n return `${method.toUpperCase()}:${path}`;\n }\n\n /**\n * Set the DI container used for resolving filters and controllers.\n * Called by WebpiecesCoreServer after appContainer is created.\n *\n * @param container - The application DI container (appContainer)\n */\n setContainer(container: Container): void {\n this.container = container;\n }\n\n /**\n * Register a route with the router.\n *\n * Uses createRouteHandlerWithMeta() to create the handler, then stores it\n * in both the routes array and the routeMap for O(1) lookup.\n *\n * @param route - Route definition with controller class and method name\n */\n addRoute(route: RouteDefinition): void {\n const routeWithMeta = this.createRouteHandlerWithMeta(route);\n this.routes.push(routeWithMeta);\n\n // Also add to map for O(1) lookup by method:path\n const key = this.createRouteKey(\n route.routeMeta.httpMethod,\n route.routeMeta.path\n );\n this.routeMap.set(key, routeWithMeta);\n }\n\n /**\n * Create RouteHandlerWithMeta from a RouteDefinition.\n *\n * Resolves controller from DI container ONCE and creates a handler that\n * invokes the controller method with the request DTO.\n *\n * This method is used by:\n * - addRoute() for production route registration\n * - createRouteInvoker() for test clients (via createApiClient)\n *\n * @param route - Route definition with controller class and method name\n * @returns RouteHandlerWithMeta containing the handler and route definition\n */\n private createRouteHandlerWithMeta<TResult = unknown>(\n route: RouteDefinition,\n ): RouteHandlerWithMeta {\n if (!this.container) {\n throw new Error('Container not set. Call setContainer() before registering routes.');\n }\n\n const routeMeta = route.routeMeta;\n\n // Resolve controller instance from DI container ONCE (not on every request!)\n const controller = this.container.get(route.controllerClass) as Record<string, unknown>;\n\n // Get the controller method\n const method = controller[routeMeta.methodName];\n if (typeof method !== 'function') {\n const controllerName = (route.controllerClass as { name?: string }).name || 'Unknown';\n throw new Error(\n `Method ${routeMeta.methodName} not found on controller ${controllerName}`,\n );\n }\n\n const handler = new RouteHandlerImpl<TResult>(\n controller,\n method as (this: unknown, requestDto?: unknown) => Promise<TResult>\n );\n\n // Return handler with route definition\n return new RouteHandlerWithMeta(\n handler as RouteHandler<unknown>,\n route,\n );\n }\n\n /**\n * Register a filter with the filter chain.\n *\n * Resolves the filter from DI container and pairs it with the filter definition.\n * The definition includes pattern information used for route-specific filtering.\n *\n * @param filterDef - Filter definition with priority, filter class, and optional filepath pattern\n */\n addFilter(filterDef: FilterDefinition): void {\n if (!this.container) {\n throw new Error('Container not set. Call setContainer() before registering filters.');\n }\n\n // Resolve filter instance from DI container\n const filter = this.container.get<HttpFilter>(filterDef.filterClass);\n\n // Store filter with its definition\n const filterWithMeta = new FilterWithMeta(filter, filterDef);\n this.filterRegistry.push(filterWithMeta);\n }\n\n /**\n * Get all registered routes.\n *\n * @returns Map of routes with handlers and definitions, keyed by \"METHOD:path\"\n */\n getRoutes(): RouteHandlerWithMeta[] {\n return this.routes;\n }\n\n /**\n * Get all filters sorted by priority (highest priority first).\n *\n * @returns Array of FilterWithMeta sorted by priority\n */\n getSortedFilters(): Array<FilterWithMeta> {\n return [...this.filterRegistry].sort(\n (a, b) => b.definition.priority - a.definition.priority,\n );\n }\n\n /**\n * Cached filter definitions for lazy route setup.\n */\n private cachedFilterDefinitions?: FilterDefinition[];\n\n /**\n * Get filter definitions, computing once and caching.\n */\n private getFilterDefinitions(): FilterDefinition[] {\n if (!this.cachedFilterDefinitions) {\n const sortedFilters = this.getSortedFilters();\n this.cachedFilterDefinitions = sortedFilters.map((fwm) => {\n const def = fwm.definition;\n def.filter = fwm.filter;\n return def;\n });\n }\n return this.cachedFilterDefinitions;\n }\n\n /**\n * Setup a single route by creating its filter chain.\n * This is called lazily by createHandler() and getRouteService().\n *\n * Creates a Service that wraps the filter chain and controller invocation.\n * The service is DTO-only and has no Express dependency.\n *\n * @param key - Route key in format \"METHOD:path\"\n * @param routeWithMeta - Route handler with metadata\n * @returns The service for this route\n */\n public createRouteHandler(\n routeWithMeta: RouteHandlerWithMeta,\n ): Service<MethodMeta, WpResponse<unknown>> {\n const route = routeWithMeta.definition;\n const routeMeta = route.routeMeta;\n\n log.info(`[RouteBuilder] Setting up route: ${routeMeta.httpMethod} ${routeMeta.path}`);\n\n // ONE chain for both HTTP and in-process — no transport tier. The fixed framework\n // filters (ErrorLogFilter, AuthFilter) are auto-installed and read the transport-neutral\n // HttpRequest, so they run identically in both.\n const filterDefinitions = this.getFilterDefinitions();\n\n // Find matching filters for this route\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<unknown>> = {\n invoke: async (meta: MethodMeta): Promise<WpResponse<unknown>> => {\n const result = await routeWithMeta.invokeControllerHandler.execute(meta);\n // A void endpoint (e.g. a @PubSub cloud-task handler returning Promise<void>)\n // yields undefined; coerce to {} so the response is a non-null JSON body\n // (downstream LogApiCall/serialization require one), mirroring `result ?? {}`.\n return new WpResponse(result ?? {});\n },\n };\n\n if (matchingFilters.length === 0) {\n throw new Error(\"No filters found for route — the framework auto-installs ErrorLogFilter + AuthFilter, so this indicates a wiring problem.\");\n }\n\n // Chain filters: highest priority (first in array) should run first (be outermost)\n // Build from innermost (lowest priority) to outermost (highest priority)\n // Start with controller, then wrap with filters in reverse priority order\n let service: Service<MethodMeta, WpResponse<unknown>> = controllerService;\n for (let i = matchingFilters.length - 1; i >= 0; i--) {\n service = matchingFilters[i].chainService(service);\n }\n\n return service;\n }\n\n /**\n * Create an invoker function for a route (for testing via createApiClient).\n * Uses routeMap for O(1) lookup, sets up the filter chain ONCE,\n * and returns a Service that can be called multiple times without\n * recreating the filter chain.\n *\n * This method is called by WebpiecesServer.createApiClient() during proxy setup.\n * The returned Service is stored as the proxy method and invoked on each call.\n *\n * @param method - HTTP method (GET, POST, etc.)\n * @param path - URL path\n * @returns A Service that invokes the route\n */\n createRouteInvoker(method: string, path: string): Service<MethodMeta, WpResponse<unknown>> {\n // Use routeMap for O(1) lookup (not linear search!)\n const key = this.createRouteKey(method, path);\n const routeWithMeta = this.routeMap.get(key);\n\n if (!routeWithMeta) {\n throw new Error(`Route not found: ${method} ${path}`);\n }\n\n // Setup filter chain ONCE (not on every invocation!). Same chain as HTTP — auth included.\n return this.createRouteHandler(routeWithMeta);\n }\n\n /**\n * Look up the RouteMetadata (incl. authMeta) for a registered route by method+path.\n * Used to build a MethodMeta for an in-process dispatch (e.g. a delivered cloud\n * task) so the filter chain sees the same routeMeta production HTTP would.\n *\n * @returns the route's RouteMetadata, or undefined if no route is registered.\n */\n getRouteMeta(method: string, path: string): RouteMetadata | undefined {\n const key = this.createRouteKey(method, path);\n return this.routeMap.get(key)?.definition.routeMeta;\n }\n}\n"]}
|
package/src/WebpiecesRouter.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { RouteBuilderImpl } from './RouteBuilderImpl';
|
|
|
3
3
|
import { ClassType } from './ApiRoutingFactory';
|
|
4
4
|
import { FilterDefinition } from './WebAppMeta';
|
|
5
5
|
import { WebpiecesConfig } from './WebpiecesConfig';
|
|
6
|
+
import { ApiClientFactory } from './ApiClientFactory';
|
|
6
7
|
import { ApiFactory } from './ApiFactory';
|
|
7
8
|
import { ApiClient } from './ApiClient';
|
|
8
9
|
/**
|
|
@@ -52,9 +53,10 @@ export interface WebpiecesRouterOptions {
|
|
|
52
53
|
*/
|
|
53
54
|
export declare class WebpiecesRouter implements ApiFactory {
|
|
54
55
|
private readonly routeBuilder;
|
|
56
|
+
private readonly apiClientFactory;
|
|
55
57
|
private webpiecesContainer;
|
|
56
58
|
private appContainer;
|
|
57
|
-
constructor(routeBuilder: RouteBuilderImpl);
|
|
59
|
+
constructor(routeBuilder: RouteBuilderImpl, apiClientFactory: ApiClientFactory);
|
|
58
60
|
/**
|
|
59
61
|
* Build the app container (child of the framework container), load the @provideSingleton
|
|
60
62
|
* auto-scan + appBindings + appOverrides, and point the RouteBuilder at it. Called once by
|
|
@@ -85,9 +87,9 @@ export declare class WebpiecesRouter implements ApiFactory {
|
|
|
85
87
|
*/
|
|
86
88
|
createApiClient<T>(apiPrototype: abstract new (...args: any[]) => T): T;
|
|
87
89
|
/**
|
|
88
|
-
* Reify the registered
|
|
89
|
-
*
|
|
90
|
-
* internal RouteBuilder never leaves
|
|
90
|
+
* Reify the registered APIs as {@link ApiClient}s (contract + the createApiClient proxy +
|
|
91
|
+
* per-endpoint route metadata) via the shared {@link ApiClientFactory}. This is the ONLY
|
|
92
|
+
* handoff to the express layer — the internal RouteBuilder never leaves.
|
|
91
93
|
*/
|
|
92
94
|
apiClients(): ApiClient[];
|
|
93
95
|
/** The application DI container (child of the framework container). */
|
package/src/WebpiecesRouter.js
CHANGED
|
@@ -10,7 +10,7 @@ const RouteBuilderImpl_1 = require("./RouteBuilderImpl");
|
|
|
10
10
|
const ApiRoutingFactory_1 = require("./ApiRoutingFactory");
|
|
11
11
|
const WebAppMeta_1 = require("./WebAppMeta");
|
|
12
12
|
const WebpiecesConfig_1 = require("./WebpiecesConfig");
|
|
13
|
-
const
|
|
13
|
+
const ApiClientFactory_1 = require("./ApiClientFactory");
|
|
14
14
|
const ErrorLogFilter_1 = require("./filters/ErrorLogFilter");
|
|
15
15
|
const AuthFilter_1 = require("./filters/AuthFilter");
|
|
16
16
|
/**
|
|
@@ -47,10 +47,12 @@ const AuthFilter_1 = require("./filters/AuthFilter");
|
|
|
47
47
|
*/
|
|
48
48
|
let WebpiecesRouter = class WebpiecesRouter {
|
|
49
49
|
routeBuilder;
|
|
50
|
+
apiClientFactory;
|
|
50
51
|
webpiecesContainer;
|
|
51
52
|
appContainer;
|
|
52
|
-
constructor(routeBuilder) {
|
|
53
|
+
constructor(routeBuilder, apiClientFactory) {
|
|
53
54
|
this.routeBuilder = routeBuilder;
|
|
55
|
+
this.apiClientFactory = apiClientFactory;
|
|
54
56
|
}
|
|
55
57
|
/**
|
|
56
58
|
* Build the app container (child of the framework container), load the @provideSingleton
|
|
@@ -113,15 +115,15 @@ let WebpiecesRouter = class WebpiecesRouter {
|
|
|
113
115
|
*/
|
|
114
116
|
// webpieces-disable no-any-unknown -- abstract constructor signature requires any[] args
|
|
115
117
|
createApiClient(apiPrototype) {
|
|
116
|
-
return
|
|
118
|
+
return this.apiClientFactory.createApiClient(apiPrototype);
|
|
117
119
|
}
|
|
118
120
|
/**
|
|
119
|
-
* Reify the registered
|
|
120
|
-
*
|
|
121
|
-
* internal RouteBuilder never leaves
|
|
121
|
+
* Reify the registered APIs as {@link ApiClient}s (contract + the createApiClient proxy +
|
|
122
|
+
* per-endpoint route metadata) via the shared {@link ApiClientFactory}. This is the ONLY
|
|
123
|
+
* handoff to the express layer — the internal RouteBuilder never leaves.
|
|
122
124
|
*/
|
|
123
125
|
apiClients() {
|
|
124
|
-
return this.
|
|
126
|
+
return this.apiClientFactory.apiClients();
|
|
125
127
|
}
|
|
126
128
|
/** The application DI container (child of the framework container). */
|
|
127
129
|
getContainer() {
|
|
@@ -133,7 +135,9 @@ exports.WebpiecesRouter = WebpiecesRouter = tslib_1.__decorate([
|
|
|
133
135
|
(0, core_util_1.DocumentDesign)(),
|
|
134
136
|
(0, core_context_1.provideFrameworkSingleton)(),
|
|
135
137
|
tslib_1.__param(0, (0, inversify_1.inject)(RouteBuilderImpl_1.RouteBuilderImpl)),
|
|
136
|
-
tslib_1.
|
|
138
|
+
tslib_1.__param(1, (0, inversify_1.inject)(ApiClientFactory_1.ApiClientFactory)),
|
|
139
|
+
tslib_1.__metadata("design:paramtypes", [RouteBuilderImpl_1.RouteBuilderImpl,
|
|
140
|
+
ApiClientFactory_1.ApiClientFactory])
|
|
137
141
|
], WebpiecesRouter);
|
|
138
142
|
/**
|
|
139
143
|
* Builds a {@link WebpiecesRouter}: constructs the platform container (mirrors
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebpiecesRouter.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/WebpiecesRouter.ts"],"names":[],"mappings":"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","sourcesContent":["import { Container, ContainerModule, inject } from 'inversify';\nimport { buildProviderModule } from '@inversifyjs/binding-decorators';\nimport { DocumentDesign } from '@webpieces/core-util';\nimport { provideFrameworkSingleton, buildFrameworkModule } from '@webpieces/core-context';\nimport { RouteBuilderImpl } from './RouteBuilderImpl';\nimport { ApiRoutingFactory, ClassType } from './ApiRoutingFactory';\nimport { FilterDefinition } from './WebAppMeta';\nimport { WebpiecesConfig, WEBPIECES_CONFIG_TOKEN } from './WebpiecesConfig';\nimport { InProcessApiClientFactory } from './InProcessApiClientFactory';\nimport { ApiFactory } from './ApiFactory';\nimport { ApiClient } from './ApiClient';\nimport { ErrorLogFilter } from './filters/ErrorLogFilter';\nimport { AuthFilter } from './filters/AuthFilter';\n\n/**\n * Options for {@link WebpiecesRouterFactory.create}.\n *\n * appBindings - REQUIRED DI ContainerModules to load (framework + app), e.g.\n * [WebpiecesModule, CompanyHeadersModule, AppModule]. Loaded after the\n * @provideSingleton auto-scan so they can add/override bindings.\n * appOverrides - A single ContainerModule loaded LAST so tests can rebind real\n * controllers/clients to mocks (see @webpieces/core-mock createMock()).\n */\nexport interface WebpiecesRouterOptions {\n appBindings: ContainerModule[];\n appOverrides?: ContainerModule;\n}\n\n/**\n * WebpiecesRouter - the node-only heart of a webpieces app: a DI container + a filter\n * chain + an in-process API client. It has NO express dependency, so it runs anywhere\n * node runs and is fully testable with zero HTTP.\n *\n * DI-resolved from the platform container (like the old WebpiecesServerImpl):\n * `@provideSingleton @injectable`, RouteBuilderImpl injected, and the two containers set in\n * initialize(). Built by {@link WebpiecesRouterFactory.create} — never `new`ed by callers.\n *\n * Two-container pattern (mirrors Java WebPieces):\n * - webpiecesContainer : framework bindings (config token, @DocumentDesign design roots)\n * - appContainer : your controllers/filters/modules (a child of the framework one)\n *\n * Usage:\n * ```typescript\n * const router = await WebpiecesRouterFactory.create(new WebpiecesConfig(), {\n * appBindings: [WebpiecesModule, CompanyHeadersModule],\n * });\n * router.addRoutes(SaveApi, SaveController);\n * router.addFilter(new FilterDefinition(1800, LogApiFilter, '*')); // your own filters\n * // (ErrorLogFilter + AuthFilter are auto-installed above yours; auth is AuthMode-driven)\n *\n * // test (no express): runs the SAME filter chain (incl. auth) -> controller\n * const api = router.createApiClient(SaveApi);\n * await api.save(new SaveRequest(...));\n * ```\n *\n * To serve real HTTP, hand this router to the express adapter in @webpieces/http-server\n * (bindExpress / bindAndStartExpress) — express lifecycle lives THERE, never here.\n *\n * @DocumentDesign marks it a design root so it appears in http-routing's designed-lib graph.\n */\n@DocumentDesign()\n@provideFrameworkSingleton()\nexport class WebpiecesRouter implements ApiFactory {\n private webpiecesContainer!: Container;\n private appContainer!: Container;\n\n constructor(\n @inject(RouteBuilderImpl) private readonly routeBuilder: RouteBuilderImpl,\n ) {}\n\n /**\n * Build the app container (child of the framework container), load the @provideSingleton\n * auto-scan + appBindings + appOverrides, and point the RouteBuilder at it. Called once by\n * the factory after this router is resolved from the framework container.\n */\n async initialize(webpiecesContainer: Container, options: WebpiecesRouterOptions): Promise<void> {\n this.webpiecesContainer = webpiecesContainer;\n\n // App container is a child so app bindings see framework bindings while staying separate.\n this.appContainer = new Container({ parent: webpiecesContainer });\n this.routeBuilder.setContainer(this.appContainer);\n\n await this.loadDIModules(options);\n this.installFixedFilters();\n }\n\n /**\n * Auto-install the two fixed framework filters on every route (apps add only their own\n * filters below these): ErrorLogFilter outermost (log + let the transport translate), then\n * AuthFilter (enforces the endpoint's AuthMode off the HttpRequest). Both run over HTTP AND\n * in-process — there is no transport tier.\n */\n private installFixedFilters(): void {\n this.addFilter(new FilterDefinition(1_000_000, ErrorLogFilter, '*'));\n this.addFilter(new FilterDefinition(900_000, AuthFilter, '*'));\n }\n\n private async loadDIModules(options: WebpiecesRouterOptions): Promise<void> {\n // Load BOTH registries: framework classes (provideFrameworkSingleton) + the client's\n // own @provideSingleton classes (binding-decorators global). A client's\n // buildProviderModule() only ever contains the client's classes — never framework internals.\n await this.appContainer.load(buildFrameworkModule());\n await 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 options.appBindings) {\n await this.appContainer.load(module);\n }\n\n // Load appOverrides LAST so they can override existing bindings\n if (options.appOverrides) {\n await this.appContainer.load(options.appOverrides);\n }\n }\n\n /**\n * Wire an API prototype (with @ApiPath/@Endpoint decorators) to its controller.\n * The controller is resolved from the container at request time.\n */\n addRoutes<TApi, TController extends TApi>(\n api: ClassType<TApi>,\n controller: ClassType<TController>,\n ): this {\n new ApiRoutingFactory(api, controller).configure(this.routeBuilder);\n return this;\n }\n\n /**\n * Register a user filter (runs in-process AND over HTTP, below the auto-installed fixed\n * ErrorLogFilter + AuthFilter).\n */\n addFilter(filter: FilterDefinition): this {\n this.routeBuilder.addFilter(filter);\n return this;\n }\n\n /**\n * Create an in-process API client that runs the api-tier filter chain + controller\n * with NO express/HTTP. The primary path for tests and node-only callers.\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 new InProcessApiClientFactory(this.routeBuilder).createApiClient(apiPrototype);\n }\n\n /**\n * Reify the registered routes as {@link ApiClient}s (api contract + routeMeta + composed\n * filter-chain→controller impl). This is the ONLY handoff to the express layer — the\n * internal RouteBuilder never leaves this class.\n */\n apiClients(): ApiClient[] {\n return this.routeBuilder.apiClients();\n }\n\n /** The application DI container (child of the framework container). */\n getContainer(): Container {\n return this.appContainer;\n }\n}\n\n/**\n * Builds a {@link WebpiecesRouter}: constructs the platform container (mirrors\n * WebpiecesServerFactory.create), RESOLVES the router from DI, then initializes its app child\n * container with the @provideSingleton auto-scan + appBindings + optional test overrides.\n */\nexport class WebpiecesRouterFactory {\n static async create(\n config: WebpiecesConfig,\n options: WebpiecesRouterOptions,\n ): Promise<WebpiecesRouter> {\n // Platform (framework) container — build via buildFrameworkModule so framework\n // singletons (WebpiecesRouter, RouteBuilderImpl) come from the webpieces registry,\n // NOT the client's global one.\n const webpiecesContainer = new Container();\n webpiecesContainer.bind(WEBPIECES_CONFIG_TOKEN).toConstantValue(config);\n await webpiecesContainer.load(buildFrameworkModule());\n\n // Resolve the router from the container (NOT new'd) so @DocumentDesign + DI hold.\n const router = webpiecesContainer.get(WebpiecesRouter);\n await router.initialize(webpiecesContainer, options);\n return router;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"WebpiecesRouter.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/WebpiecesRouter.ts"],"names":[],"mappings":"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","sourcesContent":["import { Container, ContainerModule, inject } from 'inversify';\nimport { buildProviderModule } from '@inversifyjs/binding-decorators';\nimport { DocumentDesign } from '@webpieces/core-util';\nimport { provideFrameworkSingleton, buildFrameworkModule } from '@webpieces/core-context';\nimport { RouteBuilderImpl } from './RouteBuilderImpl';\nimport { ApiRoutingFactory, ClassType } from './ApiRoutingFactory';\nimport { FilterDefinition } from './WebAppMeta';\nimport { WebpiecesConfig, WEBPIECES_CONFIG_TOKEN } from './WebpiecesConfig';\nimport { ApiClientFactory } from './ApiClientFactory';\nimport { ApiFactory } from './ApiFactory';\nimport { ApiClient } from './ApiClient';\nimport { ErrorLogFilter } from './filters/ErrorLogFilter';\nimport { AuthFilter } from './filters/AuthFilter';\n\n/**\n * Options for {@link WebpiecesRouterFactory.create}.\n *\n * appBindings - REQUIRED DI ContainerModules to load (framework + app), e.g.\n * [WebpiecesModule, CompanyHeadersModule, AppModule]. Loaded after the\n * @provideSingleton auto-scan so they can add/override bindings.\n * appOverrides - A single ContainerModule loaded LAST so tests can rebind real\n * controllers/clients to mocks (see @webpieces/core-mock createMock()).\n */\nexport interface WebpiecesRouterOptions {\n appBindings: ContainerModule[];\n appOverrides?: ContainerModule;\n}\n\n/**\n * WebpiecesRouter - the node-only heart of a webpieces app: a DI container + a filter\n * chain + an in-process API client. It has NO express dependency, so it runs anywhere\n * node runs and is fully testable with zero HTTP.\n *\n * DI-resolved from the platform container (like the old WebpiecesServerImpl):\n * `@provideSingleton @injectable`, RouteBuilderImpl injected, and the two containers set in\n * initialize(). Built by {@link WebpiecesRouterFactory.create} — never `new`ed by callers.\n *\n * Two-container pattern (mirrors Java WebPieces):\n * - webpiecesContainer : framework bindings (config token, @DocumentDesign design roots)\n * - appContainer : your controllers/filters/modules (a child of the framework one)\n *\n * Usage:\n * ```typescript\n * const router = await WebpiecesRouterFactory.create(new WebpiecesConfig(), {\n * appBindings: [WebpiecesModule, CompanyHeadersModule],\n * });\n * router.addRoutes(SaveApi, SaveController);\n * router.addFilter(new FilterDefinition(1800, LogApiFilter, '*')); // your own filters\n * // (ErrorLogFilter + AuthFilter are auto-installed above yours; auth is AuthMode-driven)\n *\n * // test (no express): runs the SAME filter chain (incl. auth) -> controller\n * const api = router.createApiClient(SaveApi);\n * await api.save(new SaveRequest(...));\n * ```\n *\n * To serve real HTTP, hand this router to the express adapter in @webpieces/http-server\n * (bindExpress / bindAndStartExpress) — express lifecycle lives THERE, never here.\n *\n * @DocumentDesign marks it a design root so it appears in http-routing's designed-lib graph.\n */\n@DocumentDesign()\n@provideFrameworkSingleton()\nexport class WebpiecesRouter implements ApiFactory {\n private webpiecesContainer!: Container;\n private appContainer!: Container;\n\n constructor(\n @inject(RouteBuilderImpl) private readonly routeBuilder: RouteBuilderImpl,\n @inject(ApiClientFactory) private readonly apiClientFactory: ApiClientFactory,\n ) {}\n\n /**\n * Build the app container (child of the framework container), load the @provideSingleton\n * auto-scan + appBindings + appOverrides, and point the RouteBuilder at it. Called once by\n * the factory after this router is resolved from the framework container.\n */\n async initialize(webpiecesContainer: Container, options: WebpiecesRouterOptions): Promise<void> {\n this.webpiecesContainer = webpiecesContainer;\n\n // App container is a child so app bindings see framework bindings while staying separate.\n this.appContainer = new Container({ parent: webpiecesContainer });\n this.routeBuilder.setContainer(this.appContainer);\n\n await this.loadDIModules(options);\n this.installFixedFilters();\n }\n\n /**\n * Auto-install the two fixed framework filters on every route (apps add only their own\n * filters below these): ErrorLogFilter outermost (log + let the transport translate), then\n * AuthFilter (enforces the endpoint's AuthMode off the HttpRequest). Both run over HTTP AND\n * in-process — there is no transport tier.\n */\n private installFixedFilters(): void {\n this.addFilter(new FilterDefinition(1_000_000, ErrorLogFilter, '*'));\n this.addFilter(new FilterDefinition(900_000, AuthFilter, '*'));\n }\n\n private async loadDIModules(options: WebpiecesRouterOptions): Promise<void> {\n // Load BOTH registries: framework classes (provideFrameworkSingleton) + the client's\n // own @provideSingleton classes (binding-decorators global). A client's\n // buildProviderModule() only ever contains the client's classes — never framework internals.\n await this.appContainer.load(buildFrameworkModule());\n await 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 options.appBindings) {\n await this.appContainer.load(module);\n }\n\n // Load appOverrides LAST so they can override existing bindings\n if (options.appOverrides) {\n await this.appContainer.load(options.appOverrides);\n }\n }\n\n /**\n * Wire an API prototype (with @ApiPath/@Endpoint decorators) to its controller.\n * The controller is resolved from the container at request time.\n */\n addRoutes<TApi, TController extends TApi>(\n api: ClassType<TApi>,\n controller: ClassType<TController>,\n ): this {\n new ApiRoutingFactory(api, controller).configure(this.routeBuilder);\n return this;\n }\n\n /**\n * Register a user filter (runs in-process AND over HTTP, below the auto-installed fixed\n * ErrorLogFilter + AuthFilter).\n */\n addFilter(filter: FilterDefinition): this {\n this.routeBuilder.addFilter(filter);\n return this;\n }\n\n /**\n * Create an in-process API client that runs the api-tier filter chain + controller\n * with NO express/HTTP. The primary path for tests and node-only callers.\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.apiClientFactory.createApiClient(apiPrototype);\n }\n\n /**\n * Reify the registered APIs as {@link ApiClient}s (contract + the createApiClient proxy +\n * per-endpoint route metadata) via the shared {@link ApiClientFactory}. This is the ONLY\n * handoff to the express layer — the internal RouteBuilder never leaves.\n */\n apiClients(): ApiClient[] {\n return this.apiClientFactory.apiClients();\n }\n\n /** The application DI container (child of the framework container). */\n getContainer(): Container {\n return this.appContainer;\n }\n}\n\n/**\n * Builds a {@link WebpiecesRouter}: constructs the platform container (mirrors\n * WebpiecesServerFactory.create), RESOLVES the router from DI, then initializes its app child\n * container with the @provideSingleton auto-scan + appBindings + optional test overrides.\n */\nexport class WebpiecesRouterFactory {\n static async create(\n config: WebpiecesConfig,\n options: WebpiecesRouterOptions,\n ): Promise<WebpiecesRouter> {\n // Platform (framework) container — build via buildFrameworkModule so framework\n // singletons (WebpiecesRouter, RouteBuilderImpl) come from the webpieces registry,\n // NOT the client's global one.\n const webpiecesContainer = new Container();\n webpiecesContainer.bind(WEBPIECES_CONFIG_TOKEN).toConstantValue(config);\n await webpiecesContainer.load(buildFrameworkModule());\n\n // Resolve the router from the container (NOT new'd) so @DocumentDesign + DI hold.\n const router = webpiecesContainer.get(WebpiecesRouter);\n await router.initialize(webpiecesContainer, options);\n return router;\n }\n}\n"]}
|
package/src/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export { MethodMeta } from './MethodMeta';
|
|
|
12
12
|
export { RouteHandler } from './RouteHandler';
|
|
13
13
|
export { FilterMatcher, HttpFilter } from './FilterMatcher';
|
|
14
14
|
export { ApiFactory } from './ApiFactory';
|
|
15
|
-
export { ApiClient } from './ApiClient';
|
|
15
|
+
export { ApiClient, ApiClientProxy } from './ApiClient';
|
|
16
16
|
export { AuthConfig, Principal } from './AuthConfig';
|
|
17
17
|
export { fillContext } from './fillContext';
|
|
18
18
|
export { WebpiecesRouter, WebpiecesRouterFactory, WebpiecesRouterOptions } from './WebpiecesRouter';
|
package/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/index.ts"],"names":[],"mappings":";;;;AAAA,0DAA0D;AAC1D,kDA8B8B;AA7B1B,oGAAA,OAAO,OAAA;AACP,qGAAA,QAAQ,OAAA;AACR,2GAAA,cAAc,OAAA;AACd,iHAAA,oBAAoB,OAAA;AACpB,mGAAA,MAAM,OAAA;AACN,oGAAA,OAAO,OAAA;AACP,qGAAA,QAAQ,OAAA;AACR,6GAAA,gBAAgB,OAAA;AAChB,gGAAA,GAAG,OAAA;AACH,mGAAA,MAAM,OAAA;AACN,kGAAA,KAAK,OAAA;AACL,uGAAA,UAAU,OAAA;AACV,yGAAA,YAAY,OAAA;AACZ,sGAAA,SAAS,OAAA;AACT,wGAAA,WAAW,OAAA;AACX,wGAAA,WAAW,OAAA;AACX,2HAAA,8BAA8B,OAAA;AAC9B,uGAAA,UAAU,OAAA;AACV,0GAAA,aAAa,OAAA;AACb,oHAAA,uBAAuB,OAAA;AACvB,yGAAA,YAAY,OAAA;AACZ,qGAAA,QAAQ,OAAA;AACR,0GAAA,aAAa,OAAA;AACb,0GAAA,aAAa,OAAA;AAEb,2EAA2E;AAC3E,oCAAoC;AACpC,2GAAA,cAAc,OAAA;AACd,6GAAA,gBAAgB,OAAA;AAIpB,+CAA+C;AAC/C,2CAGsB;AAFlB,wGAAA,UAAU,OAAA;AACV,mHAAA,qBAAqB,OAAA;AAGzB,iFAAiF;AACjF,wDAAiG;AAAxF,gHAAA,gBAAgB,OAAA;AAAE,kHAAA,kBAAkB,OAAA;AAAE,gHAAA,gBAAgB,OAAA;AAC/D,gGAAgG;AAChG,wDAIiC;AAH7B,yHAAA,yBAAyB,OAAA;AACzB,2HAAA,2BAA2B,OAAA;AAC3B,oHAAA,oBAAoB,OAAA;AAGxB,yDAAmE;AAA1D,sHAAA,iBAAiB,OAAA;AAE1B,qBAAqB;AACrB,2CAKsB;AAFlB,6GAAA,eAAe,OAAA;AACf,8GAAA,gBAAgB,OAAA;AAGpB,sFAAsF;AACtF,+FAA+F;AAC/F,wDAAsD;AAA7C,2GAAA,WAAW,OAAA;AAEpB,qFAAqF;AACrF,mCAAuD;AAA9C,gGAAA,MAAM,OAAA;AAAE,oGAAA,UAAU,OAAA;AAC3B,6CAA4C;AAAnC,0GAAA,WAAW,OAAA;AACpB,2CAA0C;AAAjC,wGAAA,UAAU,OAAA;AACnB,+CAA8C;AAArC,4GAAA,YAAY,OAAA;AAErB,oFAAoF;AACpF,sFAAsF;AAEtF,kBAAkB;AAClB,iDAA4D;AAAnD,8GAAA,aAAa,OAAA;AAItB,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/index.ts"],"names":[],"mappings":";;;;AAAA,0DAA0D;AAC1D,kDA8B8B;AA7B1B,oGAAA,OAAO,OAAA;AACP,qGAAA,QAAQ,OAAA;AACR,2GAAA,cAAc,OAAA;AACd,iHAAA,oBAAoB,OAAA;AACpB,mGAAA,MAAM,OAAA;AACN,oGAAA,OAAO,OAAA;AACP,qGAAA,QAAQ,OAAA;AACR,6GAAA,gBAAgB,OAAA;AAChB,gGAAA,GAAG,OAAA;AACH,mGAAA,MAAM,OAAA;AACN,kGAAA,KAAK,OAAA;AACL,uGAAA,UAAU,OAAA;AACV,yGAAA,YAAY,OAAA;AACZ,sGAAA,SAAS,OAAA;AACT,wGAAA,WAAW,OAAA;AACX,wGAAA,WAAW,OAAA;AACX,2HAAA,8BAA8B,OAAA;AAC9B,uGAAA,UAAU,OAAA;AACV,0GAAA,aAAa,OAAA;AACb,oHAAA,uBAAuB,OAAA;AACvB,yGAAA,YAAY,OAAA;AACZ,qGAAA,QAAQ,OAAA;AACR,0GAAA,aAAa,OAAA;AACb,0GAAA,aAAa,OAAA;AAEb,2EAA2E;AAC3E,oCAAoC;AACpC,2GAAA,cAAc,OAAA;AACd,6GAAA,gBAAgB,OAAA;AAIpB,+CAA+C;AAC/C,2CAGsB;AAFlB,wGAAA,UAAU,OAAA;AACV,mHAAA,qBAAqB,OAAA;AAGzB,iFAAiF;AACjF,wDAAiG;AAAxF,gHAAA,gBAAgB,OAAA;AAAE,kHAAA,kBAAkB,OAAA;AAAE,gHAAA,gBAAgB,OAAA;AAC/D,gGAAgG;AAChG,wDAIiC;AAH7B,yHAAA,yBAAyB,OAAA;AACzB,2HAAA,2BAA2B,OAAA;AAC3B,oHAAA,oBAAoB,OAAA;AAGxB,yDAAmE;AAA1D,sHAAA,iBAAiB,OAAA;AAE1B,qBAAqB;AACrB,2CAKsB;AAFlB,6GAAA,eAAe,OAAA;AACf,8GAAA,gBAAgB,OAAA;AAGpB,sFAAsF;AACtF,+FAA+F;AAC/F,wDAAsD;AAA7C,2GAAA,WAAW,OAAA;AAEpB,qFAAqF;AACrF,mCAAuD;AAA9C,gGAAA,MAAM,OAAA;AAAE,oGAAA,UAAU,OAAA;AAC3B,6CAA4C;AAAnC,0GAAA,WAAW,OAAA;AACpB,2CAA0C;AAAjC,wGAAA,UAAU,OAAA;AACnB,+CAA8C;AAArC,4GAAA,YAAY,OAAA;AAErB,oFAAoF;AACpF,sFAAsF;AAEtF,kBAAkB;AAClB,iDAA4D;AAAnD,8GAAA,aAAa,OAAA;AAItB,yCAAwD;AAA/C,sGAAA,SAAS,OAAA;AAElB,sFAAsF;AACtF,2CAAqD;AAA5C,wGAAA,UAAU,OAAA;AAAE,uGAAA,SAAS,OAAA;AAE9B,kEAAkE;AAClE,6CAA4C;AAAnC,0GAAA,WAAW,OAAA;AAEpB,0FAA0F;AAC1F,qDAAoG;AAA3F,kHAAA,eAAe,OAAA;AAAE,yHAAA,sBAAsB,OAAA;AAEhD,oFAAoF;AACpF,wDAA+D;AAAtD,oHAAA,oBAAoB,OAAA;AAE7B,uBAAuB;AACvB,qDAA4E;AAAnE,kHAAA,eAAe,OAAA;AAAE,yHAAA,sBAAsB,OAAA","sourcesContent":["// Re-export API decorators from core-util for convenience\nexport {\n ApiPath,\n Endpoint,\n Authentication,\n AuthenticationConfig,\n Public,\n AuthJwt,\n AuthOidc,\n AuthSharedSecret,\n Rpc,\n PubSub,\n Queue,\n getApiPath,\n getEndpoints,\n isApiPath,\n getAuthMeta,\n getAuthMode,\n assertEveryEndpointHasAuthMode,\n getApiKind,\n assertApiKind,\n assertPubSubConventions,\n getQueueName,\n AuthMeta,\n RouteMetadata,\n METADATA_KEYS,\n ValidateImplementation,\n // @DocumentDesign moved to core-util (design-root marker, browser + Node);\n // re-exported here for back-compat.\n DocumentDesign,\n isDocumentDesign,\n} from '@webpieces/core-util';\nexport type { AuthMode, ApiKind } from '@webpieces/core-util';\n\n// Server-side routing decorators and utilities\nexport {\n SourceFile,\n ROUTING_METADATA_KEYS,\n} from './decorators';\n\n// DI provider decorators moved to core-context; re-exported here for back-compat\nexport { provideSingleton, provideSingletonAs, provideTransient } from '@webpieces/core-context';\n// Framework-only DI registry (packages/** framework classes use these; see frameworkProvide.ts)\nexport {\n provideFrameworkSingleton,\n provideFrameworkSingletonAs,\n buildFrameworkModule,\n} from '@webpieces/core-context';\n\nexport { ApiRoutingFactory, ClassType } from './ApiRoutingFactory';\n\n// Core routing types\nexport {\n Routes,\n RouteBuilder,\n RouteDefinition,\n FilterDefinition,\n} from './WebAppMeta';\n\n// The transport-neutral request type (defined in core-context; this is http-routing's\n// public request — a transport adapter builds one and the chain reads it from RequestContext).\nexport { HttpRequest } from '@webpieces/core-context';\n\n// Filter-chain primitives (absorbed from the former @webpieces/http-filters package)\nexport { Filter, WpResponse, Service } from './Filter';\nexport { FilterChain } from './FilterChain';\nexport { MethodMeta } from './MethodMeta';\nexport { RouteHandler } from './RouteHandler';\n\n// RouteBuilderImpl (the route table + chain composer) is now INTERNAL — it is never\n// handed to upper layers. The express layer consumes ApiFactory.apiClients() instead.\n\n// Filter matching\nexport { FilterMatcher, HttpFilter } from './FilterMatcher';\n\n// The public API-surface abstraction: declare routes/filters, get them back as ApiClient[].\nexport { ApiFactory } from './ApiFactory';\nexport { ApiClient, ApiClientProxy } from './ApiClient';\n\n// Auth: the app-provided, container-bound verifiers the framework AuthFilter injects.\nexport { AuthConfig, Principal } from './AuthConfig';\n\n// Above-boundary context setup shared by every transport adapter.\nexport { fillContext } from './fillContext';\n\n// Node-only router (the express-free heart: container + filter chain + in-process client)\nexport { WebpiecesRouter, WebpiecesRouterFactory, WebpiecesRouterOptions } from './WebpiecesRouter';\n\n// Context readers (Node.js only) moved to core-context; re-exported for back-compat\nexport { RequestContextReader } from '@webpieces/core-context';\n\n// Server configuration\nexport { WebpiecesConfig, WEBPIECES_CONFIG_TOKEN } from './WebpiecesConfig';\n"]}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { RouteBuilderImpl } from './RouteBuilderImpl';
|
|
2
|
-
/**
|
|
3
|
-
* InProcessApiClientFactory - Creates API client proxies that invoke routes
|
|
4
|
-
* in-process (api-tier filter chain + controller) WITHOUT any HTTP/express overhead.
|
|
5
|
-
*
|
|
6
|
-
* This is the PRIMARY in-process/test builder. It lives in the node-only http-routing
|
|
7
|
-
* package (no express dependency) so both the node-only WebpiecesRouter and the express
|
|
8
|
-
* adapter (WebpiecesRouteCreator) 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
|
-
private readonly contextMgr;
|
|
20
|
-
constructor(routeBuilder: RouteBuilderImpl);
|
|
21
|
-
/**
|
|
22
|
-
* Create an API client proxy for testing.
|
|
23
|
-
*
|
|
24
|
-
* @param apiPrototype - The API prototype class with routing decorators (can be abstract)
|
|
25
|
-
* @returns A proxy that implements the API interface
|
|
26
|
-
*
|
|
27
|
-
* Example:
|
|
28
|
-
* ```typescript
|
|
29
|
-
* const saveApi = factory.createApiClient<SaveApi>(SaveApi);
|
|
30
|
-
* const response = await saveApi.save(request);
|
|
31
|
-
* ```
|
|
32
|
-
*/
|
|
33
|
-
createApiClient<T>(apiPrototype: abstract new (...args: any[]) => T): T;
|
|
34
|
-
private runMethod;
|
|
35
|
-
}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.InProcessApiClientFactory = void 0;
|
|
4
|
-
const core_util_1 = require("@webpieces/core-util");
|
|
5
|
-
const MethodMeta_1 = require("./MethodMeta");
|
|
6
|
-
const core_context_1 = require("@webpieces/core-context");
|
|
7
|
-
const fillContext_1 = require("./fillContext");
|
|
8
|
-
/**
|
|
9
|
-
* InProcessApiClientFactory - Creates API client proxies that invoke routes
|
|
10
|
-
* in-process (api-tier filter chain + controller) WITHOUT any HTTP/express overhead.
|
|
11
|
-
*
|
|
12
|
-
* This is the PRIMARY in-process/test builder. It lives in the node-only http-routing
|
|
13
|
-
* package (no express dependency) so both the node-only WebpiecesRouter and the express
|
|
14
|
-
* adapter (WebpiecesRouteCreator) share one code path.
|
|
15
|
-
*
|
|
16
|
-
* The client uses the ApiPrototype class to discover routes via decorators,
|
|
17
|
-
* then creates pre-configured invoker functions for each API method.
|
|
18
|
-
*
|
|
19
|
-
* IMPORTANT: This loops over the API methods (from decorators), NOT all routes.
|
|
20
|
-
* For each API method, it sets up the filter chain ONCE during proxy creation,
|
|
21
|
-
* so subsequent calls reuse the same filter chain (efficient!).
|
|
22
|
-
*/
|
|
23
|
-
class InProcessApiClientFactory {
|
|
24
|
-
routeBuilder;
|
|
25
|
-
// Builds request headers the SAME way the real HTTP client does — from the ambient
|
|
26
|
-
// RequestContext — so a credential a test put in context travels as a real request header.
|
|
27
|
-
contextMgr = new core_util_1.ContextMgr(new core_context_1.RequestContextReader());
|
|
28
|
-
constructor(routeBuilder) {
|
|
29
|
-
this.routeBuilder = routeBuilder;
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Create an API client proxy for testing.
|
|
33
|
-
*
|
|
34
|
-
* @param apiPrototype - The API prototype class with routing decorators (can be abstract)
|
|
35
|
-
* @returns A proxy that implements the API interface
|
|
36
|
-
*
|
|
37
|
-
* Example:
|
|
38
|
-
* ```typescript
|
|
39
|
-
* const saveApi = factory.createApiClient<SaveApi>(SaveApi);
|
|
40
|
-
* const response = await saveApi.save(request);
|
|
41
|
-
* ```
|
|
42
|
-
*/
|
|
43
|
-
// webpieces-disable no-any-unknown -- abstract constructor signature requires any[] args
|
|
44
|
-
createApiClient(apiPrototype) {
|
|
45
|
-
// Get endpoints from the API prototype using @ApiPath/@Endpoint decorators
|
|
46
|
-
const basePath = (0, core_util_1.getApiPath)(apiPrototype) || '';
|
|
47
|
-
const endpoints = (0, core_util_1.getEndpoints)(apiPrototype) || {};
|
|
48
|
-
// Create proxy object
|
|
49
|
-
// webpieces-disable no-any-unknown -- proxy holds methods of arbitrary API shapes
|
|
50
|
-
const proxy = {};
|
|
51
|
-
// Loop over API endpoints and create proxy functions
|
|
52
|
-
for (const [methodName, endpointPath] of Object.entries(endpoints)) {
|
|
53
|
-
const httpMethod = 'POST';
|
|
54
|
-
const path = basePath + endpointPath;
|
|
55
|
-
const authMeta = (0, core_util_1.getAuthMeta)(apiPrototype, methodName);
|
|
56
|
-
const routeMeta = new core_util_1.RouteMetadata(httpMethod, path, methodName, apiPrototype.name, authMeta);
|
|
57
|
-
// Create invoker service ONCE (sets up filter chain once, not on every call!)
|
|
58
|
-
const service = this.routeBuilder.createRouteInvoker(httpMethod, path);
|
|
59
|
-
// Proxy method creates MethodMeta and calls the pre-configured service
|
|
60
|
-
// webpieces-disable no-any-unknown -- request/response DTO types are erased at proxy level
|
|
61
|
-
proxy[methodName] = async (requestDto) => {
|
|
62
|
-
// Auto-activate a RequestContext if the test did not wrap the call itself
|
|
63
|
-
if (!core_context_1.RequestContext.isActive()) {
|
|
64
|
-
return core_context_1.RequestContext.run(async () => {
|
|
65
|
-
return await this.runMethod(routeMeta, requestDto, service);
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
return await this.runMethod(routeMeta, requestDto, service);
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
return proxy;
|
|
72
|
-
}
|
|
73
|
-
// webpieces-disable no-any-unknown -- DTO types are erased at the routing layer
|
|
74
|
-
async runMethod(routeMeta, requestDto, service) {
|
|
75
|
-
// In-process: publish a transport-neutral HttpRequest (headers come from whatever the
|
|
76
|
-
// caller set in the context; empty by default) so the SAME chain that runs over HTTP
|
|
77
|
-
// can read RequestContext.getRequest(). Then build the DTO-only meta.
|
|
78
|
-
const headers = new Map();
|
|
79
|
-
this.contextMgr.buildOutboundHeaders().forEach((value, name) => {
|
|
80
|
-
headers.set(name.toLowerCase(), [value]);
|
|
81
|
-
});
|
|
82
|
-
core_context_1.RequestContext.setRequest(new core_context_1.HttpRequest(routeMeta.httpMethod, routeMeta.path, headers));
|
|
83
|
-
(0, fillContext_1.fillContext)();
|
|
84
|
-
const meta = new MethodMeta_1.MethodMeta(routeMeta, requestDto);
|
|
85
|
-
const responseWrapper = await service.invoke(meta);
|
|
86
|
-
return responseWrapper.response;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
exports.InProcessApiClientFactory = InProcessApiClientFactory;
|
|
90
|
-
//# sourceMappingURL=InProcessApiClientFactory.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"InProcessApiClientFactory.js","sourceRoot":"","sources":["../../../../../packages/http/http-routing/src/InProcessApiClientFactory.ts"],"names":[],"mappings":";;;AAAA,oDAM8B;AAC9B,6CAA0C;AAE1C,0DAA4F;AAE5F,+CAA4C;AAE5C;;;;;;;;;;;;;;GAcG;AACH,MAAa,yBAAyB;IAKd;IAJpB,mFAAmF;IACnF,2FAA2F;IAC1E,UAAU,GAAG,IAAI,sBAAU,CAAC,IAAI,mCAAoB,EAAE,CAAC,CAAC;IAEzE,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,sBAAU,EAAC,YAAY,CAAC,IAAI,EAAE,CAAC;QAChD,MAAM,SAAS,GAAG,IAAA,wBAAY,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,uBAAW,EAAC,YAAY,EAAE,UAAU,CAAC,CAAC;YACvD,MAAM,SAAS,GAAG,IAAI,yBAAa,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,sFAAsF;QACtF,qFAAqF;QACrF,sEAAsE;QACtE,MAAM,OAAO,GAAG,IAAI,GAAG,EAAoB,CAAC;QAC5C,IAAI,CAAC,UAAU,CAAC,oBAAoB,EAAE,CAAC,OAAO,CAAC,CAAC,KAAa,EAAE,IAAY,EAAE,EAAE;YAC3E,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;QAC7C,CAAC,CAAC,CAAC;QACH,6BAAc,CAAC,UAAU,CAAC,IAAI,0BAAW,CAAC,SAAS,CAAC,UAAU,EAAE,SAAS,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;QAC1F,IAAA,yBAAW,GAAE,CAAC;QACd,MAAM,IAAI,GAAG,IAAI,uBAAU,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;QACnD,MAAM,eAAe,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACnD,OAAO,eAAe,CAAC,QAAQ,CAAC;IACpC,CAAC;CACJ;AAvED,8DAuEC","sourcesContent":["import {\n getApiPath,\n getAuthMeta,\n getEndpoints,\n RouteMetadata,\n ContextMgr,\n} from '@webpieces/core-util';\nimport { MethodMeta } from './MethodMeta';\nimport { Service, WpResponse } from './Filter';\nimport { RequestContext, HttpRequest, RequestContextReader } from '@webpieces/core-context';\nimport { RouteBuilderImpl } from './RouteBuilderImpl';\nimport { fillContext } from './fillContext';\n\n/**\n * InProcessApiClientFactory - Creates API client proxies that invoke routes\n * in-process (api-tier filter chain + controller) WITHOUT any HTTP/express overhead.\n *\n * This is the PRIMARY in-process/test builder. It lives in the node-only http-routing\n * package (no express dependency) so both the node-only WebpiecesRouter and the express\n * adapter (WebpiecesRouteCreator) 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 // Builds request headers the SAME way the real HTTP client does — from the ambient\n // RequestContext — so a credential a test put in context travels as a real request header.\n private readonly contextMgr = new ContextMgr(new RequestContextReader());\n\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 // In-process: publish a transport-neutral HttpRequest (headers come from whatever the\n // caller set in the context; empty by default) so the SAME chain that runs over HTTP\n // can read RequestContext.getRequest(). Then build the DTO-only meta.\n const headers = new Map<string, string[]>();\n this.contextMgr.buildOutboundHeaders().forEach((value: string, name: string) => {\n headers.set(name.toLowerCase(), [value]);\n });\n RequestContext.setRequest(new HttpRequest(routeMeta.httpMethod, routeMeta.path, headers));\n fillContext();\n const meta = new MethodMeta(routeMeta, requestDto);\n const responseWrapper = await service.invoke(meta);\n return responseWrapper.response;\n }\n}\n"]}
|