spiral-sdk 0.1.11

Rust SDK for Spiral resource and table APIs
Documentation
//! Project-scoped Connection discovery and metadata.

use serde::Serialize;

use crate::catalog_types::{ProjectBranchName, ResolveJournalPathRequest, ResourceAtSnapshot};
use crate::namespace::{Namespace, resource_path};
use crate::resources::connection::{ExternalConnection, ExternalConnectionId};
use crate::{Error, Snapshot};

/// A Connection and its slash-separated name in the selected project snapshot.
#[derive(Clone, Debug, Serialize)]
pub struct NamedConnection {
    /// Path relative to the project root.
    pub name: String,
    /// Current metadata; credentials are mutable and contain no stored secrets.
    #[serde(flatten)]
    pub connection: ExternalConnection,
}

pub(crate) fn require_connection(
    resource: &ResourceAtSnapshot,
) -> Result<ExternalConnectionId, Error> {
    resource.validate().map_err(Error::invalid)?;
    if resource.descriptor.kind.0 != "connection"
        || resource.descriptor.service.as_str() != "external-connection"
    {
        return Err(Error::invalid("resource is not a Connection"));
    }
    Ok(resource.descriptor.resource_id.0)
}

impl Snapshot {
    /// Read current Connection metadata through a name bound at this exact commit.
    ///
    /// Names are slash-separated paths without a ref suffix. Credentials are mutable;
    /// selecting an older commit does not restore a former credential or undo a drop.
    pub async fn connection(&self, path: &str) -> Result<ExternalConnection, Error> {
        let resolved = self
            .project
            .client
            .catalog()
            .resolve_journal_path(
                &self.project.id,
                self.commit_id,
                &ResolveJournalPathRequest {
                    path: resource_path(path)?,
                },
            )
            .await?;
        if resolved.commit_id != self.commit_id {
            return Err(Error::Protocol(
                "Catalog changed the selected commit".into(),
            ));
        }
        let resource = resolved
            .resource
            .ok_or_else(|| Error::NotFound(path.into()))?;
        let id = require_connection(&resource)?;
        Ok(self
            .project
            .client
            .connections()
            .read_connection(id)
            .await?
            .connection)
    }

    /// List every Connection binding at this commit, sorted by slash-separated path.
    ///
    /// Discovery follows bounded Catalog pages; each Connection's metadata is read
    /// with this client's credentials and reflects its current mutable state.
    pub async fn connections(&self) -> Result<Vec<NamedConnection>, Error> {
        let namespace = Namespace::read(self).await?;
        let mut connections = Vec::new();
        for (name, resource) in namespace.named("connection")? {
            let id = require_connection(&resource)?;
            connections.push(NamedConnection {
                name,
                connection: self
                    .project
                    .client
                    .connections()
                    .read_connection(id)
                    .await?
                    .connection,
            });
        }
        Ok(connections)
    }

    pub(crate) async fn require_main_branch(&self) -> Result<(), Error> {
        let main = self
            .project
            .client
            .catalog()
            .pin_journal(&self.project.id, &ProjectBranchName::main())
            .await?;
        main.validate().map_err(Error::invalid)?;
        if self.branch_id != Some(main.branch_id) {
            return Err(Error::invalid(
                "Connections can only be created and dropped on the main branch",
            ));
        }
        Ok(())
    }
}