@webpieces/core-util 0.4.794 → 0.4.796
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 +1 -1
- package/src/errors/ApiError.d.ts +9 -5
- package/src/errors/ApiError.js +9 -4
- package/src/errors/ApiError.js.map +1 -1
- package/src/errors/ApiErrorCodec.js +5 -0
- package/src/errors/ApiErrorCodec.js.map +1 -1
- package/src/errors/ReceivedApiErrorRule.d.ts +5 -3
- package/src/errors/ReceivedApiErrorRule.js +23 -4
- package/src/errors/ReceivedApiErrorRule.js.map +1 -1
- package/src/errors/index.d.ts +1 -1
- package/src/errors/index.js +2 -1
- package/src/errors/index.js.map +1 -1
- package/src/http/ApiCallTimeoutError.d.ts +2 -1
- package/src/http/ApiCallTimeoutError.js +2 -1
- package/src/http/ApiCallTimeoutError.js.map +1 -1
- package/src/http/ApiErrorHttpStatus.js +1 -0
- package/src/http/ApiErrorHttpStatus.js.map +1 -1
- package/src/http/EndpointCompileAssertions.d.ts +1 -0
- package/src/http/EndpointCompileAssertions.js +21 -0
- package/src/http/EndpointCompileAssertions.js.map +1 -0
- package/src/http/HttpContract.d.ts +10 -1
- package/src/http/HttpContract.js +10 -1
- package/src/http/HttpContract.js.map +1 -1
- package/src/http/HttpEndpointOptions.d.ts +14 -3
- package/src/http/HttpEndpointOptions.js +12 -0
- package/src/http/HttpEndpointOptions.js.map +1 -1
- package/src/http/RouteMetadata.d.ts +6 -1
- package/src/http/RouteMetadata.js +5 -1
- package/src/http/RouteMetadata.js.map +1 -1
- package/src/http/RouteMetadataFactory.d.ts +3 -3
- package/src/http/RouteMetadataFactory.js +13 -16
- package/src/http/RouteMetadataFactory.js.map +1 -1
- package/src/http/WebpiecesDefaultErrorTranslator.d.ts +5 -3
- package/src/http/WebpiecesDefaultErrorTranslator.js +17 -4
- package/src/http/WebpiecesDefaultErrorTranslator.js.map +1 -1
- package/src/http/api-kind.js +2 -2
- package/src/http/api-kind.js.map +1 -1
- package/src/http/decorators.d.ts +41 -143
- package/src/http/decorators.js +123 -110
- package/src/http/decorators.js.map +1 -1
- package/src/http/external-caller.d.ts +4 -4
- package/src/http/external-caller.js +12 -5
- package/src/http/external-caller.js.map +1 -1
- package/src/index.d.ts +7 -5
- package/src/index.js +21 -5
- package/src/index.js.map +1 -1
- package/src/mcp/McpCompileAssertions.js +3 -9
- package/src/mcp/McpCompileAssertions.js.map +1 -1
- package/src/mcp/McpMetadata.d.ts +8 -14
- package/src/mcp/McpMetadata.js +19 -5
- package/src/mcp/McpMetadata.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HttpContract.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/HttpContract.ts"],"names":[],"mappings":"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","sourcesContent":["import { ApiBadRequestError } from '../errors/ApiError';\n\n/** HTTP verbs currently supported by generated Webpieces request/response contracts. */\nexport type ContractHttpMethod = 'GET' | 'POST';\n\n/** Whether a generated client returns the response body or the whole transport-neutral response. */\nexport type EndpointResponseType = 'body' | 'full';\n\n/** The wire source for one explicitly mapped scalar/list method parameter. */\nexport type HttpParameterSource = 'path' | 'query';\n\n/** Runtime conversion supported for path/query values after TypeScript design metadata is read. */\nexport type HttpParameterValueType = 'string' | 'number' | 'boolean' | 'array' | 'unknown';\n\n/** Concrete runtime values produced for path/query parameters. */\ntype ConvertedHttpParameter = string | number | boolean | readonly string[];\n\n/** Metadata written by `@PathParam` / `@QueryParam` before route construction knows the value type. */\nexport class HttpParameterDeclaration {\n constructor(\n public readonly index: number,\n public readonly source: HttpParameterSource,\n public readonly wireName: string,\n ) {}\n}\n\n/** Fully resolved parameter metadata carried by every server and client route. */\nexport class HttpParameterBinding extends HttpParameterDeclaration {\n constructor(\n index: number,\n source: HttpParameterSource,\n wireName: string,\n public readonly valueType: HttpParameterValueType,\n ) {\n super(index, source, wireName);\n }\n}\n\n/** Result of mapping one API method call to its HTTP path/query/body. */\nexport class BoundHttpRequest {\n constructor(\n public readonly path: string,\n // webpieces-disable no-any-unknown -- the contract owns the request body type\n public readonly body: unknown,\n ) {}\n}\n\n/**\n * The single path/query/body mapper shared by generated clients and the incoming server adapter.\n * Java Webpieces' `HttpsJsonClientInvokeHandler` is the reference model: explicit wire names,\n * encoded placeholders, omitted null query values, and repeated list keys. This port deliberately\n * fixes Java's list-query overwrite bug and never depends on compiled JavaScript parameter names.\n */\nexport class HttpContractMapper {\n /** API arguments -> encoded relative URL + body. */\n // webpieces-disable no-function-outside-class -- pure cross-runtime mapper used by DI-free browser clients and server adapters\n static toWire(\n pathTemplate: string,\n bindings: readonly HttpParameterBinding[],\n bodyParameterIndex: number | undefined,\n // webpieces-disable no-any-unknown -- arbitrary API method parameters cross this generic boundary\n args: readonly unknown[],\n ): BoundHttpRequest {\n let path = pathTemplate;\n const query: string[] = [];\n\n for (const binding of bindings) {\n const value = args[binding.index];\n if (binding.source === 'path') {\n if (value === undefined || value === null) {\n throw new ApiBadRequestError(\n `Missing path parameter '${binding.wireName}' at argument ${binding.index}.`,\n );\n }\n const token = `{${binding.wireName}}`;\n path = path.split(token).join(encodeURIComponent(String(value)));\n continue;\n }\n\n if (value === undefined || value === null) continue;\n const values = Array.isArray(value) ? value : [value];\n for (const item of values) {\n if (item === undefined || item === null) continue;\n query.push(\n `${encodeURIComponent(binding.wireName)}=${encodeURIComponent(String(item))}`,\n );\n }\n }\n\n if (/\\{[^{}]+\\}/.test(path)) {\n throw new ApiBadRequestError(\n `Not every path placeholder was supplied for '${pathTemplate}'.`,\n );\n }\n const suffix =\n query.length === 0 ? '' : `${path.includes('?') ? '&' : '?'}${query.join('&')}`;\n return new BoundHttpRequest(\n `${path}${suffix}`,\n bodyParameterIndex === undefined ? undefined : args[bodyParameterIndex],\n );\n }\n\n /** Decoded path/query/body -> API arguments in declaration order. */\n // webpieces-disable no-function-outside-class -- pure inverse mapper paired with toWire\n static fromWire(\n bindings: readonly HttpParameterBinding[],\n bodyParameterIndex: number | undefined,\n // webpieces-disable no-any-unknown -- the contract owns the request body type\n body: unknown,\n pathValues: ReadonlyMap<string, string | readonly string[]>,\n queryValues: ReadonlyMap<string, string | readonly string[]>,\n ): unknown[] {\n const highestBinding = bindings.reduce(\n (highest: number, binding: HttpParameterBinding) => Math.max(highest, binding.index),\n -1,\n );\n const argCount = Math.max(highestBinding, bodyParameterIndex ?? -1) + 1;\n // webpieces-disable no-any-unknown -- API argument types are erased at the shared wire boundary\n const args = new Array<unknown>(argCount);\n if (bodyParameterIndex !== undefined) args[bodyParameterIndex] = body;\n\n for (const binding of bindings) {\n const values = binding.source === 'path' ? pathValues : queryValues;\n const raw = values.get(binding.wireName);\n if (raw === undefined) {\n if (binding.source === 'path') {\n throw new ApiBadRequestError(`Missing path parameter '${binding.wireName}'.`);\n }\n args[binding.index] = undefined;\n continue;\n }\n args[binding.index] = this.convert(binding, raw);\n }\n return args;\n }\n\n // webpieces-disable no-function-outside-class -- private pure helper for the static wire mapper\n private static convert(\n binding: HttpParameterBinding,\n raw: string | readonly string[],\n ): ConvertedHttpParameter {\n const values = Array.isArray(raw) ? raw : [raw];\n if (binding.valueType === 'array') return [...values];\n if (values.length !== 1) {\n throw new ApiBadRequestError(\n `Parameter '${binding.wireName}' was repeated but is not declared as an array.`,\n );\n }\n const value = values[0];\n switch (binding.valueType) {\n case 'string':\n case 'unknown':\n return value;\n case 'number': {\n if (value.trim() === '') {\n throw new ApiBadRequestError(\n `Parameter '${binding.wireName}' must be a number.`,\n );\n }\n const parsed = Number(value);\n if (!Number.isFinite(parsed)) {\n throw new ApiBadRequestError(\n `Parameter '${binding.wireName}' must be a number.`,\n );\n }\n return parsed;\n }\n case 'boolean':\n if (value === 'true') return true;\n if (value === 'false') return false;\n throw new ApiBadRequestError(\n `Parameter '${binding.wireName}' must be 'true' or 'false'.`,\n );\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"HttpContract.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/HttpContract.ts"],"names":[],"mappings":"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","sourcesContent":["import { ApiBadRequestError } from '../errors/ApiError';\n\n/** Nominal backing keeps raw string literals out of endpoint declarations. */\nenum HttpMethodValue {\n GET = 'GET',\n POST = 'POST',\n}\n\n/** Short, statically importable decorator arguments. Raw strings do not satisfy {@link HttpMethod}. */\nexport const GET = HttpMethodValue.GET;\nexport const POST = HttpMethodValue.POST;\n\n/** HTTP verbs currently supported by generated Webpieces request/response contracts. */\nexport type HttpMethod = typeof GET | typeof POST;\n\n/** Whether a generated client returns the response body or the whole transport-neutral response. */\nexport type EndpointResponseType = 'body' | 'full';\n\n/** The wire source for one explicitly mapped scalar/list method parameter. */\nexport type HttpParameterSource = 'path' | 'query';\n\n/** Runtime conversion supported for path/query values after TypeScript design metadata is read. */\nexport type HttpParameterValueType = 'string' | 'number' | 'boolean' | 'array' | 'unknown';\n\n/** Concrete runtime values produced for path/query parameters. */\ntype ConvertedHttpParameter = string | number | boolean | readonly string[];\n\n/** Metadata written by `@PathParam` / `@QueryParam` before route construction knows the value type. */\nexport class HttpParameterDeclaration {\n constructor(\n public readonly index: number,\n public readonly source: HttpParameterSource,\n public readonly wireName: string,\n ) {}\n}\n\n/** Fully resolved parameter metadata carried by every server and client route. */\nexport class HttpParameterBinding extends HttpParameterDeclaration {\n constructor(\n index: number,\n source: HttpParameterSource,\n wireName: string,\n public readonly valueType: HttpParameterValueType,\n ) {\n super(index, source, wireName);\n }\n}\n\n/** Result of mapping one API method call to its HTTP path/query/body. */\nexport class BoundHttpRequest {\n constructor(\n public readonly path: string,\n // webpieces-disable no-any-unknown -- the contract owns the request body type\n public readonly body: unknown,\n ) {}\n}\n\n/**\n * The single path/query/body mapper shared by generated clients and the incoming server adapter.\n * Java Webpieces' `HttpsJsonClientInvokeHandler` is the reference model: explicit wire names,\n * encoded placeholders, omitted null query values, and repeated list keys. This port deliberately\n * fixes Java's list-query overwrite bug and never depends on compiled JavaScript parameter names.\n */\nexport class HttpContractMapper {\n /** API arguments -> encoded relative URL + body. */\n // webpieces-disable no-function-outside-class -- pure cross-runtime mapper used by DI-free browser clients and server adapters\n static toWire(\n pathTemplate: string,\n bindings: readonly HttpParameterBinding[],\n bodyParameterIndex: number | undefined,\n // webpieces-disable no-any-unknown -- arbitrary API method parameters cross this generic boundary\n args: readonly unknown[],\n ): BoundHttpRequest {\n let path = pathTemplate;\n const query: string[] = [];\n\n for (const binding of bindings) {\n const value = args[binding.index];\n if (binding.source === 'path') {\n if (value === undefined || value === null) {\n throw new ApiBadRequestError(\n `Missing path parameter '${binding.wireName}' at argument ${binding.index}.`,\n );\n }\n const token = `{${binding.wireName}}`;\n path = path.split(token).join(encodeURIComponent(String(value)));\n continue;\n }\n\n if (value === undefined || value === null) continue;\n const values = Array.isArray(value) ? value : [value];\n for (const item of values) {\n if (item === undefined || item === null) continue;\n query.push(\n `${encodeURIComponent(binding.wireName)}=${encodeURIComponent(String(item))}`,\n );\n }\n }\n\n if (/\\{[^{}]+\\}/.test(path)) {\n throw new ApiBadRequestError(\n `Not every path placeholder was supplied for '${pathTemplate}'.`,\n );\n }\n const suffix =\n query.length === 0 ? '' : `${path.includes('?') ? '&' : '?'}${query.join('&')}`;\n return new BoundHttpRequest(\n `${path}${suffix}`,\n bodyParameterIndex === undefined ? undefined : args[bodyParameterIndex],\n );\n }\n\n /** Decoded path/query/body -> API arguments in declaration order. */\n // webpieces-disable no-function-outside-class -- pure inverse mapper paired with toWire\n static fromWire(\n bindings: readonly HttpParameterBinding[],\n bodyParameterIndex: number | undefined,\n // webpieces-disable no-any-unknown -- the contract owns the request body type\n body: unknown,\n pathValues: ReadonlyMap<string, string | readonly string[]>,\n queryValues: ReadonlyMap<string, string | readonly string[]>,\n ): unknown[] {\n const highestBinding = bindings.reduce(\n (highest: number, binding: HttpParameterBinding) => Math.max(highest, binding.index),\n -1,\n );\n const argCount = Math.max(highestBinding, bodyParameterIndex ?? -1) + 1;\n // webpieces-disable no-any-unknown -- API argument types are erased at the shared wire boundary\n const args = new Array<unknown>(argCount);\n if (bodyParameterIndex !== undefined) args[bodyParameterIndex] = body;\n\n for (const binding of bindings) {\n const values = binding.source === 'path' ? pathValues : queryValues;\n const raw = values.get(binding.wireName);\n if (raw === undefined) {\n if (binding.source === 'path') {\n throw new ApiBadRequestError(`Missing path parameter '${binding.wireName}'.`);\n }\n args[binding.index] = undefined;\n continue;\n }\n args[binding.index] = this.convert(binding, raw);\n }\n return args;\n }\n\n // webpieces-disable no-function-outside-class -- private pure helper for the static wire mapper\n private static convert(\n binding: HttpParameterBinding,\n raw: string | readonly string[],\n ): ConvertedHttpParameter {\n const values = Array.isArray(raw) ? raw : [raw];\n if (binding.valueType === 'array') return [...values];\n if (values.length !== 1) {\n throw new ApiBadRequestError(\n `Parameter '${binding.wireName}' was repeated but is not declared as an array.`,\n );\n }\n const value = values[0];\n switch (binding.valueType) {\n case 'string':\n case 'unknown':\n return value;\n case 'number': {\n if (value.trim() === '') {\n throw new ApiBadRequestError(\n `Parameter '${binding.wireName}' must be a number.`,\n );\n }\n const parsed = Number(value);\n if (!Number.isFinite(parsed)) {\n throw new ApiBadRequestError(\n `Parameter '${binding.wireName}' must be a number.`,\n );\n }\n return parsed;\n }\n case 'boolean':\n if (value === 'true') return true;\n if (value === 'false') return false;\n throw new ApiBadRequestError(\n `Parameter '${binding.wireName}' must be 'true' or 'false'.`,\n );\n }\n }\n}\n"]}
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import { ExternalSystemKind } from './external-caller';
|
|
2
|
-
import {
|
|
2
|
+
import { EndpointResponseType } from './HttpContract';
|
|
3
|
+
/** Nominal backing keeps raw string literals out of endpoint declarations. */
|
|
4
|
+
declare enum EndpointOperationValue {
|
|
5
|
+
READ = "read",
|
|
6
|
+
WRITE_IDEMPOTENT = "write-idempotent",
|
|
7
|
+
WRITE = "write"
|
|
8
|
+
}
|
|
9
|
+
/** Short, statically importable decorator arguments. */
|
|
10
|
+
export declare const READ = EndpointOperationValue.READ;
|
|
11
|
+
export declare const WRITE_IDEMPOTENT = EndpointOperationValue.WRITE_IDEMPOTENT;
|
|
12
|
+
export declare const WRITE = EndpointOperationValue.WRITE;
|
|
13
|
+
/** The side-effect contract callers use to decide whether repeating an operation is safe. */
|
|
14
|
+
export type EndpointOperation = typeof READ | typeof WRITE_IDEMPOTENT | typeof WRITE;
|
|
3
15
|
/** Options for one `@Endpoint`, stored parallel to the method-to-path metadata. */
|
|
4
16
|
export interface EndpointOptions {
|
|
5
|
-
/** HTTP verb for this route. Defaults to POST. */
|
|
6
|
-
httpMethod?: ContractHttpMethod;
|
|
7
17
|
/** Return only a JSON body (default), or the full transport-neutral status/header/body value. */
|
|
8
18
|
responseType?: EndpointResponseType;
|
|
9
19
|
/**
|
|
@@ -49,3 +59,4 @@ export interface ExternalEndpointOptions extends EndpointOptions {
|
|
|
49
59
|
/** What that caller is; picks its graph shape. Defaults to `'saas'`. */
|
|
50
60
|
callerKind?: ExternalSystemKind;
|
|
51
61
|
}
|
|
62
|
+
export {};
|
|
@@ -1,3 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WRITE = exports.WRITE_IDEMPOTENT = exports.READ = void 0;
|
|
4
|
+
/** Nominal backing keeps raw string literals out of endpoint declarations. */
|
|
5
|
+
var EndpointOperationValue;
|
|
6
|
+
(function (EndpointOperationValue) {
|
|
7
|
+
EndpointOperationValue["READ"] = "read";
|
|
8
|
+
EndpointOperationValue["WRITE_IDEMPOTENT"] = "write-idempotent";
|
|
9
|
+
EndpointOperationValue["WRITE"] = "write";
|
|
10
|
+
})(EndpointOperationValue || (EndpointOperationValue = {}));
|
|
11
|
+
/** Short, statically importable decorator arguments. */
|
|
12
|
+
exports.READ = EndpointOperationValue.READ;
|
|
13
|
+
exports.WRITE_IDEMPOTENT = EndpointOperationValue.WRITE_IDEMPOTENT;
|
|
14
|
+
exports.WRITE = EndpointOperationValue.WRITE;
|
|
3
15
|
//# sourceMappingURL=HttpEndpointOptions.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HttpEndpointOptions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/HttpEndpointOptions.ts"],"names":[],"mappings":"","sourcesContent":["import { ExternalSystemKind } from './external-caller';\nimport {
|
|
1
|
+
{"version":3,"file":"HttpEndpointOptions.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/HttpEndpointOptions.ts"],"names":[],"mappings":";;;AAGA,8EAA8E;AAC9E,IAAK,sBAIJ;AAJD,WAAK,sBAAsB;IACvB,uCAAa,CAAA;IACb,+DAAqC,CAAA;IACrC,yCAAe,CAAA;AACnB,CAAC,EAJI,sBAAsB,KAAtB,sBAAsB,QAI1B;AAED,wDAAwD;AAC3C,QAAA,IAAI,GAAG,sBAAsB,CAAC,IAAI,CAAC;AACnC,QAAA,gBAAgB,GAAG,sBAAsB,CAAC,gBAAgB,CAAC;AAC3D,QAAA,KAAK,GAAG,sBAAsB,CAAC,KAAK,CAAC","sourcesContent":["import { ExternalSystemKind } from './external-caller';\nimport { EndpointResponseType } from './HttpContract';\n\n/** Nominal backing keeps raw string literals out of endpoint declarations. */\nenum EndpointOperationValue {\n READ = 'read',\n WRITE_IDEMPOTENT = 'write-idempotent',\n WRITE = 'write',\n}\n\n/** Short, statically importable decorator arguments. */\nexport const READ = EndpointOperationValue.READ;\nexport const WRITE_IDEMPOTENT = EndpointOperationValue.WRITE_IDEMPOTENT;\nexport const WRITE = EndpointOperationValue.WRITE;\n\n/** The side-effect contract callers use to decide whether repeating an operation is safe. */\nexport type EndpointOperation = typeof READ | typeof WRITE_IDEMPOTENT | typeof WRITE;\n\n/** Options for one `@Endpoint`, stored parallel to the method-to-path metadata. */\nexport interface EndpointOptions {\n /** Return only a JSON body (default), or the full transport-neutral status/header/body value. */\n responseType?: EndpointResponseType;\n /**\n * Parse the request body as application/x-www-form-urlencoded instead of JSON. The request\n * DTO must be flat; urlencoded has no nesting. Default false = JSON.\n */\n formPost?: boolean;\n /**\n * Retain the verbatim request bytes and absolute URL for a `@WpAuthWebhook` callback to verify.\n * This is retention, not new buffering: the Express adapter already accumulates the body.\n *\n * `@WpAuthWebhook` requires this option at wiring time. It combines with `formPost` for vendors\n * that sign flat form bodies, e.g. `{ formPost: true, rawBody: true }`.\n */\n rawBody?: boolean;\n /**\n * This route is plumbing the USER did not ask for — a log shipper, a heartbeat, a telemetry\n * flush. It is not progress to report, and it must not be logged as an api call, because the\n * line it would write is itself the thing being shipped.\n *\n * Rides the route metadata for the same reason {@link formPost} and {@link rawBody} do: the\n * consumer branches on the ROUTE, without knowing the apiClass/methodName. Two consumers read\n * it today — {@link LogApiCallImpl} emits no `[API-*-req]`/`[API-*-resp-*]` line for the call,\n * and an app's `RequestLifecycleListener` can skip its progress bar and its own RPC\n * instrumentation.\n *\n * WHY a declaration and not a suppression switch: shipping a log over a logged transport logs\n * about shipping logs. The two mechanisms that do not know the endpoint both failed in prod\n * (see #976) — a wall-clock \"we are shipping\" boolean swallowed unrelated lines including an\n * error alarm, and a logger-name allowlist was incomplete by construction the moment anyone\n * added a logger, producing a steady-state 4-lines-per-second request loop on an idle page.\n * The ENDPOINT is the only stable, declarative fact, so it is where the decision belongs.\n *\n * Default false: absent means the ordinary, fully-logged route. The quiet path is the one you\n * have to say out loud, so `grep -rn background` lists every endpoint webpieces stops logging.\n */\n background?: boolean;\n}\n\n/** An external endpoint must also identify the outside system that calls it. */\nexport interface ExternalEndpointOptions extends EndpointOptions {\n /** The outside system (`'twilio'`) — graph identity, not display text. */\n calledBy: string;\n /** What that caller is; picks its graph shape. Defaults to `'saas'`. */\n callerKind?: ExternalSystemKind;\n}\n"]}
|
|
@@ -2,6 +2,7 @@ import { MaskSpec } from './LogFieldMask';
|
|
|
2
2
|
import { AuthMeta } from './auth-mode';
|
|
3
3
|
import { EndpointResponseType, HttpParameterBinding } from './HttpContract';
|
|
4
4
|
import { StreamingEndpointMetadata } from './StreamingContract';
|
|
5
|
+
import { EndpointOperation } from './HttpEndpointOptions';
|
|
5
6
|
/**
|
|
6
7
|
* Route metadata stored per-method at runtime.
|
|
7
8
|
* Used internally by http-routing and http-client as the runtime representation
|
|
@@ -12,6 +13,8 @@ import { StreamingEndpointMetadata } from './StreamingContract';
|
|
|
12
13
|
* `external-caller.ts` were split off `decorators.ts` before it. Nothing about its role changed.
|
|
13
14
|
*/
|
|
14
15
|
export declare class RouteMetadata {
|
|
16
|
+
/** Explicit side-effect semantics; deliberately independent of the HTTP verb. */
|
|
17
|
+
readonly operation: EndpointOperation;
|
|
15
18
|
/** Explicit path/query mappings, in API declaration order. */
|
|
16
19
|
readonly parameterBindings: readonly HttpParameterBinding[];
|
|
17
20
|
/** The sole unannotated POST parameter, or undefined for a bodyless route. */
|
|
@@ -58,7 +61,9 @@ export declare class RouteMetadata {
|
|
|
58
61
|
* instrumentation can skip it too. Default false = an ordinary, fully-logged route.
|
|
59
62
|
*/
|
|
60
63
|
readonly background: boolean;
|
|
61
|
-
constructor(httpMethod: string, path: string, methodName: string,
|
|
64
|
+
constructor(httpMethod: string, path: string, methodName: string,
|
|
65
|
+
/** Explicit side-effect semantics; deliberately independent of the HTTP verb. */
|
|
66
|
+
operation: EndpointOperation, controllerClassName?: string, authMeta?: AuthMeta, apiName?: string, formPost?: boolean, mask?: MaskSpec, rawBody?: boolean,
|
|
62
67
|
/** Explicit path/query mappings, in API declaration order. */
|
|
63
68
|
parameterBindings?: readonly HttpParameterBinding[],
|
|
64
69
|
/** The sole unannotated POST parameter, or undefined for a bodyless route. */
|
|
@@ -11,6 +11,7 @@ exports.RouteMetadata = void 0;
|
|
|
11
11
|
* `external-caller.ts` were split off `decorators.ts` before it. Nothing about its role changed.
|
|
12
12
|
*/
|
|
13
13
|
class RouteMetadata {
|
|
14
|
+
operation;
|
|
14
15
|
parameterBindings;
|
|
15
16
|
bodyParameterIndex;
|
|
16
17
|
responseType;
|
|
@@ -53,7 +54,9 @@ class RouteMetadata {
|
|
|
53
54
|
* instrumentation can skip it too. Default false = an ordinary, fully-logged route.
|
|
54
55
|
*/
|
|
55
56
|
background;
|
|
56
|
-
constructor(httpMethod, path, methodName,
|
|
57
|
+
constructor(httpMethod, path, methodName,
|
|
58
|
+
/** Explicit side-effect semantics; deliberately independent of the HTTP verb. */
|
|
59
|
+
operation, controllerClassName, authMeta, apiName, formPost = false, mask, rawBody = false,
|
|
57
60
|
/** Explicit path/query mappings, in API declaration order. */
|
|
58
61
|
parameterBindings = [],
|
|
59
62
|
/** The sole unannotated POST parameter, or undefined for a bodyless route. */
|
|
@@ -62,6 +65,7 @@ class RouteMetadata {
|
|
|
62
65
|
responseType = 'body',
|
|
63
66
|
/** Present only for `(ResponseStream) => Promise<RequestStream>` contracts. */
|
|
64
67
|
streaming, background = false) {
|
|
68
|
+
this.operation = operation;
|
|
65
69
|
this.parameterBindings = parameterBindings;
|
|
66
70
|
this.bodyParameterIndex = bodyParameterIndex;
|
|
67
71
|
this.responseType = responseType;
|
|
@@ -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":";;;AAMA;;;;;;;;GAQG;AACH,MAAa,aAAa;IA6CT;IAQA;IAEA;IAEA;IAEA;IA1Db,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;IAC1B;;;;;;;;;;OAUG;IACM,UAAU,CAAU;IAE7B,YACI,UAAkB,EAClB,IAAY,EACZ,UAAkB;IAClB,iFAAiF;IACxE,SAA4B,EACrC,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,EAC9C,aAAsB,KAAK;QAflB,cAAS,GAAT,SAAS,CAAmB;QAQ5B,sBAAiB,GAAjB,iBAAiB,CAAsC;QAEvD,uBAAkB,GAAlB,kBAAkB,CAAS;QAE3B,iBAAY,GAAZ,YAAY,CAA+B;QAE3C,cAAS,GAAT,SAAS,CAA4B;QAG9C,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;QAC3B,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;IACjC,CAAC;CACJ;AA7ED,sCA6EC","sourcesContent":["import { MaskSpec } from './LogFieldMask';\nimport { AuthMeta } from './auth-mode';\nimport { EndpointResponseType, HttpParameterBinding } from './HttpContract';\nimport { StreamingEndpointMetadata } from './StreamingContract';\nimport { EndpointOperation } from './HttpEndpointOptions';\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 * True when @Endpoint(..., { background: true }): this route is plumbing the user did not ask\n * for (a log shipper, a heartbeat, a telemetry flush). Rides the route metadata for the same\n * reason {@link formPost} and {@link rawBody} do — each consumer branches on the ROUTE, without\n * knowing the apiClass/methodName.\n *\n * Read by {@link LogApiCallImpl} (via ApiMethodInfo, exactly as {@link mask} is) so no\n * `[API-*-req]`/`[API-*-resp-*]` line is emitted for the call, and readable from an app's\n * `RequestLifecycleListener.onRequestStart/onRequestEnd` so a progress bar and any app-level RPC\n * instrumentation can skip it too. Default false = an ordinary, fully-logged route.\n */\n readonly background: boolean;\n\n constructor(\n httpMethod: string,\n path: string,\n methodName: string,\n /** Explicit side-effect semantics; deliberately independent of the HTTP verb. */\n readonly operation: EndpointOperation,\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 background: boolean = false,\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 this.background = background;\n }\n}\n"]}
|
|
@@ -6,12 +6,13 @@ 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 endpointPath;
|
|
9
10
|
private static validateEndpointOptions;
|
|
10
11
|
private static validateStreamingParameters;
|
|
11
12
|
/**
|
|
12
13
|
* Refuse a contract in which two endpoints resolve to the same HTTP method + path.
|
|
13
14
|
*
|
|
14
|
-
* Empty paths are LEGAL (`@ApiPath('')` + `@Endpoint('', ...)` is how a contract says "the whole
|
|
15
|
+
* Empty paths are LEGAL (`@ApiPath('')` + `@Endpoint(POST, '', ...)` is how a contract says "the whole
|
|
15
16
|
* destination arrives per call"), so the thing actually worth rejecting is ambiguity: two methods
|
|
16
17
|
* that land on one route. On the server the second registration would silently shadow the first
|
|
17
18
|
* in the route map, and on the client both would dial the same URL — neither is ever what the
|
|
@@ -28,13 +29,12 @@ export declare class RouteMetadataFactory {
|
|
|
28
29
|
/**
|
|
29
30
|
* Join `@ApiPath` and `@Endpoint` into the route path.
|
|
30
31
|
*
|
|
31
|
-
* Both empty joins to `''`, NOT `'/'`. A contract declaring `@ApiPath('')` + `@Endpoint('', ...)`
|
|
32
|
+
* Both empty joins to `''`, NOT `'/'`. A contract declaring `@ApiPath('')` + `@Endpoint(POST, '', ...)`
|
|
32
33
|
* is saying "this route adds nothing to the base URL", and a client whose base URL is a full
|
|
33
34
|
* destination (host + path + query) must send it byte for byte. Before #926 the client joined with
|
|
34
35
|
* plain concatenation (`'' + ''`), and forcing a `/` here broke exactly that (#944).
|
|
35
36
|
*/
|
|
36
37
|
private static joinPath;
|
|
37
|
-
private static httpMethod;
|
|
38
38
|
private static responseType;
|
|
39
39
|
private static parameterTypes;
|
|
40
40
|
private static validateParameters;
|
|
@@ -14,15 +14,12 @@ const StreamingContract_1 = require("./StreamingContract");
|
|
|
14
14
|
class RouteMetadataFactory {
|
|
15
15
|
// webpieces-disable no-function-outside-class -- deterministic decorator metadata factory shared by DI-free browser and server runtimes
|
|
16
16
|
static create(apiClass, methodName, controllerClassName) {
|
|
17
|
-
const
|
|
18
|
-
const endpointPath = endpoints[methodName];
|
|
19
|
-
if (endpointPath === undefined) {
|
|
20
|
-
throw new Error(`No @Endpoint metadata for ${apiClass.name || 'Unknown'}.${methodName}.`);
|
|
21
|
-
}
|
|
17
|
+
const endpointPath = this.endpointPath(apiClass, methodName);
|
|
22
18
|
const fullPath = this.joinPath((0, decorators_1.getApiPath)(apiClass) ?? '', endpointPath);
|
|
23
19
|
const options = (0, decorators_1.getEndpointOptions)(apiClass, methodName);
|
|
24
20
|
const streaming = (0, StreamingContract_1.getStreamingEndpoint)(apiClass, methodName);
|
|
25
|
-
const httpMethod =
|
|
21
|
+
const httpMethod = (0, decorators_1.getEndpointHttpMethod)(apiClass, methodName);
|
|
22
|
+
const operation = (0, decorators_1.getEndpointOperation)(apiClass, methodName);
|
|
26
23
|
this.validateEndpointOptions(apiClass, methodName, httpMethod, options.formPost, options.rawBody);
|
|
27
24
|
const parameterTypes = this.parameterTypes(apiClass, methodName);
|
|
28
25
|
const declarations = (0, http_parameter_decorators_1.getHttpParameterDeclarations)(apiClass, methodName);
|
|
@@ -32,7 +29,14 @@ class RouteMetadataFactory {
|
|
|
32
29
|
const bindings = declarations
|
|
33
30
|
.map((declaration) => new HttpContract_1.HttpParameterBinding(declaration.index, declaration.source, declaration.wireName, this.valueType(parameterTypes[declaration.index])))
|
|
34
31
|
.sort((a, b) => a.index - b.index);
|
|
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, options.background === true);
|
|
32
|
+
return new RouteMetadata_1.RouteMetadata(httpMethod, fullPath, methodName, operation, 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, options.background === true);
|
|
33
|
+
}
|
|
34
|
+
// webpieces-disable no-function-outside-class -- private pure helper for static route metadata construction
|
|
35
|
+
static endpointPath(apiClass, methodName) {
|
|
36
|
+
const path = ((0, decorators_1.getEndpoints)(apiClass) ?? {})[methodName];
|
|
37
|
+
if (path !== undefined)
|
|
38
|
+
return path;
|
|
39
|
+
throw new Error(`No @Endpoint metadata for ${apiClass.name || 'Unknown'}.${methodName}.`);
|
|
36
40
|
}
|
|
37
41
|
// webpieces-disable no-function-outside-class -- private pure helper keeps the existing static metadata factory below the method-size limit
|
|
38
42
|
static validateEndpointOptions(apiClass, methodName, httpMethod, formPost, rawBody) {
|
|
@@ -57,7 +61,7 @@ class RouteMetadataFactory {
|
|
|
57
61
|
/**
|
|
58
62
|
* Refuse a contract in which two endpoints resolve to the same HTTP method + path.
|
|
59
63
|
*
|
|
60
|
-
* Empty paths are LEGAL (`@ApiPath('')` + `@Endpoint('', ...)` is how a contract says "the whole
|
|
64
|
+
* Empty paths are LEGAL (`@ApiPath('')` + `@Endpoint(POST, '', ...)` is how a contract says "the whole
|
|
61
65
|
* destination arrives per call"), so the thing actually worth rejecting is ambiguity: two methods
|
|
62
66
|
* that land on one route. On the server the second registration would silently shadow the first
|
|
63
67
|
* in the route map, and on the client both would dial the same URL — neither is ever what the
|
|
@@ -91,7 +95,7 @@ class RouteMetadataFactory {
|
|
|
91
95
|
/**
|
|
92
96
|
* Join `@ApiPath` and `@Endpoint` into the route path.
|
|
93
97
|
*
|
|
94
|
-
* Both empty joins to `''`, NOT `'/'`. A contract declaring `@ApiPath('')` + `@Endpoint('', ...)`
|
|
98
|
+
* Both empty joins to `''`, NOT `'/'`. A contract declaring `@ApiPath('')` + `@Endpoint(POST, '', ...)`
|
|
95
99
|
* is saying "this route adds nothing to the base URL", and a client whose base URL is a full
|
|
96
100
|
* destination (host + path + query) must send it byte for byte. Before #926 the client joined with
|
|
97
101
|
* plain concatenation (`'' + ''`), and forcing a `/` here broke exactly that (#944).
|
|
@@ -104,13 +108,6 @@ class RouteMetadataFactory {
|
|
|
104
108
|
return joined.startsWith('/') ? joined : `/${joined}`;
|
|
105
109
|
}
|
|
106
110
|
// webpieces-disable no-function-outside-class -- private pure helper for the static metadata factory
|
|
107
|
-
static httpMethod(value, apiClass, methodName) {
|
|
108
|
-
const method = value ?? 'POST';
|
|
109
|
-
if (method === 'GET' || method === 'POST')
|
|
110
|
-
return method;
|
|
111
|
-
throw new Error(`${apiClass.name}.${methodName} declares unsupported httpMethod '${String(method)}'; use GET or POST.`);
|
|
112
|
-
}
|
|
113
|
-
// webpieces-disable no-function-outside-class -- private pure helper for the static metadata factory
|
|
114
111
|
static responseType(value, apiClass, methodName) {
|
|
115
112
|
const responseType = value ?? 'body';
|
|
116
113
|
if (responseType === 'body' || responseType === 'full')
|
|
@@ -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';\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 options.background === true,\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 /**\n * Refuse a contract in which two endpoints resolve to the same HTTP method + path.\n *\n * Empty paths are LEGAL (`@ApiPath('')` + `@Endpoint('', ...)` is how a contract says \"the whole\n * destination arrives per call\"), so the thing actually worth rejecting is ambiguity: two methods\n * that land on one route. On the server the second registration would silently shadow the first\n * in the route map, and on the client both would dial the same URL — neither is ever what the\n * author meant. Placeholder NAMES are ignored when comparing (`/a/{id}` and `/a/{key}` are the\n * same route to a router), `''` and `'/'` compare equal (the server serves an empty path at the\n * root), and the error names BOTH methods so the fix is obvious.\n *\n * Called by every transport that binds a whole contract (the generated clients and the server's\n * route registration), so a duplicate dies at startup however the contract is consumed.\n *\n * @throws Error naming both colliding methods and the shared route.\n */\n // webpieces-disable no-function-outside-class -- deterministic contract validation shared by DI-free browser and server runtimes\n static assertNoDuplicateRoutes(apiClass: Function): void {\n const label = apiClass.name || 'Unknown';\n const seen = new Map<string, string>();\n for (const methodName of Object.keys(getEndpoints(apiClass) ?? {})) {\n const route = this.create(apiClass, methodName);\n // '' and '/' are one route to a server (every request path starts with '/').\n const shape = (route.path === '' ? '/' : route.path).replace(/\\{[^{}]+\\}/g, '{}');\n const key = `${route.httpMethod} ${shape}`;\n const previous = seen.get(key);\n if (previous !== undefined) {\n throw new Error(\n `${label}.${previous} and ${label}.${methodName} both resolve to ` +\n `${route.httpMethod} '${route.path}'. Two endpoints of one contract cannot share ` +\n `an HTTP method + path; give one of them a distinct @Endpoint path or HTTP method.`,\n );\n }\n seen.set(key, methodName);\n }\n }\n\n /**\n * Join `@ApiPath` and `@Endpoint` into the route path.\n *\n * Both empty joins to `''`, NOT `'/'`. A contract declaring `@ApiPath('')` + `@Endpoint('', ...)`\n * is saying \"this route adds nothing to the base URL\", and a client whose base URL is a full\n * destination (host + path + query) must send it byte for byte. Before #926 the client joined with\n * plain concatenation (`'' + ''`), and forcing a `/` here broke exactly that (#944).\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 if (basePath === '' && endpointPath === '') return '';\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 getEndpointHttpMethod,\n getEndpointOperation,\n getEndpointOptions,\n getEndpoints,\n getMaskSpec,\n} from './decorators';\nimport { getHttpParameterDeclarations } from './http-parameter-decorators';\nimport {\n EndpointResponseType,\n HttpParameterBinding,\n HttpParameterDeclaration,\n HttpParameterValueType,\n HttpMethod,\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 endpointPath = this.endpointPath(apiClass, methodName);\n const fullPath = this.joinPath(getApiPath(apiClass) ?? '', endpointPath);\n const options = getEndpointOptions(apiClass, methodName);\n const streaming = getStreamingEndpoint(apiClass, methodName);\n const httpMethod = getEndpointHttpMethod(apiClass, methodName);\n const operation = getEndpointOperation(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 operation,\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 options.background === true,\n );\n }\n\n // webpieces-disable no-function-outside-class -- private pure helper for static route metadata construction\n private static endpointPath(apiClass: Function, methodName: string): string {\n const path = (getEndpoints(apiClass) ?? {})[methodName];\n if (path !== undefined) return path;\n throw new Error(`No @Endpoint metadata for ${apiClass.name || 'Unknown'}.${methodName}.`);\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: HttpMethod,\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: HttpMethod,\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 /**\n * Refuse a contract in which two endpoints resolve to the same HTTP method + path.\n *\n * Empty paths are LEGAL (`@ApiPath('')` + `@Endpoint(POST, '', ...)` is how a contract says \"the whole\n * destination arrives per call\"), so the thing actually worth rejecting is ambiguity: two methods\n * that land on one route. On the server the second registration would silently shadow the first\n * in the route map, and on the client both would dial the same URL — neither is ever what the\n * author meant. Placeholder NAMES are ignored when comparing (`/a/{id}` and `/a/{key}` are the\n * same route to a router), `''` and `'/'` compare equal (the server serves an empty path at the\n * root), and the error names BOTH methods so the fix is obvious.\n *\n * Called by every transport that binds a whole contract (the generated clients and the server's\n * route registration), so a duplicate dies at startup however the contract is consumed.\n *\n * @throws Error naming both colliding methods and the shared route.\n */\n // webpieces-disable no-function-outside-class -- deterministic contract validation shared by DI-free browser and server runtimes\n static assertNoDuplicateRoutes(apiClass: Function): void {\n const label = apiClass.name || 'Unknown';\n const seen = new Map<string, string>();\n for (const methodName of Object.keys(getEndpoints(apiClass) ?? {})) {\n const route = this.create(apiClass, methodName);\n // '' and '/' are one route to a server (every request path starts with '/').\n const shape = (route.path === '' ? '/' : route.path).replace(/\\{[^{}]+\\}/g, '{}');\n const key = `${route.httpMethod} ${shape}`;\n const previous = seen.get(key);\n if (previous !== undefined) {\n throw new Error(\n `${label}.${previous} and ${label}.${methodName} both resolve to ` +\n `${route.httpMethod} '${route.path}'. Two endpoints of one contract cannot share ` +\n `an HTTP method + path; give one of them a distinct @Endpoint path or HTTP method.`,\n );\n }\n seen.set(key, methodName);\n }\n }\n\n /**\n * Join `@ApiPath` and `@Endpoint` into the route path.\n *\n * Both empty joins to `''`, NOT `'/'`. A contract declaring `@ApiPath('')` + `@Endpoint(POST, '', ...)`\n * is saying \"this route adds nothing to the base URL\", and a client whose base URL is a full\n * destination (host + path + query) must send it byte for byte. Before #926 the client joined with\n * plain concatenation (`'' + ''`), and forcing a `/` here broke exactly that (#944).\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 if (basePath === '' && endpointPath === '') return '';\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 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: HttpMethod,\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"]}
|
|
@@ -51,9 +51,9 @@ export declare class WebpiecesDefaultErrorTranslator implements ErrorTranslator
|
|
|
51
51
|
* gets its DTO. Everything else throws, which is what lets `ProxyClient` call this and then simply
|
|
52
52
|
* stop — a failure response can never fall through to a typed caller.
|
|
53
53
|
*
|
|
54
|
-
* The rule applied is {@link ReceivedApiErrorRule}, shared verbatim with IPC: 4xx is MY
|
|
55
|
-
*
|
|
56
|
-
* user's own answer.
|
|
54
|
+
* The rule applied is {@link ReceivedApiErrorRule}, shared verbatim with IPC: ordinary 4xx is MY
|
|
55
|
+
* bug, ordinary 5xx is THEIRS, retry-relevant statuses retain their specific timeout/gateway/
|
|
56
|
+
* backoff/unavailable meaning, and 266 is the end user's own answer.
|
|
57
57
|
*/
|
|
58
58
|
fromWire(response: HttpResponseDto): void;
|
|
59
59
|
/** The reason phrase webpieces writes beside a status it chose itself. */
|
|
@@ -65,6 +65,8 @@ export declare class WebpiecesDefaultErrorTranslator implements ErrorTranslator
|
|
|
65
65
|
*/
|
|
66
66
|
private decodeBody;
|
|
67
67
|
private fallbackMessage;
|
|
68
|
+
/** Parse either legal Retry-After form and bound untrusted guidance to one day. */
|
|
69
|
+
private retryAfterSeconds;
|
|
68
70
|
}
|
|
69
71
|
/** Process-wide built-in instance — stateless, so one shared instance is enough. */
|
|
70
72
|
export declare const WEBPIECES_DEFAULT_ERROR_TRANSLATOR: WebpiecesDefaultErrorTranslator;
|
|
@@ -85,9 +85,9 @@ class WebpiecesDefaultErrorTranslator {
|
|
|
85
85
|
* gets its DTO. Everything else throws, which is what lets `ProxyClient` call this and then simply
|
|
86
86
|
* stop — a failure response can never fall through to a typed caller.
|
|
87
87
|
*
|
|
88
|
-
* The rule applied is {@link ReceivedApiErrorRule}, shared verbatim with IPC: 4xx is MY
|
|
89
|
-
*
|
|
90
|
-
* user's own answer.
|
|
88
|
+
* The rule applied is {@link ReceivedApiErrorRule}, shared verbatim with IPC: ordinary 4xx is MY
|
|
89
|
+
* bug, ordinary 5xx is THEIRS, retry-relevant statuses retain their specific timeout/gateway/
|
|
90
|
+
* backoff/unavailable meaning, and 266 is the end user's own answer.
|
|
91
91
|
*/
|
|
92
92
|
fromWire(response) {
|
|
93
93
|
const code = response.status.code;
|
|
@@ -96,7 +96,7 @@ class WebpiecesDefaultErrorTranslator {
|
|
|
96
96
|
}
|
|
97
97
|
const decoded = this.decodeBody(response);
|
|
98
98
|
const message = decoded?.message ?? this.fallbackMessage(response.body, response.status.reason);
|
|
99
|
-
throw ReceivedApiErrorRule_1.ReceivedApiErrorRule.adapt(code, message, decoded);
|
|
99
|
+
throw ReceivedApiErrorRule_1.ReceivedApiErrorRule.adapt(code, message, decoded, this.retryAfterSeconds(response.headers));
|
|
100
100
|
}
|
|
101
101
|
/** The reason phrase webpieces writes beside a status it chose itself. */
|
|
102
102
|
genericMessage(code) {
|
|
@@ -129,6 +129,19 @@ class WebpiecesDefaultErrorTranslator {
|
|
|
129
129
|
}
|
|
130
130
|
return reason || 'Request Failed';
|
|
131
131
|
}
|
|
132
|
+
/** Parse either legal Retry-After form and bound untrusted guidance to one day. */
|
|
133
|
+
retryAfterSeconds(headers) {
|
|
134
|
+
const raw = headers.find((header) => header.name.toLowerCase() === 'retry-after')?.value;
|
|
135
|
+
if (raw === undefined)
|
|
136
|
+
return undefined;
|
|
137
|
+
const seconds = Number(raw.trim());
|
|
138
|
+
if (Number.isFinite(seconds) && seconds >= 0)
|
|
139
|
+
return Math.min(Math.ceil(seconds), 86_400);
|
|
140
|
+
const at = Date.parse(raw);
|
|
141
|
+
if (!Number.isFinite(at))
|
|
142
|
+
return undefined;
|
|
143
|
+
return Math.min(Math.max(Math.ceil((at - Date.now()) / 1_000), 0), 86_400);
|
|
144
|
+
}
|
|
132
145
|
}
|
|
133
146
|
exports.WebpiecesDefaultErrorTranslator = WebpiecesDefaultErrorTranslator;
|
|
134
147
|
/** Process-wide built-in instance — stateless, so one shared instance is enough. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebpiecesDefaultErrorTranslator.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/WebpiecesDefaultErrorTranslator.ts"],"names":[],"mappings":"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","sourcesContent":["import { ApiErrorBoundary } from '../errors/ApiErrorBoundary';\nimport { ApiErrorCodec } from '../errors/ApiErrorCodec';\nimport { ApiCodedError, ApiError } from '../errors/ApiError';\nimport { ReceivedApiErrorRule } from '../errors/ReceivedApiErrorRule';\nimport { ApiErrorHttpStatus, PublishedKind } from './ApiErrorHttpStatus';\nimport { ErrorTranslator } from './ErrorTranslator';\nimport { HttpHeader, HttpResponseDto, HttpResponseStatus } from './HttpResponseDto';\n\n/**\n * THE webpieces default {@link ErrorTranslator} — ONE class, BOTH halves, used by the node server, by\n * every node client and by the browser bundle alike.\n *\n * It is what {@link ClientRegistry.getErrorTranslator} holds until an app installs its own, and it is\n * what an app's own translator calls to DECLINE an error:\n *\n * ```ts\n * private readonly fallback = new WebpiecesDefaultErrorTranslator();\n * ```\n *\n * so \"no translator registered\" and \"a translator that does not claim this error\" produce\n * byte-identical output. It lives in core-util, beside {@link ClientRegistry}, because core-util is\n * the package every API contract already depends on and the only one both a server and a browser\n * bundle can reach. Both halves needed nothing else: the outbound half wants\n * {@link ApiErrorBoundary} + {@link ApiErrorHttpStatus}, the inbound half wants\n * {@link ApiErrorCodec} + {@link ReceivedApiErrorRule}, and all four were already here.\n *\n * Stateless, so {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR} is a shared instance — but constructing\n * your own costs nothing and reads better at a delegation site.\n */\nexport class WebpiecesDefaultErrorTranslator implements ErrorTranslator {\n private readonly boundary = new ApiErrorBoundary();\n\n private readonly genericMessages: Map<number, string> = new Map<number, string>([\n [266, 'End User Error'],\n [400, 'Bad Request'],\n [401, 'Unauthorized'],\n [403, 'Forbidden'],\n [404, 'Not Found'],\n [408, 'Request Timeout'],\n [409, 'Conflict'],\n [412, 'Precondition Failed'],\n [415, 'Unsupported Media Type'],\n [422, 'Unprocessable Content'],\n [429, 'Too Many Requests'],\n [500, 'Internal Server Error'],\n [501, 'Not Implemented'],\n [502, 'Bad Gateway'],\n [503, 'Service Unavailable'],\n [504, 'Gateway Timeout'],\n ]);\n\n /**\n * SERVER: an exception in, the exact HTTP response out.\n *\n * The payload IS the answer. {@link ApiErrorBoundary.encode} has already applied both shared\n * rules — a caller-local `ApiConnectionError` (and every non-`ApiError`) publishes as\n * `implementation`, and only an `ApiEndUserError`'s own message is disclosed — and the payload\n * carries everything HTTP needs after that: `kind`, `statusCode`, `edgeHttpStatus` and\n * `retryAfterSeconds`. So there is no `instanceof` here and nothing re-derived from the raw\n * error; unknown throws and caller-local failures come out as a 500 implementation failure\n * because that is what the payload says.\n *\n * An `ApiEndUserError` publishes 266 here, ALWAYS. Whether a particular REQUEST republishes that\n * as a real 4xx is a property of the CALLER, not of the error, so it is decided once per request\n * from `WebpiecesCoreHeaders.SURFACE` by the server boundary (`ExpressWrapper.handleError`) —\n * see {@link SurfaceEndUserStatus}. That is why this class takes no constructor argument any\n * more: the old `new ApiErrorHttpMapper('gui' | 'edge')` made every delegating app translator\n * responsible for repeating its router's mode, and repeating it wrongly was silent.\n */\n toWire(error: Error): HttpResponseDto {\n const payload = this.boundary.encode(error);\n // The boundary never publishes kind 'connection' — that is the whole first rule above — so\n // this is the published subset ApiErrorHttpStatus.codeFor accepts.\n const kind = payload.kind as PublishedKind;\n const status = ApiErrorHttpStatus.codeFor(kind, payload.statusCode);\n const headers =\n payload.retryAfterSeconds !== undefined\n ? [new HttpHeader('retry-after', String(payload.retryAfterSeconds))]\n : [];\n return new HttpResponseDto(\n new HttpResponseStatus(status, this.genericMessage(status)),\n headers,\n payload,\n );\n }\n\n /**\n * CLIENT: the response in, a THROW out — or a silent return for an ordinary success.\n *\n * Called for EVERY response, 2xx included (see {@link ErrorTranslator.fromWire}). A 2xx that is\n * not 266 is exactly the case where webpieces has nothing to say, so it returns and the caller\n * gets its DTO. Everything else throws, which is what lets `ProxyClient` call this and then simply\n * stop — a failure response can never fall through to a typed caller.\n *\n * The rule applied is {@link ReceivedApiErrorRule}, shared verbatim with IPC: 4xx is MY bug, 5xx\n * is THEIRS, an `ApiDependencyError` is already attributed and passes through, 266 is the end\n * user's own answer.\n */\n fromWire(response: HttpResponseDto): void {\n const code = response.status.code;\n if (code >= 200 && code < 300 && code !== 266) {\n return;\n }\n const decoded = this.decodeBody(response);\n const message = decoded?.message ?? this.fallbackMessage(response.body, response.status.reason);\n throw ReceivedApiErrorRule.adapt(code, message, decoded);\n }\n\n /** The reason phrase webpieces writes beside a status it chose itself. */\n genericMessage(code: number): string {\n return this.genericMessages.get(code) ?? 'Request Failed';\n }\n\n /**\n * A webpieces peer sends an {@link ApiErrorPayload}; a foreign responder sends anything at all.\n * The status is only believed when it AGREES with the kind the body claims — a body saying\n * `not-found` beside a 500 is a mismatched response, not a 404, and falls back to the status.\n */\n private decodeBody(response: HttpResponseDto): ApiError | undefined {\n if (!ApiErrorCodec.isPayload(response.body)) {\n return undefined;\n }\n const decoded = ApiErrorCodec.decode(response.body);\n if (!ApiErrorHttpStatus.hasCode(decoded)) {\n return undefined;\n }\n // `statusCode` is the 'coded' kind's own status, the one thing the shared table cannot know.\n const statusCode = decoded instanceof ApiCodedError ? decoded.statusCode : undefined;\n const declared = ApiErrorHttpStatus.codeFor(decoded.kind, statusCode);\n return declared === response.status.code ? decoded : undefined;\n }\n\n // webpieces-disable no-any-unknown -- HTTP response bodies are app-owned until this boundary safely inspects them\n private fallbackMessage(body: unknown, reason: string): string {\n if (typeof body === 'object' && body !== null) {\n const message = Object.getOwnPropertyDescriptor(body, 'message')?.value;\n if (typeof message === 'string' && message.length > 0) return message.slice(0, 4096);\n }\n return reason || 'Request Failed';\n }\n}\n\n/** Process-wide built-in instance — stateless, so one shared instance is enough. */\nexport const WEBPIECES_DEFAULT_ERROR_TRANSLATOR = new WebpiecesDefaultErrorTranslator();\n"]}
|
|
1
|
+
{"version":3,"file":"WebpiecesDefaultErrorTranslator.js","sourceRoot":"","sources":["../../../../../../packages/core/core-util/src/http/WebpiecesDefaultErrorTranslator.ts"],"names":[],"mappings":"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","sourcesContent":["import { ApiErrorBoundary } from '../errors/ApiErrorBoundary';\nimport { ApiErrorCodec } from '../errors/ApiErrorCodec';\nimport { ApiCodedError, ApiError } from '../errors/ApiError';\nimport { ReceivedApiErrorRule } from '../errors/ReceivedApiErrorRule';\nimport { ApiErrorHttpStatus, PublishedKind } from './ApiErrorHttpStatus';\nimport { ErrorTranslator } from './ErrorTranslator';\nimport { HttpHeader, HttpResponseDto, HttpResponseStatus } from './HttpResponseDto';\n\n/**\n * THE webpieces default {@link ErrorTranslator} — ONE class, BOTH halves, used by the node server, by\n * every node client and by the browser bundle alike.\n *\n * It is what {@link ClientRegistry.getErrorTranslator} holds until an app installs its own, and it is\n * what an app's own translator calls to DECLINE an error:\n *\n * ```ts\n * private readonly fallback = new WebpiecesDefaultErrorTranslator();\n * ```\n *\n * so \"no translator registered\" and \"a translator that does not claim this error\" produce\n * byte-identical output. It lives in core-util, beside {@link ClientRegistry}, because core-util is\n * the package every API contract already depends on and the only one both a server and a browser\n * bundle can reach. Both halves needed nothing else: the outbound half wants\n * {@link ApiErrorBoundary} + {@link ApiErrorHttpStatus}, the inbound half wants\n * {@link ApiErrorCodec} + {@link ReceivedApiErrorRule}, and all four were already here.\n *\n * Stateless, so {@link WEBPIECES_DEFAULT_ERROR_TRANSLATOR} is a shared instance — but constructing\n * your own costs nothing and reads better at a delegation site.\n */\nexport class WebpiecesDefaultErrorTranslator implements ErrorTranslator {\n private readonly boundary = new ApiErrorBoundary();\n\n private readonly genericMessages: Map<number, string> = new Map<number, string>([\n [266, 'End User Error'],\n [400, 'Bad Request'],\n [401, 'Unauthorized'],\n [403, 'Forbidden'],\n [404, 'Not Found'],\n [408, 'Request Timeout'],\n [409, 'Conflict'],\n [412, 'Precondition Failed'],\n [415, 'Unsupported Media Type'],\n [422, 'Unprocessable Content'],\n [429, 'Too Many Requests'],\n [500, 'Internal Server Error'],\n [501, 'Not Implemented'],\n [502, 'Bad Gateway'],\n [503, 'Service Unavailable'],\n [504, 'Gateway Timeout'],\n ]);\n\n /**\n * SERVER: an exception in, the exact HTTP response out.\n *\n * The payload IS the answer. {@link ApiErrorBoundary.encode} has already applied both shared\n * rules — a caller-local `ApiConnectionError` (and every non-`ApiError`) publishes as\n * `implementation`, and only an `ApiEndUserError`'s own message is disclosed — and the payload\n * carries everything HTTP needs after that: `kind`, `statusCode`, `edgeHttpStatus` and\n * `retryAfterSeconds`. So there is no `instanceof` here and nothing re-derived from the raw\n * error; unknown throws and caller-local failures come out as a 500 implementation failure\n * because that is what the payload says.\n *\n * An `ApiEndUserError` publishes 266 here, ALWAYS. Whether a particular REQUEST republishes that\n * as a real 4xx is a property of the CALLER, not of the error, so it is decided once per request\n * from `WebpiecesCoreHeaders.SURFACE` by the server boundary (`ExpressWrapper.handleError`) —\n * see {@link SurfaceEndUserStatus}. That is why this class takes no constructor argument any\n * more: the old `new ApiErrorHttpMapper('gui' | 'edge')` made every delegating app translator\n * responsible for repeating its router's mode, and repeating it wrongly was silent.\n */\n toWire(error: Error): HttpResponseDto {\n const payload = this.boundary.encode(error);\n // The boundary never publishes kind 'connection' — that is the whole first rule above — so\n // this is the published subset ApiErrorHttpStatus.codeFor accepts.\n const kind = payload.kind as PublishedKind;\n const status = ApiErrorHttpStatus.codeFor(kind, payload.statusCode);\n const headers =\n payload.retryAfterSeconds !== undefined\n ? [new HttpHeader('retry-after', String(payload.retryAfterSeconds))]\n : [];\n return new HttpResponseDto(\n new HttpResponseStatus(status, this.genericMessage(status)),\n headers,\n payload,\n );\n }\n\n /**\n * CLIENT: the response in, a THROW out — or a silent return for an ordinary success.\n *\n * Called for EVERY response, 2xx included (see {@link ErrorTranslator.fromWire}). A 2xx that is\n * not 266 is exactly the case where webpieces has nothing to say, so it returns and the caller\n * gets its DTO. Everything else throws, which is what lets `ProxyClient` call this and then simply\n * stop — a failure response can never fall through to a typed caller.\n *\n * The rule applied is {@link ReceivedApiErrorRule}, shared verbatim with IPC: ordinary 4xx is MY\n * bug, ordinary 5xx is THEIRS, retry-relevant statuses retain their specific timeout/gateway/\n * backoff/unavailable meaning, and 266 is the end user's own answer.\n */\n fromWire(response: HttpResponseDto): void {\n const code = response.status.code;\n if (code >= 200 && code < 300 && code !== 266) {\n return;\n }\n const decoded = this.decodeBody(response);\n const message =\n decoded?.message ?? this.fallbackMessage(response.body, response.status.reason);\n throw ReceivedApiErrorRule.adapt(\n code,\n message,\n decoded,\n this.retryAfterSeconds(response.headers),\n );\n }\n\n /** The reason phrase webpieces writes beside a status it chose itself. */\n genericMessage(code: number): string {\n return this.genericMessages.get(code) ?? 'Request Failed';\n }\n\n /**\n * A webpieces peer sends an {@link ApiErrorPayload}; a foreign responder sends anything at all.\n * The status is only believed when it AGREES with the kind the body claims — a body saying\n * `not-found` beside a 500 is a mismatched response, not a 404, and falls back to the status.\n */\n private decodeBody(response: HttpResponseDto): ApiError | undefined {\n if (!ApiErrorCodec.isPayload(response.body)) {\n return undefined;\n }\n const decoded = ApiErrorCodec.decode(response.body);\n if (!ApiErrorHttpStatus.hasCode(decoded)) {\n return undefined;\n }\n // `statusCode` is the 'coded' kind's own status, the one thing the shared table cannot know.\n const statusCode = decoded instanceof ApiCodedError ? decoded.statusCode : undefined;\n const declared = ApiErrorHttpStatus.codeFor(decoded.kind, statusCode);\n return declared === response.status.code ? decoded : undefined;\n }\n\n // webpieces-disable no-any-unknown -- HTTP response bodies are app-owned until this boundary safely inspects them\n private fallbackMessage(body: unknown, reason: string): string {\n if (typeof body === 'object' && body !== null) {\n const message = Object.getOwnPropertyDescriptor(body, 'message')?.value;\n if (typeof message === 'string' && message.length > 0) return message.slice(0, 4096);\n }\n return reason || 'Request Failed';\n }\n\n /** Parse either legal Retry-After form and bound untrusted guidance to one day. */\n private retryAfterSeconds(headers: readonly HttpHeader[]): number | undefined {\n const raw = headers.find(\n (header: HttpHeader) => header.name.toLowerCase() === 'retry-after',\n )?.value;\n if (raw === undefined) return undefined;\n const seconds = Number(raw.trim());\n if (Number.isFinite(seconds) && seconds >= 0) return Math.min(Math.ceil(seconds), 86_400);\n const at = Date.parse(raw);\n if (!Number.isFinite(at)) return undefined;\n return Math.min(Math.max(Math.ceil((at - Date.now()) / 1_000), 0), 86_400);\n }\n}\n\n/** Process-wide built-in instance — stateless, so one shared instance is enough. */\nexport const WEBPIECES_DEFAULT_ERROR_TRANSLATOR = new WebpiecesDefaultErrorTranslator();\n"]}
|
package/src/http/api-kind.js
CHANGED
|
@@ -77,8 +77,8 @@ function assertApiKind(apiClass, expected) {
|
|
|
77
77
|
* Shared so the wiring-time assert below and the build-time architecture scan enforce ONE rule.
|
|
78
78
|
*/
|
|
79
79
|
exports.ENDPOINT_KINDS_BY_API_KIND = {
|
|
80
|
-
rpc: [
|
|
81
|
-
pubsub: [
|
|
80
|
+
rpc: [decorators_1.RPC, decorators_1.EXTERNAL],
|
|
81
|
+
pubsub: [decorators_1.CLOUDTASKS, decorators_1.CRON, decorators_1.EXTERNAL],
|
|
82
82
|
};
|
|
83
83
|
/**
|
|
84
84
|
* Validate @PubSub conventions at wiring time: the class must be @ApiPath + @PubSub, declare at
|