mas_handlers/graphql/model/
node.rs

1// Copyright 2024, 2025 New Vector Ltd.
2// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE in the repository root for full details.
6
7use async_graphql::{ID, Interface};
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10use ulid::Ulid;
11
12use super::{
13    Anonymous, Authentication, BrowserSession, CompatSession, CompatSsoLogin, OAuth2Client,
14    OAuth2Session, SiteConfig, UpstreamOAuth2Link, UpstreamOAuth2Provider, User, UserEmail,
15    UserEmailAuthentication, UserRecoveryTicket,
16};
17
18#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum NodeType {
20    Authentication,
21    BrowserSession,
22    CompatSession,
23    CompatSsoLogin,
24    OAuth2Client,
25    OAuth2Session,
26    UpstreamOAuth2Provider,
27    UpstreamOAuth2Link,
28    User,
29    UserEmail,
30    UserEmailAuthentication,
31    UserRecoveryTicket,
32}
33
34#[derive(Debug, Error)]
35#[error("invalid id")]
36pub enum InvalidID {
37    InvalidFormat,
38    InvalidUlid(#[from] ulid::DecodeError),
39    UnknownPrefix,
40    TypeMismatch { got: NodeType, expected: NodeType },
41}
42
43impl NodeType {
44    fn to_prefix(self) -> &'static str {
45        match self {
46            NodeType::Authentication => "authentication",
47            NodeType::BrowserSession => "browser_session",
48            NodeType::CompatSession => "compat_session",
49            NodeType::CompatSsoLogin => "compat_sso_login",
50            NodeType::OAuth2Client => "oauth2_client",
51            NodeType::OAuth2Session => "oauth2_session",
52            NodeType::UpstreamOAuth2Provider => "upstream_oauth2_provider",
53            NodeType::UpstreamOAuth2Link => "upstream_oauth2_link",
54            NodeType::User => "user",
55            NodeType::UserEmail => "user_email",
56            NodeType::UserEmailAuthentication => "user_email_authentication",
57            NodeType::UserRecoveryTicket => "user_recovery_ticket",
58        }
59    }
60
61    fn from_prefix(prefix: &str) -> Option<Self> {
62        match prefix {
63            "authentication" => Some(NodeType::Authentication),
64            "browser_session" => Some(NodeType::BrowserSession),
65            "compat_session" => Some(NodeType::CompatSession),
66            "compat_sso_login" => Some(NodeType::CompatSsoLogin),
67            "oauth2_client" => Some(NodeType::OAuth2Client),
68            "oauth2_session" => Some(NodeType::OAuth2Session),
69            "upstream_oauth2_provider" => Some(NodeType::UpstreamOAuth2Provider),
70            "upstream_oauth2_link" => Some(NodeType::UpstreamOAuth2Link),
71            "user" => Some(NodeType::User),
72            "user_email" => Some(NodeType::UserEmail),
73            "user_email_authentication" => Some(NodeType::UserEmailAuthentication),
74            "user_recovery_ticket" => Some(NodeType::UserRecoveryTicket),
75            _ => None,
76        }
77    }
78
79    pub fn serialize(self, id: impl Into<Ulid>) -> String {
80        let prefix = self.to_prefix();
81        let id = id.into();
82        format!("{prefix}:{id}")
83    }
84
85    pub fn id(self, id: impl Into<Ulid>) -> ID {
86        ID(self.serialize(id))
87    }
88
89    pub fn deserialize(serialized: &str) -> Result<(Self, Ulid), InvalidID> {
90        let (prefix, id) = serialized.split_once(':').ok_or(InvalidID::InvalidFormat)?;
91        let prefix = NodeType::from_prefix(prefix).ok_or(InvalidID::UnknownPrefix)?;
92        let id = id.parse()?;
93        Ok((prefix, id))
94    }
95
96    pub fn from_id(id: &ID) -> Result<(Self, Ulid), InvalidID> {
97        Self::deserialize(&id.0)
98    }
99
100    pub fn extract_ulid(self, id: &ID) -> Result<Ulid, InvalidID> {
101        let (node_type, ulid) = Self::deserialize(&id.0)?;
102
103        if node_type == self {
104            Ok(ulid)
105        } else {
106            Err(InvalidID::TypeMismatch {
107                got: node_type,
108                expected: self,
109            })
110        }
111    }
112}
113
114/// An object with an ID.
115#[derive(Interface)]
116#[graphql(field(name = "id", desc = "ID of the object.", ty = "ID"))]
117pub enum Node {
118    Anonymous(Box<Anonymous>),
119    Authentication(Box<Authentication>),
120    BrowserSession(Box<BrowserSession>),
121    CompatSession(Box<CompatSession>),
122    CompatSsoLogin(Box<CompatSsoLogin>),
123    OAuth2Client(Box<OAuth2Client>),
124    OAuth2Session(Box<OAuth2Session>),
125    SiteConfig(Box<SiteConfig>),
126    UpstreamOAuth2Provider(Box<UpstreamOAuth2Provider>),
127    UpstreamOAuth2Link(Box<UpstreamOAuth2Link>),
128    User(Box<User>),
129    UserEmail(Box<UserEmail>),
130    UserEmailAuthentication(Box<UserEmailAuthentication>),
131    UserRecoveryTicket(Box<UserRecoveryTicket>),
132}