From 65c0907463216c00cd4d7b35ccd55c583b97c29c Mon Sep 17 00:00:00 2001 From: osipovartem Date: Tue, 22 Oct 2024 19:01:23 +0300 Subject: [PATCH 1/3] Initial stub for UI api --- crates/nexus/Cargo.toml | 3 + crates/nexus/src/http/mod.rs | 5 + crates/nexus/src/http/router.rs | 21 +- .../nexus/src/http/ui/handlers/databases.rs | 128 ++++ crates/nexus/src/http/ui/handlers/mod.rs | 4 + crates/nexus/src/http/ui/handlers/profiles.rs | 121 ++++ crates/nexus/src/http/ui/handlers/tables.rs | 192 ++++++ .../nexus/src/http/ui/handlers/warehouses.rs | 119 ++++ crates/nexus/src/http/ui/mod.rs | 3 + crates/nexus/src/http/ui/models/aws.rs | 69 ++ crates/nexus/src/http/ui/models/database.rs | 202 ++++++ crates/nexus/src/http/ui/models/errors.rs | 49 ++ crates/nexus/src/http/ui/models/mod.rs | 6 + .../src/http/ui/models/storage_profile.rs | 82 +++ crates/nexus/src/http/ui/models/table.rs | 648 ++++++++++++++++++ crates/nexus/src/http/ui/models/warehouse.rs | 261 +++++++ crates/nexus/src/http/ui/router.rs | 36 + crates/nexus/src/main.rs | 6 + crates/utils/src/lib.rs | 2 +- 19 files changed, 1942 insertions(+), 15 deletions(-) create mode 100644 crates/nexus/src/http/ui/handlers/databases.rs create mode 100644 crates/nexus/src/http/ui/handlers/mod.rs create mode 100644 crates/nexus/src/http/ui/handlers/profiles.rs create mode 100644 crates/nexus/src/http/ui/handlers/tables.rs create mode 100644 crates/nexus/src/http/ui/handlers/warehouses.rs create mode 100644 crates/nexus/src/http/ui/mod.rs create mode 100644 crates/nexus/src/http/ui/models/aws.rs create mode 100644 crates/nexus/src/http/ui/models/database.rs create mode 100644 crates/nexus/src/http/ui/models/errors.rs create mode 100644 crates/nexus/src/http/ui/models/mod.rs create mode 100644 crates/nexus/src/http/ui/models/storage_profile.rs create mode 100644 crates/nexus/src/http/ui/models/table.rs create mode 100644 crates/nexus/src/http/ui/models/warehouse.rs create mode 100644 crates/nexus/src/http/ui/router.rs diff --git a/crates/nexus/Cargo.toml b/crates/nexus/Cargo.toml index 88b980534..437dbd01f 100644 --- a/crates/nexus/Cargo.toml +++ b/crates/nexus/Cargo.toml @@ -24,6 +24,9 @@ utils = { path = "../utils" } utoipa = { workspace = true } utoipa-axum = { workspace = true } utoipa-swagger-ui = { workspace = true } +swagger = { version = "6.1", features = ["serdejson", "server", "client", "tls", "tcp"] } +validator = { version = "0.18.1", features = ["derive"] } +thiserror = { version = "1.0.63" } [dev-dependencies] tower = { workspace = true } diff --git a/crates/nexus/src/http/mod.rs b/crates/nexus/src/http/mod.rs index d61b35cd9..aba4c7da2 100644 --- a/crates/nexus/src/http/mod.rs +++ b/crates/nexus/src/http/mod.rs @@ -4,3 +4,8 @@ pub mod control { pub mod handlers; pub mod schemas; } +pub mod ui { + pub mod handlers; + pub mod models; + pub mod models; +} diff --git a/crates/nexus/src/http/router.rs b/crates/nexus/src/http/router.rs index e96b7620e..023c9bb8f 100644 --- a/crates/nexus/src/http/router.rs +++ b/crates/nexus/src/http/router.rs @@ -1,10 +1,7 @@ -use axum::extract::Path; -use axum::{routing::delete, routing::get, routing::post, Router}; -use std::collections::HashMap; +use axum::Router; use std::fs; -use utoipa::openapi::{self, OpenApiBuilder}; +use utoipa::openapi::{self}; use utoipa::{ - openapi::security::{ApiKey, ApiKeyValue, SecurityScheme}, Modify, OpenApi, }; use utoipa_swagger_ui::SwaggerUi; @@ -13,6 +10,7 @@ use crate::http::catalog::router::create_router as create_catalog_router; use crate::http::control::handlers::storage_profiles::StorageProfileApi; use crate::http::control::handlers::warehouses::WarehouseApi; use crate::http::control::router::create_router as create_control_router; +use crate::http::ui::router::create_router as create_ui_router; use crate::state::AppState; #[derive(OpenApi)] @@ -35,10 +33,12 @@ pub fn create_app(state: AppState) -> Router { } let catalog_router = create_catalog_router(); let control_router = create_control_router(); + let ui_router = create_ui_router(); Router::new() .nest("/", control_router) .nest("/catalog", catalog_router) + .nest("/ui", ui_router) .merge(SwaggerUi::new("/").url("/openapi.yaml", spec)) .with_state(state) } @@ -56,26 +56,20 @@ mod tests { #![allow(clippy::too_many_lines)] use crate::http::catalog::schemas::Namespace as NamespaceSchema; - use crate::http::control; use crate::http::control::schemas::storage_profiles::StorageProfile as StorageProfileSchema; use crate::http::control::schemas::warehouses::Warehouse as WarehouseSchema; use super::*; - use async_trait::async_trait; - use axum::http::request; use axum::{ body::Body, http::{self, Request, StatusCode}, }; use catalog::repository::{DatabaseRepositoryDb, TableRepositoryDb}; use catalog::service::CatalogImpl; - use control_plane::error::{Error, Result}; - use control_plane::models::{StorageProfile, StorageProfileCreateRequest}; - use control_plane::models::{Warehouse, WarehouseCreateRequest}; use control_plane::repository::{StorageProfileRepositoryDb, WarehouseRepositoryDb}; - use control_plane::service::ControlService; use control_plane::service::ControlServiceImpl; - use http_body_util::BodyExt; // for `collect` + use http_body_util::BodyExt; + // for `collect` use object_store::{memory::InMemory, path::Path, ObjectStore}; use serde_json::json; use slatedb::config::DbOptions; @@ -84,7 +78,6 @@ mod tests { use tempfile::TempDir; use tower::{Service, ServiceExt}; use utils::Db; - use uuid::Uuid; lazy_static::lazy_static! { static ref TEMP_DIR: TempDir = TempDir::new().unwrap(); diff --git a/crates/nexus/src/http/ui/handlers/databases.rs b/crates/nexus/src/http/ui/handlers/databases.rs new file mode 100644 index 000000000..6226aab55 --- /dev/null +++ b/crates/nexus/src/http/ui/handlers/databases.rs @@ -0,0 +1,128 @@ +use crate::error::AppError; +use crate::http::ui::models::aws; +use crate::http::ui::models::database; +use crate::http::ui::models::storage_profile; +use crate::http::ui::models::table::Statistics; +use crate::http::ui::models::warehouse; +use crate::state::AppState; +use axum::{extract::Path, extract::State, Json}; +use swagger; +use utoipa::OpenApi; +use uuid::Uuid; + +#[derive(OpenApi)] +#[openapi( + paths( + create_database, + delete_database, + get_database, + ), + components( + schemas( + database::CreateDatabasePayload, + database::Database + ) + ), + tags( + (name = "Databases", description = "Databases management endpoints.") + ) +)] +struct ApiDoc; + +#[utoipa::path( + post, + path = "/warehouses/{warehouseId}/databases/{databaseName}", + operation_id = "createDatabase", + responses( + (status = 200, description = "Successful Response", body = database::Database), + (status = 400, description = "Bad request"), + (status = 500, description = "Internal server error") + ) +)] +pub async fn create_database( + State(state): State, + Path(payload): Path, +) -> Result, AppError> { + Ok(Json(database::Database { + name: "".to_string(), + properties: None, + id: Default::default(), + warehouse_id: Default::default(), + })) +} + +#[utoipa::path( + delete, + path = "/warehouses/{warehouseId}/databases/{databaseName}", + operation_id = "deleteDatabase", + responses( + (status = 204, description = "Successful Response"), + (status = 404, description = "Database not found") + ) +)] +pub async fn delete_database( + State(state): State, + Path((warehouse_id, database_name)): Path<(Uuid, String)>, +) -> Result<(), AppError> { + Ok(()) +} + +#[utoipa::path( + get, + path = "/warehouses/{warehouseId}/databases/{databaseName}", + operation_id = "databaseDashboard", + responses( + (status = 200, description = "Successful Response", body = database::DatabaseDashboard), + (status = 204, description = "Successful Response"), + (status = 404, description = "Database not found") + ) +)] +pub async fn get_database( + State(state): State, + Path((warehouse_id, database_name)): Path<(Uuid, String)>, +) -> Result, AppError> { + Ok(Json(database::DatabaseDashboard { + name: "".to_string(), + properties: None, + id: Default::default(), + warehouse_id: Default::default(), + warehouse: warehouse::WarehouseEntity { + name: "".to_string(), + storage_profile_id: Default::default(), + key_prefix: "".to_string(), + id: Default::default(), + external_id: Default::default(), + location: "".to_string(), + created_at: Default::default(), + updated_at: Default::default(), + storage_profile: storage_profile::StorageProfile { + r#type: aws::CloudProvider::S3, + region: "".to_string(), + bucket: "".to_string(), + credentials: Default::default(), + sts_role_arn: None, + endpoint: None, + id: Default::default(), + created_at: Default::default(), + updated_at: Default::default(), + }, + }, + tables: vec![], + statistics: Statistics { + commit_count: 0, + op_append_count: 0, + op_overwrite_count: 0, + op_delete_count: 0, + op_replace_count: 0, + total_bytes: 0, + bytes_added: 0, + bytes_removed: 0, + total_rows: 0, + rows_added: 0, + rows_deleted: 0, + table_count: None, + database_count: None, + }, + compaction_summary: None, + })) +} diff --git a/crates/nexus/src/http/ui/handlers/mod.rs b/crates/nexus/src/http/ui/handlers/mod.rs new file mode 100644 index 000000000..276413fdb --- /dev/null +++ b/crates/nexus/src/http/ui/handlers/mod.rs @@ -0,0 +1,4 @@ +pub mod databases; +pub mod profiles; +pub mod tables; +pub mod warehouses; diff --git a/crates/nexus/src/http/ui/handlers/profiles.rs b/crates/nexus/src/http/ui/handlers/profiles.rs new file mode 100644 index 000000000..c072690e9 --- /dev/null +++ b/crates/nexus/src/http/ui/handlers/profiles.rs @@ -0,0 +1,121 @@ +use crate::error::AppError; +use crate::http::ui::models::{aws, storage_profile}; +use crate::state::AppState; +use axum::{extract::Path, extract::State, Json}; +use utoipa::OpenApi; +use uuid::Uuid; + +#[derive(OpenApi)] +#[openapi( + paths( + create_storage_profile, + get_storage_profile, + delete_storage_profile, + list_storage_profiles, + ), + components( + schemas( + storage_profile::CreateStorageProfilePayload, + storage_profile::StorageProfile, + aws::Credentials, + aws::AwsAccessKeyCredential, + aws::AwsRoleCredential, + aws::CloudProvider, + ) + ), + tags( + (name = "Storage profiles", description = "Storage profiles management endpoints.") + ) +)] +struct ApiDoc; + +#[utoipa::path( + post, + operation_id = "createStorageProfile", + path = "/storage-profiles", + request_body = storage_profile::CreateStorageProfilePayload, + responses( + (status = 200, description = "Successful Response", body = storage_profile::StorageProfile), + (status = 400, description = "Bad request"), + (status = 500, description = "Internal server error") + ) +)] +pub async fn create_storage_profile( + State(state): State, + Json(payload): Json, +) -> Result, AppError> { + Ok(Json(storage_profile::StorageProfile { + r#type: aws::CloudProvider::S3, + region: "2".to_string(), + bucket: "".to_string(), + credentials: Default::default(), + sts_role_arn: None, + endpoint: None, + id: Default::default(), + created_at: Default::default(), + updated_at: Default::default(), + })) +} + +#[utoipa::path( + get, + operation_id = "getStorageProfile", + path = "/storage-profiles/{storageProfileId}", + params( + ("storageProfileId" = Uuid, Path, description = "Storage profile ID") + ), + responses( + (status = 200, description = "Successful Response", body = storage_profile::StorageProfile), + (status = 404, description = "Not found"), + ) +)] +pub async fn get_storage_profile( + State(state): State, + Path(id): Path, +) -> Result, AppError> { + Ok(Json(storage_profile::StorageProfile { + r#type: aws::CloudProvider::S3, + region: "1".to_string(), + bucket: "".to_string(), + credentials: Default::default(), + sts_role_arn: None, + endpoint: None, + id: Default::default(), + created_at: Default::default(), + updated_at: Default::default(), + })) +} + +#[utoipa::path( + delete, + operation_id = "deleteStorageProfile", + path = "/storage-profiles/{storageProfileId}", + params( + ("storageProfileId" = Uuid, Path, description = "Storage profile ID") + ), + responses( + (status = 200, description = "Successful Response", body = storage_profile::StorageProfile), + (status = 404, description = "Not found"), + ) +)] +pub async fn delete_storage_profile( + State(state): State, + Path(id): Path, +) -> Result, AppError> { + Ok(Json(())) +} + +#[utoipa::path( + get, + operation_id = "listStorageProfiles", + path = "/storage-profiles/", + responses( + (status = 200, body = Vec), + (status = 500, description = "Internal server error") + ) +)] +pub async fn list_storage_profiles( + State(state): State, +) -> Result>, AppError> { + Ok(Json(vec![])) +} diff --git a/crates/nexus/src/http/ui/handlers/tables.rs b/crates/nexus/src/http/ui/handlers/tables.rs new file mode 100644 index 000000000..a08d7e730 --- /dev/null +++ b/crates/nexus/src/http/ui/handlers/tables.rs @@ -0,0 +1,192 @@ +use crate::error::AppError; +use crate::http::ui::models::{aws, database, storage_profile, table, warehouse}; +use crate::state::AppState; +use axum::{extract::Path, extract::State, Json}; +use utoipa::OpenApi; +use uuid::Uuid; + +#[derive(OpenApi)] +#[openapi( + paths( + create_table, + delete_table, + get_table, + list_tables, + ), + components( + schemas( + table::TableExtended, + database::Database, + ) + ), + tags( + (name = "Tables", description = "Tables management endpoints.") + ) +)] +struct ApiDoc; + +#[utoipa::path( + get, + path = "/warehouses/{warehouseId}/databases/{databaseName}/tables", + operation_id = "tablesDashboard", + responses( + (status = 200, description = "List all warehouses", body = Vec), + (status = 500, description = "Internal server error") + ) +)] +pub async fn list_tables( + State(state): State, +) -> Result>, AppError> { + Ok(Json(vec![])) +} + +#[utoipa::path( + get, + path = "/warehouses/{warehouseId}/databases/{databaseName}/tables/{tableName}", + operation_id = "tablesDashboard", + params( + ("warehouseId" = Uuid, Path, description = "Warehouse ID"), + ("databaseName" = Uuid, Path, description = "Database Name"), + ("tableName" = Uuid, Path, description = "Table name") + ), + responses( + (status = 200, description = "List all warehouses", body = Vec), + (status = 500, description = "Internal server error") + ) +)] +pub async fn get_table( + State(state): State, + Path((warehouse_id, database_name, table_name)): Path<(Uuid, String, String)>, +) -> Result, AppError> { + Ok(Json(table::TableExtended { + id: Default::default(), + name: table_name, + database_name: Default::default(), + warehouse_id: Default::default(), + properties: None, + metadata: Default::default(), + statistics: None, + compaction_summary: None, + created_at: Default::default(), + updated_at: Default::default(), + database: database::DatabaseExtended { + name: "1".to_string(), + properties: None, + id: Default::default(), + warehouse_id, + statistics: None, + compaction_summary: None, + created_at: Default::default(), + updated_at: Default::default(), + warehouse: warehouse::WarehouseExtended { + name: "11".to_string(), + storage_profile_id: Default::default(), + key_prefix: "".to_string(), + id: Default::default(), + external_id: Default::default(), + location: "".to_string(), + created_at: Default::default(), + updated_at: Default::default(), + statistics: None, + compaction_summary: None, + storage_profile: storage_profile::StorageProfile { + r#type: aws::CloudProvider::S3, + region: "22".to_string(), + bucket: "2".to_string(), + credentials: Default::default(), + sts_role_arn: None, + endpoint: None, + id: Default::default(), + created_at: Default::default(), + updated_at: Default::default(), + }, + }, + }, + })) +} + +#[utoipa::path( + get, + operation_id = "createTable", + path = "/warehouses/{warehouseId}/databases/{databaseName}/tables/{tableName}", + params( + ("warehouseId" = Uuid, Path, description = "Warehouse ID"), + ("databaseName" = Uuid, Path, description = "Database Name"), + ("tableName" = Uuid, Path, description = "Table name") + ), + responses( + (status = 200, description = "Successful Response", body = table::TableExtended), + (status = 404, description = "Not found"), + ) +)] +pub async fn create_table( + State(state): State, + Json(payload): Json, +) -> Result, AppError> { + Ok(Json(table::TableExtended { + id: Default::default(), + name: "3".to_string(), + database_name: "3".to_string(), + warehouse_id: Default::default(), + properties: None, + metadata: Default::default(), + statistics: None, + compaction_summary: None, + created_at: Default::default(), + updated_at: Default::default(), + database: database::DatabaseExtended { + name: "4".to_string(), + properties: None, + id: Default::default(), + warehouse_id: Default::default(), + statistics: None, + compaction_summary: None, + created_at: Default::default(), + updated_at: Default::default(), + warehouse: warehouse::WarehouseExtended { + name: "1".to_string(), + storage_profile_id: Default::default(), + key_prefix: "".to_string(), + id: Default::default(), + external_id: Default::default(), + location: "".to_string(), + created_at: Default::default(), + updated_at: Default::default(), + statistics: None, + compaction_summary: None, + storage_profile: storage_profile::StorageProfile { + r#type: aws::CloudProvider::S3, + region: "2".to_string(), + bucket: "2".to_string(), + credentials: Default::default(), + sts_role_arn: None, + endpoint: None, + id: Default::default(), + created_at: Default::default(), + updated_at: Default::default(), + }, + }, + }, + })) +} + +#[utoipa::path( + get, + operation_id = "tableDashboard", + path = "/warehouses/{warehouseId}/databases/{databaseName}/tables/{tableName}", + params( + ("warehouseId" = Uuid, Path, description = "Warehouse ID"), + ("databaseName" = Uuid, Path, description = "Database Name"), + ("tableName" = Uuid, Path, description = "Table name") + ), + responses( + (status = 200, description = "Successful Response", body = table::TableExtended), + (status = 404, description = "Not found"), + ) +)] +pub async fn delete_table( + State(state): State, + Path((warehouse_id, database_name, table_name)): Path<(Uuid, String, String)>, +) -> Result<(), AppError> { + Ok(()) +} diff --git a/crates/nexus/src/http/ui/handlers/warehouses.rs b/crates/nexus/src/http/ui/handlers/warehouses.rs new file mode 100644 index 000000000..6ff98f4be --- /dev/null +++ b/crates/nexus/src/http/ui/handlers/warehouses.rs @@ -0,0 +1,119 @@ +use crate::error::AppError; +use crate::http::ui::models::warehouse; +use crate::state::AppState; +use axum::{extract::Path, extract::State, Json}; +use utoipa::OpenApi; +use uuid::Uuid; + +#[derive(OpenApi)] +#[openapi( + paths( + get_warehouse, + list_warehouses, + create_warehouse, + delete_warehouse, + ), + components( + schemas( + warehouse::Warehouse, + warehouse::WarehousesDashboard, + warehouse::CreateWarehousePayload, + ) + ), + tags( + (name = "Warehouse", description = "Warehouse management endpoints.") + ) +)] +struct ApiDoc; + +#[utoipa::path( + get, + path = "/warehouses", + operation_id = "warehousesDashboard", + responses( + (status = 200, description = "List all warehouses", body = Vec), + (status = 500, description = "Internal server error") + ) +)] +pub async fn list_warehouses( + State(state): State, +) -> Result>, AppError> { + Ok(Json(vec![])) +} + +#[utoipa::path( + get, + path = "/warehouses/{warehouseId}", + operation_id = "warehouseDashboard", + params( + ("warehouseId" = Uuid, Path, description = "Warehouse ID") + ), + responses( + (status = 200, description = "Warehouse found", body = warehouse::Warehouse), + (status = 404, description = "Warehouse not found") + ) +)] +pub async fn get_warehouse( + State(state): State, + Path(warehouse_id): Path, +) -> Result, AppError> { + // let warehouse = state.warehouse_service.get_warehouse(warehouse_id).await?; + Ok(Json(warehouse::Warehouse { + name: "".to_string(), + storage_profile_id: Default::default(), + key_prefix: "key".to_string(), + id: Default::default(), + external_id: Default::default(), + location: "".to_string(), + created_at: Default::default(), + updated_at: Default::default(), + })) +} + +#[utoipa::path( + post, + path = "/warehouses", + request_body = warehouse::CreateWarehousePayload, + operation_id = "createWarehouse", + responses( + (status = 201, description = "Warehouse created", body = warehouse::Warehouse), + (status = 400, description = "Bad request"), + (status = 500, description = "Internal server error") + ) +)] +pub async fn create_warehouse( + State(state): State, + Json(payload): Json, +) -> Result, AppError> { + Ok(Json(warehouse::Warehouse { + name: "".to_string(), + storage_profile_id: Default::default(), + key_prefix: "".to_string(), + id: Default::default(), + external_id: Default::default(), + location: "".to_string(), + created_at: Default::default(), + updated_at: Default::default(), + })) +} + +#[utoipa::path( + delete, + path = "/warehouses/{warehouseId}", + operation_id = "deleteWarehouse", + params( + ("warehouseId" = Uuid, Path, description = "Warehouse ID") + ), + responses( + (status = 204, description = "Warehouse deleted"), + (status = 404, description = "Warehouse not found") + ) +)] +pub async fn delete_warehouse( + State(state): State, + Path(warehouse_id): Path, +) -> Result<(), AppError> { + Ok(()) +} + + diff --git a/crates/nexus/src/http/ui/mod.rs b/crates/nexus/src/http/ui/mod.rs new file mode 100644 index 000000000..0b03afec4 --- /dev/null +++ b/crates/nexus/src/http/ui/mod.rs @@ -0,0 +1,3 @@ +pub mod router; +pub mod models; +pub mod handlers; diff --git a/crates/nexus/src/http/ui/models/aws.rs b/crates/nexus/src/http/ui/models/aws.rs new file mode 100644 index 000000000..c9ac48747 --- /dev/null +++ b/crates/nexus/src/http/ui/models/aws.rs @@ -0,0 +1,69 @@ +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use validator::Validate; + +#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, Validate, ToSchema)] +pub struct AwsAccessKeyCredential { + #[validate(length(min = 1))] + pub aws_access_key_id: String, + #[validate(length(min = 1))] + pub aws_secret_access_key: String, +} + +impl AwsAccessKeyCredential { + #[allow(clippy::new_without_default)] + pub fn new( + aws_access_key_id: String, + aws_secret_access_key: String, + ) -> AwsAccessKeyCredential { + AwsAccessKeyCredential { + aws_access_key_id, + aws_secret_access_key, + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct AwsRoleCredential { + #[validate(length(min = 1))] + pub role_arn: String, + #[validate(length(min = 1))] + pub external_id: String, +} + +impl AwsRoleCredential { + #[allow(clippy::new_without_default)] + pub fn new( + role_arn: String, + external_id: String, + ) -> AwsRoleCredential { + AwsRoleCredential { + role_arn, + external_id, + } + } +} + +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash, ToSchema +)] +pub enum CloudProvider { + #[serde(rename = "s3")] + S3, + #[serde(rename = "gcs")] + Gcs, + #[serde(rename = "azure")] + Azure, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)] +pub enum Credentials { + AwsAccessKeyCredential(AwsAccessKeyCredential), + AwsRoleCredential(AwsRoleCredential), +} + +impl Default for Credentials { + fn default() -> Self { + Credentials::AwsAccessKeyCredential(AwsAccessKeyCredential::default()) + } +} \ No newline at end of file diff --git a/crates/nexus/src/http/ui/models/database.rs b/crates/nexus/src/http/ui/models/database.rs new file mode 100644 index 000000000..2b45024b6 --- /dev/null +++ b/crates/nexus/src/http/ui/models/database.rs @@ -0,0 +1,202 @@ +use crate::http::ui::models::table::{Statistics, TableEntity, TableShort}; +use crate::http::ui::models::warehouse::{WarehouseEntity, WarehouseExtended}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use validator::Validate; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct CreateDatabasePayload { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub properties: Option, +} + +impl CreateDatabasePayload { + #[allow(clippy::new_without_default)] + pub fn new(name: String) -> CreateDatabasePayload { + CreateDatabasePayload { + name, + properties: None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct Database { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub properties: Option, + pub id: uuid::Uuid, + pub warehouse_id: uuid::Uuid, +} + +impl Database { + #[allow(clippy::new_without_default)] + pub fn new(name: String, id: uuid::Uuid, warehouse_id: uuid::Uuid) -> Database { + Database { + name, + properties: None, + id, + warehouse_id, + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct DatabaseDashboard { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub properties: Option, + pub id: uuid::Uuid, + pub warehouse_id: uuid::Uuid, + pub warehouse: WarehouseEntity, + pub tables: Vec, + pub statistics: Statistics, + #[serde(skip_serializing_if = "Option::is_none")] + pub compaction_summary: Option, +} + +impl DatabaseDashboard { + #[allow(clippy::new_without_default)] + pub fn new( + name: String, + id: uuid::Uuid, + warehouse_id: uuid::Uuid, + warehouse: WarehouseEntity, + tables: Vec, + statistics: Statistics, + ) -> DatabaseDashboard { + DatabaseDashboard { + name, + properties: None, + id, + warehouse_id, + warehouse, + tables, + statistics, + compaction_summary: None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct DatabaseEntity { + pub id: uuid::Uuid, + pub name: String, + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, + pub statistics: Statistics, + #[serde(skip_serializing_if = "Option::is_none")] + pub compaction_summary: Option, +} + +impl DatabaseEntity { + #[allow(clippy::new_without_default)] + pub fn new( + id: uuid::Uuid, + name: String, + created_at: chrono::DateTime, + updated_at: chrono::DateTime, + statistics: Statistics, + ) -> DatabaseEntity { + DatabaseEntity { + id, + name, + created_at, + updated_at, + statistics, + compaction_summary: None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct DatabaseExtended { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub properties: Option, + pub id: uuid::Uuid, + pub warehouse_id: uuid::Uuid, + #[serde(skip_serializing_if = "Option::is_none")] + pub statistics: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub compaction_summary: Option, + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, + pub warehouse: WarehouseExtended, +} + +impl DatabaseExtended { + #[allow(clippy::new_without_default)] + pub fn new( + name: String, + id: uuid::Uuid, + warehouse_id: uuid::Uuid, + created_at: chrono::DateTime, + updated_at: chrono::DateTime, + warehouse: WarehouseExtended, + ) -> DatabaseExtended { + DatabaseExtended { + name, + properties: None, + id, + warehouse_id, + statistics: None, + compaction_summary: None, + created_at, + updated_at, + warehouse, + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct DatabaseShort { + pub id: uuid::Uuid, + pub name: String, + pub tables: Vec, +} + +impl DatabaseShort { + #[allow(clippy::new_without_default)] + pub fn new(id: uuid::Uuid, name: String, tables: Vec) -> DatabaseShort { + DatabaseShort { id, name, tables } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct CompactionSummary { + pub compactions: i32, + pub starting_files: i32, + pub rewritten_files: i32, + pub file_percent: i32, + pub starting_size: i32, + pub rewritten_size: i32, + pub size_change: i32, + pub size_percent: i32, +} + +impl CompactionSummary { + #[allow(clippy::new_without_default)] + pub fn new( + compactions: i32, + starting_files: i32, + rewritten_files: i32, + file_percent: i32, + starting_size: i32, + rewritten_size: i32, + size_change: i32, + size_percent: i32, + ) -> CompactionSummary { + CompactionSummary { + compactions, + starting_files, + rewritten_files, + file_percent, + starting_size, + rewritten_size, + size_change, + size_percent, + } + } +} diff --git a/crates/nexus/src/http/ui/models/errors.rs b/crates/nexus/src/http/ui/models/errors.rs new file mode 100644 index 000000000..de5c934f1 --- /dev/null +++ b/crates/nexus/src/http/ui/models/errors.rs @@ -0,0 +1,49 @@ +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("bad request: {0}")] + BadRequest(String), + #[error("not found")] + NotFound, + #[error("internal server error")] + InternalServerError, + #[error("already exists")] + AlreadyExists, + #[error("not empty")] + NotEmpty, + #[error("unprocessable entity")] + UnprocessableEntity, + #[error("not implemented")] + Implemented, + #[error("DB error: {0}")] + DbError(String), + #[error("Iceberg error: {0}")] + IcebergError(#[from] iceberg::Error), +} + +impl IntoResponse for Error { + fn into_response(self) -> Response { + let (status, error_message) = match self { + Error::NotFound => (StatusCode::NOT_FOUND, self.to_string()), + Error::DbError(ref e) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), + Error::InternalServerError => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), + Error::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg), + _ => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), + }; + + (status, error_message).into_response() + } +} + +impl From for Error { + fn from(e: utils::Error) -> Self { + match e { + utils::Error::DbError(e) => Error::DbError(e.to_string()), + utils::Error::SerializeError(e) => Error::DbError(e.to_string()), + utils::Error::DeserializeError(e) => Error::DbError(e.to_string()), + utils::Error::ErrNotFound => Error::NotFound, + } + } +} diff --git a/crates/nexus/src/http/ui/models/mod.rs b/crates/nexus/src/http/ui/models/mod.rs new file mode 100644 index 000000000..4f204fabe --- /dev/null +++ b/crates/nexus/src/http/ui/models/mod.rs @@ -0,0 +1,6 @@ +pub mod errors; +pub mod aws; +pub mod database; +pub mod table; +pub mod warehouse; +pub mod storage_profile; diff --git a/crates/nexus/src/http/ui/models/storage_profile.rs b/crates/nexus/src/http/ui/models/storage_profile.rs new file mode 100644 index 000000000..d48c44db6 --- /dev/null +++ b/crates/nexus/src/http/ui/models/storage_profile.rs @@ -0,0 +1,82 @@ +use crate::http::ui::models::aws::{CloudProvider, Credentials}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use validator::Validate; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct CreateStorageProfilePayload { + #[serde(rename = "type")] + pub r#type: CloudProvider, + #[validate(length(min = 1))] + pub region: String, + #[validate(length(min = 6, max = 63))] + pub bucket: String, + pub credentials: Credentials, + #[serde(skip_serializing_if = "Option::is_none")] + pub sts_role_arn: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub endpoint: Option, +} + +impl CreateStorageProfilePayload { + #[allow(clippy::new_without_default)] + pub fn new( + r#type: CloudProvider, + region: String, + bucket: String, + credentials: Credentials, + ) -> CreateStorageProfilePayload { + CreateStorageProfilePayload { + r#type, + region, + bucket, + credentials, + sts_role_arn: None, + endpoint: None, + } + } +} + + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct StorageProfile { + #[serde(rename = "type")] + pub r#type: CloudProvider, + #[validate(length(min = 1))] + pub region: String, + #[validate(length(min = 6, max = 63))] + pub bucket: String, + pub credentials: Credentials, + #[serde(skip_serializing_if = "Option::is_none")] + pub sts_role_arn: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub endpoint: Option, + pub id: uuid::Uuid, + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, +} + +impl StorageProfile { + #[allow(clippy::new_without_default)] + pub fn new( + r#type: CloudProvider, + region: String, + bucket: String, + credentials: Credentials, + id: uuid::Uuid, + created_at: chrono::DateTime, + updated_at: chrono::DateTime, + ) -> StorageProfile { + StorageProfile { + r#type, + region, + bucket, + credentials, + sts_role_arn: None, + endpoint: None, + id, + created_at, + updated_at, + } + } +} diff --git a/crates/nexus/src/http/ui/models/table.rs b/crates/nexus/src/http/ui/models/table.rs new file mode 100644 index 000000000..1111592a3 --- /dev/null +++ b/crates/nexus/src/http/ui/models/table.rs @@ -0,0 +1,648 @@ +use crate::http::ui::models::database::{CompactionSummary, DatabaseExtended}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use validator::Validate; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct TableShort { + pub id: uuid::Uuid, + pub name: String, +} + +impl TableShort { + #[allow(clippy::new_without_default)] + pub fn new(id: uuid::Uuid, name: String) -> TableShort { + TableShort { id, name } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct TableMetadataV2 { + pub location: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub table_uuid: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub last_updated_ms: Option, + pub last_column_id: i32, + #[serde(skip_serializing_if = "Option::is_none")] + pub schemas: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub current_schema_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub partition_specs: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub default_spec_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub last_partition_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub properties: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub current_snapshot_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub snapshots: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub snapshot_log: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata_log: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub sort_orders: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub default_sort_order_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub refs: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub format_version: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub last_sequence_number: Option, +} + +impl TableMetadataV2 { + #[allow(clippy::new_without_default)] + pub fn new(location: String, last_column_id: i32) -> TableMetadataV2 { + TableMetadataV2 { + location, + table_uuid: None, + last_updated_ms: None, + last_column_id, + schemas: None, + current_schema_id: Some(0), + partition_specs: None, + default_spec_id: Some(0), + last_partition_id: None, + properties: None, + current_snapshot_id: None, + snapshots: None, + snapshot_log: None, + metadata_log: None, + sort_orders: None, + default_sort_order_id: Some(0), + refs: None, + format_version: None, + last_sequence_number: Some(0), + } + } +} + +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Serialize, + Deserialize, + ToSchema, + Hash +)] +pub enum FormatVersion1 { + #[serde(rename = "2")] + Variant2, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct MetadataLogEntry { + pub metadata_file: String, + pub timestamp_ms: i32, +} + +impl MetadataLogEntry { + #[allow(clippy::new_without_default)] + pub fn new(metadata_file: String, timestamp_ms: i32) -> MetadataLogEntry { + MetadataLogEntry { + metadata_file, + timestamp_ms, + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct PartitionSpec { + #[serde(skip_serializing_if = "Option::is_none")] + pub spec_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub fields: Option>, +} + +impl PartitionSpec { + #[allow(clippy::new_without_default)] + pub fn new() -> PartitionSpec { + PartitionSpec { + spec_id: Some(0), + fields: None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct Schema { + #[serde(skip_serializing_if = "Option::is_none")] + pub r#type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub fields: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub schema_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub identifier_field_ids: Option>, +} + +impl Schema { + #[allow(clippy::new_without_default)] + pub fn new() -> Schema { + Schema { + r#type: None, + fields: None, + schema_id: Some(0), + identifier_field_ids: None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct Snapshot { + pub snapshot_id: i32, + #[serde(skip_serializing_if = "Option::is_none")] + pub parent_snapshot_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub sequence_number: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub timestamp_ms: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub manifest_list: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub summary: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub schema_id: Option, +} + +impl Snapshot { + #[allow(clippy::new_without_default)] + pub fn new(snapshot_id: i32) -> Snapshot { + Snapshot { + snapshot_id, + parent_snapshot_id: None, + sequence_number: None, + timestamp_ms: None, + manifest_list: None, + summary: None, + schema_id: None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct SnapshotLogEntry { + pub snapshot_id: i32, + pub timestamp_ms: i32, +} + +impl SnapshotLogEntry { + #[allow(clippy::new_without_default)] + pub fn new(snapshot_id: i32, timestamp_ms: i32) -> SnapshotLogEntry { + SnapshotLogEntry { + snapshot_id, + timestamp_ms, + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct SnapshotRef { + pub snapshot_id: i32, + pub r#type: SnapshotRefType, + #[serde(skip_serializing_if = "Option::is_none")] + pub min_snapshots_to_keep: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub max_snapshot_age_ms: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub max_ref_age_ms: Option, +} + +impl SnapshotRef { + #[allow(clippy::new_without_default)] + pub fn new(snapshot_id: i32, r#type: SnapshotRefType) -> SnapshotRef { + SnapshotRef { + snapshot_id, + r#type, + min_snapshots_to_keep: None, + max_snapshot_age_ms: None, + max_ref_age_ms: None, + } + } +} + +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Serialize, + Deserialize, + Hash, + ToSchema +)] +pub enum SnapshotRefType { + #[serde(rename = "branch")] + Branch, + #[serde(rename = "tag")] + Tag, +} + +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Serialize, + Deserialize, + Hash, + ToSchema +)] +pub enum FormatVersion { + #[serde(rename = "1")] + Variant1, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct NestedField { + pub id: i32, + pub name: String, + pub r#type: serde_json::Value, + #[serde(skip_serializing_if = "Option::is_none")] + pub required: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub doc: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub initial_default: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub write_default: Option, +} + +impl NestedField { + #[allow(clippy::new_without_default)] + pub fn new(id: i32, name: String, r#type: serde_json::Value) -> NestedField { + NestedField { + id, + name, + r#type, + required: Some(false), + doc: None, + initial_default: None, + write_default: None, + } + } +} + +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Serialize, + Deserialize, + Hash, + ToSchema +)] +pub enum NullOrder { + #[serde(rename = "nulls-first")] + First, + #[serde(rename = "nulls-last")] + Last, +} + +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Serialize, + Deserialize, + Hash, + ToSchema +)] +pub enum Operation { + #[serde(rename = "append")] + Append, + #[serde(rename = "replace")] + Replace, + #[serde(rename = "overwrite")] + Overwrite, + #[serde(rename = "delete")] + Delete, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct PartitionField { + pub source_id: i32, + pub field_id: i32, + pub transform: String, + pub name: String, +} + +impl PartitionField { + #[allow(clippy::new_without_default)] + pub fn new(source_id: i32, field_id: i32, transform: String, name: String) -> PartitionField { + PartitionField { + source_id, + field_id, + transform, + name, + } + } +} + +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Serialize, + Deserialize, + Hash, + ToSchema +)] +pub enum SortDirection { + #[serde(rename = "asc")] + Asc, + #[serde(rename = "desc")] + Desc, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct SortField { + pub source_id: i32, + pub transform: String, + pub direction: SortDirection, + pub null_order: NullOrder, +} + +impl SortField { + #[allow(clippy::new_without_default)] + pub fn new( + source_id: i32, + transform: String, + direction: SortDirection, + null_order: NullOrder, + ) -> SortField { + SortField { + source_id, + transform, + direction, + null_order, + } + } +} + +/// Describes how the data is sorted within the table. Users can sort their data within partitions by columns to gain performance. The order of the sort fields within the list defines the order in which the sort is applied to the data. Args: fields (List[SortField]): The fields how the table is sorted. Keyword Args: order_id (int): An unique id of the sort-order of a table. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct SortOrder { + #[serde(skip_serializing_if = "Option::is_none")] + pub order_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub fields: Option>, +} + +impl SortOrder { + #[allow(clippy::new_without_default)] + pub fn new() -> SortOrder { + SortOrder { + order_id: Some(1), + fields: None, + } + } +} + +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Serialize, + Deserialize, + Hash, + ToSchema +)] +pub enum Type { + #[serde(rename = "struct")] + Struct, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct TableEntity { + pub id: uuid::Uuid, + pub name: String, + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, + pub statistics: Statistics, + #[serde(skip_serializing_if = "Option::is_none")] + pub compaction_summary: Option, +} + +impl TableEntity { + #[allow(clippy::new_without_default)] + pub fn new( + id: uuid::Uuid, + name: String, + created_at: chrono::DateTime, + updated_at: chrono::DateTime, + statistics: Statistics, + ) -> TableEntity { + TableEntity { + id, + name, + created_at, + updated_at, + statistics, + compaction_summary: None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct TableExtended { + pub id: uuid::Uuid, + pub name: String, + pub database_name: String, + pub warehouse_id: uuid::Uuid, + #[serde(skip_serializing_if = "Option::is_none")] + pub properties: Option, + pub metadata: serde_json::Value, + #[serde(skip_serializing_if = "Option::is_none")] + pub statistics: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub compaction_summary: Option, + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, + pub database: DatabaseExtended, +} + +impl TableExtended { + #[allow(clippy::new_without_default)] + pub fn new( + id: uuid::Uuid, + name: String, + database_name: String, + warehouse_id: uuid::Uuid, + metadata: serde_json::Value, + created_at: chrono::DateTime, + updated_at: chrono::DateTime, + database: DatabaseExtended, + ) -> TableExtended { + TableExtended { + id, + name, + database_name, + warehouse_id, + properties: None, + metadata, + statistics: None, + compaction_summary: None, + created_at, + updated_at, + database, + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct TableMetadataV1 { + pub location: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub table_uuid: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub last_updated_ms: Option, + pub last_column_id: i32, + #[serde(skip_serializing_if = "Option::is_none")] + pub schemas: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub current_schema_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub partition_specs: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub default_spec_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub last_partition_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub properties: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub current_snapshot_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub snapshots: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub snapshot_log: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata_log: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub sort_orders: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub default_sort_order_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub refs: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub format_version: Option, + pub schema: Schema, + #[serde(skip_serializing_if = "Option::is_none")] + pub partition_spec: Option>, +} + +impl TableMetadataV1 { + #[allow(clippy::new_without_default)] + pub fn new(location: String, last_column_id: i32, schema: Schema) -> TableMetadataV1 { + TableMetadataV1 { + location, + table_uuid: None, + last_updated_ms: None, + last_column_id, + schemas: None, + current_schema_id: Some(0), + partition_specs: None, + default_spec_id: Some(0), + last_partition_id: None, + properties: None, + current_snapshot_id: None, + snapshots: None, + snapshot_log: None, + metadata_log: None, + sort_orders: None, + default_sort_order_id: Some(0), + refs: None, + format_version: None, + schema, + partition_spec: None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct Statistics { + pub commit_count: i32, + pub op_append_count: i32, + pub op_overwrite_count: i32, + pub op_delete_count: i32, + pub op_replace_count: i32, + pub total_bytes: i32, + pub bytes_added: i32, + pub bytes_removed: i32, + pub total_rows: i32, + pub rows_added: i32, + pub rows_deleted: i32, + #[serde(skip_serializing_if = "Option::is_none")] + pub table_count: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub database_count: Option, +} + +impl Statistics { + #[allow(clippy::new_without_default)] + pub fn new( + commit_count: i32, + op_append_count: i32, + op_overwrite_count: i32, + op_delete_count: i32, + op_replace_count: i32, + total_bytes: i32, + bytes_added: i32, + bytes_removed: i32, + total_rows: i32, + rows_added: i32, + rows_deleted: i32, + ) -> Statistics { + Statistics { + commit_count, + op_append_count, + op_overwrite_count, + op_delete_count, + op_replace_count, + total_bytes, + bytes_added, + bytes_removed, + total_rows, + rows_added, + rows_deleted, + table_count: None, + database_count: None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct WriteDefault(swagger::AnyOf6); diff --git a/crates/nexus/src/http/ui/models/warehouse.rs b/crates/nexus/src/http/ui/models/warehouse.rs new file mode 100644 index 000000000..7999fd161 --- /dev/null +++ b/crates/nexus/src/http/ui/models/warehouse.rs @@ -0,0 +1,261 @@ +#![allow(unused_qualifications)] + +use crate::http::ui::models::database::{CompactionSummary, DatabaseEntity, DatabaseShort}; +use crate::http::ui::models::storage_profile::StorageProfile; +use crate::http::ui::models::table::Statistics; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; +use validator::Validate; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct Navigation { + pub warehouses: Vec, +} + +impl Navigation { + #[allow(clippy::new_without_default)] + pub fn new(warehouses: Vec) -> Navigation { + Navigation { warehouses } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct CreateWarehousePayload { + #[validate(length(min = 1))] + pub name: String, + pub storage_profile_id: uuid::Uuid, + #[validate(length(min = 1))] + pub key_prefix: String, +} + +impl CreateWarehousePayload { + #[allow(clippy::new_without_default)] + pub fn new( + name: String, + storage_profile_id: uuid::Uuid, + key_prefix: String, + ) -> CreateWarehousePayload { + CreateWarehousePayload { + name, + storage_profile_id, + key_prefix, + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct Warehouse { + #[validate(length(min = 1))] + pub name: String, + pub storage_profile_id: uuid::Uuid, + #[validate(length(min = 1))] + pub key_prefix: String, + pub id: uuid::Uuid, + pub external_id: uuid::Uuid, + pub location: String, + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, +} + +impl Warehouse { + #[allow(clippy::new_without_default)] + pub fn new( + name: String, + storage_profile_id: uuid::Uuid, + key_prefix: String, + id: uuid::Uuid, + external_id: uuid::Uuid, + location: String, + created_at: chrono::DateTime, + updated_at: chrono::DateTime, + ) -> Warehouse { + Warehouse { + name, + storage_profile_id, + key_prefix, + id, + external_id, + location, + created_at, + updated_at, + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct WarehouseDashboard { + #[validate(length(min = 1))] + pub name: String, + pub storage_profile_id: uuid::Uuid, + #[validate(length(min = 1))] + pub key_prefix: String, + pub id: uuid::Uuid, + pub external_id: uuid::Uuid, + pub location: String, + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, + pub storage_profile: StorageProfile, + pub databases: Vec, + pub statistics: Statistics, + #[serde(skip_serializing_if = "Option::is_none")] + pub compaction_summary: Option, +} + +impl WarehouseDashboard { + #[allow(clippy::new_without_default)] + pub fn new( + name: String, + storage_profile_id: uuid::Uuid, + key_prefix: String, + id: uuid::Uuid, + external_id: uuid::Uuid, + location: String, + created_at: chrono::DateTime, + updated_at: chrono::DateTime, + storage_profile: StorageProfile, + databases: Vec, + statistics: Statistics, + ) -> WarehouseDashboard { + WarehouseDashboard { + name, + storage_profile_id, + key_prefix, + id, + external_id, + location, + created_at, + updated_at, + storage_profile, + databases, + statistics, + compaction_summary: None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct WarehouseEntity { + #[validate(length(min = 1))] + pub name: String, + pub storage_profile_id: uuid::Uuid, + #[validate(length(min = 1))] + pub key_prefix: String, + pub id: uuid::Uuid, + pub external_id: uuid::Uuid, + pub location: String, + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, + pub storage_profile: StorageProfile, +} + +impl WarehouseEntity { + #[allow(clippy::new_without_default)] + pub fn new( + name: String, + storage_profile_id: uuid::Uuid, + key_prefix: String, + id: uuid::Uuid, + external_id: uuid::Uuid, + location: String, + created_at: chrono::DateTime, + updated_at: chrono::DateTime, + storage_profile: StorageProfile, + ) -> WarehouseEntity { + WarehouseEntity { + name, + storage_profile_id, + key_prefix, + id, + external_id, + location, + created_at, + updated_at, + storage_profile, + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct WarehouseExtended { + #[validate(length(min = 1))] + pub name: String, + pub storage_profile_id: uuid::Uuid, + #[validate(length(min = 1))] + pub key_prefix: String, + pub id: uuid::Uuid, + pub external_id: uuid::Uuid, + pub location: String, + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, + #[serde(skip_serializing_if = "Option::is_none")] + pub statistics: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub compaction_summary: Option, + pub storage_profile: StorageProfile, +} + +impl WarehouseExtended { + #[allow(clippy::new_without_default)] + pub fn new( + name: String, + storage_profile_id: uuid::Uuid, + key_prefix: String, + id: uuid::Uuid, + external_id: uuid::Uuid, + location: String, + created_at: chrono::DateTime, + updated_at: chrono::DateTime, + storage_profile: StorageProfile, + ) -> WarehouseExtended { + WarehouseExtended { + name, + storage_profile_id, + key_prefix, + id, + external_id, + location, + created_at, + updated_at, + statistics: None, + compaction_summary: None, + storage_profile, + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct WarehouseShort { + pub id: uuid::Uuid, + pub name: String, + pub databases: Vec, +} + +impl WarehouseShort { + #[allow(clippy::new_without_default)] + pub fn new(id: uuid::Uuid, name: String, databases: Vec) -> WarehouseShort { + WarehouseShort { + id, + name, + databases, + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] +pub struct WarehousesDashboard { + pub warehouses: Vec, + pub statistics: Statistics, + #[serde(skip_serializing_if = "Option::is_none")] + pub compaction_summary: Option, +} + +impl WarehousesDashboard { + #[allow(clippy::new_without_default)] + pub fn new(warehouses: Vec, statistics: Statistics) -> WarehousesDashboard { + WarehousesDashboard { + warehouses, + statistics, + compaction_summary: None, + } + } +} diff --git a/crates/nexus/src/http/ui/router.rs b/crates/nexus/src/http/ui/router.rs new file mode 100644 index 000000000..a9bf0008d --- /dev/null +++ b/crates/nexus/src/http/ui/router.rs @@ -0,0 +1,36 @@ +use crate::http::ui::handlers::databases::{delete_database, get_database}; +use crate::http::ui::handlers::profiles::{ + create_storage_profile, delete_storage_profile, get_storage_profile, list_storage_profiles, +}; +use crate::http::ui::handlers::tables::get_table; +use crate::http::ui::handlers::warehouses::{ + create_warehouse, delete_warehouse, get_warehouse, list_warehouses, +}; +use crate::state::AppState; +use axum::routing::{delete, get, post}; +use axum::Router; + +pub fn create_router() -> Router { + Router::new() + .route("/warehouses", post(create_warehouse).get(list_warehouses)) + .route( + "/warehouses/:warehouseId", + delete(delete_warehouse).get(get_warehouse), + ) + .route( + "/warehouses/:warehouseId/databases/:databaseName", + get(get_database).delete(delete_database), + ) + .route( + "/warehouses/:warehouseId/databases/:databaseName/tables/:tableName", + get(get_table), + ) + .route( + "/storage-profiles", + post(create_storage_profile).get(list_storage_profiles), + ) + .route( + "/storage-profiles/:storageProfileId", + delete(delete_storage_profile).get(get_storage_profile), + ) +} diff --git a/crates/nexus/src/main.rs b/crates/nexus/src/main.rs index 4c944ad05..c73deb1cf 100644 --- a/crates/nexus/src/main.rs +++ b/crates/nexus/src/main.rs @@ -21,6 +21,12 @@ pub mod http { pub mod router; pub mod schemas; } + + pub mod ui { + pub mod handlers; + pub mod router; + pub mod models; + } } pub mod error; pub mod state; diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index 158b78d6e..c71d48540 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -83,7 +83,7 @@ impl Db { // it then gets value from the clousre, serialize it and write it back to the db pub async fn modify(&self, key: &str, f: impl Fn(&mut T)) -> Result<()> where - T: serde::Serialize + serde::de::DeserializeOwned + Default, + T: serde::Serialize + DeserializeOwned + Default, { let mut value: T = self.get(key).await?.unwrap_or_default(); From 31932609310153d82b3d13f3a78321af002df7b4 Mon Sep 17 00:00:00 2001 From: osipovartem Date: Tue, 22 Oct 2024 19:14:39 +0300 Subject: [PATCH 2/3] fmt --- crates/catalog/src/lib.rs | 6 +- crates/control_plane/src/lib.rs | 1 - .../http/control/handlers/storage_profiles.rs | 23 +++-- .../src/http/control/handlers/warehouses.rs | 7 +- .../nexus/src/http/ui/handlers/warehouses.rs | 2 - crates/nexus/src/http/ui/models/aws.rs | 14 +--- crates/nexus/src/http/ui/models/mod.rs | 4 +- .../src/http/ui/models/storage_profile.rs | 1 - crates/nexus/src/http/ui/models/table.rs | 84 ++----------------- 9 files changed, 35 insertions(+), 107 deletions(-) diff --git a/crates/catalog/src/lib.rs b/crates/catalog/src/lib.rs index 6ba937723..52635d4f2 100644 --- a/crates/catalog/src/lib.rs +++ b/crates/catalog/src/lib.rs @@ -1,4 +1,4 @@ -pub mod service; -pub mod repository; +pub mod error; pub mod models; -pub mod error; \ No newline at end of file +pub mod repository; +pub mod service; diff --git a/crates/control_plane/src/lib.rs b/crates/control_plane/src/lib.rs index 3c17b1d35..ec2570387 100644 --- a/crates/control_plane/src/lib.rs +++ b/crates/control_plane/src/lib.rs @@ -8,4 +8,3 @@ pub mod repository; // This will expose everything inside repository.rs pub mod service; // This will expose everything inside service.rs pub mod error; - diff --git a/crates/nexus/src/http/control/handlers/storage_profiles.rs b/crates/nexus/src/http/control/handlers/storage_profiles.rs index 639e8158c..3dadfe8fc 100644 --- a/crates/nexus/src/http/control/handlers/storage_profiles.rs +++ b/crates/nexus/src/http/control/handlers/storage_profiles.rs @@ -1,24 +1,35 @@ use crate::http::control::schemas::storage_profiles::{ - AwsAccessKeyCredential, AwsRoleCredential, CloudProvider, CreateStorageProfilePayload, Credentials, StorageProfile + AwsAccessKeyCredential, AwsRoleCredential, CloudProvider, CreateStorageProfilePayload, + Credentials, StorageProfile, }; use axum::{extract::Path, extract::State, Json}; use control_plane::models::{StorageProfile as StorageProfileModel, StorageProfileCreateRequest}; use std::result::Result; -use uuid::Uuid; use utoipa::OpenApi; +use uuid::Uuid; use crate::error::AppError; use crate::state::AppState; - #[derive(OpenApi)] #[openapi( - paths(create_storage_profile, get_storage_profile, delete_storage_profile, list_storage_profiles,), - components(schemas(CreateStorageProfilePayload, StorageProfile, Credentials, AwsAccessKeyCredential, AwsRoleCredential, CloudProvider),) + paths( + create_storage_profile, + get_storage_profile, + delete_storage_profile, + list_storage_profiles, + ), + components(schemas( + CreateStorageProfilePayload, + StorageProfile, + Credentials, + AwsAccessKeyCredential, + AwsRoleCredential, + CloudProvider + ),) )] pub struct StorageProfileApi; - #[utoipa::path( post, operation_id = "createStorageProfile", diff --git a/crates/nexus/src/http/control/handlers/warehouses.rs b/crates/nexus/src/http/control/handlers/warehouses.rs index 3ffe92ef3..dee603346 100644 --- a/crates/nexus/src/http/control/handlers/warehouses.rs +++ b/crates/nexus/src/http/control/handlers/warehouses.rs @@ -1,6 +1,4 @@ -use crate::http::control::schemas::warehouses::{ - CreateWarehouseRequest, Warehouse, -}; +use crate::http::control::schemas::warehouses::{CreateWarehouseRequest, Warehouse}; use axum::{extract::Path, extract::State, Json}; use control_plane::models::{Warehouse as WarehouseModel, WarehouseCreateRequest}; use std::result::Result; @@ -10,7 +8,6 @@ use uuid::Uuid; use crate::error::AppError; use crate::state::AppState; - // #[derive(OpenApi)] // #[openapi( // paths(create_storage_profile, get_storage_profile, delete_storage_profile, list_storage_profiles,), @@ -21,7 +18,7 @@ use crate::state::AppState; #[derive(OpenApi)] #[openapi( paths(create_warehouse, get_warehouse, delete_warehouse, list_warehouses,), - components(schemas(CreateWarehouseRequest, Warehouse, ),) + components(schemas(CreateWarehouseRequest, Warehouse,),) )] pub struct WarehouseApi; diff --git a/crates/nexus/src/http/ui/handlers/warehouses.rs b/crates/nexus/src/http/ui/handlers/warehouses.rs index 6ff98f4be..157af5b67 100644 --- a/crates/nexus/src/http/ui/handlers/warehouses.rs +++ b/crates/nexus/src/http/ui/handlers/warehouses.rs @@ -115,5 +115,3 @@ pub async fn delete_warehouse( ) -> Result<(), AppError> { Ok(()) } - - diff --git a/crates/nexus/src/http/ui/models/aws.rs b/crates/nexus/src/http/ui/models/aws.rs index c9ac48747..13e424da2 100644 --- a/crates/nexus/src/http/ui/models/aws.rs +++ b/crates/nexus/src/http/ui/models/aws.rs @@ -12,10 +12,7 @@ pub struct AwsAccessKeyCredential { impl AwsAccessKeyCredential { #[allow(clippy::new_without_default)] - pub fn new( - aws_access_key_id: String, - aws_secret_access_key: String, - ) -> AwsAccessKeyCredential { + pub fn new(aws_access_key_id: String, aws_secret_access_key: String) -> AwsAccessKeyCredential { AwsAccessKeyCredential { aws_access_key_id, aws_secret_access_key, @@ -33,10 +30,7 @@ pub struct AwsRoleCredential { impl AwsRoleCredential { #[allow(clippy::new_without_default)] - pub fn new( - role_arn: String, - external_id: String, - ) -> AwsRoleCredential { + pub fn new(role_arn: String, external_id: String) -> AwsRoleCredential { AwsRoleCredential { role_arn, external_id, @@ -45,7 +39,7 @@ impl AwsRoleCredential { } #[derive( - Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash, ToSchema + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash, ToSchema, )] pub enum CloudProvider { #[serde(rename = "s3")] @@ -66,4 +60,4 @@ impl Default for Credentials { fn default() -> Self { Credentials::AwsAccessKeyCredential(AwsAccessKeyCredential::default()) } -} \ No newline at end of file +} diff --git a/crates/nexus/src/http/ui/models/mod.rs b/crates/nexus/src/http/ui/models/mod.rs index 4f204fabe..0973978ac 100644 --- a/crates/nexus/src/http/ui/models/mod.rs +++ b/crates/nexus/src/http/ui/models/mod.rs @@ -1,6 +1,6 @@ -pub mod errors; pub mod aws; pub mod database; +pub mod errors; +pub mod storage_profile; pub mod table; pub mod warehouse; -pub mod storage_profile; diff --git a/crates/nexus/src/http/ui/models/storage_profile.rs b/crates/nexus/src/http/ui/models/storage_profile.rs index d48c44db6..d1a382170 100644 --- a/crates/nexus/src/http/ui/models/storage_profile.rs +++ b/crates/nexus/src/http/ui/models/storage_profile.rs @@ -37,7 +37,6 @@ impl CreateStorageProfilePayload { } } - #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] pub struct StorageProfile { #[serde(rename = "type")] diff --git a/crates/nexus/src/http/ui/models/table.rs b/crates/nexus/src/http/ui/models/table.rs index 1111592a3..24281b5b7 100644 --- a/crates/nexus/src/http/ui/models/table.rs +++ b/crates/nexus/src/http/ui/models/table.rs @@ -84,17 +84,7 @@ impl TableMetadataV2 { } #[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - PartialOrd, - Ord, - Serialize, - Deserialize, - ToSchema, - Hash + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, ToSchema, Hash, )] pub enum FormatVersion1 { #[serde(rename = "2")] @@ -233,17 +223,7 @@ impl SnapshotRef { } #[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - PartialOrd, - Ord, - Serialize, - Deserialize, - Hash, - ToSchema + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash, ToSchema, )] pub enum SnapshotRefType { #[serde(rename = "branch")] @@ -253,17 +233,7 @@ pub enum SnapshotRefType { } #[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - PartialOrd, - Ord, - Serialize, - Deserialize, - Hash, - ToSchema + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash, ToSchema, )] pub enum FormatVersion { #[serde(rename = "1")] @@ -301,17 +271,7 @@ impl NestedField { } #[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - PartialOrd, - Ord, - Serialize, - Deserialize, - Hash, - ToSchema + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash, ToSchema, )] pub enum NullOrder { #[serde(rename = "nulls-first")] @@ -321,17 +281,7 @@ pub enum NullOrder { } #[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - PartialOrd, - Ord, - Serialize, - Deserialize, - Hash, - ToSchema + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash, ToSchema, )] pub enum Operation { #[serde(rename = "append")] @@ -365,17 +315,7 @@ impl PartitionField { } #[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - PartialOrd, - Ord, - Serialize, - Deserialize, - Hash, - ToSchema + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash, ToSchema, )] pub enum SortDirection { #[serde(rename = "asc")] @@ -429,17 +369,7 @@ impl SortOrder { } #[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - PartialOrd, - Ord, - Serialize, - Deserialize, - Hash, - ToSchema + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash, ToSchema, )] pub enum Type { #[serde(rename = "struct")] From 7256cc31356f50a0cd0cbaee3e09ee1fb49137f5 Mon Sep 17 00:00:00 2001 From: osipovartem Date: Tue, 22 Oct 2024 21:47:36 +0300 Subject: [PATCH 3/3] openapi --- crates/nexus/src/http/router.rs | 13 +++++++++++-- crates/nexus/src/http/ui/handlers/databases.rs | 14 +++++++------- crates/nexus/src/http/ui/handlers/profiles.rs | 18 +++++++++--------- crates/nexus/src/http/ui/handlers/tables.rs | 18 +++++++++--------- .../nexus/src/http/ui/handlers/warehouses.rs | 18 +++++++++--------- 5 files changed, 45 insertions(+), 36 deletions(-) diff --git a/crates/nexus/src/http/router.rs b/crates/nexus/src/http/router.rs index 023c9bb8f..e046be21d 100644 --- a/crates/nexus/src/http/router.rs +++ b/crates/nexus/src/http/router.rs @@ -10,6 +10,10 @@ use crate::http::catalog::router::create_router as create_catalog_router; use crate::http::control::handlers::storage_profiles::StorageProfileApi; use crate::http::control::handlers::warehouses::WarehouseApi; use crate::http::control::router::create_router as create_control_router; +use crate::http::ui::handlers::databases::ApiDoc as DatabaseApiDoc; +use crate::http::ui::handlers::profiles::ApiDoc as ProfileApiDoc; +use crate::http::ui::handlers::tables::ApiDoc as TableApiDoc; +use crate::http::ui::handlers::warehouses::ApiDoc as WarehouseApiDoc; use crate::http::ui::router::create_router as create_ui_router; use crate::state::AppState; @@ -22,14 +26,19 @@ use crate::state::AppState; tags( (name = "storage-profile", description = "Storage profile API"), (name = "warehouse", description = "Warehouse API"), + (name = "ui", description = "Web UI API"), ) )] -struct ApiDoc; +pub struct ApiDoc; pub fn create_app(state: AppState) -> Router { let mut spec = ApiDoc::openapi(); if let Some(extra_spec) = load_openapi_spec() { - spec = spec.merge_from(extra_spec); + spec = spec.merge_from(extra_spec) + .merge_from(WarehouseApiDoc::openapi()) + .merge_from(TableApiDoc::openapi()) + .merge_from(DatabaseApiDoc::openapi()) + .merge_from(ProfileApiDoc::openapi()); } let catalog_router = create_catalog_router(); let control_router = create_control_router(); diff --git a/crates/nexus/src/http/ui/handlers/databases.rs b/crates/nexus/src/http/ui/handlers/databases.rs index 6226aab55..7cdf9975b 100644 --- a/crates/nexus/src/http/ui/handlers/databases.rs +++ b/crates/nexus/src/http/ui/handlers/databases.rs @@ -27,12 +27,12 @@ use uuid::Uuid; (name = "Databases", description = "Databases management endpoints.") ) )] -struct ApiDoc; +pub struct ApiDoc; #[utoipa::path( post, - path = "/warehouses/{warehouseId}/databases/{databaseName}", - operation_id = "createDatabase", + path = "/ui/warehouses/{warehouseId}/databases", + operation_id = "webCreateDatabase", responses( (status = 200, description = "Successful Response", body = database::Database), (status = 400, description = "Bad request"), @@ -53,8 +53,8 @@ pub async fn create_database( #[utoipa::path( delete, - path = "/warehouses/{warehouseId}/databases/{databaseName}", - operation_id = "deleteDatabase", + path = "/ui/warehouses/{warehouseId}/databases/{databaseName}", + operation_id = "webDeleteDatabase", responses( (status = 204, description = "Successful Response"), (status = 404, description = "Database not found") @@ -69,8 +69,8 @@ pub async fn delete_database( #[utoipa::path( get, - path = "/warehouses/{warehouseId}/databases/{databaseName}", - operation_id = "databaseDashboard", + path = "/ui/warehouses/{warehouseId}/databases/{databaseName}", + operation_id = "webDatabaseDashboard", responses( (status = 200, description = "Successful Response", body = database::DatabaseDashboard), (status = 204, description = "Successful Response"), diff --git a/crates/nexus/src/http/ui/handlers/profiles.rs b/crates/nexus/src/http/ui/handlers/profiles.rs index c072690e9..7ff37415d 100644 --- a/crates/nexus/src/http/ui/handlers/profiles.rs +++ b/crates/nexus/src/http/ui/handlers/profiles.rs @@ -27,12 +27,12 @@ use uuid::Uuid; (name = "Storage profiles", description = "Storage profiles management endpoints.") ) )] -struct ApiDoc; +pub struct ApiDoc; #[utoipa::path( post, - operation_id = "createStorageProfile", - path = "/storage-profiles", + operation_id = "webCreateStorageProfile", + path = "/ui/storage-profiles", request_body = storage_profile::CreateStorageProfilePayload, responses( (status = 200, description = "Successful Response", body = storage_profile::StorageProfile), @@ -59,8 +59,8 @@ pub async fn create_storage_profile( #[utoipa::path( get, - operation_id = "getStorageProfile", - path = "/storage-profiles/{storageProfileId}", + operation_id = "webGetStorageProfile", + path = "/ui/storage-profiles/{storageProfileId}", params( ("storageProfileId" = Uuid, Path, description = "Storage profile ID") ), @@ -88,8 +88,8 @@ pub async fn get_storage_profile( #[utoipa::path( delete, - operation_id = "deleteStorageProfile", - path = "/storage-profiles/{storageProfileId}", + operation_id = "webDeleteStorageProfile", + path = "/ui/storage-profiles/{storageProfileId}", params( ("storageProfileId" = Uuid, Path, description = "Storage profile ID") ), @@ -107,8 +107,8 @@ pub async fn delete_storage_profile( #[utoipa::path( get, - operation_id = "listStorageProfiles", - path = "/storage-profiles/", + operation_id = "webListStorageProfiles", + path = "/ui/storage-profiles/", responses( (status = 200, body = Vec), (status = 500, description = "Internal server error") diff --git a/crates/nexus/src/http/ui/handlers/tables.rs b/crates/nexus/src/http/ui/handlers/tables.rs index a08d7e730..d42fde963 100644 --- a/crates/nexus/src/http/ui/handlers/tables.rs +++ b/crates/nexus/src/http/ui/handlers/tables.rs @@ -23,12 +23,12 @@ use uuid::Uuid; (name = "Tables", description = "Tables management endpoints.") ) )] -struct ApiDoc; +pub struct ApiDoc; #[utoipa::path( get, - path = "/warehouses/{warehouseId}/databases/{databaseName}/tables", - operation_id = "tablesDashboard", + path = "/ui/warehouses/{warehouseId}/databases/{databaseName}/tables", + operation_id = "webTablesDashboard", responses( (status = 200, description = "List all warehouses", body = Vec), (status = 500, description = "Internal server error") @@ -42,8 +42,8 @@ pub async fn list_tables( #[utoipa::path( get, - path = "/warehouses/{warehouseId}/databases/{databaseName}/tables/{tableName}", - operation_id = "tablesDashboard", + path = "/ui/warehouses/{warehouseId}/databases/{databaseName}/tables/{tableName}", + operation_id = "webTableDashboard", params( ("warehouseId" = Uuid, Path, description = "Warehouse ID"), ("databaseName" = Uuid, Path, description = "Database Name"), @@ -107,8 +107,8 @@ pub async fn get_table( #[utoipa::path( get, - operation_id = "createTable", - path = "/warehouses/{warehouseId}/databases/{databaseName}/tables/{tableName}", + operation_id = "webCreateTable", + path = "/ui/warehouses/{warehouseId}/databases/{databaseName}/tables/{tableName}", params( ("warehouseId" = Uuid, Path, description = "Warehouse ID"), ("databaseName" = Uuid, Path, description = "Database Name"), @@ -172,8 +172,8 @@ pub async fn create_table( #[utoipa::path( get, - operation_id = "tableDashboard", - path = "/warehouses/{warehouseId}/databases/{databaseName}/tables/{tableName}", + operation_id = "webDeleteTable", + path = "/ui/warehouses/{warehouseId}/databases/{databaseName}/tables/{tableName}", params( ("warehouseId" = Uuid, Path, description = "Warehouse ID"), ("databaseName" = Uuid, Path, description = "Database Name"), diff --git a/crates/nexus/src/http/ui/handlers/warehouses.rs b/crates/nexus/src/http/ui/handlers/warehouses.rs index 157af5b67..7aa69072c 100644 --- a/crates/nexus/src/http/ui/handlers/warehouses.rs +++ b/crates/nexus/src/http/ui/handlers/warehouses.rs @@ -24,12 +24,12 @@ use uuid::Uuid; (name = "Warehouse", description = "Warehouse management endpoints.") ) )] -struct ApiDoc; +pub struct ApiDoc; #[utoipa::path( get, - path = "/warehouses", - operation_id = "warehousesDashboard", + path = "/ui/warehouses", + operation_id = "webWarehousesDashboard", responses( (status = 200, description = "List all warehouses", body = Vec), (status = 500, description = "Internal server error") @@ -43,8 +43,8 @@ pub async fn list_warehouses( #[utoipa::path( get, - path = "/warehouses/{warehouseId}", - operation_id = "warehouseDashboard", + path = "/ui/warehouses/{warehouseId}", + operation_id = "webWarehouseDashboard", params( ("warehouseId" = Uuid, Path, description = "Warehouse ID") ), @@ -72,9 +72,9 @@ pub async fn get_warehouse( #[utoipa::path( post, - path = "/warehouses", + path = "/ui/warehouses", request_body = warehouse::CreateWarehousePayload, - operation_id = "createWarehouse", + operation_id = "webCreateWarehouse", responses( (status = 201, description = "Warehouse created", body = warehouse::Warehouse), (status = 400, description = "Bad request"), @@ -99,8 +99,8 @@ pub async fn create_warehouse( #[utoipa::path( delete, - path = "/warehouses/{warehouseId}", - operation_id = "deleteWarehouse", + path = "/ui/warehouses/{warehouseId}", + operation_id = "webCeleteWarehouse", params( ("warehouseId" = Uuid, Path, description = "Warehouse ID") ),