@danypops/vehicle-core 0.10.0 → 0.12.0
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/vehicle-errors.d.ts +25 -0
- package/dist/vehicle-errors.js +31 -0
- package/package.json +1 -1
- package/src/vehicle-errors.ts +60 -0
package/dist/vehicle-errors.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export interface VehicleFailure {
|
|
|
14
14
|
readonly recovery?: VehicleRecovery;
|
|
15
15
|
readonly details?: JsonValue;
|
|
16
16
|
readonly operationId?: string;
|
|
17
|
+
/** The underlying cause's own message, bounded (never a full stack trace). Only set when the throw site opts into exposeCause. */
|
|
18
|
+
readonly causeMessage?: string;
|
|
17
19
|
}
|
|
18
20
|
export interface VehicleErrorOptions {
|
|
19
21
|
readonly category: VehicleFailureCategory;
|
|
@@ -23,7 +25,27 @@ export interface VehicleErrorOptions {
|
|
|
23
25
|
readonly details?: JsonValue;
|
|
24
26
|
readonly operationId?: string;
|
|
25
27
|
readonly cause?: unknown;
|
|
28
|
+
/** Includes cause's message in toFailure().causeMessage. Default false -- an arbitrary cause could carry a credential or internal detail. */
|
|
29
|
+
readonly exposeCause?: boolean;
|
|
26
30
|
}
|
|
31
|
+
export type VehicleErrorClass = abstract new (...args: never[]) => Error;
|
|
32
|
+
export interface VehicleErrorClassMapping {
|
|
33
|
+
readonly errorClass: VehicleErrorClass;
|
|
34
|
+
readonly category: VehicleFailureCategory;
|
|
35
|
+
readonly code?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface VehicleErrorPredicateMapping {
|
|
38
|
+
readonly matches: (error: unknown) => boolean;
|
|
39
|
+
readonly category: VehicleFailureCategory;
|
|
40
|
+
readonly code?: string;
|
|
41
|
+
}
|
|
42
|
+
export type VehicleErrorMapping = VehicleErrorClassMapping | VehicleErrorPredicateMapping;
|
|
43
|
+
export interface DefineErrorMappingOptions {
|
|
44
|
+
readonly fallbackCategory?: VehicleFailureCategory;
|
|
45
|
+
readonly fallbackCode?: string;
|
|
46
|
+
}
|
|
47
|
+
/** Maps reviewed domain errors into wire-safe Vehicle errors while preserving already-mapped failures. */
|
|
48
|
+
export declare function defineErrorMapping(rules: readonly VehicleErrorMapping[], options?: DefineErrorMappingOptions): <T>(run: () => T | Promise<T>) => Promise<T>;
|
|
27
49
|
export declare class VehicleError extends Error {
|
|
28
50
|
readonly code: string;
|
|
29
51
|
readonly category: VehicleFailureCategory;
|
|
@@ -32,7 +54,10 @@ export declare class VehicleError extends Error {
|
|
|
32
54
|
readonly recovery?: VehicleRecovery;
|
|
33
55
|
readonly details?: JsonValue;
|
|
34
56
|
readonly operationId?: string;
|
|
57
|
+
private readonly exposeCause;
|
|
35
58
|
constructor(code: string, message: string, options: VehicleErrorOptions);
|
|
36
59
|
toFailure(): VehicleFailure;
|
|
37
60
|
}
|
|
61
|
+
/** Extracts a bounded, wire-safe message from an unknown cause -- never the full stack trace, never an unbounded payload. */
|
|
62
|
+
export declare function boundedCauseMessage(cause: unknown): string | undefined;
|
|
38
63
|
export declare function boundedValidationDetails(issues: readonly VehicleSchemaIssue[] | undefined): JsonValue | undefined;
|
package/dist/vehicle-errors.js
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
/** Maps reviewed domain errors into wire-safe Vehicle errors while preserving already-mapped failures. */
|
|
2
|
+
export function defineErrorMapping(rules, options = {}) {
|
|
3
|
+
return async (run) => {
|
|
4
|
+
try {
|
|
5
|
+
return await run();
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
if (error instanceof VehicleError)
|
|
9
|
+
throw error;
|
|
10
|
+
const rule = rules.find((candidate) => "errorClass" in candidate ? error instanceof candidate.errorClass : candidate.matches(error));
|
|
11
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
12
|
+
throw new VehicleError(rule?.code ?? options.fallbackCode ?? "operation-rejected", message, {
|
|
13
|
+
category: rule?.category ?? options.fallbackCategory ?? "validation",
|
|
14
|
+
cause: error,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
1
19
|
export class VehicleError extends Error {
|
|
2
20
|
code;
|
|
3
21
|
category;
|
|
@@ -6,6 +24,7 @@ export class VehicleError extends Error {
|
|
|
6
24
|
recovery;
|
|
7
25
|
details;
|
|
8
26
|
operationId;
|
|
27
|
+
exposeCause;
|
|
9
28
|
constructor(code, message, options) {
|
|
10
29
|
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
11
30
|
this.code = code;
|
|
@@ -16,8 +35,10 @@ export class VehicleError extends Error {
|
|
|
16
35
|
this.recovery = options.recovery;
|
|
17
36
|
this.details = options.details;
|
|
18
37
|
this.operationId = options.operationId;
|
|
38
|
+
this.exposeCause = options.exposeCause ?? false;
|
|
19
39
|
}
|
|
20
40
|
toFailure() {
|
|
41
|
+
const causeMessage = this.exposeCause ? boundedCauseMessage(this.cause) : undefined;
|
|
21
42
|
return {
|
|
22
43
|
code: this.code,
|
|
23
44
|
category: this.category,
|
|
@@ -27,9 +48,19 @@ export class VehicleError extends Error {
|
|
|
27
48
|
...(this.recovery === undefined ? {} : { recovery: this.recovery }),
|
|
28
49
|
...(this.details === undefined ? {} : { details: this.details }),
|
|
29
50
|
...(this.operationId === undefined ? {} : { operationId: this.operationId }),
|
|
51
|
+
...(causeMessage === undefined ? {} : { causeMessage }),
|
|
30
52
|
};
|
|
31
53
|
}
|
|
32
54
|
}
|
|
55
|
+
const MAX_CAUSE_MESSAGE_LENGTH = 500;
|
|
56
|
+
/** Extracts a bounded, wire-safe message from an unknown cause -- never the full stack trace, never an unbounded payload. */
|
|
57
|
+
export function boundedCauseMessage(cause) {
|
|
58
|
+
if (cause instanceof Error && cause.message.length > 0)
|
|
59
|
+
return cause.message.slice(0, MAX_CAUSE_MESSAGE_LENGTH);
|
|
60
|
+
if (typeof cause === "string" && cause.length > 0)
|
|
61
|
+
return cause.slice(0, MAX_CAUSE_MESSAGE_LENGTH);
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
33
64
|
const MAX_VALIDATION_ISSUES = 10;
|
|
34
65
|
const MAX_ISSUE_MESSAGE_LENGTH = 500;
|
|
35
66
|
const MAX_ISSUE_PATH_LENGTH = 20;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/vehicle-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Vehicle's runtime-neutral wire contract: operation descriptors, schema codecs, failure shapes. Zero runtime dependencies, zero Bun-specific code -- the one thing every Vehicle client and server package depends on.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/vehicle-errors.ts
CHANGED
|
@@ -45,6 +45,8 @@ export interface VehicleFailure {
|
|
|
45
45
|
readonly recovery?: VehicleRecovery;
|
|
46
46
|
readonly details?: JsonValue;
|
|
47
47
|
readonly operationId?: string;
|
|
48
|
+
/** The underlying cause's own message, bounded (never a full stack trace). Only set when the throw site opts into exposeCause. */
|
|
49
|
+
readonly causeMessage?: string;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
export interface VehicleErrorOptions {
|
|
@@ -55,6 +57,51 @@ export interface VehicleErrorOptions {
|
|
|
55
57
|
readonly details?: JsonValue;
|
|
56
58
|
readonly operationId?: string;
|
|
57
59
|
readonly cause?: unknown;
|
|
60
|
+
/** Includes cause's message in toFailure().causeMessage. Default false -- an arbitrary cause could carry a credential or internal detail. */
|
|
61
|
+
readonly exposeCause?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type VehicleErrorClass = abstract new (...args: never[]) => Error;
|
|
65
|
+
|
|
66
|
+
export interface VehicleErrorClassMapping {
|
|
67
|
+
readonly errorClass: VehicleErrorClass;
|
|
68
|
+
readonly category: VehicleFailureCategory;
|
|
69
|
+
readonly code?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface VehicleErrorPredicateMapping {
|
|
73
|
+
readonly matches: (error: unknown) => boolean;
|
|
74
|
+
readonly category: VehicleFailureCategory;
|
|
75
|
+
readonly code?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type VehicleErrorMapping = VehicleErrorClassMapping | VehicleErrorPredicateMapping;
|
|
79
|
+
|
|
80
|
+
export interface DefineErrorMappingOptions {
|
|
81
|
+
readonly fallbackCategory?: VehicleFailureCategory;
|
|
82
|
+
readonly fallbackCode?: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Maps reviewed domain errors into wire-safe Vehicle errors while preserving already-mapped failures. */
|
|
86
|
+
export function defineErrorMapping(
|
|
87
|
+
rules: readonly VehicleErrorMapping[],
|
|
88
|
+
options: DefineErrorMappingOptions = {},
|
|
89
|
+
): <T>(run: () => T | Promise<T>) => Promise<T> {
|
|
90
|
+
return async <T>(run: () => T | Promise<T>): Promise<T> => {
|
|
91
|
+
try {
|
|
92
|
+
return await run();
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (error instanceof VehicleError) throw error;
|
|
95
|
+
const rule = rules.find((candidate) =>
|
|
96
|
+
"errorClass" in candidate ? error instanceof candidate.errorClass : candidate.matches(error),
|
|
97
|
+
);
|
|
98
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
99
|
+
throw new VehicleError(rule?.code ?? options.fallbackCode ?? "operation-rejected", message, {
|
|
100
|
+
category: rule?.category ?? options.fallbackCategory ?? "validation",
|
|
101
|
+
cause: error,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
};
|
|
58
105
|
}
|
|
59
106
|
|
|
60
107
|
export class VehicleError extends Error {
|
|
@@ -64,6 +111,7 @@ export class VehicleError extends Error {
|
|
|
64
111
|
readonly recovery?: VehicleRecovery;
|
|
65
112
|
readonly details?: JsonValue;
|
|
66
113
|
readonly operationId?: string;
|
|
114
|
+
private readonly exposeCause: boolean;
|
|
67
115
|
|
|
68
116
|
constructor(
|
|
69
117
|
readonly code: string,
|
|
@@ -78,9 +126,11 @@ export class VehicleError extends Error {
|
|
|
78
126
|
this.recovery = options.recovery;
|
|
79
127
|
this.details = options.details;
|
|
80
128
|
this.operationId = options.operationId;
|
|
129
|
+
this.exposeCause = options.exposeCause ?? false;
|
|
81
130
|
}
|
|
82
131
|
|
|
83
132
|
toFailure(): VehicleFailure {
|
|
133
|
+
const causeMessage = this.exposeCause ? boundedCauseMessage(this.cause) : undefined;
|
|
84
134
|
return {
|
|
85
135
|
code: this.code,
|
|
86
136
|
category: this.category,
|
|
@@ -90,10 +140,20 @@ export class VehicleError extends Error {
|
|
|
90
140
|
...(this.recovery === undefined ? {} : { recovery: this.recovery }),
|
|
91
141
|
...(this.details === undefined ? {} : { details: this.details }),
|
|
92
142
|
...(this.operationId === undefined ? {} : { operationId: this.operationId }),
|
|
143
|
+
...(causeMessage === undefined ? {} : { causeMessage }),
|
|
93
144
|
};
|
|
94
145
|
}
|
|
95
146
|
}
|
|
96
147
|
|
|
148
|
+
const MAX_CAUSE_MESSAGE_LENGTH = 500;
|
|
149
|
+
|
|
150
|
+
/** Extracts a bounded, wire-safe message from an unknown cause -- never the full stack trace, never an unbounded payload. */
|
|
151
|
+
export function boundedCauseMessage(cause: unknown): string | undefined {
|
|
152
|
+
if (cause instanceof Error && cause.message.length > 0) return cause.message.slice(0, MAX_CAUSE_MESSAGE_LENGTH);
|
|
153
|
+
if (typeof cause === "string" && cause.length > 0) return cause.slice(0, MAX_CAUSE_MESSAGE_LENGTH);
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
97
157
|
const MAX_VALIDATION_ISSUES = 10;
|
|
98
158
|
const MAX_ISSUE_MESSAGE_LENGTH = 500;
|
|
99
159
|
const MAX_ISSUE_PATH_LENGTH = 20;
|