Skip to main content

platform_provider/
admin_data.rs

1use crate::ProviderHostEffectCoordinator;
2use crate::config::ProviderConfig;
3use crate::invocation::{self, InvocationContext};
4use crate::protocol::{
5    ProviderAdminGetRequest, ProviderAdminListRequest, ProviderAdminQueryRequest,
6    ProviderGetResponse, ProviderInvocationMode, ProviderListResponse, ProviderOperationKind,
7    ProviderQueryResponse,
8};
9use platform_core::{ActorContext, AppError, AppResult, ErrorCode, TraceContext};
10use platform_module::{AdminDataSource, AdminListQuery, AdminPage, AdminQuerySource};
11use serde_json::Value;
12use std::time::Duration;
13
14#[derive(Debug, Clone)]
15pub struct ProviderAdminDataSource {
16    client: reqwest::Client,
17    config: ProviderConfig,
18    effects: ProviderHostEffectCoordinator,
19}
20
21impl ProviderAdminDataSource {
22    pub fn new(config: ProviderConfig) -> AppResult<Self> {
23        let client = reqwest::Client::builder()
24            .timeout(Duration::from_millis(config.timeout_ms))
25            .build()
26            .map_err(|error| {
27                AppError::new(
28                    ErrorCode::Internal,
29                    format!("failed to build Provider Service client: {error}"),
30                )
31            })?;
32        Ok(Self {
33            client,
34            config,
35            effects: ProviderHostEffectCoordinator::rejecting(),
36        })
37    }
38
39    #[must_use]
40    pub fn with_effect_coordinator(mut self, effects: ProviderHostEffectCoordinator) -> Self {
41        self.effects = effects;
42        self
43    }
44
45    async fn invoke<T: serde::de::DeserializeOwned>(
46        &self,
47        kind: ProviderOperationKind,
48        binding: &str,
49        operation: &str,
50        payload: Value,
51    ) -> AppResult<T> {
52        let invocation_id = uuid::Uuid::now_v7().to_string();
53        let invocation = invocation::build(
54            &self.config,
55            kind,
56            operation,
57            "1",
58            ProviderInvocationMode::ReadOnly,
59            InvocationContext {
60                request_id: invocation_id.clone(),
61                invocation_id,
62                attempt: 1,
63                actor: ActorContext::System,
64                tenant_id: None,
65                correlation_id: uuid::Uuid::now_v7().to_string(),
66                causation_id: None,
67                trace: TraceContext::default(),
68            },
69            payload,
70        )?;
71        let outcome = invocation::send(
72            &self.client,
73            &self.config,
74            &self.effects,
75            binding,
76            &invocation,
77        )
78        .await?;
79        serde_json::from_value(invocation::result(&invocation, outcome)?).map_err(|error| {
80            AppError::new(
81                ErrorCode::ExternalDependency,
82                format!("Provider admin result violated its contract: {error}"),
83            )
84        })
85    }
86}
87
88#[async_trait::async_trait]
89impl AdminDataSource for ProviderAdminDataSource {
90    async fn list(&self, entity: &str, query: &AdminListQuery) -> AppResult<AdminPage> {
91        let response: ProviderListResponse = self
92            .invoke(
93                ProviderOperationKind::AdminList,
94                "admin:list",
95                entity,
96                serde_json::to_value(ProviderAdminListRequest {
97                    entity: entity.to_owned(),
98                    limit: query.limit,
99                    cursor: query.cursor.clone(),
100                })
101                .map_err(|error| AppError::new(ErrorCode::Internal, error.to_string()))?,
102            )
103            .await?;
104        Ok(response.into())
105    }
106
107    async fn get(&self, entity: &str, id: &str) -> AppResult<Option<Value>> {
108        let response: ProviderGetResponse = self
109            .invoke(
110                ProviderOperationKind::AdminGet,
111                "admin:get",
112                entity,
113                serde_json::to_value(ProviderAdminGetRequest {
114                    entity: entity.to_owned(),
115                    id: id.to_owned(),
116                })
117                .map_err(|error| AppError::new(ErrorCode::Internal, error.to_string()))?,
118            )
119            .await?;
120        Ok(response.record)
121    }
122}
123
124#[async_trait::async_trait]
125impl AdminQuerySource for ProviderAdminDataSource {
126    async fn query(&self, query: &str) -> AppResult<Value> {
127        validate_admin_query_name(query)?;
128        let response: ProviderQueryResponse = self
129            .invoke(
130                ProviderOperationKind::AdminQuery,
131                "admin:query",
132                query,
133                serde_json::to_value(ProviderAdminQueryRequest {
134                    query: query.to_owned(),
135                })
136                .map_err(|error| AppError::new(ErrorCode::Internal, error.to_string()))?,
137            )
138            .await?;
139        Ok(response.data)
140    }
141}
142
143fn validate_admin_query_name(query: &str) -> AppResult<()> {
144    let valid = !query.is_empty()
145        && query.chars().all(|character| {
146            character.is_ascii_alphanumeric()
147                || character == '.'
148                || character == '_'
149                || character == '-'
150        });
151    if valid {
152        return Ok(());
153    }
154
155    Err(AppError::new(
156        ErrorCode::Validation,
157        "provider admin query name must be a stable path segment",
158    ))
159}