1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
//! Cascade-spec error classification.
//!
//! `MutationErrorClass` mirrors the `app.mutation_error_class` PostgreSQL enum
//! emitted by `app.mutation_response` rows. `CascadeErrorCode` is the wire
//! representation used by the graphql-cascade error envelope. The mapping
//! between them is 1:1 — no fallbacks, no HTTP-code tiebreakers.
//!
//! See `docs/architecture/mutation-response.md` (semantics table + mapping).
use serde::Deserialize;
/// Classification of a failed mutation.
///
/// Mirrors `app.mutation_error_class` in PostgreSQL. Variants serialize to the
/// `snake_case` form used in the PG enum so rows containing `error_class` strings
/// deserialize directly. `NULL` in the PG column corresponds to
/// `Option::None` in the parent struct.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum MutationErrorClass {
/// Input failed schema or business-rule validation.
Validation,
/// Uniqueness, optimistic-concurrency, or state conflict.
Conflict,
/// Target entity does not exist or the caller cannot see it.
NotFound,
/// Caller is unauthenticated.
Unauthorized,
/// Caller is authenticated but lacks permission.
Forbidden,
/// Unhandled server-side failure. Implementation details must not leak.
Internal,
/// Transaction was rolled back (serialization, deadlock, explicit abort).
TransactionFailed,
/// Operation exceeded a deadline.
Timeout,
/// Caller exceeded quota.
RateLimited,
/// Downstream dependency unreachable.
ServiceUnavailable,
}
/// graphql-cascade wire-level error code.
///
/// Serialized as `SCREAMING_SNAKE_CASE` on the wire; derived 1:1 from a
/// `MutationErrorClass` via [`MutationErrorClass::to_cascade_code`].
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[non_exhaustive]
pub enum CascadeErrorCode {
/// Input validation failed.
ValidationError,
/// Uniqueness or concurrency conflict.
Conflict,
/// Entity not found.
NotFound,
/// Unauthenticated.
Unauthorized,
/// Forbidden.
Forbidden,
/// Internal server error.
InternalError,
/// Transaction rolled back.
TransactionFailed,
/// Operation timed out.
Timeout,
/// Rate limit exceeded.
RateLimited,
/// Downstream service unavailable.
ServiceUnavailable,
}
impl MutationErrorClass {
/// The `snake_case` string that identifies this class on the wire.
///
/// Mirrors the `app.mutation_error_class` PostgreSQL enum label and the
/// `serde(rename_all = "snake_case")` serialisation form used in v2 rows.
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Validation => "validation",
Self::Conflict => "conflict",
Self::NotFound => "not_found",
Self::Unauthorized => "unauthorized",
Self::Forbidden => "forbidden",
Self::Internal => "internal",
Self::TransactionFailed => "transaction_failed",
Self::Timeout => "timeout",
Self::RateLimited => "rate_limited",
Self::ServiceUnavailable => "service_unavailable",
}
}
/// Map the error class to its graphql-cascade wire code (1:1, no fallbacks).
#[must_use]
pub const fn to_cascade_code(self) -> CascadeErrorCode {
match self {
Self::Validation => CascadeErrorCode::ValidationError,
Self::Conflict => CascadeErrorCode::Conflict,
Self::NotFound => CascadeErrorCode::NotFound,
Self::Unauthorized => CascadeErrorCode::Unauthorized,
Self::Forbidden => CascadeErrorCode::Forbidden,
Self::Internal => CascadeErrorCode::InternalError,
Self::TransactionFailed => CascadeErrorCode::TransactionFailed,
Self::Timeout => CascadeErrorCode::Timeout,
Self::RateLimited => CascadeErrorCode::RateLimited,
Self::ServiceUnavailable => CascadeErrorCode::ServiceUnavailable,
}
}
}