Skip to main content

spiral/
connections.rs

1//! Project-scoped Connection discovery and metadata.
2
3use serde::Serialize;
4
5use crate::catalog_types::{ProjectBranchName, ResolveJournalPathRequest, ResourceAtSnapshot};
6use crate::namespace::{Namespace, resource_path};
7use crate::resources::connection::{ExternalConnection, ExternalConnectionId};
8use crate::{Error, Snapshot};
9
10/// A Connection and its slash-separated name in the selected project snapshot.
11#[derive(Clone, Debug, Serialize)]
12pub struct NamedConnection {
13    /// Path relative to the project root.
14    pub name: String,
15    /// Current metadata; credentials are mutable and contain no stored secrets.
16    #[serde(flatten)]
17    pub connection: ExternalConnection,
18}
19
20pub(crate) fn require_connection(
21    resource: &ResourceAtSnapshot,
22) -> Result<ExternalConnectionId, Error> {
23    resource.validate().map_err(Error::invalid)?;
24    if resource.descriptor.kind.0 != "connection"
25        || resource.descriptor.service.as_str() != "external-connection"
26    {
27        return Err(Error::invalid("resource is not a Connection"));
28    }
29    Ok(resource.descriptor.resource_id.0)
30}
31
32impl Snapshot {
33    /// Read current Connection metadata through a name bound at this exact commit.
34    ///
35    /// Names are slash-separated paths without a ref suffix. Credentials are mutable;
36    /// selecting an older commit does not restore a former credential or undo a drop.
37    pub async fn connection(&self, path: &str) -> Result<ExternalConnection, Error> {
38        let resolved = self
39            .project
40            .client
41            .catalog()
42            .resolve_journal_path(
43                &self.project.id,
44                self.commit_id,
45                &ResolveJournalPathRequest {
46                    path: resource_path(path)?,
47                },
48            )
49            .await?;
50        if resolved.commit_id != self.commit_id {
51            return Err(Error::Protocol(
52                "Catalog changed the selected commit".into(),
53            ));
54        }
55        let resource = resolved
56            .resource
57            .ok_or_else(|| Error::NotFound(path.into()))?;
58        let id = require_connection(&resource)?;
59        Ok(self
60            .project
61            .client
62            .connections()
63            .read_connection(id)
64            .await?
65            .connection)
66    }
67
68    /// List every Connection binding at this commit, sorted by slash-separated path.
69    ///
70    /// Discovery follows bounded Catalog pages; each Connection's metadata is read
71    /// with this client's credentials and reflects its current mutable state.
72    pub async fn connections(&self) -> Result<Vec<NamedConnection>, Error> {
73        let namespace = Namespace::read(self).await?;
74        let mut connections = Vec::new();
75        for (name, resource) in namespace.named("connection")? {
76            let id = require_connection(&resource)?;
77            connections.push(NamedConnection {
78                name,
79                connection: self
80                    .project
81                    .client
82                    .connections()
83                    .read_connection(id)
84                    .await?
85                    .connection,
86            });
87        }
88        Ok(connections)
89    }
90
91    pub(crate) async fn require_main_branch(&self) -> Result<(), Error> {
92        let main = self
93            .project
94            .client
95            .catalog()
96            .pin_journal(&self.project.id, &ProjectBranchName::main())
97            .await?;
98        main.validate().map_err(Error::invalid)?;
99        if self.branch_id != Some(main.branch_id) {
100            return Err(Error::invalid(
101                "Connections can only be created and dropped on the main branch",
102            ));
103        }
104        Ok(())
105    }
106}