@benzenejs/results 0.0.0-stage → 0.1.0-beta.2
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/dist/BenzeneResult.d.ts +49 -0
- package/dist/BenzeneResult.js +138 -0
- package/dist/BenzeneResultExtensions.d.ts +14 -0
- package/dist/BenzeneResultExtensions.js +55 -0
- package/dist/BenzeneResultStatus.d.ts +33 -0
- package/dist/BenzeneResultStatus.js +59 -0
- package/dist/ProblemDetails.d.ts +52 -0
- package/dist/ProblemDetails.js +55 -0
- package/dist/ProblemTypes.d.ts +58 -0
- package/dist/ProblemTypes.js +111 -0
- package/dist/TransportFailure.d.ts +40 -0
- package/dist/TransportFailure.js +111 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +22 -0
- package/package.json +13 -4
- package/README.md +0 -3
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { BenzeneError, IBenzeneResultOf, VoidResult } from '@benzenejs/abstractions';
|
|
2
|
+
import { ProblemDetails } from './ProblemDetails';
|
|
3
|
+
export declare const BenzeneResult: {
|
|
4
|
+
/** Port of the C# `Set` overloads. */
|
|
5
|
+
readonly set: <T = VoidResult>(status: string, payload?: T, isSuccessful?: boolean) => IBenzeneResultOf<T>;
|
|
6
|
+
/**
|
|
7
|
+
* Port of C# `Set(status, params BenzeneError[] errors)` and its `string[]` sibling: accepts
|
|
8
|
+
* either, so `setErrors(status, 'a message')` and
|
|
9
|
+
* `setErrors(status, { message: 'a message', field: 'Name', code: 'NotEmpty' })` both work.
|
|
10
|
+
*/
|
|
11
|
+
readonly setErrors: <T = VoidResult>(status: string, ...errors: (string | BenzeneError)[]) => IBenzeneResultOf<T>;
|
|
12
|
+
/**
|
|
13
|
+
* Builds a failed result carrying a deliberately-authored problem document - the port of C#
|
|
14
|
+
* `BenzeneResult.Problem<T>(ProblemDetails)`.
|
|
15
|
+
*
|
|
16
|
+
* The status comes from the document's `benzeneStatus`, which is required: a problem document
|
|
17
|
+
* with no status cannot be classified by anything downstream. The result's `errors` are projected
|
|
18
|
+
* from the document's own `errors`, and the result is always unsuccessful - this factory exists
|
|
19
|
+
* to fail deliberately with a richer document than the status-derived one, most often to set an
|
|
20
|
+
* application-owned `type` (wire-contracts.md §1.3).
|
|
21
|
+
*/
|
|
22
|
+
readonly problem: <T = VoidResult>(problem: ProblemDetails) => IBenzeneResultOf<T>;
|
|
23
|
+
/**
|
|
24
|
+
* Builds a failed result for a problem document a CLIENT received off the wire - the port of C#
|
|
25
|
+
* `BenzeneResult.AttachReceivedProblem<T>` (internal there, reached from
|
|
26
|
+
* `Benzene.Clients.Common.ClientResultExtensions`; exported here because the clients live in another
|
|
27
|
+
* package). The status is the one the client decided - the document's `benzeneStatus` where it has one
|
|
28
|
+
* (wire-contracts §4.1), else the envelope/HTTP classification - and is taken as given here; `errors`
|
|
29
|
+
* are the ones rebuilt from the document
|
|
30
|
+
* (core-concepts §12 "Error fidelity", wire-contracts.md §1.3); and the document, when there is one,
|
|
31
|
+
* is attached so `ProblemTypes.from(result)` returns exactly what was received - an application's own
|
|
32
|
+
* problem `type` survives the hop.
|
|
33
|
+
*/
|
|
34
|
+
readonly receivedProblem: <T = VoidResult>(status: string, errors: readonly BenzeneError[], problem?: ProblemDetails) => IBenzeneResultOf<T>;
|
|
35
|
+
readonly ok: <T = VoidResult>(payload?: T) => IBenzeneResultOf<T>;
|
|
36
|
+
readonly created: <T = VoidResult>(payload?: T) => IBenzeneResultOf<T>;
|
|
37
|
+
readonly accepted: <T = VoidResult>(payload?: T) => IBenzeneResultOf<T>;
|
|
38
|
+
readonly updated: <T = VoidResult>(payload?: T) => IBenzeneResultOf<T>;
|
|
39
|
+
readonly deleted: <T = VoidResult>(payload?: T) => IBenzeneResultOf<T>;
|
|
40
|
+
readonly ignored: <T = VoidResult>() => IBenzeneResultOf<T>;
|
|
41
|
+
readonly notFound: <T = VoidResult>(...errors: (string | BenzeneError)[]) => IBenzeneResultOf<T>;
|
|
42
|
+
readonly badRequest: <T = VoidResult>(...errors: (string | BenzeneError)[]) => IBenzeneResultOf<T>;
|
|
43
|
+
readonly unauthorized: <T = VoidResult>(...errors: (string | BenzeneError)[]) => IBenzeneResultOf<T>;
|
|
44
|
+
readonly forbidden: <T = VoidResult>(...errors: (string | BenzeneError)[]) => IBenzeneResultOf<T>;
|
|
45
|
+
readonly validationError: <T = VoidResult>(...errors: (string | BenzeneError)[]) => IBenzeneResultOf<T>;
|
|
46
|
+
readonly serviceUnavailable: <T = VoidResult>(...errors: (string | BenzeneError)[]) => IBenzeneResultOf<T>;
|
|
47
|
+
readonly tooManyRequests: <T = VoidResult>(...errors: (string | BenzeneError)[]) => IBenzeneResultOf<T>;
|
|
48
|
+
readonly unexpectedError: <T = VoidResult>(...errors: (string | BenzeneError)[]) => IBenzeneResultOf<T>;
|
|
49
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BenzeneResult = void 0;
|
|
4
|
+
const abstractions_1 = require("@benzenejs/abstractions");
|
|
5
|
+
const BenzeneResultStatus_1 = require("./BenzeneResultStatus");
|
|
6
|
+
/**
|
|
7
|
+
* Factory for creating result instances.
|
|
8
|
+
* Port of Benzene.Results.BenzeneResult.
|
|
9
|
+
*
|
|
10
|
+
* The C# generic/non-generic overload pairs (`Ok()` / `Ok<T>(payload)`) collapse
|
|
11
|
+
* into single functions with an optional payload defaulting to `VoidResult`
|
|
12
|
+
* (the port of C# `Void`). The status-specific factories mirror the C# names.
|
|
13
|
+
*/
|
|
14
|
+
class BenzeneResultInternal {
|
|
15
|
+
status;
|
|
16
|
+
payload;
|
|
17
|
+
errors;
|
|
18
|
+
isSuccessful;
|
|
19
|
+
constructor(status, payload, errors, isSuccessful) {
|
|
20
|
+
this.status = status;
|
|
21
|
+
this.payload = payload;
|
|
22
|
+
this.errors = errors;
|
|
23
|
+
this.isSuccessful = isSuccessful;
|
|
24
|
+
}
|
|
25
|
+
get payloadAsObject() {
|
|
26
|
+
return this.payload;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Widens the caller's errors to structured {@link BenzeneError}s. A plain string becomes
|
|
31
|
+
* `{ message }` - the shape a message-only failure has always had on the wire - so every existing
|
|
32
|
+
* call site that passes strings is unchanged, while a caller that has a `field` or `code` can now
|
|
33
|
+
* pass those through instead of losing them.
|
|
34
|
+
*/
|
|
35
|
+
function toBenzeneErrors(errors) {
|
|
36
|
+
return errors.map((error) => (typeof error === 'string' ? { message: error } : error));
|
|
37
|
+
}
|
|
38
|
+
function create(status, payload, errors = [], isSuccessful) {
|
|
39
|
+
return new BenzeneResultInternal(status, payload, errors, isSuccessful ?? BenzeneResultStatus_1.BenzeneResultStatus.isSuccess(status));
|
|
40
|
+
}
|
|
41
|
+
const voidPayload = new abstractions_1.VoidResult();
|
|
42
|
+
exports.BenzeneResult = {
|
|
43
|
+
/** Port of the C# `Set` overloads. */
|
|
44
|
+
set(status, payload, isSuccessful) {
|
|
45
|
+
return create(status, (payload ?? voidPayload), [], isSuccessful);
|
|
46
|
+
},
|
|
47
|
+
/**
|
|
48
|
+
* Port of C# `Set(status, params BenzeneError[] errors)` and its `string[]` sibling: accepts
|
|
49
|
+
* either, so `setErrors(status, 'a message')` and
|
|
50
|
+
* `setErrors(status, { message: 'a message', field: 'Name', code: 'NotEmpty' })` both work.
|
|
51
|
+
*/
|
|
52
|
+
setErrors(status, ...errors) {
|
|
53
|
+
return create(status, voidPayload, toBenzeneErrors(errors));
|
|
54
|
+
},
|
|
55
|
+
/**
|
|
56
|
+
* Builds a failed result carrying a deliberately-authored problem document - the port of C#
|
|
57
|
+
* `BenzeneResult.Problem<T>(ProblemDetails)`.
|
|
58
|
+
*
|
|
59
|
+
* The status comes from the document's `benzeneStatus`, which is required: a problem document
|
|
60
|
+
* with no status cannot be classified by anything downstream. The result's `errors` are projected
|
|
61
|
+
* from the document's own `errors`, and the result is always unsuccessful - this factory exists
|
|
62
|
+
* to fail deliberately with a richer document than the status-derived one, most often to set an
|
|
63
|
+
* application-owned `type` (wire-contracts.md §1.3).
|
|
64
|
+
*/
|
|
65
|
+
problem(problem) {
|
|
66
|
+
if (problem.benzeneStatus === undefined || problem.benzeneStatus === '') {
|
|
67
|
+
throw new Error('problem.benzeneStatus is required to build a BenzeneResult.problem(...) - a problem ' +
|
|
68
|
+
'document with no status cannot be classified by anything downstream. Set ' +
|
|
69
|
+
'ProblemDetails.benzeneStatus before passing it here.');
|
|
70
|
+
}
|
|
71
|
+
const result = create(problem.benzeneStatus, voidPayload, [...(problem.errors ?? [])], false);
|
|
72
|
+
// The authored document travels alongside the result so the response mapper emits it verbatim
|
|
73
|
+
// rather than re-deriving one from the status - otherwise an application-owned `type` would be
|
|
74
|
+
// overwritten by the registry URI on the way out.
|
|
75
|
+
result.problem = problem;
|
|
76
|
+
return result;
|
|
77
|
+
},
|
|
78
|
+
/**
|
|
79
|
+
* Builds a failed result for a problem document a CLIENT received off the wire - the port of C#
|
|
80
|
+
* `BenzeneResult.AttachReceivedProblem<T>` (internal there, reached from
|
|
81
|
+
* `Benzene.Clients.Common.ClientResultExtensions`; exported here because the clients live in another
|
|
82
|
+
* package). The status is the one the client decided - the document's `benzeneStatus` where it has one
|
|
83
|
+
* (wire-contracts §4.1), else the envelope/HTTP classification - and is taken as given here; `errors`
|
|
84
|
+
* are the ones rebuilt from the document
|
|
85
|
+
* (core-concepts §12 "Error fidelity", wire-contracts.md §1.3); and the document, when there is one,
|
|
86
|
+
* is attached so `ProblemTypes.from(result)` returns exactly what was received - an application's own
|
|
87
|
+
* problem `type` survives the hop.
|
|
88
|
+
*/
|
|
89
|
+
receivedProblem(status, errors, problem) {
|
|
90
|
+
const result = create(status, voidPayload, [...errors], false);
|
|
91
|
+
if (problem !== undefined) {
|
|
92
|
+
result.problem = problem;
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
},
|
|
96
|
+
ok(payload) {
|
|
97
|
+
return exports.BenzeneResult.set(BenzeneResultStatus_1.BenzeneResultStatus.ok, payload);
|
|
98
|
+
},
|
|
99
|
+
created(payload) {
|
|
100
|
+
return exports.BenzeneResult.set(BenzeneResultStatus_1.BenzeneResultStatus.created, payload);
|
|
101
|
+
},
|
|
102
|
+
accepted(payload) {
|
|
103
|
+
return exports.BenzeneResult.set(BenzeneResultStatus_1.BenzeneResultStatus.accepted, payload);
|
|
104
|
+
},
|
|
105
|
+
updated(payload) {
|
|
106
|
+
return exports.BenzeneResult.set(BenzeneResultStatus_1.BenzeneResultStatus.updated, payload);
|
|
107
|
+
},
|
|
108
|
+
deleted(payload) {
|
|
109
|
+
return exports.BenzeneResult.set(BenzeneResultStatus_1.BenzeneResultStatus.deleted, payload);
|
|
110
|
+
},
|
|
111
|
+
ignored() {
|
|
112
|
+
return exports.BenzeneResult.set(BenzeneResultStatus_1.BenzeneResultStatus.ignored);
|
|
113
|
+
},
|
|
114
|
+
notFound(...errors) {
|
|
115
|
+
return exports.BenzeneResult.setErrors(BenzeneResultStatus_1.BenzeneResultStatus.notFound, ...errors);
|
|
116
|
+
},
|
|
117
|
+
badRequest(...errors) {
|
|
118
|
+
return exports.BenzeneResult.setErrors(BenzeneResultStatus_1.BenzeneResultStatus.badRequest, ...errors);
|
|
119
|
+
},
|
|
120
|
+
unauthorized(...errors) {
|
|
121
|
+
return exports.BenzeneResult.setErrors(BenzeneResultStatus_1.BenzeneResultStatus.unauthorized, ...errors);
|
|
122
|
+
},
|
|
123
|
+
forbidden(...errors) {
|
|
124
|
+
return exports.BenzeneResult.setErrors(BenzeneResultStatus_1.BenzeneResultStatus.forbidden, ...errors);
|
|
125
|
+
},
|
|
126
|
+
validationError(...errors) {
|
|
127
|
+
return exports.BenzeneResult.setErrors(BenzeneResultStatus_1.BenzeneResultStatus.validationError, ...errors);
|
|
128
|
+
},
|
|
129
|
+
serviceUnavailable(...errors) {
|
|
130
|
+
return exports.BenzeneResult.setErrors(BenzeneResultStatus_1.BenzeneResultStatus.serviceUnavailable, ...errors);
|
|
131
|
+
},
|
|
132
|
+
tooManyRequests(...errors) {
|
|
133
|
+
return exports.BenzeneResult.setErrors(BenzeneResultStatus_1.BenzeneResultStatus.tooManyRequests, ...errors);
|
|
134
|
+
},
|
|
135
|
+
unexpectedError(...errors) {
|
|
136
|
+
return exports.BenzeneResult.setErrors(BenzeneResultStatus_1.BenzeneResultStatus.unexpectedError, ...errors);
|
|
137
|
+
},
|
|
138
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { IBenzeneResultOf, VoidResult } from '@benzenejs/abstractions';
|
|
2
|
+
/**
|
|
3
|
+
* Free-function ports of the `Benzene.Results.BenzeneResultExtensions` C# extension methods.
|
|
4
|
+
*
|
|
5
|
+
* Only the HTTP-status reverse mapping (`Convert(this HttpStatusCode)`) is ported here so far — the
|
|
6
|
+
* client-side mapping of an HTTP response code back to a Benzene result status
|
|
7
|
+
* (`wire-contracts.md` §4.1, the "reverse" table). The `As`/`AsTask` result-shape helpers are ported
|
|
8
|
+
* elsewhere as they are needed.
|
|
9
|
+
*
|
|
10
|
+
* TS bend: .NET keys the switch on the `System.Net.HttpStatusCode` enum; TypeScript has no such enum,
|
|
11
|
+
* so the port takes the numeric status code directly (`convertHttpStatusCode(204)`), which is how the
|
|
12
|
+
* conformance runner and HTTP clients already carry it. The mapping is byte-for-byte the same table.
|
|
13
|
+
*/
|
|
14
|
+
export declare function convertHttpStatusCode(httpStatusCode: number): IBenzeneResultOf<VoidResult>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.convertHttpStatusCode = convertHttpStatusCode;
|
|
4
|
+
const BenzeneResult_1 = require("./BenzeneResult");
|
|
5
|
+
const BenzeneResultStatus_1 = require("./BenzeneResultStatus");
|
|
6
|
+
/**
|
|
7
|
+
* Free-function ports of the `Benzene.Results.BenzeneResultExtensions` C# extension methods.
|
|
8
|
+
*
|
|
9
|
+
* Only the HTTP-status reverse mapping (`Convert(this HttpStatusCode)`) is ported here so far — the
|
|
10
|
+
* client-side mapping of an HTTP response code back to a Benzene result status
|
|
11
|
+
* (`wire-contracts.md` §4.1, the "reverse" table). The `As`/`AsTask` result-shape helpers are ported
|
|
12
|
+
* elsewhere as they are needed.
|
|
13
|
+
*
|
|
14
|
+
* TS bend: .NET keys the switch on the `System.Net.HttpStatusCode` enum; TypeScript has no such enum,
|
|
15
|
+
* so the port takes the numeric status code directly (`convertHttpStatusCode(204)`), which is how the
|
|
16
|
+
* conformance runner and HTTP clients already carry it. The mapping is byte-for-byte the same table.
|
|
17
|
+
*/
|
|
18
|
+
function convertHttpStatusCode(httpStatusCode) {
|
|
19
|
+
switch (httpStatusCode) {
|
|
20
|
+
case 202:
|
|
21
|
+
return BenzeneResult_1.BenzeneResult.accepted();
|
|
22
|
+
case 200:
|
|
23
|
+
return BenzeneResult_1.BenzeneResult.ok();
|
|
24
|
+
case 201:
|
|
25
|
+
return BenzeneResult_1.BenzeneResult.created();
|
|
26
|
+
case 204:
|
|
27
|
+
return BenzeneResult_1.BenzeneResult.deleted();
|
|
28
|
+
case 404:
|
|
29
|
+
return BenzeneResult_1.BenzeneResult.notFound();
|
|
30
|
+
case 400:
|
|
31
|
+
return BenzeneResult_1.BenzeneResult.badRequest();
|
|
32
|
+
case 409:
|
|
33
|
+
return BenzeneResult_1.BenzeneResult.setErrors(BenzeneResultStatus_1.BenzeneResultStatus.conflict);
|
|
34
|
+
case 403:
|
|
35
|
+
return BenzeneResult_1.BenzeneResult.forbidden();
|
|
36
|
+
case 401:
|
|
37
|
+
return BenzeneResult_1.BenzeneResult.unauthorized();
|
|
38
|
+
case 422:
|
|
39
|
+
return BenzeneResult_1.BenzeneResult.validationError();
|
|
40
|
+
case 429:
|
|
41
|
+
return BenzeneResult_1.BenzeneResult.tooManyRequests();
|
|
42
|
+
case 408:
|
|
43
|
+
return BenzeneResult_1.BenzeneResult.setErrors(BenzeneResultStatus_1.BenzeneResultStatus.timeout);
|
|
44
|
+
case 504:
|
|
45
|
+
return BenzeneResult_1.BenzeneResult.setErrors(BenzeneResultStatus_1.BenzeneResultStatus.timeout);
|
|
46
|
+
case 501:
|
|
47
|
+
return BenzeneResult_1.BenzeneResult.setErrors(BenzeneResultStatus_1.BenzeneResultStatus.notImplemented);
|
|
48
|
+
case 502:
|
|
49
|
+
return BenzeneResult_1.BenzeneResult.serviceUnavailable();
|
|
50
|
+
case 503:
|
|
51
|
+
return BenzeneResult_1.BenzeneResult.serviceUnavailable();
|
|
52
|
+
default:
|
|
53
|
+
return BenzeneResult_1.BenzeneResult.unexpectedError();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Well-known result statuses and their success/failure classification.
|
|
3
|
+
* Port of Benzene.Results.BenzeneResultStatus.
|
|
4
|
+
*
|
|
5
|
+
* The string VALUES are the normative **wire** status vocabulary from `docs/specification/wire-contracts.md`
|
|
6
|
+
* §3 - lowercase-kebab-case and case-sensitive (`ok`, `not-found`, `validation-error`), identical to the
|
|
7
|
+
* .NET constants. This is a cross-language contract: a Benzene service in any language writes these exact
|
|
8
|
+
* strings as the response envelope's `statusCode`, so a TypeScript service and a .NET service (or mesh
|
|
9
|
+
* aggregator) reading each other's responses classify statuses identically.
|
|
10
|
+
*/
|
|
11
|
+
export declare const BenzeneResultStatus: {
|
|
12
|
+
readonly accepted: "accepted";
|
|
13
|
+
readonly ok: "ok";
|
|
14
|
+
readonly created: "created";
|
|
15
|
+
readonly updated: "updated";
|
|
16
|
+
readonly deleted: "deleted";
|
|
17
|
+
readonly ignored: "ignored";
|
|
18
|
+
readonly notFound: "not-found";
|
|
19
|
+
readonly badRequest: "bad-request";
|
|
20
|
+
readonly validationError: "validation-error";
|
|
21
|
+
readonly serviceUnavailable: "service-unavailable";
|
|
22
|
+
readonly notImplemented: "not-implemented";
|
|
23
|
+
readonly unexpectedError: "unexpected-error";
|
|
24
|
+
readonly conflict: "conflict";
|
|
25
|
+
readonly forbidden: "forbidden";
|
|
26
|
+
readonly unauthorized: "unauthorized";
|
|
27
|
+
readonly tooManyRequests: "too-many-requests";
|
|
28
|
+
readonly timeout: "timeout";
|
|
29
|
+
readonly isSuccess: (status: string | undefined) => boolean;
|
|
30
|
+
readonly isFailure: (status: string | undefined) => boolean;
|
|
31
|
+
readonly isKnown: (status: string | undefined) => boolean;
|
|
32
|
+
readonly isTransient: (status: string | undefined) => boolean;
|
|
33
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BenzeneResultStatus = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Well-known result statuses and their success/failure classification.
|
|
6
|
+
* Port of Benzene.Results.BenzeneResultStatus.
|
|
7
|
+
*
|
|
8
|
+
* The string VALUES are the normative **wire** status vocabulary from `docs/specification/wire-contracts.md`
|
|
9
|
+
* §3 - lowercase-kebab-case and case-sensitive (`ok`, `not-found`, `validation-error`), identical to the
|
|
10
|
+
* .NET constants. This is a cross-language contract: a Benzene service in any language writes these exact
|
|
11
|
+
* strings as the response envelope's `statusCode`, so a TypeScript service and a .NET service (or mesh
|
|
12
|
+
* aggregator) reading each other's responses classify statuses identically.
|
|
13
|
+
*/
|
|
14
|
+
exports.BenzeneResultStatus = {
|
|
15
|
+
accepted: 'accepted',
|
|
16
|
+
ok: 'ok',
|
|
17
|
+
created: 'created',
|
|
18
|
+
updated: 'updated',
|
|
19
|
+
deleted: 'deleted',
|
|
20
|
+
ignored: 'ignored',
|
|
21
|
+
notFound: 'not-found',
|
|
22
|
+
badRequest: 'bad-request',
|
|
23
|
+
validationError: 'validation-error',
|
|
24
|
+
serviceUnavailable: 'service-unavailable',
|
|
25
|
+
notImplemented: 'not-implemented',
|
|
26
|
+
unexpectedError: 'unexpected-error',
|
|
27
|
+
conflict: 'conflict',
|
|
28
|
+
forbidden: 'forbidden',
|
|
29
|
+
unauthorized: 'unauthorized',
|
|
30
|
+
tooManyRequests: 'too-many-requests',
|
|
31
|
+
timeout: 'timeout',
|
|
32
|
+
isSuccess(status) {
|
|
33
|
+
return status !== undefined && successStatuses.has(status);
|
|
34
|
+
},
|
|
35
|
+
isFailure(status) {
|
|
36
|
+
return status !== undefined && failureStatuses.has(status);
|
|
37
|
+
},
|
|
38
|
+
isKnown(status) {
|
|
39
|
+
return exports.BenzeneResultStatus.isSuccess(status) || exports.BenzeneResultStatus.isFailure(status);
|
|
40
|
+
},
|
|
41
|
+
isTransient(status) {
|
|
42
|
+
return status !== undefined && transientStatuses.has(status);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
const successStatuses = new Set(['ok', 'created', 'accepted', 'updated', 'deleted', 'ignored']);
|
|
46
|
+
const failureStatuses = new Set([
|
|
47
|
+
'bad-request',
|
|
48
|
+
'validation-error',
|
|
49
|
+
'unauthorized',
|
|
50
|
+
'forbidden',
|
|
51
|
+
'not-found',
|
|
52
|
+
'conflict',
|
|
53
|
+
'too-many-requests',
|
|
54
|
+
'timeout',
|
|
55
|
+
'not-implemented',
|
|
56
|
+
'service-unavailable',
|
|
57
|
+
'unexpected-error',
|
|
58
|
+
]);
|
|
59
|
+
const transientStatuses = new Set(['service-unavailable', 'too-many-requests', 'timeout']);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { BenzeneError } from '@benzenejs/abstractions';
|
|
2
|
+
/**
|
|
3
|
+
* An [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem document — the body of a failed
|
|
4
|
+
* result (wire-contracts.md §1.3). Port of Benzene.Results.ProblemDetails.
|
|
5
|
+
*
|
|
6
|
+
* Deviation: C# declares nullable auto-properties with `[JsonIgnore(WhenWritingNull)]`; the port
|
|
7
|
+
* types them optional (`| undefined`) so an unset member is simply omitted by `JSON.stringify`
|
|
8
|
+
* rather than serialized as an explicit `null` the way System.Text.Json would.
|
|
9
|
+
*/
|
|
10
|
+
export declare class ProblemDetails {
|
|
11
|
+
/**
|
|
12
|
+
* A URI reference identifying the problem *type*. Framework-produced failures use the registry
|
|
13
|
+
* URI for the status (`ProblemTypes`); applications should use their own absolute URI. Readers
|
|
14
|
+
* treat it as an **opaque identifier** — compare by string equality, never dereference.
|
|
15
|
+
*/
|
|
16
|
+
type: string | undefined;
|
|
17
|
+
/** A short human summary of the *type*, fixed per type. Wording is never asserted by fixtures. */
|
|
18
|
+
title: string | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* The **HTTP** response status code — HTTP bindings only, and a `number`, not the Benzene status.
|
|
21
|
+
* Must equal the actual HTTP response status when present, and must be **omitted** where no HTTP
|
|
22
|
+
* response exists (an envelope over a queue), which is why it is filled in by the HTTP-aware
|
|
23
|
+
* response mapper rather than by the transport-neutral `ProblemTypes.from`. Clients must not
|
|
24
|
+
* classify success/failure from this member — classification is envelope-first (§1.2).
|
|
25
|
+
*/
|
|
26
|
+
status: number | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Human-readable detail for this occurrence — the result's error messages joined with `", "`.
|
|
29
|
+
* The compatibility member: every reader of the pre-RFC-9457 `ErrorPayload` shape used only this
|
|
30
|
+
* one and keeps working unchanged.
|
|
31
|
+
*/
|
|
32
|
+
detail: string | undefined;
|
|
33
|
+
/** Optional, application-owned. The framework never fabricates a value. */
|
|
34
|
+
instance: string | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* **Required on framework-produced documents.** The Benzene status string (§3), mirroring the
|
|
37
|
+
* envelope's `statusCode`. This is the transport-neutral discriminator — present on every
|
|
38
|
+
* transport, whereas {@link status} only exists on HTTP.
|
|
39
|
+
*
|
|
40
|
+
* It replaced a `status: string` member that carried the Benzene status and collided with RFC
|
|
41
|
+
* 9457's own `status` (defined as the integer HTTP code). The collision was resolved by rename,
|
|
42
|
+
* not by dropping the RFC alignment. The `benzene` marker is there because this member namespace
|
|
43
|
+
* is shared with RFC 9457 itself and with applications.
|
|
44
|
+
*/
|
|
45
|
+
benzeneStatus: string | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* The result's errors, when present — authoritative and ordered. Omitted from the wire when the
|
|
48
|
+
* result carries none; a reader without this member treats {@link detail} as a single opaque
|
|
49
|
+
* message.
|
|
50
|
+
*/
|
|
51
|
+
errors: BenzeneError[] | undefined;
|
|
52
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ProblemDetails = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* An [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem document — the body of a failed
|
|
6
|
+
* result (wire-contracts.md §1.3). Port of Benzene.Results.ProblemDetails.
|
|
7
|
+
*
|
|
8
|
+
* Deviation: C# declares nullable auto-properties with `[JsonIgnore(WhenWritingNull)]`; the port
|
|
9
|
+
* types them optional (`| undefined`) so an unset member is simply omitted by `JSON.stringify`
|
|
10
|
+
* rather than serialized as an explicit `null` the way System.Text.Json would.
|
|
11
|
+
*/
|
|
12
|
+
class ProblemDetails {
|
|
13
|
+
/**
|
|
14
|
+
* A URI reference identifying the problem *type*. Framework-produced failures use the registry
|
|
15
|
+
* URI for the status (`ProblemTypes`); applications should use their own absolute URI. Readers
|
|
16
|
+
* treat it as an **opaque identifier** — compare by string equality, never dereference.
|
|
17
|
+
*/
|
|
18
|
+
type;
|
|
19
|
+
/** A short human summary of the *type*, fixed per type. Wording is never asserted by fixtures. */
|
|
20
|
+
title;
|
|
21
|
+
/**
|
|
22
|
+
* The **HTTP** response status code — HTTP bindings only, and a `number`, not the Benzene status.
|
|
23
|
+
* Must equal the actual HTTP response status when present, and must be **omitted** where no HTTP
|
|
24
|
+
* response exists (an envelope over a queue), which is why it is filled in by the HTTP-aware
|
|
25
|
+
* response mapper rather than by the transport-neutral `ProblemTypes.from`. Clients must not
|
|
26
|
+
* classify success/failure from this member — classification is envelope-first (§1.2).
|
|
27
|
+
*/
|
|
28
|
+
status;
|
|
29
|
+
/**
|
|
30
|
+
* Human-readable detail for this occurrence — the result's error messages joined with `", "`.
|
|
31
|
+
* The compatibility member: every reader of the pre-RFC-9457 `ErrorPayload` shape used only this
|
|
32
|
+
* one and keeps working unchanged.
|
|
33
|
+
*/
|
|
34
|
+
detail;
|
|
35
|
+
/** Optional, application-owned. The framework never fabricates a value. */
|
|
36
|
+
instance;
|
|
37
|
+
/**
|
|
38
|
+
* **Required on framework-produced documents.** The Benzene status string (§3), mirroring the
|
|
39
|
+
* envelope's `statusCode`. This is the transport-neutral discriminator — present on every
|
|
40
|
+
* transport, whereas {@link status} only exists on HTTP.
|
|
41
|
+
*
|
|
42
|
+
* It replaced a `status: string` member that carried the Benzene status and collided with RFC
|
|
43
|
+
* 9457's own `status` (defined as the integer HTTP code). The collision was resolved by rename,
|
|
44
|
+
* not by dropping the RFC alignment. The `benzene` marker is there because this member namespace
|
|
45
|
+
* is shared with RFC 9457 itself and with applications.
|
|
46
|
+
*/
|
|
47
|
+
benzeneStatus;
|
|
48
|
+
/**
|
|
49
|
+
* The result's errors, when present — authoritative and ordered. Omitted from the wire when the
|
|
50
|
+
* result carries none; a reader without this member treats {@link detail} as a single opaque
|
|
51
|
+
* message.
|
|
52
|
+
*/
|
|
53
|
+
errors;
|
|
54
|
+
}
|
|
55
|
+
exports.ProblemDetails = ProblemDetails;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { IBenzeneResult } from '@benzenejs/abstractions';
|
|
2
|
+
import { ProblemDetails } from './ProblemDetails';
|
|
3
|
+
export declare const ProblemTypes: {
|
|
4
|
+
/** The {@link ProblemDetails.type} URI for `bad-request`. */
|
|
5
|
+
readonly badRequest: "https://benzene.app/problems/bad-request";
|
|
6
|
+
/** The {@link ProblemDetails.type} URI for `unauthorized`. */
|
|
7
|
+
readonly unauthorized: "https://benzene.app/problems/unauthorized";
|
|
8
|
+
/** The {@link ProblemDetails.type} URI for `forbidden`. */
|
|
9
|
+
readonly forbidden: "https://benzene.app/problems/forbidden";
|
|
10
|
+
/** The {@link ProblemDetails.type} URI for `not-found`. */
|
|
11
|
+
readonly notFound: "https://benzene.app/problems/not-found";
|
|
12
|
+
/** The {@link ProblemDetails.type} URI for `conflict`. */
|
|
13
|
+
readonly conflict: "https://benzene.app/problems/conflict";
|
|
14
|
+
/** The {@link ProblemDetails.type} URI for `validation-error`. */
|
|
15
|
+
readonly validationError: "https://benzene.app/problems/validation-error";
|
|
16
|
+
/** The {@link ProblemDetails.type} URI for `too-many-requests`. */
|
|
17
|
+
readonly tooManyRequests: "https://benzene.app/problems/too-many-requests";
|
|
18
|
+
/** The {@link ProblemDetails.type} URI for `unexpected-error`. */
|
|
19
|
+
readonly unexpectedError: "https://benzene.app/problems/unexpected-error";
|
|
20
|
+
/** The {@link ProblemDetails.type} URI for `not-implemented`. */
|
|
21
|
+
readonly notImplemented: "https://benzene.app/problems/not-implemented";
|
|
22
|
+
/** The {@link ProblemDetails.type} URI for `service-unavailable`. */
|
|
23
|
+
readonly serviceUnavailable: "https://benzene.app/problems/service-unavailable";
|
|
24
|
+
/** The {@link ProblemDetails.type} URI for `timeout`. */
|
|
25
|
+
readonly timeout: "https://benzene.app/problems/timeout";
|
|
26
|
+
/**
|
|
27
|
+
* The registry {@link ProblemDetails.type} URI for `status`, or `undefined` for an
|
|
28
|
+
* application-defined status the registry doesn't know (the application owns its own `type`, or
|
|
29
|
+
* omits one).
|
|
30
|
+
*/
|
|
31
|
+
readonly typeFor: (status: string | undefined) => string | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* The registry {@link ProblemDetails.title} for `status`, or `undefined` for an
|
|
34
|
+
* application-defined status the registry doesn't know.
|
|
35
|
+
*/
|
|
36
|
+
readonly titleFor: (status: string | undefined) => string | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* The HTTP status code the registry maps `status` to, or `500` for an application-defined status —
|
|
39
|
+
* the same "unknown status falls to the generic-error row" rule the {@link BenzeneResultStatus}-based
|
|
40
|
+
* HTTP mappings use elsewhere (e.g. `DefaultHttpStatusCodeMapper`).
|
|
41
|
+
*/
|
|
42
|
+
readonly httpStatusFor: (status: string | undefined) => number;
|
|
43
|
+
/**
|
|
44
|
+
* Builds the problem document for a failed `result`: {@link ProblemDetails.type}/
|
|
45
|
+
* {@link ProblemDetails.title} from the registry (both `undefined` for an application-defined
|
|
46
|
+
* status), {@link ProblemDetails.detail} as the result's error messages joined with `", "`,
|
|
47
|
+
* {@link ProblemDetails.benzeneStatus} as the result's status, and {@link ProblemDetails.errors}
|
|
48
|
+
* as the result's structured errors when non-empty (else `undefined`, so the member is omitted
|
|
49
|
+
* from the wire). Deliberately never sets {@link ProblemDetails.status} — the numeric HTTP status
|
|
50
|
+
* is an HTTP-binding concern, filled in by the HTTP-aware response mapper, not this
|
|
51
|
+
* transport-neutral factory.
|
|
52
|
+
*
|
|
53
|
+
* A result built by `BenzeneResult.problem(...)` carries its authored document, which is returned
|
|
54
|
+
* verbatim: re-deriving one here would overwrite an application-owned `type` with the registry
|
|
55
|
+
* URI, which is the whole reason that factory exists (wire-contracts.md §1.3).
|
|
56
|
+
*/
|
|
57
|
+
readonly from: (result: IBenzeneResult) => ProblemDetails;
|
|
58
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ProblemTypes = void 0;
|
|
4
|
+
const BenzeneResultStatus_1 = require("./BenzeneResultStatus");
|
|
5
|
+
const ProblemDetails_1 = require("./ProblemDetails");
|
|
6
|
+
/**
|
|
7
|
+
* The problem-type registry: `type` URI / `title` / HTTP status keyed by the existing
|
|
8
|
+
* {@link BenzeneResultStatus} failure vocabulary — no new taxonomy (see
|
|
9
|
+
* `docs/specification/wire-contracts.md` §3.1 in the cross-language Benzene spec repo).
|
|
10
|
+
* Port of Benzene.Results.ProblemTypes.
|
|
11
|
+
*
|
|
12
|
+
* A pure, static, spec-pinned table, matching {@link BenzeneResultStatus}'s own pattern: C#'s
|
|
13
|
+
* `static class` of `const string` members plus static methods collapses into a frozen object
|
|
14
|
+
* literal here.
|
|
15
|
+
*/
|
|
16
|
+
const baseUri = 'https://benzene.app/problems/';
|
|
17
|
+
const unknownHttpStatus = 500;
|
|
18
|
+
// One row per failure status (docs/specification/wire-contracts.md §3.1). Success statuses have no
|
|
19
|
+
// problem type - problems exist only on failure - so they're deliberately absent here.
|
|
20
|
+
const registry = {
|
|
21
|
+
[BenzeneResultStatus_1.BenzeneResultStatus.badRequest]: { type: `${baseUri}bad-request`, title: 'Bad request', httpStatus: 400 },
|
|
22
|
+
[BenzeneResultStatus_1.BenzeneResultStatus.unauthorized]: { type: `${baseUri}unauthorized`, title: 'Unauthorized', httpStatus: 401 },
|
|
23
|
+
[BenzeneResultStatus_1.BenzeneResultStatus.forbidden]: { type: `${baseUri}forbidden`, title: 'Forbidden', httpStatus: 403 },
|
|
24
|
+
[BenzeneResultStatus_1.BenzeneResultStatus.notFound]: { type: `${baseUri}not-found`, title: 'Not found', httpStatus: 404 },
|
|
25
|
+
[BenzeneResultStatus_1.BenzeneResultStatus.conflict]: { type: `${baseUri}conflict`, title: 'Conflict', httpStatus: 409 },
|
|
26
|
+
[BenzeneResultStatus_1.BenzeneResultStatus.validationError]: { type: `${baseUri}validation-error`, title: 'Validation failed', httpStatus: 422 },
|
|
27
|
+
[BenzeneResultStatus_1.BenzeneResultStatus.tooManyRequests]: { type: `${baseUri}too-many-requests`, title: 'Too many requests', httpStatus: 429 },
|
|
28
|
+
[BenzeneResultStatus_1.BenzeneResultStatus.unexpectedError]: { type: `${baseUri}unexpected-error`, title: 'Unexpected error', httpStatus: 500 },
|
|
29
|
+
[BenzeneResultStatus_1.BenzeneResultStatus.notImplemented]: { type: `${baseUri}not-implemented`, title: 'Not implemented', httpStatus: 501 },
|
|
30
|
+
[BenzeneResultStatus_1.BenzeneResultStatus.serviceUnavailable]: { type: `${baseUri}service-unavailable`, title: 'Service unavailable', httpStatus: 503 },
|
|
31
|
+
[BenzeneResultStatus_1.BenzeneResultStatus.timeout]: { type: `${baseUri}timeout`, title: 'Timeout', httpStatus: 504 },
|
|
32
|
+
};
|
|
33
|
+
function rowFor(status) {
|
|
34
|
+
return status === undefined ? undefined : registry[status];
|
|
35
|
+
}
|
|
36
|
+
exports.ProblemTypes = {
|
|
37
|
+
/** The {@link ProblemDetails.type} URI for `bad-request`. */
|
|
38
|
+
badRequest: `${baseUri}bad-request`,
|
|
39
|
+
/** The {@link ProblemDetails.type} URI for `unauthorized`. */
|
|
40
|
+
unauthorized: `${baseUri}unauthorized`,
|
|
41
|
+
/** The {@link ProblemDetails.type} URI for `forbidden`. */
|
|
42
|
+
forbidden: `${baseUri}forbidden`,
|
|
43
|
+
/** The {@link ProblemDetails.type} URI for `not-found`. */
|
|
44
|
+
notFound: `${baseUri}not-found`,
|
|
45
|
+
/** The {@link ProblemDetails.type} URI for `conflict`. */
|
|
46
|
+
conflict: `${baseUri}conflict`,
|
|
47
|
+
/** The {@link ProblemDetails.type} URI for `validation-error`. */
|
|
48
|
+
validationError: `${baseUri}validation-error`,
|
|
49
|
+
/** The {@link ProblemDetails.type} URI for `too-many-requests`. */
|
|
50
|
+
tooManyRequests: `${baseUri}too-many-requests`,
|
|
51
|
+
/** The {@link ProblemDetails.type} URI for `unexpected-error`. */
|
|
52
|
+
unexpectedError: `${baseUri}unexpected-error`,
|
|
53
|
+
/** The {@link ProblemDetails.type} URI for `not-implemented`. */
|
|
54
|
+
notImplemented: `${baseUri}not-implemented`,
|
|
55
|
+
/** The {@link ProblemDetails.type} URI for `service-unavailable`. */
|
|
56
|
+
serviceUnavailable: `${baseUri}service-unavailable`,
|
|
57
|
+
/** The {@link ProblemDetails.type} URI for `timeout`. */
|
|
58
|
+
timeout: `${baseUri}timeout`,
|
|
59
|
+
/**
|
|
60
|
+
* The registry {@link ProblemDetails.type} URI for `status`, or `undefined` for an
|
|
61
|
+
* application-defined status the registry doesn't know (the application owns its own `type`, or
|
|
62
|
+
* omits one).
|
|
63
|
+
*/
|
|
64
|
+
typeFor(status) {
|
|
65
|
+
return rowFor(status)?.type;
|
|
66
|
+
},
|
|
67
|
+
/**
|
|
68
|
+
* The registry {@link ProblemDetails.title} for `status`, or `undefined` for an
|
|
69
|
+
* application-defined status the registry doesn't know.
|
|
70
|
+
*/
|
|
71
|
+
titleFor(status) {
|
|
72
|
+
return rowFor(status)?.title;
|
|
73
|
+
},
|
|
74
|
+
/**
|
|
75
|
+
* The HTTP status code the registry maps `status` to, or `500` for an application-defined status —
|
|
76
|
+
* the same "unknown status falls to the generic-error row" rule the {@link BenzeneResultStatus}-based
|
|
77
|
+
* HTTP mappings use elsewhere (e.g. `DefaultHttpStatusCodeMapper`).
|
|
78
|
+
*/
|
|
79
|
+
httpStatusFor(status) {
|
|
80
|
+
return rowFor(status)?.httpStatus ?? unknownHttpStatus;
|
|
81
|
+
},
|
|
82
|
+
/**
|
|
83
|
+
* Builds the problem document for a failed `result`: {@link ProblemDetails.type}/
|
|
84
|
+
* {@link ProblemDetails.title} from the registry (both `undefined` for an application-defined
|
|
85
|
+
* status), {@link ProblemDetails.detail} as the result's error messages joined with `", "`,
|
|
86
|
+
* {@link ProblemDetails.benzeneStatus} as the result's status, and {@link ProblemDetails.errors}
|
|
87
|
+
* as the result's structured errors when non-empty (else `undefined`, so the member is omitted
|
|
88
|
+
* from the wire). Deliberately never sets {@link ProblemDetails.status} — the numeric HTTP status
|
|
89
|
+
* is an HTTP-binding concern, filled in by the HTTP-aware response mapper, not this
|
|
90
|
+
* transport-neutral factory.
|
|
91
|
+
*
|
|
92
|
+
* A result built by `BenzeneResult.problem(...)` carries its authored document, which is returned
|
|
93
|
+
* verbatim: re-deriving one here would overwrite an application-owned `type` with the registry
|
|
94
|
+
* URI, which is the whole reason that factory exists (wire-contracts.md §1.3).
|
|
95
|
+
*/
|
|
96
|
+
from(result) {
|
|
97
|
+
const authored = result.problem;
|
|
98
|
+
if (authored !== undefined) {
|
|
99
|
+
return authored;
|
|
100
|
+
}
|
|
101
|
+
const errors = result.errors ?? [];
|
|
102
|
+
const detail = errors.map((error) => error.message).join(', ');
|
|
103
|
+
const problem = new ProblemDetails_1.ProblemDetails();
|
|
104
|
+
problem.type = exports.ProblemTypes.typeFor(result.status);
|
|
105
|
+
problem.title = exports.ProblemTypes.titleFor(result.status);
|
|
106
|
+
problem.detail = detail === '' ? undefined : detail;
|
|
107
|
+
problem.benzeneStatus = result.status;
|
|
108
|
+
problem.errors = errors.length > 0 ? [...errors] : undefined;
|
|
109
|
+
return problem;
|
|
110
|
+
},
|
|
111
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { IBenzeneResultOf } from '@benzenejs/abstractions';
|
|
2
|
+
/**
|
|
3
|
+
* transport-bindings.md §1 (spec 1.1): a send that fails in transport - no connection, a refused or reset
|
|
4
|
+
* request, the client's own deadline - before any reply arrives (or, for a one-way client, before the broker
|
|
5
|
+
* accepts the message) SHOULD yield a result rather than an exception: `timeout` when the deadline elapsed,
|
|
6
|
+
* otherwise `service-unavailable`, so a caller handles a dead peer as it handles every other failure. These
|
|
7
|
+
* functions are that rule, shared by every client in the port - reply-receiving and one-way alike.
|
|
8
|
+
*
|
|
9
|
+
* What counts as "the deadline elapsed", for the errors a Node client actually sees:
|
|
10
|
+
* - an aborted call (`AbortError`, or the `TimeoutError` an `AbortSignal.timeout(ms)` aborts with): in
|
|
11
|
+
* JavaScript an `AbortSignal` is how a caller bounds a `fetch` (it has no timeout option of its own), so an
|
|
12
|
+
* abort is the caller's own deadline;
|
|
13
|
+
* - an error whose name ends in `TimeoutError` (the AWS SDK's request/connection timeouts, kafkajs's
|
|
14
|
+
* `KafkaJSRequestTimeoutError`);
|
|
15
|
+
* - a timeout code: undici/Node's (`UND_ERR_CONNECT_TIMEOUT`, `UND_ERR_HEADERS_TIMEOUT`, `UND_ERR_BODY_TIMEOUT`,
|
|
16
|
+
* `ETIMEDOUT`, `ESOCKETTIMEDOUT`) or the Azure messaging SDKs' (`ServiceTimeout`, `OperationTimeoutError`);
|
|
17
|
+
* - a gRPC-shaped error (numeric `code` beside a string `details`, as the Pub/Sub client throws) whose code is
|
|
18
|
+
* 4, `DEADLINE_EXCEEDED`;
|
|
19
|
+
*
|
|
20
|
+
* each looked for on the error and its `cause` chain - `fetch` rejects with `TypeError('fetch failed')` and
|
|
21
|
+
* puts the socket error in `cause`.
|
|
22
|
+
*
|
|
23
|
+
* Everything else - `ECONNREFUSED`, `ECONNRESET`, a DNS failure, a TLS failure - is `service-unavailable`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function transportFailureStatus(error: unknown): string;
|
|
26
|
+
/**
|
|
27
|
+
* The failed result for a call that failed before any reply arrived: {@link transportFailureStatus}'s status,
|
|
28
|
+
* with one error carrying the failure's message (and its `cause`'s, which is where `fetch` puts the socket
|
|
29
|
+
* error), so the reason is not lost with the exception.
|
|
30
|
+
*/
|
|
31
|
+
export declare function transportFailureResult<T>(error: unknown): IBenzeneResultOf<T>;
|
|
32
|
+
/**
|
|
33
|
+
* The result for a one-way send whose SDK call rejected. When the broker itself answered - an AWS SDK service
|
|
34
|
+
* exception (`$metadata.httpStatusCode`) or an Azure `RestError` (`statusCode`), 400 or above - the message
|
|
35
|
+
* was refused, not lost in transport, so that native status maps through the reverse HTTP table
|
|
36
|
+
* (wire-contracts §4.1: `403` → `forbidden`, `404` → `not-found`, `429` → `too-many-requests`, ...).
|
|
37
|
+
* Otherwise the send failed before the broker accepted it: {@link transportFailureResult}. Either way the
|
|
38
|
+
* one error carries the failure's message.
|
|
39
|
+
*/
|
|
40
|
+
export declare function failedSendResult<T>(error: unknown): IBenzeneResultOf<T>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.transportFailureStatus = transportFailureStatus;
|
|
4
|
+
exports.transportFailureResult = transportFailureResult;
|
|
5
|
+
exports.failedSendResult = failedSendResult;
|
|
6
|
+
const BenzeneResult_1 = require("./BenzeneResult");
|
|
7
|
+
const BenzeneResultExtensions_1 = require("./BenzeneResultExtensions");
|
|
8
|
+
const BenzeneResultStatus_1 = require("./BenzeneResultStatus");
|
|
9
|
+
/**
|
|
10
|
+
* transport-bindings.md §1 (spec 1.1): a send that fails in transport - no connection, a refused or reset
|
|
11
|
+
* request, the client's own deadline - before any reply arrives (or, for a one-way client, before the broker
|
|
12
|
+
* accepts the message) SHOULD yield a result rather than an exception: `timeout` when the deadline elapsed,
|
|
13
|
+
* otherwise `service-unavailable`, so a caller handles a dead peer as it handles every other failure. These
|
|
14
|
+
* functions are that rule, shared by every client in the port - reply-receiving and one-way alike.
|
|
15
|
+
*
|
|
16
|
+
* What counts as "the deadline elapsed", for the errors a Node client actually sees:
|
|
17
|
+
* - an aborted call (`AbortError`, or the `TimeoutError` an `AbortSignal.timeout(ms)` aborts with): in
|
|
18
|
+
* JavaScript an `AbortSignal` is how a caller bounds a `fetch` (it has no timeout option of its own), so an
|
|
19
|
+
* abort is the caller's own deadline;
|
|
20
|
+
* - an error whose name ends in `TimeoutError` (the AWS SDK's request/connection timeouts, kafkajs's
|
|
21
|
+
* `KafkaJSRequestTimeoutError`);
|
|
22
|
+
* - a timeout code: undici/Node's (`UND_ERR_CONNECT_TIMEOUT`, `UND_ERR_HEADERS_TIMEOUT`, `UND_ERR_BODY_TIMEOUT`,
|
|
23
|
+
* `ETIMEDOUT`, `ESOCKETTIMEDOUT`) or the Azure messaging SDKs' (`ServiceTimeout`, `OperationTimeoutError`);
|
|
24
|
+
* - a gRPC-shaped error (numeric `code` beside a string `details`, as the Pub/Sub client throws) whose code is
|
|
25
|
+
* 4, `DEADLINE_EXCEEDED`;
|
|
26
|
+
*
|
|
27
|
+
* each looked for on the error and its `cause` chain - `fetch` rejects with `TypeError('fetch failed')` and
|
|
28
|
+
* puts the socket error in `cause`.
|
|
29
|
+
*
|
|
30
|
+
* Everything else - `ECONNREFUSED`, `ECONNRESET`, a DNS failure, a TLS failure - is `service-unavailable`.
|
|
31
|
+
*/
|
|
32
|
+
function transportFailureStatus(error) {
|
|
33
|
+
for (const link of causeChain(error)) {
|
|
34
|
+
if (typeof link !== 'object' || link === null) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const { name, code, details } = link;
|
|
38
|
+
if (name === 'AbortError' || (typeof name === 'string' && name.endsWith('TimeoutError'))) {
|
|
39
|
+
return BenzeneResultStatus_1.BenzeneResultStatus.timeout;
|
|
40
|
+
}
|
|
41
|
+
if (typeof code === 'string' && TimeoutCodes.has(code)) {
|
|
42
|
+
return BenzeneResultStatus_1.BenzeneResultStatus.timeout;
|
|
43
|
+
}
|
|
44
|
+
if (code === GrpcDeadlineExceeded && typeof details === 'string') {
|
|
45
|
+
return BenzeneResultStatus_1.BenzeneResultStatus.timeout;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return BenzeneResultStatus_1.BenzeneResultStatus.serviceUnavailable;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The failed result for a call that failed before any reply arrived: {@link transportFailureStatus}'s status,
|
|
52
|
+
* with one error carrying the failure's message (and its `cause`'s, which is where `fetch` puts the socket
|
|
53
|
+
* error), so the reason is not lost with the exception.
|
|
54
|
+
*/
|
|
55
|
+
function transportFailureResult(error) {
|
|
56
|
+
return BenzeneResult_1.BenzeneResult.setErrors(transportFailureStatus(error), transportFailureMessage(error));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The result for a one-way send whose SDK call rejected. When the broker itself answered - an AWS SDK service
|
|
60
|
+
* exception (`$metadata.httpStatusCode`) or an Azure `RestError` (`statusCode`), 400 or above - the message
|
|
61
|
+
* was refused, not lost in transport, so that native status maps through the reverse HTTP table
|
|
62
|
+
* (wire-contracts §4.1: `403` → `forbidden`, `404` → `not-found`, `429` → `too-many-requests`, ...).
|
|
63
|
+
* Otherwise the send failed before the broker accepted it: {@link transportFailureResult}. Either way the
|
|
64
|
+
* one error carries the failure's message.
|
|
65
|
+
*/
|
|
66
|
+
function failedSendResult(error) {
|
|
67
|
+
const brokerStatus = brokerHttpStatus(error);
|
|
68
|
+
return brokerStatus === undefined
|
|
69
|
+
? transportFailureResult(error)
|
|
70
|
+
: BenzeneResult_1.BenzeneResult.setErrors((0, BenzeneResultExtensions_1.convertHttpStatusCode)(brokerStatus).status, transportFailureMessage(error));
|
|
71
|
+
}
|
|
72
|
+
/** The HTTP status a broker answered a rejected SDK call with, when it answered at all. */
|
|
73
|
+
function brokerHttpStatus(error) {
|
|
74
|
+
if (typeof error !== 'object' || error === null) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
const { $metadata, statusCode } = error;
|
|
78
|
+
const status = $metadata?.httpStatusCode ?? statusCode;
|
|
79
|
+
return typeof status === 'number' && status >= 400 ? status : undefined;
|
|
80
|
+
}
|
|
81
|
+
const TimeoutCodes = new Set([
|
|
82
|
+
'UND_ERR_CONNECT_TIMEOUT',
|
|
83
|
+
'UND_ERR_HEADERS_TIMEOUT',
|
|
84
|
+
'UND_ERR_BODY_TIMEOUT',
|
|
85
|
+
'ETIMEDOUT',
|
|
86
|
+
'ESOCKETTIMEDOUT',
|
|
87
|
+
'ServiceTimeout',
|
|
88
|
+
'OperationTimeoutError',
|
|
89
|
+
]);
|
|
90
|
+
/** gRPC's `DEADLINE_EXCEEDED`. */
|
|
91
|
+
const GrpcDeadlineExceeded = 4;
|
|
92
|
+
function transportFailureMessage(error) {
|
|
93
|
+
const messages = [];
|
|
94
|
+
for (const link of causeChain(error)) {
|
|
95
|
+
const message = link instanceof Error ? link.message : typeof link === 'string' ? link : undefined;
|
|
96
|
+
if (message !== undefined && message !== '' && !messages.includes(message)) {
|
|
97
|
+
messages.push(message);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return messages.length > 0 ? messages.join(': ') : 'The call failed before any reply arrived';
|
|
101
|
+
}
|
|
102
|
+
/** `error`, then each `cause` beneath it (bounded, so a cyclic chain cannot loop). */
|
|
103
|
+
function causeChain(error) {
|
|
104
|
+
const chain = [];
|
|
105
|
+
let current = error;
|
|
106
|
+
while (current !== undefined && current !== null && chain.length < 8 && !chain.includes(current)) {
|
|
107
|
+
chain.push(current);
|
|
108
|
+
current = typeof current === 'object' ? current.cause : undefined;
|
|
109
|
+
}
|
|
110
|
+
return chain;
|
|
111
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { BenzeneError } from '@benzenejs/abstractions';
|
|
2
|
+
export * from './BenzeneResult';
|
|
3
|
+
export * from './BenzeneResultExtensions';
|
|
4
|
+
export * from './BenzeneResultStatus';
|
|
5
|
+
export * from './ProblemDetails';
|
|
6
|
+
export * from './ProblemTypes';
|
|
7
|
+
export * from './TransportFailure';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./BenzeneResult"), exports);
|
|
18
|
+
__exportStar(require("./BenzeneResultExtensions"), exports);
|
|
19
|
+
__exportStar(require("./BenzeneResultStatus"), exports);
|
|
20
|
+
__exportStar(require("./ProblemDetails"), exports);
|
|
21
|
+
__exportStar(require("./ProblemTypes"), exports);
|
|
22
|
+
__exportStar(require("./TransportFailure"), exports);
|
package/package.json
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@benzenejs/results",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0-beta.2",
|
|
4
|
+
"description": "TypeScript port of Benzene.Results",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@benzenejs/abstractions": "0.1.0-beta.2"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
]
|
|
15
|
+
}
|
package/README.md
DELETED