diff --git a/file-service/src/admin.rs b/file-service/src/admin.rs index fa5f554..2185b19 100644 --- a/file-service/src/admin.rs +++ b/file-service/src/admin.rs @@ -7,19 +7,15 @@ use axum::{extract::State, routing::get, Router, Server}; use http::HeaderMap; use tokio::sync::Mutex; -use crate::file_server::{ - cost::{GraphQlCostModel, PriceQuery}, - status::{GraphQlBundle, GraphQlFileManifestMeta, StatusQuery}, - util::graphql_playground, - FileServiceError, ServerContext, -}; +use crate::file_server::{util::graphql_playground, FileServiceError, ServerContext}; +use crate::graphql_types::{GraphQlBundle, GraphQlCostModel, GraphQlFileManifestMeta}; use file_exchange::{ errors::{Error, ServerError}, manifest::{ ipfs::IpfsClient, manifest_fetcher::{fetch_file_manifest_from_ipfs, read_bundle}, store::Store, - FileManifestMeta, FileMetaInfo, LocalBundle, + Bundle, FileManifestMeta, FileMetaInfo, LocalBundle, }, }; @@ -96,13 +92,15 @@ pub fn serve_admin(context: ServerContext) { } .into(), ); - tracing::info!(address = %context.state.config.server.admin_host_and_port, "Serve admin metrics"); + let addr = context.state.config.server.admin_host_and_port; + tracing::info!(address = %addr + , "Serve admin metrics"); let router = Router::new() .route("/admin", get(graphql_playground).post(graphql_handler)) .with_state(admin_context); - Server::bind(&context.state.config.server.admin_host_and_port) + Server::bind(&addr) .serve(router.into_make_service()) .await .expect("Failed to initialize admin server") @@ -658,3 +656,236 @@ impl PriceMutation { removed_prices } } + +/* StatusQuery and CostQuer are repeated from the status/cost endpoints +** This is due to the difference in ServerContext +** It isn't possible to use generic types for these queries due to async-graphql +** But we can later restructure admin/server contexts with the arc bundles/files/cost */ +#[derive(Default)] +pub struct StatusQuery; + +#[Object] +impl StatusQuery { + /// Files inside some bundles + async fn bundled_files( + &self, + ctx: &Context<'_>, + deployments: Option>, + ) -> Result, anyhow::Error> { + let bundles: Vec = ctx + .data_unchecked::() + .state + .bundles + .lock() + .await + .values() + .map(|b| b.bundle.clone()) + .collect(); + let file_metas: Vec = bundles + .iter() + .flat_map(|b| b.file_manifests.clone()) + .collect(); + + if deployments.is_none() { + return Ok(file_metas + .iter() + .map(|m| GraphQlFileManifestMeta::from(m.clone())) + .collect::>()); + }; + let ids = deployments.unwrap(); + Ok(file_metas + .iter() + .filter(|m| ids.contains(&m.meta_info.hash)) + .cloned() + .map(GraphQlFileManifestMeta::from) + .collect()) + } + + /// A file inside some bundles + async fn bundled_file( + &self, + ctx: &Context<'_>, + deployment: String, + ) -> Result, anyhow::Error> { + let bundles: Vec = ctx + .data_unchecked::() + .state + .bundles + .lock() + .await + .values() + .map(|b| b.bundle.clone()) + .collect(); + let file_metas: Vec = bundles + .iter() + .flat_map(|b| b.file_manifests.clone()) + .collect(); + let manifest_graphql = file_metas + .iter() + .find(|m| m.meta_info.hash == deployment) + .cloned() + .map(GraphQlFileManifestMeta::from); + + Ok(manifest_graphql) + } + + /// Bundles, optional deployments filter + async fn bundles( + &self, + ctx: &Context<'_>, + deployments: Option>, + ) -> Result, anyhow::Error> { + tracing::trace!("received bundles request"); + let all_bundles = &ctx + .data_unchecked::() + .state + .bundles + .lock() + .await + .clone(); + + let bundles = if deployments.is_none() { + tracing::trace!( + bundles = tracing::field::debug(&all_bundles), + "no deployment filter" + ); + all_bundles + .values() + .cloned() + .map(|b| GraphQlBundle::from(b.bundle)) + .collect() + } else { + let ids = deployments.unwrap(); + ids.iter() + .filter_map(|key| all_bundles.get(key)) + .cloned() + .map(|b| GraphQlBundle::from(b.bundle)) + .collect() + }; + tracing::debug!(bundles = tracing::field::debug(&bundles), "queried bundles"); + Ok(bundles) + } + + /// A single bundle by deployment hash + async fn bundle( + &self, + ctx: &Context<'_>, + deployment: String, + ) -> Result, anyhow::Error> { + tracing::trace!("received bundle request"); + let bundle: Option = ctx + .data_unchecked::() + .state + .bundles + .lock() + .await + .get(&deployment) + .map(|b| b.bundle.clone()); + + Ok(bundle.map(GraphQlBundle::from)) + } + + /// Serving files with optional deployments filter + async fn files( + &self, + ctx: &Context<'_>, + deployments: Option>, + ) -> Result, anyhow::Error> { + let file_metas: Vec = ctx + .data_unchecked::() + .state + .files + .lock() + .await + .values() + .cloned() + .collect(); + + if deployments.is_none() { + return Ok(file_metas + .iter() + .map(|m| GraphQlFileManifestMeta::from(m.clone())) + .collect::>()); + }; + let ids = deployments.unwrap(); + Ok(file_metas + .iter() + .filter(|m| ids.contains(&m.meta_info.hash)) + .cloned() + .map(GraphQlFileManifestMeta::from) + .collect()) + } + + /// A single file by deployment hash + async fn file( + &self, + ctx: &Context<'_>, + deployment: String, + ) -> Result, anyhow::Error> { + let file_meta: Option = ctx + .data_unchecked::() + .state + .files + .lock() + .await + .values() + .find(|m| m.meta_info.hash == deployment) + .cloned() + .map(GraphQlFileManifestMeta::from); + + Ok(file_meta) + } +} + +#[derive(Default)] +pub struct PriceQuery; + +#[Object] +impl PriceQuery { + /// Provide an array of cost model to the queried deployment whether it is served or not + async fn cost_models( + &self, + ctx: &Context<'_>, + deployments: Vec, + ) -> Result, anyhow::Error> { + let mut cost_models = vec![]; + for deployment in deployments { + let price: Option = ctx + .data_unchecked::() + .state + .prices + .lock() + .await + .get(&deployment) + .cloned(); + if let Some(p) = price { + cost_models.push(GraphQlCostModel { + deployment, + price_per_byte: p, + }) + } + } + Ok(cost_models) + } + + /// provide a cost model for a specific file/bundle served + async fn cost_model( + &self, + ctx: &Context<'_>, + deployment: String, + ) -> Result, anyhow::Error> { + let model: Option = ctx + .data_unchecked::() + .state + .prices + .lock() + .await + .get(&deployment) + .cloned() + .map(|p| GraphQlCostModel { + deployment, + price_per_byte: p, + }); + Ok(model) + } +} diff --git a/file-service/src/file_server/cost.rs b/file-service/src/file_server/cost.rs index c1ae635..a8f67dd 100644 --- a/file-service/src/file_server/cost.rs +++ b/file-service/src/file_server/cost.rs @@ -1,16 +1,9 @@ -use async_graphql::{Context, EmptyMutation, EmptySubscription, Object, Schema, SimpleObject}; +use async_graphql::{Context, EmptyMutation, EmptySubscription, Object, Schema}; use async_graphql_axum::{GraphQLRequest, GraphQLResponse}; use axum::extract::State; -use serde::{Deserialize, Serialize}; - use crate::file_server::ServerContext; - -#[derive(Clone, Debug, Serialize, Deserialize, SimpleObject)] -pub struct GraphQlCostModel { - pub deployment: String, - pub price_per_byte: f64, -} +use crate::graphql_types::GraphQlCostModel; #[derive(Default)] pub struct PriceQuery; diff --git a/file-service/src/file_server/mod.rs b/file-service/src/file_server/mod.rs index 9592820..ac86b09 100644 --- a/file-service/src/file_server/mod.rs +++ b/file-service/src/file_server/mod.rs @@ -38,7 +38,7 @@ pub struct ServerState { pub client: IpfsClient, pub operator_public_key: String, pub bundles: Arc>>, // Keyed by IPFS hash, valued by Bundle and Local path - pub files: Arc>>, // Keyed by IPFS hash, valued by Bundle and Local path + pub files: Arc>>, // Keyed by IPFS hash, valued by file and Local path pub prices: Arc>>, // Keyed by IPFS hash, valued by price per byte pub admin_auth_token: Option, // Add bearer prefix pub config: Config, diff --git a/file-service/src/file_server/status.rs b/file-service/src/file_server/status.rs index 24b2ef0..ebbf113 100644 --- a/file-service/src/file_server/status.rs +++ b/file-service/src/file_server/status.rs @@ -1,108 +1,12 @@ -use async_graphql::{Context, EmptyMutation, EmptySubscription, Object, Schema, SimpleObject}; +use async_graphql::{Context, EmptyMutation, EmptySubscription, Object, Schema}; use async_graphql_axum::{GraphQLRequest, GraphQLResponse}; use axum::extract::State; -use file_exchange::manifest::{ - Bundle, BundleManifest, FileManifest, FileManifestMeta, FileMetaInfo, -}; +use crate::graphql_types::{GraphQlBundle, GraphQlFileManifestMeta}; +use file_exchange::manifest::{Bundle, FileManifestMeta}; use super::ServerContext; -/* Manifest types with GraphQL type derivation */ - -#[derive(Clone, Debug, SimpleObject)] -pub struct GraphQlBundleManifest { - pub files: Vec, - pub file_type: String, - pub spec_version: String, - pub description: String, - pub chain_id: String, -} - -impl From for GraphQlBundleManifest { - fn from(manifest: BundleManifest) -> Self { - Self { - files: manifest - .files - .into_iter() - .map(GraphQlFileMetaInfo::from) - .collect(), - file_type: manifest.file_type, - spec_version: manifest.spec_version, - description: manifest.description, - chain_id: manifest.chain_id, - } - } -} - -#[derive(Clone, Debug, SimpleObject)] -pub struct GraphQlBundle { - pub ipfs_hash: String, - pub manifest: GraphQlBundleManifest, - pub file_manifests: Vec, -} - -impl From for GraphQlBundle { - fn from(bundle: Bundle) -> Self { - Self { - ipfs_hash: bundle.ipfs_hash, - manifest: GraphQlBundleManifest::from(bundle.manifest), - file_manifests: bundle - .file_manifests - .into_iter() - .map(GraphQlFileManifestMeta::from) - .collect(), - } - } -} - -#[derive(Clone, Debug, SimpleObject)] -pub struct GraphQlFileManifestMeta { - pub meta_info: GraphQlFileMetaInfo, - pub file_manifest: GraphQlFileManifest, -} - -impl From for GraphQlFileManifestMeta { - fn from(meta: FileManifestMeta) -> Self { - Self { - meta_info: GraphQlFileMetaInfo::from(meta.meta_info), - file_manifest: GraphQlFileManifest::from(meta.file_manifest), - } - } -} - -#[derive(Clone, Debug, SimpleObject)] -pub struct GraphQlFileMetaInfo { - pub name: String, - pub hash: String, -} - -impl From for GraphQlFileMetaInfo { - fn from(manifest: FileMetaInfo) -> Self { - Self { - name: manifest.name, - hash: manifest.hash, - } - } -} - -#[derive(Debug, Clone, SimpleObject)] -pub struct GraphQlFileManifest { - pub total_bytes: u64, - pub chunk_size: u64, - pub chunk_hashes: Vec, -} - -impl From for GraphQlFileManifest { - fn from(manifest: FileManifest) -> Self { - Self { - total_bytes: manifest.total_bytes, - chunk_size: manifest.chunk_size, - chunk_hashes: manifest.chunk_hashes, - } - } -} - #[derive(Default)] pub struct StatusQuery; diff --git a/file-service/src/graphql_types.rs b/file-service/src/graphql_types.rs new file mode 100644 index 0000000..58ad551 --- /dev/null +++ b/file-service/src/graphql_types.rs @@ -0,0 +1,107 @@ +use async_graphql::SimpleObject; +use file_exchange::manifest::{ + Bundle, BundleManifest, FileManifest, FileManifestMeta, FileMetaInfo, +}; +use serde::{Deserialize, Serialize}; + +//TODO: would be better to find a way to inherit the GraphQL types from file-exchange crate + +/* Manifest types with GraphQL type derivation */ +#[derive(Clone, Debug, SimpleObject)] +pub struct GraphQlBundleManifest { + pub files: Vec, + pub file_type: String, + pub spec_version: String, + pub description: String, + pub chain_id: String, +} + +impl From for GraphQlBundleManifest { + fn from(manifest: BundleManifest) -> Self { + Self { + files: manifest + .files + .into_iter() + .map(GraphQlFileMetaInfo::from) + .collect(), + file_type: manifest.file_type, + spec_version: manifest.spec_version, + description: manifest.description, + chain_id: manifest.chain_id, + } + } +} + +#[derive(Clone, Debug, SimpleObject)] +pub struct GraphQlBundle { + pub ipfs_hash: String, + pub manifest: GraphQlBundleManifest, + pub file_manifests: Vec, +} + +impl From for GraphQlBundle { + fn from(bundle: Bundle) -> Self { + Self { + ipfs_hash: bundle.ipfs_hash, + manifest: GraphQlBundleManifest::from(bundle.manifest), + file_manifests: bundle + .file_manifests + .into_iter() + .map(GraphQlFileManifestMeta::from) + .collect(), + } + } +} + +#[derive(Clone, Debug, SimpleObject)] +pub struct GraphQlFileManifestMeta { + pub meta_info: GraphQlFileMetaInfo, + pub file_manifest: GraphQlFileManifest, +} + +impl From for GraphQlFileManifestMeta { + fn from(meta: FileManifestMeta) -> Self { + Self { + meta_info: GraphQlFileMetaInfo::from(meta.meta_info), + file_manifest: GraphQlFileManifest::from(meta.file_manifest), + } + } +} + +#[derive(Clone, Debug, SimpleObject)] +pub struct GraphQlFileMetaInfo { + pub name: String, + pub hash: String, +} + +impl From for GraphQlFileMetaInfo { + fn from(manifest: FileMetaInfo) -> Self { + Self { + name: manifest.name, + hash: manifest.hash, + } + } +} + +#[derive(Debug, Clone, SimpleObject)] +pub struct GraphQlFileManifest { + pub total_bytes: u64, + pub chunk_size: u64, + pub chunk_hashes: Vec, +} + +impl From for GraphQlFileManifest { + fn from(manifest: FileManifest) -> Self { + Self { + total_bytes: manifest.total_bytes, + chunk_size: manifest.chunk_size, + chunk_hashes: manifest.chunk_hashes, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, SimpleObject)] +pub struct GraphQlCostModel { + pub deployment: String, + pub price_per_byte: f64, +} diff --git a/file-service/src/lib.rs b/file-service/src/lib.rs index 96ba980..a62e2c1 100644 --- a/file-service/src/lib.rs +++ b/file-service/src/lib.rs @@ -2,4 +2,5 @@ pub mod admin; pub mod config; pub mod database; pub mod file_server; +pub mod graphql_types; pub mod metrics;