diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..665eae358 Binary files /dev/null and b/.DS_Store differ diff --git a/crates/.DS_Store b/crates/.DS_Store new file mode 100644 index 000000000..6fb3e4249 Binary files /dev/null and b/crates/.DS_Store differ diff --git a/crates/nexus/.DS_Store b/crates/nexus/.DS_Store new file mode 100644 index 000000000..4ff5bc484 Binary files /dev/null and b/crates/nexus/.DS_Store differ diff --git a/crates/nexus/src/.DS_Store b/crates/nexus/src/.DS_Store new file mode 100644 index 000000000..f29b635df Binary files /dev/null and b/crates/nexus/src/.DS_Store differ diff --git a/crates/nexus/src/http/mod.rs b/crates/nexus/src/http/mod.rs index aba4c7da2..8ad6642be 100644 --- a/crates/nexus/src/http/mod.rs +++ b/crates/nexus/src/http/mod.rs @@ -7,5 +7,4 @@ pub mod control { pub mod ui { pub mod handlers; pub mod models; - pub mod models; } diff --git a/crates/nexus/src/http/ui/handlers/common.rs b/crates/nexus/src/http/ui/handlers/common.rs new file mode 100644 index 000000000..60727d745 --- /dev/null +++ b/crates/nexus/src/http/ui/handlers/common.rs @@ -0,0 +1,59 @@ +use crate::http::ui::models::database::Database; +use crate::http::ui::models::errors::AppError; +use crate::http::ui::models::storage_profile::StorageProfile; +use crate::http::ui::models::warehouse::Warehouse; +use crate::state::AppState; +use catalog::models::{DatabaseIdent, Table}; +use control_plane::models::Warehouse as WarehouseModel; +use uuid::Uuid; + +impl AppState { + pub async fn get_warehouse_model( + &self, + warehouse_id: Uuid, + ) -> Result { + self.control_svc + .get_warehouse(warehouse_id) + .await + .map_err(|e| { + let fmt = format!("{}: failed to get warehouse by id {}", e, warehouse_id); + AppError::new(e, fmt.as_str()) + }) + } + pub async fn get_warehouse_by_id(&self, warehouse_id: Uuid) -> Result { + self.get_warehouse_model(warehouse_id).await.map(|warehouse| warehouse.into()) + } + + pub async fn get_profile_by_id( + &self, + storage_profile_id: Uuid, + ) -> Result { + self.control_svc + .get_profile(storage_profile_id) + .await + .map_err(|e| { + let fmt = format!("{}: failed to get profile by id {}", e, storage_profile_id); + AppError::new(e, fmt.as_str()) + }).map(|profile| profile.into()) + } + + pub async fn get_database_by_ident(&self, ident: &DatabaseIdent) -> Result { + self.catalog_svc + .get_namespace(ident) + .await + .map_err(|e| { + let fmt = format!("{}: failed to get database with db ident {}", e, &ident); + AppError::new(e, fmt.as_str()) + }).map(|database| database.into()) + } + + pub async fn list_tables(&self, ident: &DatabaseIdent) -> Result, AppError> { + self.catalog_svc.list_tables(ident).await.map_err(|e| { + let fmt = format!( + "{}: failed to get database tables with db ident {}", + e, &ident + ); + AppError::new(e, fmt.as_str()) + }) + } +} diff --git a/crates/nexus/src/http/ui/handlers/databases.rs b/crates/nexus/src/http/ui/handlers/databases.rs index 11bf287f9..447519f0c 100644 --- a/crates/nexus/src/http/ui/handlers/databases.rs +++ b/crates/nexus/src/http/ui/handlers/databases.rs @@ -55,14 +55,7 @@ pub async fn create_database( Path(warehouse_id): Path, Json(payload): Json, ) -> Result, AppError> { - let warehouse = state - .control_svc - .get_warehouse(warehouse_id) - .await - .map_err(|e| { - let fmt = format!("{}: failed to get warehouse by id {}", e, warehouse_id); - AppError::new(e, fmt.as_str()) - })?; + let warehouse = state.get_warehouse_by_id(warehouse_id).await?; let ident = DatabaseIdent { warehouse: WarehouseIdent::new(warehouse.id), namespace: NamespaceIdent::new(payload.name), @@ -130,48 +123,18 @@ pub async fn get_database( State(state): State, Path((warehouse_id, database_name)): Path<(Uuid, String)>, ) -> Result, AppError> { - let warehouse = state - .control_svc - .get_warehouse(warehouse_id) - .await - .map_err(|e| { - let fmt = format!("{}: failed to get warehouse by id {}", e, warehouse_id); - AppError::new(e, fmt.as_str()) - })?; - let profile = state - .control_svc - .get_profile(warehouse.storage_profile_id) - .await - .map_err(|e| { - let fmt = format!( - "{}: failed to get profile by id {}", - e, warehouse.storage_profile_id - ); - AppError::new(e, fmt.as_str()) - })?; + let warehouse = state.get_warehouse_by_id(warehouse_id).await?; + let profile = state.get_profile_by_id(warehouse.storage_profile_id).await?; let ident = DatabaseIdent { warehouse: WarehouseIdent::new(warehouse.id), namespace: NamespaceIdent::new(database_name), }; - let database = state.catalog_svc.get_namespace(&ident).await.map_err(|e| { - let fmt = format!( - "{}: failed to get database with db ident {}", - e, &ident - ); - AppError::new(e, fmt.as_str()) - })?; - let tables = state.catalog_svc.list_tables(&ident).await - .map_err(|e| { - let fmt = format!( - "{}: failed to get database tables with db ident {}", - e, &ident - ); - AppError::new(e, fmt.as_str()) - })?; + let database = state.get_database_by_ident(&ident).await?; + let tables = state.catalog_svc.list_tables(&ident).await?; Ok(Json(DatabaseDashboard { - name: database.ident.to_string(), + name: ident.namespace.first().unwrap().to_string(), properties: Option::from(database.properties), - id: get_database_id(database.ident), + id: get_database_id(ident), warehouse_id, warehouse: WarehouseEntity::new(warehouse.into(), profile.into()), tables: tables diff --git a/crates/nexus/src/http/ui/handlers/mod.rs b/crates/nexus/src/http/ui/handlers/mod.rs index 276413fdb..04bf86f25 100644 --- a/crates/nexus/src/http/ui/handlers/mod.rs +++ b/crates/nexus/src/http/ui/handlers/mod.rs @@ -2,3 +2,5 @@ pub mod databases; pub mod profiles; pub mod tables; pub mod warehouses; + +pub mod common; diff --git a/crates/nexus/src/http/ui/handlers/profiles.rs b/crates/nexus/src/http/ui/handlers/profiles.rs index f7e1a4819..8f3ee3602 100644 --- a/crates/nexus/src/http/ui/handlers/profiles.rs +++ b/crates/nexus/src/http/ui/handlers/profiles.rs @@ -77,17 +77,7 @@ pub async fn get_storage_profile( State(state): State, Path(storage_profile_id): Path, ) -> Result, AppError> { - let profile: StorageProfile = state - .control_svc - .get_profile(storage_profile_id) - .await - .map_err(|e| { - let fmt = format!( - "{}: failed to get storage profile with id {}", - e, storage_profile_id - ); - AppError::new(e, fmt.as_str()) - })?; + let profile = state.get_profile_by_id(storage_profile_id).await?; Ok(Json(profile.into())) } diff --git a/crates/nexus/src/http/ui/handlers/tables.rs b/crates/nexus/src/http/ui/handlers/tables.rs index 67eddfd82..bba17b8bd 100644 --- a/crates/nexus/src/http/ui/handlers/tables.rs +++ b/crates/nexus/src/http/ui/handlers/tables.rs @@ -1,7 +1,9 @@ use crate::http::ui::models::errors::AppError; use crate::http::ui::models::table; use crate::http::ui::models::table::TableQueryRequest; -use crate::http::ui::models::table::{Table, TableCreateRequest, TableExtended}; +use crate::http::ui::models::table::{ + Table, TableCreateRequest, TableExtended, TableQueryResponse, +}; use crate::state::AppState; use axum::{extract::Path, extract::State, Json}; use catalog::models::{DatabaseIdent, TableIdent, WarehouseIdent}; @@ -19,7 +21,11 @@ use uuid::Uuid; ), components( schemas( - table::TableQueryResponse, + TableQueryResponse, + TableQueryRequest, + TableExtended, + TableCreateRequest, + Table, AppError, ) ), @@ -49,34 +55,15 @@ pub async fn get_table( State(state): State, Path((warehouse_id, database_name, table_name)): Path<(Uuid, String, String)>, ) -> Result, AppError> { - let warehouse = state - .control_svc - .get_warehouse(warehouse_id) - .await - .map_err(|e| { - let fmt = format!("{}: failed to get warehouse by id {}", e, warehouse_id); - AppError::new(e, fmt.as_str()) - })?; + let warehouse = state.get_warehouse_by_id(warehouse_id).await?; let profile = state - .control_svc - .get_profile(warehouse.storage_profile_id) - .await - .map_err(|e| { - let fmt = format!( - "{}: failed to get profile by id {}", - e, warehouse.storage_profile_id - ); - AppError::new(e, fmt.as_str()) - })?; + .get_profile_by_id(warehouse.storage_profile_id) + .await?; let ident = DatabaseIdent { warehouse: WarehouseIdent::new(warehouse.id), namespace: NamespaceIdent::new(database_name), }; - let database = state.catalog_svc.get_namespace(&ident).await.map_err(|e| { - let fmt = format!("{}: failed to get database with db ident {}", e, &ident); - AppError::new(e, fmt.as_str()) - })?; - + let database = state.get_database_by_ident(&ident).await?; let table_ident = TableIdent { database: ident, table: table_name, @@ -116,14 +103,7 @@ pub async fn create_table( Path((warehouse_id, database_name)): Path<(Uuid, String)>, Json(payload): Json, ) -> Result, AppError> { - let warehouse = state - .control_svc - .get_warehouse(warehouse_id) - .await - .map_err(|e| { - let fmt = format!("{}: failed to get warehouse by id {}", e, warehouse_id); - AppError::new(e, fmt.as_str()) - })?; + let warehouse = state.get_warehouse_model(warehouse_id).await?; let db_ident = DatabaseIdent { warehouse: WarehouseIdent::new(warehouse.id), namespace: NamespaceIdent::new(database_name), @@ -158,14 +138,7 @@ pub async fn delete_table( State(state): State, Path((warehouse_id, database_name, table_name)): Path<(Uuid, String, String)>, ) -> Result<(), AppError> { - let warehouse = state - .control_svc - .get_warehouse(warehouse_id) - .await - .map_err(|e| { - let fmt = format!("{}: failed to get warehouse by id {}", e, warehouse_id); - AppError::new(e, fmt.as_str()) - })?; + let warehouse = state.get_warehouse_by_id(warehouse_id).await?; let table_ident = TableIdent { database: DatabaseIdent { warehouse: WarehouseIdent::new(warehouse.id), @@ -187,7 +160,7 @@ pub async fn delete_table( #[utoipa::path( post, path = "/ui/warehouses/{warehouseId}/databases/{databaseName}/tables/{tableName}/query", - request_body = table::TableQueryRequest, + request_body = TableQueryRequest, operation_id = "webTableQuery", params( ("warehouseId" = Uuid, Path, description = "Warehouse ID"), @@ -195,7 +168,7 @@ pub async fn delete_table( ("tableName" = Uuid, Path, description = "Table name") ), responses( - (status = 200, description = "Returns result of the query", body = Vec), + (status = 200, description = "Returns result of the query", body = TableQueryResponse), (status = 500, description = "Internal server error") ) )] diff --git a/crates/nexus/src/http/ui/handlers/warehouses.rs b/crates/nexus/src/http/ui/handlers/warehouses.rs index 0279c45db..236b4b4d8 100644 --- a/crates/nexus/src/http/ui/handlers/warehouses.rs +++ b/crates/nexus/src/http/ui/handlers/warehouses.rs @@ -67,17 +67,7 @@ pub async fn navigation( let mut databases_short = Vec::new(); for database in databases { - let tables = state - .catalog_svc - .list_tables(&database.ident) - .await - .map_err(|e| { - let fmt = format!( - "{}: failed to get database tables with db ident {}", - e, &database.ident - ); - AppError::new(e, fmt.as_str()) - })?; + let tables = state.catalog_svc.list_tables(&database.ident).await?; let ident = database.ident.clone(); databases_short.push(DatabaseShort { id: get_database_id(database.ident), @@ -110,7 +100,7 @@ pub async fn navigation( operation_id = "webWarehousesDashboard", responses( (status = 200, description = "List all warehouses", body = warehouse::WarehousesDashboard), - (status = 500, description = "List all warehouses", body = AppError), + (status = 500, description = "List all warehouses error", body = AppError), ) )] @@ -161,25 +151,10 @@ pub async fn get_warehouse( State(state): State, Path(warehouse_id): Path, ) -> Result, AppError> { - let warehouse = state - .control_svc - .get_warehouse(warehouse_id) - .await - .map_err(|e| { - let fmt = format!("{}: failed to get warehouse by id {}", e, warehouse_id); - AppError::new(e, fmt.as_str()) - })?; + let warehouse = state.get_warehouse_by_id(warehouse_id).await?; let profile = state - .control_svc - .get_profile(warehouse.storage_profile_id) - .await - .map_err(|e| { - let fmt = format!( - "{}: failed to get profile by id {}", - e, warehouse.storage_profile_id - ); - AppError::new(e, fmt.as_str()) - })?; + .get_profile_by_id(warehouse.storage_profile_id) + .await?; let databases = state .catalog_svc .list_namespaces(&WarehouseIdent::new(warehouse.id), None) @@ -215,8 +190,8 @@ pub async fn get_warehouse( operation_id = "webCreateWarehouse", responses( (status = 201, description = "Warehouse created", body = warehouse::Warehouse), - (status = 422, description = "Unprocessable Entity"), - (status = 500, description = "Internal server error") + (status = 422, description = "Unprocessable Entity", body = AppError), + (status = 500, description = "Internal server error", body = AppError) ) )] pub async fn create_warehouse( @@ -224,17 +199,7 @@ pub async fn create_warehouse( Json(payload): Json, ) -> Result, AppError> { let request: WarehouseCreateRequest = payload.into(); - state - .control_svc - .get_profile(request.storage_profile_id) - .await - .map_err(|e| { - let fmt = format!( - "{}: failed to get profile with id {}", - e, request.storage_profile_id - ); - AppError::new(e, fmt.as_str()) - })?; + state.get_profile_by_id(request.storage_profile_id).await?; let warehouse: WarehouseModel = state .control_svc @@ -253,13 +218,13 @@ pub async fn create_warehouse( #[utoipa::path( delete, path = "/ui/warehouses/{warehouseId}", - operation_id = "webCreteWarehouse", + operation_id = "webCreateWarehouse", params( ("warehouseId" = Uuid, Path, description = "Warehouse ID") ), responses( (status = 204, description = "Warehouse deleted"), - (status = 404, description = "Warehouse not found") + (status = 404, description = "Warehouse not found", body = AppError) ) )] pub async fn delete_warehouse( diff --git a/crates/nexus/src/http/ui/models/metadata.rs b/crates/nexus/src/http/ui/models/metadata.rs new file mode 100644 index 000000000..1d1694922 --- /dev/null +++ b/crates/nexus/src/http/ui/models/metadata.rs @@ -0,0 +1,42 @@ +use iceberg::spec::TableMetadata; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use utoipa::openapi::{ObjectBuilder, RefOr, Schema}; +use utoipa::{PartialSchema, ToSchema}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct TableMetadataWrapper(pub TableMetadata); + +impl PartialSchema for TableMetadataWrapper { + fn schema() -> RefOr { + RefOr::from(Schema::Object( + ObjectBuilder::new() + .property("format_version", i32::schema()) + .property("table_uuid", String::schema()) + .property("location", String::schema()) + .property("last_sequence_number", i64::schema()) + .property("last_updated_ms", i64::schema()) + .property("last_column_id", i32::schema()) + .property("schemas", Schema::Array(Default::default())) + .property("current_schema_id", i32::schema()) + .property("partition_specs", Schema::Array(Default::default())) + .property("default_spec", Schema::Object(Default::default())) + .property("last_partition_id", i32::schema()) + .property("properties", HashMap::::schema()) + .property("current_snapshot_id", Option::::schema()) + .property("snapshots", Schema::Array(Default::default())) + .property("snapshot_log", Schema::Array(Default::default())) + .property("metadata_log", Schema::Array(Default::default())) + .property("sort_orders", Schema::Array(Default::default())) + .property("default_sort_order_id", i32::schema()) + .property("refs", Schema::Object(Default::default())) + .build(), + )) + } +} + +impl ToSchema for TableMetadataWrapper { + fn name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("TableMetadataWrapper") + } +} diff --git a/crates/nexus/src/http/ui/models/mod.rs b/crates/nexus/src/http/ui/models/mod.rs index 0973978ac..1f43cc0a4 100644 --- a/crates/nexus/src/http/ui/models/mod.rs +++ b/crates/nexus/src/http/ui/models/mod.rs @@ -4,3 +4,4 @@ pub mod errors; pub mod storage_profile; pub mod table; pub mod warehouse; +pub mod metadata; diff --git a/crates/nexus/src/http/ui/models/table.rs b/crates/nexus/src/http/ui/models/table.rs index def21b080..9610d835a 100644 --- a/crates/nexus/src/http/ui/models/table.rs +++ b/crates/nexus/src/http/ui/models/table.rs @@ -1,21 +1,23 @@ use crate::http::ui::models::database::{CompactionSummary, Database, DatabaseExtended}; +use crate::http::ui::models::metadata::TableMetadataWrapper; use crate::http::ui::models::storage_profile::StorageProfile; use crate::http::ui::models::warehouse::Warehouse; use catalog::models as CatalogModels; -use iceberg::spec::{Schema, SortOrder, TableMetadata, UnboundPartitionSpec}; +use iceberg::spec::{Schema, SortOrder, UnboundPartitionSpec}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use utoipa::openapi::{ArrayBuilder, ObjectBuilder, RefOr, Schema as OpenApiSchema}; use utoipa::{PartialSchema, ToSchema}; use uuid::Uuid; use validator::Validate; -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Validate, ToSchema)] pub struct TableCreateRequest { pub name: String, pub location: Option, - pub schema: Schema, - pub partition_spec: Option, - pub write_order: Option, + pub schema: SchemaWrapper, + pub partition_spec: Option, + pub write_order: Option, pub stage_create: Option, pub properties: Option>, } @@ -25,15 +27,15 @@ impl From for catalog::models::TableCreation { catalog::models::TableCreation { name: schema.name, location: schema.location, - schema: schema.schema, - partition_spec: schema.partition_spec.map(std::convert::Into::into), - sort_order: schema.write_order, + schema: schema.schema.0, + partition_spec: schema.partition_spec.map(|x| x.0), + sort_order: schema.write_order.map(|x| x.0), properties: schema.properties.unwrap_or_default(), } } } -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)] pub struct Table { pub id: Uuid, pub name: String, @@ -63,7 +65,7 @@ pub fn get_table_id(ident: CatalogModels::TableIdent) -> Uuid { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] pub struct TableEntity { - pub id: uuid::Uuid, + pub id: Uuid, pub name: String, pub created_at: chrono::DateTime, pub updated_at: chrono::DateTime, @@ -75,7 +77,7 @@ pub struct TableEntity { impl TableEntity { #[allow(clippy::new_without_default)] pub fn new( - id: uuid::Uuid, + id: Uuid, name: String, created_at: chrono::DateTime, updated_at: chrono::DateTime, @@ -91,11 +93,7 @@ impl TableEntity { } } } - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct TableMetadataWrapper(TableMetadata); - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)] pub struct TableExtended { pub id: Uuid, pub name: String, @@ -188,10 +186,6 @@ impl Statistics { } } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct WriteDefault(swagger::AnyOf6); - - #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] pub struct TableQueryRequest { pub query: String, @@ -199,33 +193,101 @@ pub struct TableQueryRequest { impl TableQueryRequest { #[allow(clippy::new_without_default)] - pub fn new( - query: String, - ) -> TableQueryRequest { - TableQueryRequest { - query, - } + pub fn new(query: String) -> TableQueryRequest { + TableQueryRequest { query } } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Validate, ToSchema)] pub struct TableQueryResponse { - pub id: uuid::Uuid, + pub id: Uuid, pub query: String, pub result: String, } -impl crate::http::ui::models::table::TableQueryResponse { +impl TableQueryResponse { #[allow(clippy::new_without_default)] - pub fn new( - id: uuid::Uuid, - query: String, - result: String, - ) -> crate::http::ui::models::table::TableQueryResponse { - crate::http::ui::models::table::TableQueryResponse { - id, - query, - result, - } + pub fn new(id: Uuid, query: String, result: String) -> TableQueryResponse { + TableQueryResponse { id, query, result } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SchemaWrapper(Schema); + +impl ToSchema for SchemaWrapper { + fn name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("SchemaWrapper") + } +} +impl PartialSchema for SchemaWrapper { + fn schema() -> RefOr { + RefOr::from(utoipa::openapi::Schema::Object( + ObjectBuilder::new() + .property("r#struct", OpenApiSchema::Object(Default::default())) + .property("schema_id", i32::schema()) + .property("highest_field_id", i32::schema()) + .property( + "identifier_field_ids", + OpenApiSchema::Array(ArrayBuilder::new().items(i32::schema()).build()), + ) + .property("alias_to_id", OpenApiSchema::Object(Default::default())) + .property("id_to_field", OpenApiSchema::Object(Default::default())) + .property("name_to_id", OpenApiSchema::Object(Default::default())) + .property( + "lowercase_name_to_id", + OpenApiSchema::Object(Default::default()), + ) + .property("id_to_name", OpenApiSchema::Object(Default::default())) + .property( + "field_id_to_accessor", + OpenApiSchema::Object(Default::default()), + ) + .build(), + )) + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct UnboundPartitionSpecWrapper(UnboundPartitionSpec); + +impl ToSchema for UnboundPartitionSpecWrapper { + fn name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("UnboundPartitionSpecWrapper") + } +} +impl PartialSchema for UnboundPartitionSpecWrapper { + fn schema() -> RefOr { + RefOr::from(utoipa::openapi::Schema::Object( + ObjectBuilder::new() + .property("spec_id", i32::schema()) + .property( + "fields", + OpenApiSchema::Array(ArrayBuilder::new().items(String::schema()).build()), + ) + .build(), + )) + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SortOrderWrapper(SortOrder); + +impl ToSchema for SortOrderWrapper { + fn name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("SortOrderWrapper") + } +} +impl PartialSchema for SortOrderWrapper { + fn schema() -> RefOr { + RefOr::from(utoipa::openapi::Schema::Object( + ObjectBuilder::new() + .property("order_id", i64::schema()) + .property( + "fields", + OpenApiSchema::Array(ArrayBuilder::new().items(String::schema()).build()), + ) + .build(), + )) } } diff --git a/crates/nexus/src/tests/ui_endpoints.postman_collection.json b/crates/nexus/src/tests/ui_tests.postman_collection.json similarity index 64% rename from crates/nexus/src/tests/ui_endpoints.postman_collection.json rename to crates/nexus/src/tests/ui_tests.postman_collection.json index 3158b62e9..4bbb2afc9 100644 --- a/crates/nexus/src/tests/ui_endpoints.postman_collection.json +++ b/crates/nexus/src/tests/ui_tests.postman_collection.json @@ -1,8 +1,7 @@ { "info": { - "_postman_id": "e4830234-355b-47d0-b846-e5989ec251a4", - "name": "End-to-End Tests", - "description": "> This collection features end-to-end tests that ensure the Intergalactic Bank API components function in an expected sequence. Check out the [Functional testing collection](https://www.postman.com/templates/f26ad070-d626-4d75-b151-7cbf1a48ed11/Functional-testing) and [Integration testing collection](https://www.postman.com/templates/6f788c17-067a-4074-a528-d07df1df9529/Integration-testing-%23example) for other test cases for this API. \n \n\n## **🪐 Get started**\n\nThe fictional Intergalactic Bank Services team is responsible for testing the end-to-end workflows in this collection. You can use this collection to validate specific workflows for your team or figure out ways to increase your test coverage and make your applications more resilient.\n\nTo test the Intergalactic Bank API, you can:\n\n1. **Review tests and scripts:** Check out the [tests](https://learning.postman.com/docs/writing-scripts/test-scripts/) in the Scripts tab of a collection, folder, or request. You’ll also find additional steps in the Pre-request Script, such as for passing data between requests or building up POST request payloads.\n \n2. **Run tests:** To run the collection, select Run collection from the collection menu. If you are interested in testing one of the use cases, select Run folder from the use case folder menu. Keep in mind that many of these tests contain logic that passes information between requests, and they are meant to be run in order from top to bottom. If you send the requests manually or out of order, your results may vary.\n \n3. **Review test results:** Many of the test cases contain error messages to help debug service errors. If you’re using this collection as a pre-merge check, ensure that all tests are passing before submitting (and no, deleting the failing test cases isn’t an option 😉).\n \n\nCheck out the additional sections below or select View complete documentation. For context-sensitive help with a folder or request, look for the documentation icon.\n\n## 🧑‍💻How your team can use this collection\n\n- **Add a mock server to speed up development:** Adding functionality to this service? Since this collection has example responses, you can add a mock server that will return the expected results. Start writing tests against the mock, and once your additions are live, you can swap out the URLs and have a live test suite.\n \n- **Integrate tests with your CI/CD pipeline:** This comprehensive test suite is a great addition to your existing CI/CD pipeline. Generate the Postman CLI command from the Collection Runner and add this step to your existing checks each time you make a commit affecting this service.\n \n- **Add a monitor:** If your team is contributing to or relying on this service, it’s a good idea to keep tabs on the status. Adding a monitor to this collection can give you confidence that the service is up and running and alert you to any breaking changes that may have occurred.\n \n\n## 🔍 What we’re testing\n\n- A very common workflow is the transfer of funds between accounts. We have two scenarios: one which creates all resources and successfully transfers funds, and one which tries the transfer with insufficient funds.\n \n- During these workflows, we are testing for data correctness, data types, and that data is successfully passed through the system during creation and retrieval.\n \n\n## 🔒A note on authorization\n\nThis API uses an API key-based authorization. In this collection, we set the authorization at the collection level and generate a new API Key for each test run.\n\n## 👀 View and share run results\n\nInterested in seeing previous run results? We’re happy to share, and have a few ways for you to stay in the loop:\n\n- **Runs tab:** View past collection runs in the `Runs` tab of this collection. Here, you can see the results of past runs, including who ran them and whether it was via the Collection Runner or the CLI. You can also share individual run reports from this page.\n \n- **Monitors:** If you have a monitor set up for this collection, you can see historical run information when you select the Monitors tab in the left sidebar. You can also have the results sent to Slack or other services when you set up a corresponding integration.\n \n\n### 🤝Increase test coverage\n\nThe Intergalactic Bank Services team wants your team to have everything they need to successfully test our services with their applications. Is your team utilizing this API in a use case not represented here? Reach out to us on our internal Slack by tagging `@bank-services`. Or add an [inline comment](https://learning.postman.com/docs/collaborating-in-postman/working-with-your-team/discussing-your-work/) to this collection with your test case suggestions.", + "_postman_id": "e9a79523-263d-4f5c-8289-c7b38c4d854b", + "name": "UI Tests", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", "_exporter_id": "39228746" }, @@ -75,7 +74,8 @@ " pm.response.to.have.status(200);", "});" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -83,18 +83,14 @@ "method": "GET", "header": [], "url": { - "raw": "{{base_url}}/info?id=1", + "raw": "{{rust_url}}/ui/storage-profiles/{{storage_profile_id}}", "host": [ - "{{base_url}}" + "{{rust_url}}" ], "path": [ - "info" - ], - "query": [ - { - "key": "id", - "value": "1" - } + "ui", + "storage-profiles", + "{{storage_profile_id}}" ] }, "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." @@ -112,7 +108,8 @@ " pm.response.to.have.status(200);", "});" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -120,18 +117,14 @@ "method": "GET", "header": [], "url": { - "raw": "{{base_url}}/info?id=1", + "raw": "{{rust_url}}/ui/storage-profiles/00000000-a59e-4031-951f-6204466fb70f", "host": [ - "{{base_url}}" + "{{rust_url}}" ], "path": [ - "info" - ], - "query": [ - { - "key": "id", - "value": "1" - } + "ui", + "storage-profiles", + "00000000-a59e-4031-951f-6204466fb70f" ] }, "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." @@ -149,7 +142,8 @@ " pm.response.to.have.status(200);", "});" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -157,18 +151,13 @@ "method": "GET", "header": [], "url": { - "raw": "{{base_url}}/info?id=1", + "raw": "{{rust_url}}/ui/storage-profiles", "host": [ - "{{base_url}}" + "{{rust_url}}" ], "path": [ - "info" - ], - "query": [ - { - "key": "id", - "value": "1" - } + "ui", + "storage-profiles" ] }, "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." @@ -186,7 +175,8 @@ " pm.response.to.have.status(200);", "});" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -194,18 +184,13 @@ "method": "GET", "header": [], "url": { - "raw": "{{base_url}}/info?id=1", + "raw": "{{rust_url}}/ui/navigation", "host": [ - "{{base_url}}" + "{{rust_url}}" ], "path": [ - "info" - ], - "query": [ - { - "key": "id", - "value": "1" - } + "ui", + "navigation" ] }, "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." @@ -221,6 +206,7 @@ "exec": [ "pm.test(\"Successful POST request\", function () {", " pm.expect(pm.response.code).to.be.oneOf([200, 201]);", + " pm.collectionVariables.set(\"wh_id\", pm.response.json().id)", "});", "" ], @@ -234,7 +220,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"bucket\": \"artem_test\",\n \"credentials\": {\n \"aacess_key\": {\n \"aws_access_key_id\": \"123\",\n \"aws_secret_access_key\": \"123\"\n }\n\n },\n \"region\": \"us-east-2\",\n \"endpoint\": \"\",\n \"sts_role_arn\": \"\",\n \"type\": \"aws\"\n}", + "raw": "{\n \"storage_profile_id\": \"{{storage_profile_id}}\",\n \"name\": \"name\",\n \"key_prefix\": \"test\"\n}", "options": { "raw": { "language": "json" @@ -242,18 +228,13 @@ } }, "url": { - "raw": "http://0.0.0.0:3000/ui/storage-profiles", - "protocol": "http", + "raw": "{{rust_url}}/ui/warehouses", "host": [ - "0", - "0", - "0", - "0" + "{{rust_url}}" ], - "port": "3000", "path": [ "ui", - "storage-profiles" + "warehouses" ] }, "description": "This is a POST request, submitting data to an API via the request body. This request submits JSON data, and the data is reflected in the response.\n\nA successful POST request typically returns a `200 OK` or `201 Created` response code." @@ -282,7 +263,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"bucket\": \"artem_test\",\n \"credentials\": {\n \"aacess_key\": {\n \"aws_access_key_id\": \"123\",\n \"aws_secret_access_key\": \"123\"\n }\n\n },\n \"region\": \"us-east-2\",\n \"endpoint\": \"\",\n \"sts_role_arn\": \"\",\n \"type\": \"aws\"\n}", + "raw": "{\n \"storage_profile_id\": \"00000000-9560-42db-ab5a-d3e14b5ed300\",\n \"name\": \"name\",\n \"key_prefix\": \"test\"\n}", "options": { "raw": { "language": "json" @@ -290,18 +271,13 @@ } }, "url": { - "raw": "http://0.0.0.0:3000/ui/storage-profiles", - "protocol": "http", + "raw": "{{rust_url}}/ui/warehouses", "host": [ - "0", - "0", - "0", - "0" + "{{rust_url}}" ], - "port": "3000", "path": [ "ui", - "storage-profiles" + "warehouses" ] }, "description": "This is a POST request, submitting data to an API via the request body. This request submits JSON data, and the data is reflected in the response.\n\nA successful POST request typically returns a `200 OK` or `201 Created` response code." @@ -319,7 +295,8 @@ " pm.response.to.have.status(200);", "});" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -327,18 +304,13 @@ "method": "GET", "header": [], "url": { - "raw": "{{base_url}}/info?id=1", + "raw": "{{rust_url}}/ui/warehouses", "host": [ - "{{base_url}}" + "{{rust_url}}" ], "path": [ - "info" - ], - "query": [ - { - "key": "id", - "value": "1" - } + "ui", + "warehouses" ] }, "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." @@ -356,7 +328,8 @@ " pm.response.to.have.status(200);", "});" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -364,18 +337,14 @@ "method": "GET", "header": [], "url": { - "raw": "{{base_url}}/info?id=1", + "raw": "{{rust_url}}/ui/warehouses/{{wh_id}}", "host": [ - "{{base_url}}" + "{{rust_url}}" ], "path": [ - "info" - ], - "query": [ - { - "key": "id", - "value": "1" - } + "ui", + "warehouses", + "{{wh_id}}" ] }, "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." @@ -391,6 +360,7 @@ "exec": [ "pm.test(\"Successful POST request\", function () {", " pm.expect(pm.response.code).to.be.oneOf([200, 201]);", + " pm.collectionVariables.set(\"dn_name\", pm.response.json().name)", "});", "" ], @@ -404,7 +374,7 @@ "header": [], "body": { "mode": "raw", - "raw": " { \n \"warehouse_id\": \"0c648454-00fe-44a9-a729-270c3e000cff\",\n \"name\": \"name\",\n \"properties\" : {\"test\": \"eqqq\"}\n}", + "raw": " { \n \"name\": \"name\",\n \"properties\" : {\"test\": \"eqqq\"}\n}", "options": { "raw": { "language": "json" @@ -412,19 +382,14 @@ } }, "url": { - "raw": "http://0.0.0.0:3000/ui/warehouses/b548baed-66b7-48b7-8fcd-3eff003e89c8/databases", - "protocol": "http", + "raw": "{{rust_url}}/ui/warehouses/{{wh_id}}/databases", "host": [ - "0", - "0", - "0", - "0" + "{{rust_url}}" ], - "port": "3000", "path": [ "ui", "warehouses", - "b548baed-66b7-48b7-8fcd-3eff003e89c8", + "{{wh_id}}", "databases" ] }, @@ -443,7 +408,8 @@ " pm.response.to.have.status(200);", "});" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -451,18 +417,16 @@ "method": "GET", "header": [], "url": { - "raw": "{{base_url}}/info?id=1", + "raw": "{{rust_url}}/ui/warehouses/{{wh_id}}/databases/{{dn_name}}", "host": [ - "{{base_url}}" + "{{rust_url}}" ], "path": [ - "info" - ], - "query": [ - { - "key": "id", - "value": "1" - } + "ui", + "warehouses", + "{{wh_id}}", + "databases", + "{{dn_name}}" ] }, "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." @@ -480,7 +444,8 @@ " pm.response.to.have.status(200);", "});" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -488,18 +453,16 @@ "method": "GET", "header": [], "url": { - "raw": "{{base_url}}/info?id=1", + "raw": "{{rust_url}}/ui/warehouses/{{wh_id}}/databases/test_error", "host": [ - "{{base_url}}" + "{{rust_url}}" ], "path": [ - "info" - ], - "query": [ - { - "key": "id", - "value": "1" - } + "ui", + "warehouses", + "{{wh_id}}", + "databases", + "test_error" ] }, "description": "This is a GET request and it is used to \"get\" data from an endpoint. There is no request body for a GET request, but you can use query parameters to help specify the resource you want data on (e.g., in this request, we have `id=1`).\n\nA successful GET response will have a `200 OK` status, and should include some kind of response body - for example, HTML web content or JSON data." @@ -515,6 +478,7 @@ "exec": [ "pm.test(\"Successful POST request\", function () {", " pm.expect(pm.response.code).to.be.oneOf([200, 201]);", + " pm.collectionVariables.set(\"table_name\", pm.response.json().name)", "});", "" ], @@ -528,7 +492,7 @@ "header": [], "body": { "mode": "raw", - "raw": " { \n \"warehouse_id\": \"0c648454-00fe-44a9-a729-270c3e000cff\",\n \"name\": \"name\",\n \"properties\" : {\"test\": \"eqqq\"}\n}", + "raw": "{\n \"name\": \"table_data_1\",\n \"location\": \"/home/iceberg/warehouse/nyc/taxis\",\n \"schema\": {\n \"type\": \"struct\",\n \"schema-id\": 0,\n \"fields\": [\n {\n \"id\": 1,\n \"name\": \"vendor_id\",\n \"required\": false,\n \"type\": \"long\"\n },\n {\n \"id\": 2,\n \"name\": \"trip_id\",\n \"required\": false,\n \"type\": \"long\"\n },\n {\n \"id\": 3,\n \"name\": \"trip_distance\",\n \"required\": false,\n \"type\": \"float\"\n },\n {\n \"id\": 4,\n \"name\": \"fare_amount\",\n \"required\": false,\n \"type\": \"double\"\n },\n {\n \"id\": 5,\n \"name\": \"store_and_fwd_flag\",\n \"required\": false,\n \"type\": \"string\"\n }\n ]\n },\n \"partition-spec\": [\n {\n \"name\": \"vendor_id\",\n \"transform\": \"identity\",\n \"source-id\": 1,\n \"field-id\": 1000\n }\n ],\n \"sort-orders\": [\n {\n \"order-id\": 0,\n \"fields\": []\n }\n ],\n \"properties\": {\n \"owner\": \"root\"\n }\n}", "options": { "raw": { "language": "json" @@ -536,20 +500,17 @@ } }, "url": { - "raw": "http://0.0.0.0:3000/ui/warehouses/b548baed-66b7-48b7-8fcd-3eff003e89c8/databases", - "protocol": "http", + "raw": "{{rust_url}}/ui/warehouses/{{wh_id}}/databases/{{dn_name}}/tables", "host": [ - "0", - "0", - "0", - "0" + "{{rust_url}}" ], - "port": "3000", "path": [ "ui", "warehouses", - "b548baed-66b7-48b7-8fcd-3eff003e89c8", - "databases" + "{{wh_id}}", + "databases", + "{{dn_name}}", + "tables" ] }, "description": "This is a POST request, submitting data to an API via the request body. This request submits JSON data, and the data is reflected in the response.\n\nA successful POST request typically returns a `200 OK` or `201 Created` response code." @@ -573,8 +534,11 @@ } } ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, "request": { - "method": "POST", + "method": "GET", "header": [], "body": { "mode": "raw", @@ -586,20 +550,18 @@ } }, "url": { - "raw": "http://0.0.0.0:3000/ui/warehouses/b548baed-66b7-48b7-8fcd-3eff003e89c8/databases", - "protocol": "http", + "raw": "{{rust_url}}/ui/warehouses/{{wh_id}}/databases/{{dn_name}}/tables/{{table_name}}", "host": [ - "0", - "0", - "0", - "0" + "{{rust_url}}" ], - "port": "3000", "path": [ "ui", "warehouses", - "b548baed-66b7-48b7-8fcd-3eff003e89c8", - "databases" + "{{wh_id}}", + "databases", + "{{dn_name}}", + "tables", + "{{table_name}}" ] }, "description": "This is a POST request, submitting data to an API via the request body. This request submits JSON data, and the data is reflected in the response.\n\nA successful POST request typically returns a `200 OK` or `201 Created` response code." @@ -701,6 +663,10 @@ { "key": "table_name", "value": "" + }, + { + "key": "dn_name", + "value": "" } ] } \ No newline at end of file diff --git a/object_store/test/873fa8c7-7819-4949-b050-403ffe6d2c91/table_data_1/metadata/196b2daf-e1e6-4c65-ab41-7c88aef05e6f.metadata.json b/object_store/test/873fa8c7-7819-4949-b050-403ffe6d2c91/table_data_1/metadata/196b2daf-e1e6-4c65-ab41-7c88aef05e6f.metadata.json new file mode 100644 index 000000000..d4c909f99 --- /dev/null +++ b/object_store/test/873fa8c7-7819-4949-b050-403ffe6d2c91/table_data_1/metadata/196b2daf-e1e6-4c65-ab41-7c88aef05e6f.metadata.json @@ -0,0 +1 @@ +{"format-version":1,"table-uuid":"0192d2bd-ab85-76f1-9285-e0d8d22accb1","location":"test/873fa8c7-7819-4949-b050-403ffe6d2c91/table_data_1","last-updated-ms":1730112498565,"last-column-id":5,"schema":{"schema-id":0,"type":"struct","fields":[{"id":1,"name":"vendor_id","required":false,"type":"long"},{"id":2,"name":"trip_id","required":false,"type":"long"},{"id":3,"name":"trip_distance","required":false,"type":"float"},{"id":4,"name":"fare_amount","required":false,"type":"double"},{"id":5,"name":"store_and_fwd_flag","required":false,"type":"string"}]},"schemas":[{"schema-id":0,"type":"struct","fields":[{"id":1,"name":"vendor_id","required":false,"type":"long"},{"id":2,"name":"trip_id","required":false,"type":"long"},{"id":3,"name":"trip_distance","required":false,"type":"float"},{"id":4,"name":"fare_amount","required":false,"type":"double"},{"id":5,"name":"store_and_fwd_flag","required":false,"type":"string"}]}],"current-schema-id":0,"partition-spec":[],"partition-specs":[{"spec-id":0,"fields":[]}],"default-spec-id":0,"last-partition-id":999,"properties":{"owner":"root"},"current-snapshot-id":-1,"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0} \ No newline at end of file diff --git a/object_store/test/873fa8c7-7819-4949-b050-403ffe6d2c91/table_data_2/metadata/348e11f4-f1fa-45f5-b58f-fea65c1eb16a.metadata.json b/object_store/test/873fa8c7-7819-4949-b050-403ffe6d2c91/table_data_2/metadata/348e11f4-f1fa-45f5-b58f-fea65c1eb16a.metadata.json new file mode 100644 index 000000000..db373bf8b --- /dev/null +++ b/object_store/test/873fa8c7-7819-4949-b050-403ffe6d2c91/table_data_2/metadata/348e11f4-f1fa-45f5-b58f-fea65c1eb16a.metadata.json @@ -0,0 +1 @@ +{"format-version":1,"table-uuid":"0192d2bc-7c29-7ac2-8458-a4adc6a8117a","location":"test/873fa8c7-7819-4949-b050-403ffe6d2c91/table_data_2","last-updated-ms":1730112420905,"last-column-id":5,"schema":{"schema-id":0,"type":"struct","fields":[{"id":1,"name":"vendor_id","required":false,"type":"long"},{"id":2,"name":"trip_id","required":false,"type":"long"},{"id":3,"name":"trip_distance","required":false,"type":"float"},{"id":4,"name":"fare_amount","required":false,"type":"double"},{"id":5,"name":"store_and_fwd_flag","required":false,"type":"string"}]},"schemas":[{"schema-id":0,"type":"struct","fields":[{"id":1,"name":"vendor_id","required":false,"type":"long"},{"id":2,"name":"trip_id","required":false,"type":"long"},{"id":3,"name":"trip_distance","required":false,"type":"float"},{"id":4,"name":"fare_amount","required":false,"type":"double"},{"id":5,"name":"store_and_fwd_flag","required":false,"type":"string"}]}],"current-schema-id":0,"partition-spec":[],"partition-specs":[{"spec-id":0,"fields":[]}],"default-spec-id":0,"last-partition-id":999,"properties":{"owner":"root"},"current-snapshot-id":-1,"sort-orders":[{"order-id":0,"fields":[]}],"default-sort-order-id":0} \ No newline at end of file