@webpieces/core-util 0.4.772 → 0.4.774
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/README.md +23 -0
- package/package.json +1 -1
- package/src/errors/ApiError.d.ts +14 -2
- package/src/errors/ApiError.js +14 -2
- package/src/errors/ApiError.js.map +1 -1
- package/src/errors/ApiErrorBoundary.d.ts +24 -0
- package/src/errors/ApiErrorBoundary.js +57 -0
- package/src/errors/ApiErrorBoundary.js.map +1 -0
- package/src/errors/index.d.ts +1 -0
- package/src/errors/index.js +3 -1
- package/src/errors/index.js.map +1 -1
- package/src/http/RouteMetadata.d.ts +6 -1
- package/src/http/RouteMetadata.js +6 -1
- package/src/http/RouteMetadata.js.map +1 -1
- package/src/http/RouteMetadataFactory.d.ts +2 -0
- package/src/http/RouteMetadataFactory.js +27 -8
- package/src/http/RouteMetadataFactory.js.map +1 -1
- package/src/http/StreamingContract.d.ts +82 -0
- package/src/http/StreamingContract.js +157 -0
- package/src/http/StreamingContract.js.map +1 -0
- package/src/http/decorators.d.ts +4 -0
- package/src/http/decorators.js +4 -0
- package/src/http/decorators.js.map +1 -1
- package/src/index.d.ts +6 -3
- package/src/index.js +19 -3
- package/src/index.js.map +1 -1
- package/src/mcp/DtoSchema.d.ts +54 -9
- package/src/mcp/DtoSchema.js +150 -34
- package/src/mcp/DtoSchema.js.map +1 -1
- package/src/mcp/McpAuthMetadata.d.ts +9 -0
- package/src/mcp/McpAuthMetadata.js +31 -0
- package/src/mcp/McpAuthMetadata.js.map +1 -0
- package/src/mcp/McpCompileAssertions.js +10 -0
- package/src/mcp/McpCompileAssertions.js.map +1 -1
package/README.md
CHANGED
|
@@ -103,6 +103,29 @@ try {
|
|
|
103
103
|
|
|
104
104
|
This package has zero dependencies and works in all modern browsers and Node.js environments.
|
|
105
105
|
|
|
106
|
+
## Typed streaming contracts
|
|
107
|
+
|
|
108
|
+
`@WpStream(() => RequestEvent, () => ResponseEvent)` marks one API method with the shared duplex
|
|
109
|
+
contract. The server receives request events through the returned `RequestStream`; callers receive
|
|
110
|
+
response events through the `ResponseStream` passed to the method. The same declaration drives the
|
|
111
|
+
HTTP server, Node and browser clients, and in-process feature client.
|
|
112
|
+
|
|
113
|
+
Every `event`, `fail`, and `complete` call awaits its next transport/consumer boundary. Events are
|
|
114
|
+
therefore ordered and backpressured rather than accumulated in an unbounded framework buffer.
|
|
115
|
+
`complete` and terminal `fail` permanently close that direction, writes after termination reject, and
|
|
116
|
+
`cancel` runs registered cancellation work once. Failures are terminal by default. A non-terminal
|
|
117
|
+
failure continues only on an adapter that explicitly supports it; otherwise it is promoted to
|
|
118
|
+
terminal. `StreamCorrelation` is an explicit stable application key and never relies on object
|
|
119
|
+
identity or event arrival order.
|
|
120
|
+
|
|
121
|
+
The generic HTTP request direction uses NDJSON: each `StreamEnvelope` is one JSON record followed by
|
|
122
|
+
`\n`. The response direction uses SSE: `event: message`, one or more `data:` lines, then a blank line.
|
|
123
|
+
Blank lines (`\n\n` or `\r\n\r\n`) dispatch an event, multiple `data:` lines join with `\n`, and a
|
|
124
|
+
leading `:` is a comment/keepalive. Generic SSE does not use `id` or `Last-Event-ID` for resumption.
|
|
125
|
+
Open failures use the ordinary typed Webpieces HTTP error/status mapping. Legal in-band failures carry
|
|
126
|
+
the standard `ApiErrorPayload`; disconnects, malformed frames, and failures that cannot be written are
|
|
127
|
+
reported locally as `StreamTransportError` with their cause and available correlation metadata.
|
|
128
|
+
|
|
106
129
|
## Related Packages
|
|
107
130
|
|
|
108
131
|
- [@webpieces/nx-webpieces-rules](https://www.npmjs.com/package/@webpieces/nx-webpieces-rules) - Includes ESLint rule that enforces this pattern
|
package/package.json
CHANGED
package/src/errors/ApiError.d.ts
CHANGED
|
@@ -40,13 +40,25 @@ export declare class ApiConnectionError extends ApiError {
|
|
|
40
40
|
export declare class ApiEndpointNotFoundError extends ApiNotFoundError {
|
|
41
41
|
readonly kind: "endpoint-not-found";
|
|
42
42
|
}
|
|
43
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Expected mistake by the ACTOR driving the call: the input was well-formed but wrong in a way the actor
|
|
45
|
+
* must fix (for example "the two passwords you entered do not match"). The actor may be a human user in
|
|
46
|
+
* a GUI OR an LLM calling an MCP tool; the same class serves both. Its message is deliberately
|
|
47
|
+
* caller-safe and is the only ApiError message published verbatim: `ApiErrorCodec` keeps it (and
|
|
48
|
+
* `errorCode`) across every remote hop, so a downstream service's text reaches the GUI or the model
|
|
49
|
+
* byte-for-byte.
|
|
50
|
+
*/
|
|
44
51
|
export declare class ApiEndUserError extends ApiError {
|
|
45
52
|
errorCode?: string | undefined;
|
|
46
53
|
readonly kind: "end-user";
|
|
47
54
|
constructor(message: string, errorCode?: string | undefined, cause?: Error);
|
|
48
55
|
}
|
|
49
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Invalid API caller input (malformed, or failed validation). `message` is operator-only diagnostic
|
|
58
|
+
* text and may name internals; it is never published. `callerMessage` (plus `field`) is what a GUI OR
|
|
59
|
+
* an MCP model is shown so it can correct its input, so throw sites that want the caller to
|
|
60
|
+
* self-correct MUST set it.
|
|
61
|
+
*/
|
|
50
62
|
export declare class ApiBadRequestError extends ApiError {
|
|
51
63
|
field?: string | undefined;
|
|
52
64
|
callerMessage?: string | undefined;
|
package/src/errors/ApiError.js
CHANGED
|
@@ -58,7 +58,14 @@ class ApiEndpointNotFoundError extends ApiNotFoundError {
|
|
|
58
58
|
kind = 'endpoint-not-found';
|
|
59
59
|
}
|
|
60
60
|
exports.ApiEndpointNotFoundError = ApiEndpointNotFoundError;
|
|
61
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* Expected mistake by the ACTOR driving the call: the input was well-formed but wrong in a way the actor
|
|
63
|
+
* must fix (for example "the two passwords you entered do not match"). The actor may be a human user in
|
|
64
|
+
* a GUI OR an LLM calling an MCP tool; the same class serves both. Its message is deliberately
|
|
65
|
+
* caller-safe and is the only ApiError message published verbatim: `ApiErrorCodec` keeps it (and
|
|
66
|
+
* `errorCode`) across every remote hop, so a downstream service's text reaches the GUI or the model
|
|
67
|
+
* byte-for-byte.
|
|
68
|
+
*/
|
|
62
69
|
class ApiEndUserError extends ApiError {
|
|
63
70
|
errorCode;
|
|
64
71
|
kind = 'end-user';
|
|
@@ -69,7 +76,12 @@ class ApiEndUserError extends ApiError {
|
|
|
69
76
|
}
|
|
70
77
|
}
|
|
71
78
|
exports.ApiEndUserError = ApiEndUserError;
|
|
72
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* Invalid API caller input (malformed, or failed validation). `message` is operator-only diagnostic
|
|
81
|
+
* text and may name internals; it is never published. `callerMessage` (plus `field`) is what a GUI OR
|
|
82
|
+
* an MCP model is shown so it can correct its input, so throw sites that want the caller to
|
|
83
|
+
* self-correct MUST set it.
|
|
84
|
+
*/
|
|
73
85
|
class ApiBadRequestError extends ApiError {
|
|
74
86
|
field;
|
|
75
87
|
callerMessage;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ApiError.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/ApiError.ts"],"names":[],"mappings":";;;AAgBA,8FAA8F;AAC9F,MAAsB,QAAS,SAAQ,KAAK;IAEjC,OAAO,CAAU;IAExB,YAAY,OAAe,EAAE,KAAa;QACtC,KAAK,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QAC1B,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;IAChC,CAAC;CACJ;AARD,4BAQC;AAED,MAAa,gBAAiB,SAAQ,QAAQ;IACxB,IAAI,GAAiB,WAAW,CAAC;CACtD;AAFD,4CAEC;AACD,MAAa,kBAAmB,SAAQ,QAAQ;IAC1B,IAAI,GAAG,YAAqB,CAAC;CAClD;AAFD,gDAEC;AACD,MAAa,mBAAoB,SAAQ,QAAQ;IAC3B,IAAI,GAAG,aAAsB,CAAC;CACnD;AAFD,kDAEC;AACD,MAAa,yBAA0B,SAAQ,QAAQ;IACjC,IAAI,GAAG,oBAA6B,CAAC;CAC1D;AAFD,8DAEC;AAED,2GAA2G;AAC3G,MAAa,sBAAuB,SAAQ,QAAQ;IAM5B;IALF,IAAI,GAAG,gBAAyB,CAAC;IAEnD,YACI,OAAe,EACf,KAAa,EACG,cAAc,KAAK;QAEnC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAFN,gBAAW,GAAX,WAAW,CAAQ;IAGvC,CAAC;CACJ;AAVD,wDAUC;AAED,MAAa,mBAAoB,SAAQ,QAAQ;IAC3B,IAAI,GAAG,cAAuB,CAAC;CACpD;AAFD,kDAEC;AACD,MAAa,iBAAkB,SAAQ,QAAQ;IACzB,IAAI,GAAG,WAAoB,CAAC;CACjD;AAFD,8CAEC;AACD,MAAa,sBAAuB,SAAQ,QAAQ;IAC9B,IAAI,GAAG,iBAA0B,CAAC;CACvD;AAFD,wDAEC;AAED,gGAAgG;AAChG,MAAa,kBAAmB,SAAQ,QAAQ;IAC1B,IAAI,GAAG,YAAqB,CAAC;CAClD;AAFD,gDAEC;AAED,2EAA2E;AAC3E,MAAa,wBAAyB,SAAQ,gBAAgB;IACxC,IAAI,GAAG,oBAA6B,CAAC;CAC1D;AAFD,4DAEC;AAED
|
|
1
|
+
{"version":3,"file":"ApiError.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/ApiError.ts"],"names":[],"mappings":";;;AAgBA,8FAA8F;AAC9F,MAAsB,QAAS,SAAQ,KAAK;IAEjC,OAAO,CAAU;IAExB,YAAY,OAAe,EAAE,KAAa;QACtC,KAAK,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QAC1B,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;IAChC,CAAC;CACJ;AARD,4BAQC;AAED,MAAa,gBAAiB,SAAQ,QAAQ;IACxB,IAAI,GAAiB,WAAW,CAAC;CACtD;AAFD,4CAEC;AACD,MAAa,kBAAmB,SAAQ,QAAQ;IAC1B,IAAI,GAAG,YAAqB,CAAC;CAClD;AAFD,gDAEC;AACD,MAAa,mBAAoB,SAAQ,QAAQ;IAC3B,IAAI,GAAG,aAAsB,CAAC;CACnD;AAFD,kDAEC;AACD,MAAa,yBAA0B,SAAQ,QAAQ;IACjC,IAAI,GAAG,oBAA6B,CAAC;CAC1D;AAFD,8DAEC;AAED,2GAA2G;AAC3G,MAAa,sBAAuB,SAAQ,QAAQ;IAM5B;IALF,IAAI,GAAG,gBAAyB,CAAC;IAEnD,YACI,OAAe,EACf,KAAa,EACG,cAAc,KAAK;QAEnC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAFN,gBAAW,GAAX,WAAW,CAAQ;IAGvC,CAAC;CACJ;AAVD,wDAUC;AAED,MAAa,mBAAoB,SAAQ,QAAQ;IAC3B,IAAI,GAAG,cAAuB,CAAC;CACpD;AAFD,kDAEC;AACD,MAAa,iBAAkB,SAAQ,QAAQ;IACzB,IAAI,GAAG,WAAoB,CAAC;CACjD;AAFD,8CAEC;AACD,MAAa,sBAAuB,SAAQ,QAAQ;IAC9B,IAAI,GAAG,iBAA0B,CAAC;CACvD;AAFD,wDAEC;AAED,gGAAgG;AAChG,MAAa,kBAAmB,SAAQ,QAAQ;IAC1B,IAAI,GAAG,YAAqB,CAAC;CAClD;AAFD,gDAEC;AAED,2EAA2E;AAC3E,MAAa,wBAAyB,SAAQ,gBAAgB;IACxC,IAAI,GAAG,oBAA6B,CAAC;CAC1D;AAFD,4DAEC;AAED;;;;;;;GAOG;AACH,MAAa,eAAgB,SAAQ,QAAQ;IAK9B;IAJO,IAAI,GAAG,UAAmB,CAAC;IAE7C,YACI,OAAe,EACR,SAAkB,EACzB,KAAa;QAEb,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAHf,cAAS,GAAT,SAAS,CAAS;QAIzB,IAAI,CAAC,OAAO,GAAG,YAAY,CAAC;IAChC,CAAC;CACJ;AAXD,0CAWC;AAED;;;;;GAKG;AACH,MAAa,kBAAmB,SAAQ,QAAQ;IAKjC;IACA;IALO,IAAI,GAAG,aAAsB,CAAC;IAEhD,YACI,OAAe,EACR,KAAc,EACd,aAAsB,EAC7B,KAAa;QAEb,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAJf,UAAK,GAAL,KAAK,CAAS;QACd,kBAAa,GAAb,aAAa,CAAS;IAIjC,CAAC;CACJ;AAXD,gDAWC;AAED,MAAa,oBAAqB,SAAQ,QAAQ;IAC5B,IAAI,GAAG,cAAuB,CAAC;IAEjD,YAAY,OAAe,EAAE,OAAgB,EAAE,KAAa;QACxD,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACtB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IAC3B,CAAC;CACJ;AAPD,oDAOC;AAED,MAAa,yBAA0B,SAAQ,QAAQ;IAKxC;IAJO,IAAI,GAAG,oBAA6B,CAAC;IAEvD,YACI,OAAe,EACR,oBAAoB,EAAE,EAC7B,KAAa;QAEb,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAHf,sBAAiB,GAAjB,iBAAiB,CAAK;IAIjC,CAAC;CACJ;AAVD,8DAUC","sourcesContent":["export type ApiErrorKind =\n | 'end-user'\n | 'bad-request'\n | 'unauthorized'\n | 'forbidden'\n | 'not-found'\n | 'endpoint-not-found'\n | 'request-timeout'\n | 'rate-limited'\n | 'implementation'\n | 'dependency'\n | 'unavailable'\n | 'dependency-timeout'\n | 'dependency-backoff'\n | 'connection';\n\n/** Category for portable API failures. Concrete subclasses alone define protocol mappings. */\nexport abstract class ApiError extends Error {\n abstract readonly kind: ApiErrorKind;\n public subType?: string;\n\n constructor(message: string, cause?: Error) {\n super(message, { cause });\n this.name = new.target.name;\n }\n}\n\nexport class ApiNotFoundError extends ApiError {\n override readonly kind: ApiErrorKind = 'not-found';\n}\nexport class ApiDependencyError extends ApiError {\n override readonly kind = 'dependency' as const;\n}\nexport class ApiUnavailableError extends ApiError {\n override readonly kind = 'unavailable' as const;\n}\nexport class ApiDependencyTimeoutError extends ApiError {\n override readonly kind = 'dependency-timeout' as const;\n}\n\n/** A bug or invalid implementation state. Local errors become server errors only after a remote decode. */\nexport class ApiImplementationError extends ApiError {\n override readonly kind = 'implementation' as const;\n\n constructor(\n message: string,\n cause?: Error,\n public readonly serverError = false,\n ) {\n super(message, cause);\n }\n}\n\nexport class ApiRateLimitedError extends ApiError {\n override readonly kind = 'rate-limited' as const;\n}\nexport class ApiForbiddenError extends ApiError {\n override readonly kind = 'forbidden' as const;\n}\nexport class ApiRequestTimeoutError extends ApiError {\n override readonly kind = 'request-timeout' as const;\n}\n\n/** A caller-local connection failure. HTTP servers normalize this to ApiImplementationError. */\nexport class ApiConnectionError extends ApiError {\n override readonly kind = 'connection' as const;\n}\n\n/** The API operation is missing, distinct from an absent domain entity. */\nexport class ApiEndpointNotFoundError extends ApiNotFoundError {\n override readonly kind = 'endpoint-not-found' as const;\n}\n\n/**\n * Expected mistake by the ACTOR driving the call: the input was well-formed but wrong in a way the actor\n * must fix (for example \"the two passwords you entered do not match\"). The actor may be a human user in\n * a GUI OR an LLM calling an MCP tool; the same class serves both. Its message is deliberately\n * caller-safe and is the only ApiError message published verbatim: `ApiErrorCodec` keeps it (and\n * `errorCode`) across every remote hop, so a downstream service's text reaches the GUI or the model\n * byte-for-byte.\n */\nexport class ApiEndUserError extends ApiError {\n override readonly kind = 'end-user' as const;\n\n constructor(\n message: string,\n public errorCode?: string,\n cause?: Error,\n ) {\n super(message, cause);\n this.subType = 'USER_ERROR';\n }\n}\n\n/**\n * Invalid API caller input (malformed, or failed validation). `message` is operator-only diagnostic\n * text and may name internals; it is never published. `callerMessage` (plus `field`) is what a GUI OR\n * an MCP model is shown so it can correct its input, so throw sites that want the caller to\n * self-correct MUST set it.\n */\nexport class ApiBadRequestError extends ApiError {\n override readonly kind = 'bad-request' as const;\n\n constructor(\n message: string,\n public field?: string,\n public callerMessage?: string,\n cause?: Error,\n ) {\n super(message, cause);\n }\n}\n\nexport class ApiUnauthorizedError extends ApiError {\n override readonly kind = 'unauthorized' as const;\n\n constructor(message: string, subType?: string, cause?: Error) {\n super(message, cause);\n this.subType = subType;\n }\n}\n\nexport class ApiDependencyBackoffError extends ApiError {\n override readonly kind = 'dependency-backoff' as const;\n\n constructor(\n message: string,\n public retryAfterSeconds = 30,\n cause?: Error,\n ) {\n super(message, cause);\n }\n}\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Logger } from '../logging/Logger';
|
|
2
|
+
import { ApiError } from './ApiError';
|
|
3
|
+
/**
|
|
4
|
+
* The ONE rule every outbound protocol boundary (HTTP server, MCP) applies to a thrown value before
|
|
5
|
+
* rendering it, plus the one operator log line per failure. Adapters differ only in the wire shape
|
|
6
|
+
* they render; normalization and log levels live here so they cannot drift apart.
|
|
7
|
+
*/
|
|
8
|
+
export declare class ApiErrorBoundary {
|
|
9
|
+
private readonly log;
|
|
10
|
+
constructor(log: Logger);
|
|
11
|
+
/**
|
|
12
|
+
* An `ApiError` (except a caller-local `ApiConnectionError`) stays as-is. Anything else, including
|
|
13
|
+
* a raw `Error`, a non-Error throw, or a downstream connection failure, is a bug or broken state of
|
|
14
|
+
* THIS service and becomes `ApiImplementationError`. Its message stays operator-only: encoders
|
|
15
|
+
* publish only `ApiEndUserError` text.
|
|
16
|
+
*/
|
|
17
|
+
normalize(thrown: unknown): ApiError;
|
|
18
|
+
/**
|
|
19
|
+
* Logs one operator line at the per-kind level: expected caller outcomes at info, everything else
|
|
20
|
+
* at error. `correlation` is appended verbatim (for example `requestId=... tool=...`).
|
|
21
|
+
*/
|
|
22
|
+
logOperatorDetail(error: ApiError, correlation?: string): void;
|
|
23
|
+
private operatorDetail;
|
|
24
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ApiErrorBoundary = void 0;
|
|
4
|
+
const errorUtils_1 = require("../lib/errorUtils");
|
|
5
|
+
const ApiError_1 = require("./ApiError");
|
|
6
|
+
/**
|
|
7
|
+
* The ONE rule every outbound protocol boundary (HTTP server, MCP) applies to a thrown value before
|
|
8
|
+
* rendering it, plus the one operator log line per failure. Adapters differ only in the wire shape
|
|
9
|
+
* they render; normalization and log levels live here so they cannot drift apart.
|
|
10
|
+
*/
|
|
11
|
+
class ApiErrorBoundary {
|
|
12
|
+
log;
|
|
13
|
+
constructor(log) {
|
|
14
|
+
this.log = log;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* An `ApiError` (except a caller-local `ApiConnectionError`) stays as-is. Anything else, including
|
|
18
|
+
* a raw `Error`, a non-Error throw, or a downstream connection failure, is a bug or broken state of
|
|
19
|
+
* THIS service and becomes `ApiImplementationError`. Its message stays operator-only: encoders
|
|
20
|
+
* publish only `ApiEndUserError` text.
|
|
21
|
+
*/
|
|
22
|
+
// webpieces-disable no-any-unknown -- thrown values are unknown until normalized at this boundary
|
|
23
|
+
normalize(thrown) {
|
|
24
|
+
if (thrown instanceof ApiError_1.ApiError && !(thrown instanceof ApiError_1.ApiConnectionError))
|
|
25
|
+
return thrown;
|
|
26
|
+
const error = (0, errorUtils_1.toError)(thrown);
|
|
27
|
+
return new ApiError_1.ApiImplementationError(error.message, error);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Logs one operator line at the per-kind level: expected caller outcomes at info, everything else
|
|
31
|
+
* at error. `correlation` is appended verbatim (for example `requestId=... tool=...`).
|
|
32
|
+
*/
|
|
33
|
+
logOperatorDetail(error, correlation) {
|
|
34
|
+
const suffix = correlation ? ` ${correlation}` : '';
|
|
35
|
+
const detail = `${this.operatorDetail(error)}${suffix}`;
|
|
36
|
+
if (error instanceof ApiError_1.ApiEndUserError)
|
|
37
|
+
this.log.info(`End User Error: ${detail}`);
|
|
38
|
+
else if (error instanceof ApiError_1.ApiBadRequestError)
|
|
39
|
+
this.log.info(`Bad Request: ${detail}`);
|
|
40
|
+
else if (error instanceof ApiError_1.ApiNotFoundError)
|
|
41
|
+
this.log.info(`Not Found: ${detail}`);
|
|
42
|
+
else if (error instanceof ApiError_1.ApiUnauthorizedError)
|
|
43
|
+
this.log.info(`Unauthorized: ${detail}`);
|
|
44
|
+
else if (error instanceof ApiError_1.ApiForbiddenError)
|
|
45
|
+
this.log.info(`Forbidden: ${detail}`);
|
|
46
|
+
else if (error instanceof ApiError_1.ApiRequestTimeoutError)
|
|
47
|
+
this.log.error(`Request Timeout: ${detail}`);
|
|
48
|
+
else
|
|
49
|
+
this.log.error(`API failure: ${detail}`);
|
|
50
|
+
}
|
|
51
|
+
operatorDetail(error) {
|
|
52
|
+
const cause = error.cause instanceof Error ? ` cause=${error.cause.message}` : '';
|
|
53
|
+
return `[name=${error.name} kind=${error.kind} subType=${error.subType ?? 'none'}] ${error.message}${cause}`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
exports.ApiErrorBoundary = ApiErrorBoundary;
|
|
57
|
+
//# sourceMappingURL=ApiErrorBoundary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ApiErrorBoundary.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/ApiErrorBoundary.ts"],"names":[],"mappings":";;;AACA,kDAA4C;AAC5C,yCAUoB;AAEpB;;;;GAIG;AACH,MAAa,gBAAgB;IACI;IAA7B,YAA6B,GAAW;QAAX,QAAG,GAAH,GAAG,CAAQ;IAAG,CAAC;IAE5C;;;;;OAKG;IACH,kGAAkG;IAClG,SAAS,CAAC,MAAe;QACrB,IAAI,MAAM,YAAY,mBAAQ,IAAI,CAAC,CAAC,MAAM,YAAY,6BAAkB,CAAC;YAAE,OAAO,MAAM,CAAC;QACzF,MAAM,KAAK,GAAG,IAAA,oBAAO,EAAC,MAAM,CAAC,CAAC;QAC9B,OAAO,IAAI,iCAAsB,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IAC5D,CAAC;IAED;;;OAGG;IACH,iBAAiB,CAAC,KAAe,EAAE,WAAoB;QACnD,MAAM,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACpD,MAAM,MAAM,GAAG,GAAG,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,GAAG,MAAM,EAAE,CAAC;QACxD,IAAI,KAAK,YAAY,0BAAe;YAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,mBAAmB,MAAM,EAAE,CAAC,CAAC;aAC5E,IAAI,KAAK,YAAY,6BAAkB;YAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,gBAAgB,MAAM,EAAE,CAAC,CAAC;aACjF,IAAI,KAAK,YAAY,2BAAgB;YAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,cAAc,MAAM,EAAE,CAAC,CAAC;aAC7E,IAAI,KAAK,YAAY,+BAAoB;YAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,iBAAiB,MAAM,EAAE,CAAC,CAAC;aACpF,IAAI,KAAK,YAAY,4BAAiB;YAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,cAAc,MAAM,EAAE,CAAC,CAAC;aAC9E,IAAI,KAAK,YAAY,iCAAsB;YAC5C,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,oBAAoB,MAAM,EAAE,CAAC,CAAC;;YAC5C,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,gBAAgB,MAAM,EAAE,CAAC,CAAC;IAClD,CAAC;IAEO,cAAc,CAAC,KAAe;QAClC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,UAAU,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAClF,OAAO,SAAS,KAAK,CAAC,IAAI,SAAS,KAAK,CAAC,IAAI,YAAY,KAAK,CAAC,OAAO,IAAI,MAAM,KAAK,KAAK,CAAC,OAAO,GAAG,KAAK,EAAE,CAAC;IACjH,CAAC;CACJ;AArCD,4CAqCC","sourcesContent":["import { Logger } from '../logging/Logger';\nimport { toError } from '../lib/errorUtils';\nimport {\n ApiBadRequestError,\n ApiConnectionError,\n ApiEndUserError,\n ApiError,\n ApiForbiddenError,\n ApiImplementationError,\n ApiNotFoundError,\n ApiRequestTimeoutError,\n ApiUnauthorizedError,\n} from './ApiError';\n\n/**\n * The ONE rule every outbound protocol boundary (HTTP server, MCP) applies to a thrown value before\n * rendering it, plus the one operator log line per failure. Adapters differ only in the wire shape\n * they render; normalization and log levels live here so they cannot drift apart.\n */\nexport class ApiErrorBoundary {\n constructor(private readonly log: Logger) {}\n\n /**\n * An `ApiError` (except a caller-local `ApiConnectionError`) stays as-is. Anything else, including\n * a raw `Error`, a non-Error throw, or a downstream connection failure, is a bug or broken state of\n * THIS service and becomes `ApiImplementationError`. Its message stays operator-only: encoders\n * publish only `ApiEndUserError` text.\n */\n // webpieces-disable no-any-unknown -- thrown values are unknown until normalized at this boundary\n normalize(thrown: unknown): ApiError {\n if (thrown instanceof ApiError && !(thrown instanceof ApiConnectionError)) return thrown;\n const error = toError(thrown);\n return new ApiImplementationError(error.message, error);\n }\n\n /**\n * Logs one operator line at the per-kind level: expected caller outcomes at info, everything else\n * at error. `correlation` is appended verbatim (for example `requestId=... tool=...`).\n */\n logOperatorDetail(error: ApiError, correlation?: string): void {\n const suffix = correlation ? ` ${correlation}` : '';\n const detail = `${this.operatorDetail(error)}${suffix}`;\n if (error instanceof ApiEndUserError) this.log.info(`End User Error: ${detail}`);\n else if (error instanceof ApiBadRequestError) this.log.info(`Bad Request: ${detail}`);\n else if (error instanceof ApiNotFoundError) this.log.info(`Not Found: ${detail}`);\n else if (error instanceof ApiUnauthorizedError) this.log.info(`Unauthorized: ${detail}`);\n else if (error instanceof ApiForbiddenError) this.log.info(`Forbidden: ${detail}`);\n else if (error instanceof ApiRequestTimeoutError)\n this.log.error(`Request Timeout: ${detail}`);\n else this.log.error(`API failure: ${detail}`);\n }\n\n private operatorDetail(error: ApiError): string {\n const cause = error.cause instanceof Error ? ` cause=${error.cause.message}` : '';\n return `[name=${error.name} kind=${error.kind} subType=${error.subType ?? 'none'}] ${error.message}${cause}`;\n }\n}\n"]}
|
package/src/errors/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { ApiError, ApiNotFoundError, ApiDependencyError, ApiUnavailableError, ApiDependencyTimeoutError, ApiImplementationError, ApiRateLimitedError, ApiForbiddenError, ApiRequestTimeoutError, ApiConnectionError, ApiEndpointNotFoundError, ApiEndUserError, ApiBadRequestError, ApiUnauthorizedError, ApiDependencyBackoffError, } from './ApiError';
|
|
2
2
|
export type { ApiErrorKind } from './ApiError';
|
|
3
3
|
export { ApiErrorCodec, ApiErrorPayload } from './ApiErrorCodec';
|
|
4
|
+
export { ApiErrorBoundary } from './ApiErrorBoundary';
|
|
4
5
|
export { ENTITY_NOT_FOUND, WRONG_LOGIN_TYPE, WRONG_LOGIN, NOT_APPROVED, EMAIL_NOT_CONFIRMED, WRONG_DOMAIN, WRONG_COMPANY, NO_REG_CODE, } from './AuthErrorSubtypes';
|
package/src/errors/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.NO_REG_CODE = exports.WRONG_COMPANY = exports.WRONG_DOMAIN = exports.EMAIL_NOT_CONFIRMED = exports.NOT_APPROVED = exports.WRONG_LOGIN = exports.WRONG_LOGIN_TYPE = exports.ENTITY_NOT_FOUND = exports.ApiErrorPayload = exports.ApiErrorCodec = exports.ApiDependencyBackoffError = exports.ApiUnauthorizedError = exports.ApiBadRequestError = exports.ApiEndUserError = exports.ApiEndpointNotFoundError = exports.ApiConnectionError = exports.ApiRequestTimeoutError = exports.ApiForbiddenError = exports.ApiRateLimitedError = exports.ApiImplementationError = exports.ApiDependencyTimeoutError = exports.ApiUnavailableError = exports.ApiDependencyError = exports.ApiNotFoundError = exports.ApiError = void 0;
|
|
3
|
+
exports.NO_REG_CODE = exports.WRONG_COMPANY = exports.WRONG_DOMAIN = exports.EMAIL_NOT_CONFIRMED = exports.NOT_APPROVED = exports.WRONG_LOGIN = exports.WRONG_LOGIN_TYPE = exports.ENTITY_NOT_FOUND = exports.ApiErrorBoundary = exports.ApiErrorPayload = exports.ApiErrorCodec = exports.ApiDependencyBackoffError = exports.ApiUnauthorizedError = exports.ApiBadRequestError = exports.ApiEndUserError = exports.ApiEndpointNotFoundError = exports.ApiConnectionError = exports.ApiRequestTimeoutError = exports.ApiForbiddenError = exports.ApiRateLimitedError = exports.ApiImplementationError = exports.ApiDependencyTimeoutError = exports.ApiUnavailableError = exports.ApiDependencyError = exports.ApiNotFoundError = exports.ApiError = void 0;
|
|
4
4
|
var ApiError_1 = require("./ApiError");
|
|
5
5
|
Object.defineProperty(exports, "ApiError", { enumerable: true, get: function () { return ApiError_1.ApiError; } });
|
|
6
6
|
Object.defineProperty(exports, "ApiNotFoundError", { enumerable: true, get: function () { return ApiError_1.ApiNotFoundError; } });
|
|
@@ -20,6 +20,8 @@ Object.defineProperty(exports, "ApiDependencyBackoffError", { enumerable: true,
|
|
|
20
20
|
var ApiErrorCodec_1 = require("./ApiErrorCodec");
|
|
21
21
|
Object.defineProperty(exports, "ApiErrorCodec", { enumerable: true, get: function () { return ApiErrorCodec_1.ApiErrorCodec; } });
|
|
22
22
|
Object.defineProperty(exports, "ApiErrorPayload", { enumerable: true, get: function () { return ApiErrorCodec_1.ApiErrorPayload; } });
|
|
23
|
+
var ApiErrorBoundary_1 = require("./ApiErrorBoundary");
|
|
24
|
+
Object.defineProperty(exports, "ApiErrorBoundary", { enumerable: true, get: function () { return ApiErrorBoundary_1.ApiErrorBoundary; } });
|
|
23
25
|
var AuthErrorSubtypes_1 = require("./AuthErrorSubtypes");
|
|
24
26
|
Object.defineProperty(exports, "ENTITY_NOT_FOUND", { enumerable: true, get: function () { return AuthErrorSubtypes_1.ENTITY_NOT_FOUND; } });
|
|
25
27
|
Object.defineProperty(exports, "WRONG_LOGIN_TYPE", { enumerable: true, get: function () { return AuthErrorSubtypes_1.WRONG_LOGIN_TYPE; } });
|
package/src/errors/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/index.ts"],"names":[],"mappings":";;;AAAA,uCAgBoB;AAfhB,oGAAA,QAAQ,OAAA;AACR,4GAAA,gBAAgB,OAAA;AAChB,8GAAA,kBAAkB,OAAA;AAClB,+GAAA,mBAAmB,OAAA;AACnB,qHAAA,yBAAyB,OAAA;AACzB,kHAAA,sBAAsB,OAAA;AACtB,+GAAA,mBAAmB,OAAA;AACnB,6GAAA,iBAAiB,OAAA;AACjB,kHAAA,sBAAsB,OAAA;AACtB,8GAAA,kBAAkB,OAAA;AAClB,oHAAA,wBAAwB,OAAA;AACxB,2GAAA,eAAe,OAAA;AACf,8GAAA,kBAAkB,OAAA;AAClB,gHAAA,oBAAoB,OAAA;AACpB,qHAAA,yBAAyB,OAAA;AAG7B,iDAAiE;AAAxD,8GAAA,aAAa,OAAA;AAAE,gHAAA,eAAe,OAAA;AACvC,yDAS6B;AARzB,qHAAA,gBAAgB,OAAA;AAChB,qHAAA,gBAAgB,OAAA;AAChB,gHAAA,WAAW,OAAA;AACX,iHAAA,YAAY,OAAA;AACZ,wHAAA,mBAAmB,OAAA;AACnB,iHAAA,YAAY,OAAA;AACZ,kHAAA,aAAa,OAAA;AACb,gHAAA,WAAW,OAAA","sourcesContent":["export {\n ApiError,\n ApiNotFoundError,\n ApiDependencyError,\n ApiUnavailableError,\n ApiDependencyTimeoutError,\n ApiImplementationError,\n ApiRateLimitedError,\n ApiForbiddenError,\n ApiRequestTimeoutError,\n ApiConnectionError,\n ApiEndpointNotFoundError,\n ApiEndUserError,\n ApiBadRequestError,\n ApiUnauthorizedError,\n ApiDependencyBackoffError,\n} from './ApiError';\nexport type { ApiErrorKind } from './ApiError';\nexport { ApiErrorCodec, ApiErrorPayload } from './ApiErrorCodec';\nexport {\n ENTITY_NOT_FOUND,\n WRONG_LOGIN_TYPE,\n WRONG_LOGIN,\n NOT_APPROVED,\n EMAIL_NOT_CONFIRMED,\n WRONG_DOMAIN,\n WRONG_COMPANY,\n NO_REG_CODE,\n} from './AuthErrorSubtypes';\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/errors/index.ts"],"names":[],"mappings":";;;AAAA,uCAgBoB;AAfhB,oGAAA,QAAQ,OAAA;AACR,4GAAA,gBAAgB,OAAA;AAChB,8GAAA,kBAAkB,OAAA;AAClB,+GAAA,mBAAmB,OAAA;AACnB,qHAAA,yBAAyB,OAAA;AACzB,kHAAA,sBAAsB,OAAA;AACtB,+GAAA,mBAAmB,OAAA;AACnB,6GAAA,iBAAiB,OAAA;AACjB,kHAAA,sBAAsB,OAAA;AACtB,8GAAA,kBAAkB,OAAA;AAClB,oHAAA,wBAAwB,OAAA;AACxB,2GAAA,eAAe,OAAA;AACf,8GAAA,kBAAkB,OAAA;AAClB,gHAAA,oBAAoB,OAAA;AACpB,qHAAA,yBAAyB,OAAA;AAG7B,iDAAiE;AAAxD,8GAAA,aAAa,OAAA;AAAE,gHAAA,eAAe,OAAA;AACvC,uDAAsD;AAA7C,oHAAA,gBAAgB,OAAA;AACzB,yDAS6B;AARzB,qHAAA,gBAAgB,OAAA;AAChB,qHAAA,gBAAgB,OAAA;AAChB,gHAAA,WAAW,OAAA;AACX,iHAAA,YAAY,OAAA;AACZ,wHAAA,mBAAmB,OAAA;AACnB,iHAAA,YAAY,OAAA;AACZ,kHAAA,aAAa,OAAA;AACb,gHAAA,WAAW,OAAA","sourcesContent":["export {\n ApiError,\n ApiNotFoundError,\n ApiDependencyError,\n ApiUnavailableError,\n ApiDependencyTimeoutError,\n ApiImplementationError,\n ApiRateLimitedError,\n ApiForbiddenError,\n ApiRequestTimeoutError,\n ApiConnectionError,\n ApiEndpointNotFoundError,\n ApiEndUserError,\n ApiBadRequestError,\n ApiUnauthorizedError,\n ApiDependencyBackoffError,\n} from './ApiError';\nexport type { ApiErrorKind } from './ApiError';\nexport { ApiErrorCodec, ApiErrorPayload } from './ApiErrorCodec';\nexport { ApiErrorBoundary } from './ApiErrorBoundary';\nexport {\n ENTITY_NOT_FOUND,\n WRONG_LOGIN_TYPE,\n WRONG_LOGIN,\n NOT_APPROVED,\n EMAIL_NOT_CONFIRMED,\n WRONG_DOMAIN,\n WRONG_COMPANY,\n NO_REG_CODE,\n} from './AuthErrorSubtypes';\n"]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { MaskSpec } from './LogFieldMask';
|
|
2
2
|
import { AuthMeta } from './auth-mode';
|
|
3
3
|
import { EndpointResponseType, HttpParameterBinding } from './HttpContract';
|
|
4
|
+
import { StreamingEndpointMetadata } from './StreamingContract';
|
|
4
5
|
/**
|
|
5
6
|
* Route metadata stored per-method at runtime.
|
|
6
7
|
* Used internally by http-routing and http-client as the runtime representation
|
|
@@ -17,6 +18,8 @@ export declare class RouteMetadata {
|
|
|
17
18
|
readonly bodyParameterIndex?: number | undefined;
|
|
18
19
|
/** Whether clients receive only the body or the complete status/header/body response. */
|
|
19
20
|
readonly responseType: EndpointResponseType;
|
|
21
|
+
/** Present only for `(ResponseStream) => Promise<RequestStream>` contracts. */
|
|
22
|
+
readonly streaming?: StreamingEndpointMetadata | undefined;
|
|
20
23
|
httpMethod: string;
|
|
21
24
|
path: string;
|
|
22
25
|
methodName: string;
|
|
@@ -49,5 +52,7 @@ export declare class RouteMetadata {
|
|
|
49
52
|
/** The sole unannotated POST parameter, or undefined for a bodyless route. */
|
|
50
53
|
bodyParameterIndex?: number | undefined,
|
|
51
54
|
/** Whether clients receive only the body or the complete status/header/body response. */
|
|
52
|
-
responseType?: EndpointResponseType
|
|
55
|
+
responseType?: EndpointResponseType,
|
|
56
|
+
/** Present only for `(ResponseStream) => Promise<RequestStream>` contracts. */
|
|
57
|
+
streaming?: StreamingEndpointMetadata | undefined);
|
|
53
58
|
}
|
|
@@ -14,6 +14,7 @@ class RouteMetadata {
|
|
|
14
14
|
parameterBindings;
|
|
15
15
|
bodyParameterIndex;
|
|
16
16
|
responseType;
|
|
17
|
+
streaming;
|
|
17
18
|
httpMethod;
|
|
18
19
|
path;
|
|
19
20
|
methodName;
|
|
@@ -46,10 +47,13 @@ class RouteMetadata {
|
|
|
46
47
|
/** The sole unannotated POST parameter, or undefined for a bodyless route. */
|
|
47
48
|
bodyParameterIndex,
|
|
48
49
|
/** Whether clients receive only the body or the complete status/header/body response. */
|
|
49
|
-
responseType = 'body'
|
|
50
|
+
responseType = 'body',
|
|
51
|
+
/** Present only for `(ResponseStream) => Promise<RequestStream>` contracts. */
|
|
52
|
+
streaming) {
|
|
50
53
|
this.parameterBindings = parameterBindings;
|
|
51
54
|
this.bodyParameterIndex = bodyParameterIndex;
|
|
52
55
|
this.responseType = responseType;
|
|
56
|
+
this.streaming = streaming;
|
|
53
57
|
this.httpMethod = httpMethod;
|
|
54
58
|
this.path = path;
|
|
55
59
|
this.methodName = methodName;
|
|
@@ -62,6 +66,7 @@ class RouteMetadata {
|
|
|
62
66
|
this.parameterBindings = parameterBindings;
|
|
63
67
|
this.bodyParameterIndex = bodyParameterIndex;
|
|
64
68
|
this.responseType = responseType;
|
|
69
|
+
this.streaming = streaming;
|
|
65
70
|
}
|
|
66
71
|
}
|
|
67
72
|
exports.RouteMetadata = RouteMetadata;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RouteMetadata.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/RouteMetadata.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"RouteMetadata.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/RouteMetadata.ts"],"names":[],"mappings":";;;AAKA;;;;;;;;GAQG;AACH,MAAa,aAAa;IAuCT;IAEA;IAEA;IAEA;IA5Cb,UAAU,CAAS;IACnB,IAAI,CAAS;IACb,UAAU,CAAS;IACnB,mBAAmB,CAAU;IAC7B,QAAQ,CAAY;IACpB,wFAAwF;IACxF,OAAO,CAAU;IACjB;;;;OAIG;IACM,QAAQ,CAAU;IAC3B;;;;OAIG;IACM,IAAI,CAAY;IACzB;;;;;OAKG;IACM,OAAO,CAAU;IAE1B,YACI,UAAkB,EAClB,IAAY,EACZ,UAAkB,EAClB,mBAA4B,EAC5B,QAAmB,EACnB,OAAgB,EAChB,WAAoB,KAAK,EACzB,IAAe,EACf,UAAmB,KAAK;IACxB,8DAA8D;IACrD,oBAAqD,EAAE;IAChE,8EAA8E;IACrE,kBAA2B;IACpC,yFAAyF;IAChF,eAAqC,MAAM;IACpD,+EAA+E;IACtE,SAAqC;QANrC,sBAAiB,GAAjB,iBAAiB,CAAsC;QAEvD,uBAAkB,GAAlB,kBAAkB,CAAS;QAE3B,iBAAY,GAAZ,YAAY,CAA+B;QAE3C,cAAS,GAAT,SAAS,CAA4B;QAE9C,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,mBAAmB,GAAG,mBAAmB,CAAC;QAC/C,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,iBAAiB,GAAG,iBAAiB,CAAC;QAC3C,IAAI,CAAC,kBAAkB,GAAG,kBAAkB,CAAC;QAC7C,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC/B,CAAC;CACJ;AA7DD,sCA6DC","sourcesContent":["import { MaskSpec } from './LogFieldMask';\nimport { AuthMeta } from './auth-mode';\nimport { EndpointResponseType, HttpParameterBinding } from './HttpContract';\nimport { StreamingEndpointMetadata } from './StreamingContract';\n\n/**\n * Route metadata stored per-method at runtime.\n * Used internally by http-routing and http-client as the runtime representation\n * of a route. Constructed from @ApiPath + @Endpoint metadata by ProxyClient\n * and ApiRoutingFactory.\n *\n * Lives in its own file (one class per file) purely for file size, exactly as `api-kind.ts` and\n * `external-caller.ts` were split off `decorators.ts` before it. Nothing about its role changed.\n */\nexport class RouteMetadata {\n httpMethod: string;\n path: string;\n methodName: string;\n controllerClassName?: string;\n authMeta?: AuthMeta;\n /** The API contract class name (e.g. 'SaveApi') — distinct from the controller name. */\n apiName?: string;\n /**\n * True when @Endpoint(..., { formPost: true }): the body is application/x-www-form-urlencoded\n * (flat key→value), not JSON. Rides the route metadata so the per-route body parse can branch\n * without knowing the apiClass/methodName. Default false = JSON.\n */\n readonly formPost: boolean;\n /**\n * The @MaskLog field-mask spec for this route, or undefined when the method declared none. Read\n * ONCE here at route-build time and handed to {@link LogApiCallImpl} via ApiMethodInfo, so the per-call\n * log path pays for masking only on routes that opted in (the rest stay on plain JSON.stringify).\n */\n readonly mask?: MaskSpec;\n /**\n * True when @Endpoint(..., { rawBody: true }): the transport must retain the verbatim bytes +\n * absolute url for the `@WpAuthWebhook` hook to verify a vendor signature over. Rides the route\n * metadata for the same reason {@link formPost} does — the transport adapter decides how to read\n * the body from the ROUTE, without knowing the apiClass/methodName.\n */\n readonly rawBody: boolean;\n\n constructor(\n httpMethod: string,\n path: string,\n methodName: string,\n controllerClassName?: string,\n authMeta?: AuthMeta,\n apiName?: string,\n formPost: boolean = false,\n mask?: MaskSpec,\n rawBody: boolean = false,\n /** Explicit path/query mappings, in API declaration order. */\n readonly parameterBindings: readonly HttpParameterBinding[] = [],\n /** The sole unannotated POST parameter, or undefined for a bodyless route. */\n readonly bodyParameterIndex?: number,\n /** Whether clients receive only the body or the complete status/header/body response. */\n readonly responseType: EndpointResponseType = 'body',\n /** Present only for `(ResponseStream) => Promise<RequestStream>` contracts. */\n readonly streaming?: StreamingEndpointMetadata,\n ) {\n this.httpMethod = httpMethod;\n this.path = path;\n this.methodName = methodName;\n this.controllerClassName = controllerClassName;\n this.authMeta = authMeta;\n this.apiName = apiName;\n this.formPost = formPost;\n this.mask = mask;\n this.rawBody = rawBody;\n this.parameterBindings = parameterBindings;\n this.bodyParameterIndex = bodyParameterIndex;\n this.responseType = responseType;\n this.streaming = streaming;\n }\n}\n"]}
|
|
@@ -6,6 +6,8 @@ import { RouteMetadata } from './RouteMetadata';
|
|
|
6
6
|
*/
|
|
7
7
|
export declare class RouteMetadataFactory {
|
|
8
8
|
static create(apiClass: Function, methodName: string, controllerClassName?: string): RouteMetadata;
|
|
9
|
+
private static validateEndpointOptions;
|
|
10
|
+
private static validateStreamingParameters;
|
|
9
11
|
private static joinPath;
|
|
10
12
|
private static httpMethod;
|
|
11
13
|
private static responseType;
|
|
@@ -5,6 +5,7 @@ const decorators_1 = require("./decorators");
|
|
|
5
5
|
const http_parameter_decorators_1 = require("./http-parameter-decorators");
|
|
6
6
|
const HttpContract_1 = require("./HttpContract");
|
|
7
7
|
const RouteMetadata_1 = require("./RouteMetadata");
|
|
8
|
+
const StreamingContract_1 = require("./StreamingContract");
|
|
8
9
|
/**
|
|
9
10
|
* Build the one runtime route model consumed by incoming routing, in-process clients, and both
|
|
10
11
|
* generated HTTP clients. Keeping this join/validation here prevents four subtly different
|
|
@@ -20,20 +21,38 @@ class RouteMetadataFactory {
|
|
|
20
21
|
}
|
|
21
22
|
const fullPath = this.joinPath((0, decorators_1.getApiPath)(apiClass) ?? '', endpointPath);
|
|
22
23
|
const options = (0, decorators_1.getEndpointOptions)(apiClass, methodName);
|
|
24
|
+
const streaming = (0, StreamingContract_1.getStreamingEndpoint)(apiClass, methodName);
|
|
23
25
|
const httpMethod = this.httpMethod(options.httpMethod, apiClass, methodName);
|
|
24
|
-
|
|
25
|
-
throw new Error(`${apiClass.name}.${methodName} is GET and cannot declare formPost:true because GET has no request body.`);
|
|
26
|
-
}
|
|
27
|
-
if (httpMethod === 'GET' && options.rawBody === true) {
|
|
28
|
-
throw new Error(`${apiClass.name}.${methodName} is GET and cannot declare rawBody:true because GET has no request body.`);
|
|
29
|
-
}
|
|
26
|
+
this.validateEndpointOptions(apiClass, methodName, httpMethod, options.formPost, options.rawBody);
|
|
30
27
|
const parameterTypes = this.parameterTypes(apiClass, methodName);
|
|
31
28
|
const declarations = (0, http_parameter_decorators_1.getHttpParameterDeclarations)(apiClass, methodName);
|
|
32
|
-
const bodyParameterIndex =
|
|
29
|
+
const bodyParameterIndex = streaming
|
|
30
|
+
? this.validateStreamingParameters(apiClass, methodName, httpMethod, parameterTypes, declarations)
|
|
31
|
+
: this.validateParameters(apiClass, methodName, fullPath, httpMethod, parameterTypes, declarations);
|
|
33
32
|
const bindings = declarations
|
|
34
33
|
.map((declaration) => new HttpContract_1.HttpParameterBinding(declaration.index, declaration.source, declaration.wireName, this.valueType(parameterTypes[declaration.index])))
|
|
35
34
|
.sort((a, b) => a.index - b.index);
|
|
36
|
-
return new RouteMetadata_1.RouteMetadata(httpMethod, fullPath, methodName, controllerClassName, (0, decorators_1.getAuthMeta)(apiClass, methodName), apiClass.name || 'UnknownApi', options.formPost === true, (0, decorators_1.getMaskSpec)(apiClass, methodName), options.rawBody === true, bindings, bodyParameterIndex, this.responseType(options.responseType, apiClass, methodName));
|
|
35
|
+
return new RouteMetadata_1.RouteMetadata(httpMethod, fullPath, methodName, controllerClassName, (0, decorators_1.getAuthMeta)(apiClass, methodName), apiClass.name || 'UnknownApi', options.formPost === true, (0, decorators_1.getMaskSpec)(apiClass, methodName), options.rawBody === true, bindings, bodyParameterIndex, this.responseType(options.responseType, apiClass, methodName), streaming);
|
|
36
|
+
}
|
|
37
|
+
// webpieces-disable no-function-outside-class -- private pure helper keeps the existing static metadata factory below the method-size limit
|
|
38
|
+
static validateEndpointOptions(apiClass, methodName, httpMethod, formPost, rawBody) {
|
|
39
|
+
if (httpMethod === 'GET' && formPost === true) {
|
|
40
|
+
throw new Error(`${apiClass.name}.${methodName} is GET and cannot declare formPost:true because GET has no request body.`);
|
|
41
|
+
}
|
|
42
|
+
if (httpMethod === 'GET' && rawBody === true) {
|
|
43
|
+
throw new Error(`${apiClass.name}.${methodName} is GET and cannot declare rawBody:true because GET has no request body.`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// webpieces-disable no-function-outside-class -- private pure validator alongside existing contract validators
|
|
47
|
+
static validateStreamingParameters(apiClass, methodName, httpMethod, parameterTypes, declarations) {
|
|
48
|
+
const label = `${apiClass.name || 'Unknown'}.${methodName}`;
|
|
49
|
+
if (httpMethod !== 'POST')
|
|
50
|
+
throw new Error(`${label} is streaming and must use POST.`);
|
|
51
|
+
if (declarations.length > 0)
|
|
52
|
+
throw new Error(`${label} is streaming and cannot declare path/query parameters.`);
|
|
53
|
+
if (parameterTypes.length !== 1)
|
|
54
|
+
throw new Error(`${label} must take exactly one ResponseStream parameter.`);
|
|
55
|
+
return undefined;
|
|
37
56
|
}
|
|
38
57
|
// webpieces-disable no-function-outside-class -- private pure helper for the static metadata factory
|
|
39
58
|
static joinPath(basePath, endpointPath) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RouteMetadataFactory.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/RouteMetadataFactory.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n getApiPath,\n getAuthMeta,\n getEndpointOptions,\n getEndpoints,\n getMaskSpec,\n} from './decorators';\nimport { getHttpParameterDeclarations } from './http-parameter-decorators';\nimport {\n ContractHttpMethod,\n EndpointResponseType,\n HttpParameterBinding,\n HttpParameterDeclaration,\n HttpParameterValueType,\n} from './HttpContract';\nimport { RouteMetadata } from './RouteMetadata';\n\n/**\n * Build the one runtime route model consumed by incoming routing, in-process clients, and both\n * generated HTTP clients. Keeping this join/validation here prevents four subtly different\n * interpretations of the same decorators.\n */\nexport class RouteMetadataFactory {\n // webpieces-disable no-function-outside-class -- deterministic decorator metadata factory shared by DI-free browser and server runtimes\n static create(\n apiClass: Function,\n methodName: string,\n controllerClassName?: string,\n ): RouteMetadata {\n const endpoints = getEndpoints(apiClass) ?? {};\n const endpointPath = endpoints[methodName];\n if (endpointPath === undefined) {\n throw new Error(\n `No @Endpoint metadata for ${apiClass.name || 'Unknown'}.${methodName}.`,\n );\n }\n const fullPath = this.joinPath(getApiPath(apiClass) ?? '', endpointPath);\n const options = getEndpointOptions(apiClass, methodName);\n const httpMethod = this.httpMethod(options.httpMethod, apiClass, methodName);\n if (httpMethod === 'GET' && options.formPost === true) {\n throw new Error(\n `${apiClass.name}.${methodName} is GET and cannot declare formPost:true because GET has no request body.`,\n );\n }\n if (httpMethod === 'GET' && options.rawBody === true) {\n throw new Error(\n `${apiClass.name}.${methodName} is GET and cannot declare rawBody:true because GET has no request body.`,\n );\n }\n\n const parameterTypes = this.parameterTypes(apiClass, methodName);\n const declarations = getHttpParameterDeclarations(apiClass, methodName);\n const bodyParameterIndex = this.validateParameters(\n apiClass,\n methodName,\n fullPath,\n httpMethod,\n parameterTypes,\n declarations,\n );\n const bindings = declarations\n .map(\n (declaration: HttpParameterDeclaration) =>\n new HttpParameterBinding(\n declaration.index,\n declaration.source,\n declaration.wireName,\n this.valueType(parameterTypes[declaration.index]),\n ),\n )\n .sort((a: HttpParameterBinding, b: HttpParameterBinding) => a.index - b.index);\n\n return new RouteMetadata(\n httpMethod,\n fullPath,\n methodName,\n controllerClassName,\n getAuthMeta(apiClass, methodName),\n apiClass.name || 'UnknownApi',\n options.formPost === true,\n getMaskSpec(apiClass, methodName),\n options.rawBody === true,\n bindings,\n bodyParameterIndex,\n this.responseType(options.responseType, apiClass, methodName),\n );\n }\n\n // webpieces-disable no-function-outside-class -- private pure helper for the static metadata factory\n private static joinPath(basePath: string, endpointPath: string): string {\n const joined = `${basePath}/${endpointPath}`.replace(/\\/{2,}/g, '/');\n return joined.startsWith('/') ? joined : `/${joined}`;\n }\n\n // webpieces-disable no-function-outside-class -- private pure helper for the static metadata factory\n private static httpMethod(\n value: ContractHttpMethod | undefined,\n apiClass: Function,\n methodName: string,\n ): ContractHttpMethod {\n const method = value ?? 'POST';\n if (method === 'GET' || method === 'POST') return method;\n throw new Error(\n `${apiClass.name}.${methodName} declares unsupported httpMethod '${String(method)}'; use GET or POST.`,\n );\n }\n\n // webpieces-disable no-function-outside-class -- private pure helper for the static metadata factory\n private static responseType(\n value: EndpointResponseType | undefined,\n apiClass: Function,\n methodName: string,\n ): EndpointResponseType {\n const responseType = value ?? 'body';\n if (responseType === 'body' || responseType === 'full') return responseType;\n throw new Error(\n `${apiClass.name}.${methodName} declares unsupported responseType '${String(responseType)}'; use body or full.`,\n );\n }\n\n // webpieces-disable no-function-outside-class -- Reflect metadata lookup has no injectable dependency\n private static parameterTypes(apiClass: Function, methodName: string): Function[] {\n return Reflect.getMetadata('design:paramtypes', apiClass.prototype, methodName) ?? [];\n }\n\n // webpieces-disable no-function-outside-class -- private deterministic contract validation helper\n private static validateParameters(\n apiClass: Function,\n methodName: string,\n fullPath: string,\n httpMethod: ContractHttpMethod,\n parameterTypes: readonly Function[],\n declarations: readonly HttpParameterDeclaration[],\n ): number | undefined {\n const label = `${apiClass.name || 'Unknown'}.${methodName}`;\n const placeholders = [...fullPath.matchAll(/\\{([^{}]+)\\}/g)].map(\n (match: RegExpMatchArray) => match[1],\n );\n const pathNames = declarations\n .filter((declaration: HttpParameterDeclaration) => declaration.source === 'path')\n .map((declaration: HttpParameterDeclaration) => declaration.wireName);\n for (const placeholder of new Set(placeholders)) {\n if (!pathNames.includes(placeholder)) {\n throw new Error(\n `${label} path '${fullPath}' contains {${placeholder}} but no @PathParam('${placeholder}') parameter.`,\n );\n }\n }\n for (const pathName of pathNames) {\n if (!placeholders.includes(pathName)) {\n throw new Error(\n `${label} declares @PathParam('${pathName}') but '${fullPath}' has no {${pathName}} placeholder.`,\n );\n }\n }\n\n const seenWire = new Set<string>();\n const mappedIndexes = new Set<number>();\n for (const declaration of declarations) {\n const wireKey = `${declaration.source}:${declaration.wireName}`;\n if (seenWire.has(wireKey)) {\n throw new Error(`${label} maps '${wireKey}' more than once.`);\n }\n seenWire.add(wireKey);\n mappedIndexes.add(declaration.index);\n }\n\n const declaredCount = Math.max(\n parameterTypes.length,\n declarations.reduce(\n (highest: number, declaration: HttpParameterDeclaration) =>\n Math.max(highest, declaration.index + 1),\n 0,\n ),\n );\n const unbound: number[] = [];\n for (let index = 0; index < declaredCount; index++) {\n if (!mappedIndexes.has(index)) unbound.push(index);\n }\n if (httpMethod === 'GET' && unbound.length > 0) {\n throw new Error(\n `${label} is GET, so every parameter must use @PathParam(...) or @QueryParam(...); argument ${unbound[0]} is unmapped.`,\n );\n }\n if (unbound.length > 1) {\n throw new Error(\n `${label} has ${unbound.length} unmapped parameters. POST permits exactly one unannotated request body; map every other parameter explicitly.`,\n );\n }\n return unbound[0];\n }\n\n // webpieces-disable no-function-outside-class -- private conversion of reflected constructors to wire types\n private static valueType(type: Function | undefined): HttpParameterValueType {\n if (type === String) return 'string';\n if (type === Number) return 'number';\n if (type === Boolean) return 'boolean';\n if (type === Array) return 'array';\n return 'unknown';\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"RouteMetadataFactory.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/RouteMetadataFactory.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n getApiPath,\n getAuthMeta,\n getEndpointOptions,\n getEndpoints,\n getMaskSpec,\n} from './decorators';\nimport { getHttpParameterDeclarations } from './http-parameter-decorators';\nimport {\n ContractHttpMethod,\n EndpointResponseType,\n HttpParameterBinding,\n HttpParameterDeclaration,\n HttpParameterValueType,\n} from './HttpContract';\nimport { RouteMetadata } from './RouteMetadata';\nimport { getStreamingEndpoint } from './StreamingContract';\n\n/**\n * Build the one runtime route model consumed by incoming routing, in-process clients, and both\n * generated HTTP clients. Keeping this join/validation here prevents four subtly different\n * interpretations of the same decorators.\n */\nexport class RouteMetadataFactory {\n // webpieces-disable no-function-outside-class -- deterministic decorator metadata factory shared by DI-free browser and server runtimes\n static create(\n apiClass: Function,\n methodName: string,\n controllerClassName?: string,\n ): RouteMetadata {\n const endpoints = getEndpoints(apiClass) ?? {};\n const endpointPath = endpoints[methodName];\n if (endpointPath === undefined) {\n throw new Error(\n `No @Endpoint metadata for ${apiClass.name || 'Unknown'}.${methodName}.`,\n );\n }\n const fullPath = this.joinPath(getApiPath(apiClass) ?? '', endpointPath);\n const options = getEndpointOptions(apiClass, methodName);\n const streaming = getStreamingEndpoint(apiClass, methodName);\n const httpMethod = this.httpMethod(options.httpMethod, apiClass, methodName);\n this.validateEndpointOptions(\n apiClass,\n methodName,\n httpMethod,\n options.formPost,\n options.rawBody,\n );\n const parameterTypes = this.parameterTypes(apiClass, methodName);\n const declarations = getHttpParameterDeclarations(apiClass, methodName);\n const bodyParameterIndex = streaming\n ? this.validateStreamingParameters(\n apiClass,\n methodName,\n httpMethod,\n parameterTypes,\n declarations,\n )\n : this.validateParameters(\n apiClass,\n methodName,\n fullPath,\n httpMethod,\n parameterTypes,\n declarations,\n );\n const bindings = declarations\n .map(\n (declaration: HttpParameterDeclaration) =>\n new HttpParameterBinding(\n declaration.index,\n declaration.source,\n declaration.wireName,\n this.valueType(parameterTypes[declaration.index]),\n ),\n )\n .sort((a: HttpParameterBinding, b: HttpParameterBinding) => a.index - b.index);\n\n return new RouteMetadata(\n httpMethod,\n fullPath,\n methodName,\n controllerClassName,\n getAuthMeta(apiClass, methodName),\n apiClass.name || 'UnknownApi',\n options.formPost === true,\n getMaskSpec(apiClass, methodName),\n options.rawBody === true,\n bindings,\n bodyParameterIndex,\n this.responseType(options.responseType, apiClass, methodName),\n streaming,\n );\n }\n\n // webpieces-disable no-function-outside-class -- private pure helper keeps the existing static metadata factory below the method-size limit\n private static validateEndpointOptions(\n apiClass: Function,\n methodName: string,\n httpMethod: ContractHttpMethod,\n formPost?: boolean,\n rawBody?: boolean,\n ): void {\n if (httpMethod === 'GET' && formPost === true) {\n throw new Error(\n `${apiClass.name}.${methodName} is GET and cannot declare formPost:true because GET has no request body.`,\n );\n }\n if (httpMethod === 'GET' && rawBody === true) {\n throw new Error(\n `${apiClass.name}.${methodName} is GET and cannot declare rawBody:true because GET has no request body.`,\n );\n }\n }\n\n // webpieces-disable no-function-outside-class -- private pure validator alongside existing contract validators\n private static validateStreamingParameters(\n apiClass: Function,\n methodName: string,\n httpMethod: ContractHttpMethod,\n parameterTypes: readonly Function[],\n declarations: readonly HttpParameterDeclaration[],\n ): undefined {\n const label = `${apiClass.name || 'Unknown'}.${methodName}`;\n if (httpMethod !== 'POST') throw new Error(`${label} is streaming and must use POST.`);\n if (declarations.length > 0)\n throw new Error(`${label} is streaming and cannot declare path/query parameters.`);\n if (parameterTypes.length !== 1)\n throw new Error(`${label} must take exactly one ResponseStream parameter.`);\n return undefined;\n }\n\n // webpieces-disable no-function-outside-class -- private pure helper for the static metadata factory\n private static joinPath(basePath: string, endpointPath: string): string {\n const joined = `${basePath}/${endpointPath}`.replace(/\\/{2,}/g, '/');\n return joined.startsWith('/') ? joined : `/${joined}`;\n }\n\n // webpieces-disable no-function-outside-class -- private pure helper for the static metadata factory\n private static httpMethod(\n value: ContractHttpMethod | undefined,\n apiClass: Function,\n methodName: string,\n ): ContractHttpMethod {\n const method = value ?? 'POST';\n if (method === 'GET' || method === 'POST') return method;\n throw new Error(\n `${apiClass.name}.${methodName} declares unsupported httpMethod '${String(method)}'; use GET or POST.`,\n );\n }\n\n // webpieces-disable no-function-outside-class -- private pure helper for the static metadata factory\n private static responseType(\n value: EndpointResponseType | undefined,\n apiClass: Function,\n methodName: string,\n ): EndpointResponseType {\n const responseType = value ?? 'body';\n if (responseType === 'body' || responseType === 'full') return responseType;\n throw new Error(\n `${apiClass.name}.${methodName} declares unsupported responseType '${String(responseType)}'; use body or full.`,\n );\n }\n\n // webpieces-disable no-function-outside-class -- Reflect metadata lookup has no injectable dependency\n private static parameterTypes(apiClass: Function, methodName: string): Function[] {\n return Reflect.getMetadata('design:paramtypes', apiClass.prototype, methodName) ?? [];\n }\n\n // webpieces-disable no-function-outside-class -- private deterministic contract validation helper\n private static validateParameters(\n apiClass: Function,\n methodName: string,\n fullPath: string,\n httpMethod: ContractHttpMethod,\n parameterTypes: readonly Function[],\n declarations: readonly HttpParameterDeclaration[],\n ): number | undefined {\n const label = `${apiClass.name || 'Unknown'}.${methodName}`;\n const placeholders = [...fullPath.matchAll(/\\{([^{}]+)\\}/g)].map(\n (match: RegExpMatchArray) => match[1],\n );\n const pathNames = declarations\n .filter((declaration: HttpParameterDeclaration) => declaration.source === 'path')\n .map((declaration: HttpParameterDeclaration) => declaration.wireName);\n for (const placeholder of new Set(placeholders)) {\n if (!pathNames.includes(placeholder)) {\n throw new Error(\n `${label} path '${fullPath}' contains {${placeholder}} but no @PathParam('${placeholder}') parameter.`,\n );\n }\n }\n for (const pathName of pathNames) {\n if (!placeholders.includes(pathName)) {\n throw new Error(\n `${label} declares @PathParam('${pathName}') but '${fullPath}' has no {${pathName}} placeholder.`,\n );\n }\n }\n\n const seenWire = new Set<string>();\n const mappedIndexes = new Set<number>();\n for (const declaration of declarations) {\n const wireKey = `${declaration.source}:${declaration.wireName}`;\n if (seenWire.has(wireKey)) {\n throw new Error(`${label} maps '${wireKey}' more than once.`);\n }\n seenWire.add(wireKey);\n mappedIndexes.add(declaration.index);\n }\n\n const declaredCount = Math.max(\n parameterTypes.length,\n declarations.reduce(\n (highest: number, declaration: HttpParameterDeclaration) =>\n Math.max(highest, declaration.index + 1),\n 0,\n ),\n );\n const unbound: number[] = [];\n for (let index = 0; index < declaredCount; index++) {\n if (!mappedIndexes.has(index)) unbound.push(index);\n }\n if (httpMethod === 'GET' && unbound.length > 0) {\n throw new Error(\n `${label} is GET, so every parameter must use @PathParam(...) or @QueryParam(...); argument ${unbound[0]} is unmapped.`,\n );\n }\n if (unbound.length > 1) {\n throw new Error(\n `${label} has ${unbound.length} unmapped parameters. POST permits exactly one unannotated request body; map every other parameter explicitly.`,\n );\n }\n return unbound[0];\n }\n\n // webpieces-disable no-function-outside-class -- private conversion of reflected constructors to wire types\n private static valueType(type: Function | undefined): HttpParameterValueType {\n if (type === String) return 'string';\n if (type === Number) return 'number';\n if (type === Boolean) return 'boolean';\n if (type === Array) return 'array';\n return 'unknown';\n }\n}\n"]}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { ApiError, ApiConnectionError } from '../errors/ApiError';
|
|
3
|
+
import { ApiErrorPayload } from '../errors/ApiErrorCodec';
|
|
4
|
+
import { ApiJsonSchema, DtoClass, DtoValue } from '../mcp/DtoSchema';
|
|
5
|
+
/** Stable application-level key for relating stream events and failures. */
|
|
6
|
+
export declare class StreamCorrelation {
|
|
7
|
+
readonly key: string | number;
|
|
8
|
+
readonly requestId?: string | undefined;
|
|
9
|
+
constructor(key: string | number, requestId?: string | undefined);
|
|
10
|
+
}
|
|
11
|
+
export interface StreamFailureOptions {
|
|
12
|
+
/** Defaults to true. Wire adapters may promote false to true when continuation is unsafe. */
|
|
13
|
+
terminal?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** Server-to-client half of a typed stream. Every write is an explicit backpressure boundary. */
|
|
16
|
+
export interface ResponseStream<T> {
|
|
17
|
+
event(value: T, correlation?: StreamCorrelation): Promise<void>;
|
|
18
|
+
fail(error: ApiError, correlation?: StreamCorrelation, options?: StreamFailureOptions): Promise<void>;
|
|
19
|
+
complete(): Promise<void>;
|
|
20
|
+
onCancel(handler: (reason?: unknown) => void | Promise<void>): void;
|
|
21
|
+
}
|
|
22
|
+
/** Client-to-server half. Cancellation exists immediately and is independent of writer readiness. */
|
|
23
|
+
export interface RequestStream<T> {
|
|
24
|
+
event(value: T, correlation?: StreamCorrelation): Promise<void>;
|
|
25
|
+
fail(error: ApiError, correlation?: StreamCorrelation, options?: StreamFailureOptions): Promise<void>;
|
|
26
|
+
complete(): Promise<void>;
|
|
27
|
+
cancel(reason?: unknown): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export type StreamEnvelopeKind = 'event' | 'failure' | 'complete';
|
|
30
|
+
/** Transport-neutral record used by NDJSON and SSE adapters. */
|
|
31
|
+
export declare class StreamEnvelope<T = DtoValue> {
|
|
32
|
+
readonly kind: StreamEnvelopeKind;
|
|
33
|
+
readonly value?: T | undefined;
|
|
34
|
+
readonly error?: ApiErrorPayload | undefined;
|
|
35
|
+
readonly correlation?: StreamCorrelation | undefined;
|
|
36
|
+
readonly terminal?: boolean | undefined;
|
|
37
|
+
constructor(kind: StreamEnvelopeKind, value?: T | undefined, error?: ApiErrorPayload | undefined, correlation?: StreamCorrelation | undefined, terminal?: boolean | undefined);
|
|
38
|
+
}
|
|
39
|
+
/** Local failure where no legal in-band envelope could be sent or decoded. */
|
|
40
|
+
export declare class StreamTransportError extends ApiConnectionError {
|
|
41
|
+
readonly correlation?: StreamCorrelation | undefined;
|
|
42
|
+
readonly requestId?: string | undefined;
|
|
43
|
+
constructor(message: string, correlation?: StreamCorrelation | undefined, requestId?: string | undefined, cause?: Error);
|
|
44
|
+
}
|
|
45
|
+
/** Runtime schema and wire-policy metadata for one streaming method. */
|
|
46
|
+
export declare class StreamingEndpointMetadata {
|
|
47
|
+
readonly requestEventClass: DtoClass;
|
|
48
|
+
readonly responseEventClass: DtoClass;
|
|
49
|
+
/** Generic NDJSON/SSE supports non-terminal failures in both directions. */
|
|
50
|
+
readonly supportsNonTerminalFailures: boolean;
|
|
51
|
+
readonly requestSchema: ApiJsonSchema;
|
|
52
|
+
readonly responseSchema: ApiJsonSchema;
|
|
53
|
+
constructor(requestEventClass: DtoClass, responseEventClass: DtoClass,
|
|
54
|
+
/** Generic NDJSON/SSE supports non-terminal failures in both directions. */
|
|
55
|
+
supportsNonTerminalFailures?: boolean);
|
|
56
|
+
}
|
|
57
|
+
/** Marks `(ResponseStream<ResponseEvent>) => Promise<RequestStream<RequestEvent>>`. */
|
|
58
|
+
export declare function WpStream(requestEvent: () => DtoClass, responseEvent: () => DtoClass): MethodDecorator;
|
|
59
|
+
export declare function getStreamingEndpoint(apiClass: Function, methodName: string): StreamingEndpointMetadata | undefined;
|
|
60
|
+
/** Validate a typed event at every adapter boundary, with one consistent diagnostic. */
|
|
61
|
+
export declare class StreamEventValidator {
|
|
62
|
+
private readonly schemas;
|
|
63
|
+
validate(dtoClass: DtoClass, value: unknown, direction: 'request' | 'response'): void;
|
|
64
|
+
}
|
|
65
|
+
/** Awaiting the sink acknowledges the write, so adapters cannot outrun their consumer. */
|
|
66
|
+
export declare class StreamWriter<T> implements RequestStream<T>, ResponseStream<T> {
|
|
67
|
+
private readonly sink;
|
|
68
|
+
private readonly validateEvent?;
|
|
69
|
+
private readonly allowNonTerminalFailures;
|
|
70
|
+
private terminal;
|
|
71
|
+
private cancelled;
|
|
72
|
+
private pending;
|
|
73
|
+
private readonly cancelHandlers;
|
|
74
|
+
constructor(sink: (envelope: StreamEnvelope<T>) => Promise<void>, validateEvent?: ((value: T) => void) | undefined, allowNonTerminalFailures?: boolean);
|
|
75
|
+
event(value: T, correlation?: StreamCorrelation): Promise<void>;
|
|
76
|
+
fail(error: ApiError, correlation?: StreamCorrelation, options?: StreamFailureOptions): Promise<void>;
|
|
77
|
+
complete(): Promise<void>;
|
|
78
|
+
onCancel(handler: (reason?: unknown) => void | Promise<void>): void;
|
|
79
|
+
cancel(reason?: unknown): Promise<void>;
|
|
80
|
+
private requireWritable;
|
|
81
|
+
private enqueue;
|
|
82
|
+
}
|