Skip to main content

fraiseql_core/runtime/
cascade.rs

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