platform_provider/
admin_data.rs1use 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}