diff --git a/crates/common/src/http/mod.rs b/crates/common/src/http/mod.rs index d2773b3e9..2befb77b7 100644 --- a/crates/common/src/http/mod.rs +++ b/crates/common/src/http/mod.rs @@ -852,6 +852,7 @@ pub async fn stats_middleware( pub struct InstanceNameExt(pub String); #[derive(ToSchema, Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Ord, PartialOrd)] +#[cfg_attr(any(test, feature = "testing"), derive(proptest_derive::Arbitrary))] #[serde(rename_all = "camelCase")] pub enum RequestDestination { ConvexCloud, diff --git a/crates/model/src/canonical_urls/mod.rs b/crates/model/src/canonical_urls/mod.rs new file mode 100644 index 000000000..303d6fd07 --- /dev/null +++ b/crates/model/src/canonical_urls/mod.rs @@ -0,0 +1,102 @@ +use std::{ + collections::BTreeMap, + sync::LazyLock, +}; + +use common::{ + document::{ + ParsedDocument, + ResolvedDocument, + }, + http::RequestDestination, + query::{ + Order, + Query, + }, + runtime::Runtime, + types::TableName, +}; +use database::{ + ResolvedQuery, + SystemMetadataModel, + Transaction, +}; +use value::TableNamespace; + +use self::types::CanonicalUrl; +use crate::SystemTable; + +pub mod types; + +pub static CANONICAL_URLS_TABLE: LazyLock = LazyLock::new(|| { + "_canonical_urls" + .parse() + .expect("Invalid built-in table name") +}); + +pub struct CanonicalUrlsTable; + +impl SystemTable for CanonicalUrlsTable { + fn table_name(&self) -> &'static TableName { + &CANONICAL_URLS_TABLE + } + + fn indexes(&self) -> Vec { + vec![] + } + + fn validate_document(&self, document: ResolvedDocument) -> anyhow::Result<()> { + ParsedDocument::::try_from(document).map(|_| ()) + } +} + +pub struct CanonicalUrlsModel<'a, RT: Runtime> { + tx: &'a mut Transaction, +} + +impl<'a, RT: Runtime> CanonicalUrlsModel<'a, RT> { + pub fn new(tx: &'a mut Transaction) -> Self { + Self { tx } + } + + pub async fn get_canonical_urls( + &mut self, + ) -> anyhow::Result>> { + let query = Query::full_table_scan(CANONICAL_URLS_TABLE.clone(), Order::Asc); + let mut query_stream = ResolvedQuery::new(self.tx, TableNamespace::Global, query)?; + let mut canonical_urls = BTreeMap::new(); + while let Some(document) = query_stream.next(self.tx, None).await? { + let canonical_url = ParsedDocument::::try_from(document)?; + canonical_urls.insert(canonical_url.request_destination, canonical_url); + } + Ok(canonical_urls) + } + + pub async fn set_canonical_url( + &mut self, + request_destination: RequestDestination, + url: String, + ) -> anyhow::Result<()> { + if let Some(existing_canonical_url) = + self.get_canonical_urls().await?.get(&request_destination) + { + if existing_canonical_url.url == url { + // Url isn't changing, so no-op. + return Ok(()); + } else { + // Delete the existing canonical url. + SystemMetadataModel::new_global(self.tx) + .delete(existing_canonical_url.id()) + .await?; + } + } + let canonical_url = CanonicalUrl { + request_destination, + url, + }; + SystemMetadataModel::new_global(self.tx) + .insert(&CANONICAL_URLS_TABLE, canonical_url.try_into()?) + .await?; + Ok(()) + } +} diff --git a/crates/model/src/canonical_urls/types.rs b/crates/model/src/canonical_urls/types.rs new file mode 100644 index 000000000..01648165b --- /dev/null +++ b/crates/model/src/canonical_urls/types.rs @@ -0,0 +1,49 @@ +use common::http::RequestDestination; +use serde::{ + Deserialize, + Serialize, +}; +use value::codegen_convex_serialization; + +#[derive(Clone, Debug, Eq, PartialEq)] +#[cfg_attr(any(test, feature = "testing"), derive(proptest_derive::Arbitrary))] +pub struct CanonicalUrl { + pub request_destination: RequestDestination, + pub url: String, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct SerializedCanonicalUrl { + request_destination: String, + url: String, +} + +impl From for SerializedCanonicalUrl { + fn from(value: CanonicalUrl) -> Self { + Self { + request_destination: match value.request_destination { + RequestDestination::ConvexCloud => "convexCloud".to_string(), + RequestDestination::ConvexSite => "convexSite".to_string(), + }, + url: value.url, + } + } +} + +impl TryFrom for CanonicalUrl { + type Error = anyhow::Error; + + fn try_from(value: SerializedCanonicalUrl) -> Result { + Ok(Self { + request_destination: match value.request_destination.as_str() { + "convexCloud" => RequestDestination::ConvexCloud, + "convexSite" => RequestDestination::ConvexSite, + _ => anyhow::bail!("Invalid request destination: {}", value.request_destination), + }, + url: value.url, + }) + } +} + +codegen_convex_serialization!(CanonicalUrl, SerializedCanonicalUrl); diff --git a/crates/model/src/lib.rs b/crates/model/src/lib.rs index 4151fd0ab..d9cd95a34 100644 --- a/crates/model/src/lib.rs +++ b/crates/model/src/lib.rs @@ -43,6 +43,7 @@ use backend_state::{ BackendStateTable, BACKEND_STATE_TABLE, }; +use canonical_urls::CANONICAL_URLS_TABLE; use common::{ bootstrap_model::index::{ IndexConfig, @@ -119,6 +120,7 @@ use value::{ use crate::{ auth::AuthTable, backend_state::BackendStateModel, + canonical_urls::CanonicalUrlsTable, cron_jobs::{ CronJobLogsTable, CronJobsTable, @@ -138,6 +140,7 @@ use crate::{ pub mod auth; pub mod backend_state; +pub mod canonical_urls; pub mod components; pub mod config; pub mod cron_jobs; @@ -188,9 +191,10 @@ enum DefaultTableNumber { ComponentDefinitionsTable = 31, ComponentsTable = 32, FunctionHandlesTable = 33, + CanonicalUrls = 34, // Keep this number and your user name up to date. The number makes it easy to know // what to use next. The username on the same line detects merge conflicts - // Next Number - 34 - sujayakar + // Next Number - 35 - lee } impl From for TableNumber { @@ -227,6 +231,7 @@ impl From for &'static dyn SystemTable { DefaultTableNumber::ComponentDefinitionsTable => &ComponentDefinitionsTable, DefaultTableNumber::ComponentsTable => &ComponentsTable, DefaultTableNumber::FunctionHandlesTable => &FunctionHandlesTable, + DefaultTableNumber::CanonicalUrls => &CanonicalUrlsTable, } } } @@ -427,6 +432,7 @@ pub fn app_system_tables() -> Vec<&'static dyn SystemTable> { &ExportsTable, &SnapshotImportsTable, &FunctionHandlesTable, + &CanonicalUrlsTable, ]; system_tables.extend(component_system_tables()); system_tables @@ -455,6 +461,7 @@ static APP_TABLES_TO_LOAD_IN_MEMORY: LazyLock> = LazyLock::n ENVIRONMENT_VARIABLES_TABLE.clone(), CRON_JOBS_TABLE.clone(), BACKEND_STATE_TABLE.clone(), + CANONICAL_URLS_TABLE.clone(), } }); diff --git a/crates/model/src/migrations.rs b/crates/model/src/migrations.rs index b609145e8..fa74cae7d 100644 --- a/crates/model/src/migrations.rs +++ b/crates/model/src/migrations.rs @@ -34,6 +34,7 @@ use value::{ }; use crate::{ + canonical_urls::CANONICAL_URLS_TABLE, database_globals::{ types::DatabaseVersion, DatabaseGlobalsModel, @@ -77,7 +78,7 @@ impl fmt::Display for MigrationCompletionCriterion { // migrations unless explicitly dropping support. // Add a user name next to the version when you make a change to highlight merge // conflicts. -pub const DATABASE_VERSION: DatabaseVersion = 115; // nipunn +pub const DATABASE_VERSION: DatabaseVersion = 116; // lee pub struct MigrationWorker { rt: RT, @@ -372,6 +373,9 @@ impl MigrationWorker { } MigrationCompletionCriterion::MigrationComplete(to_version) }, + 116 => MigrationCompletionCriterion::LogLine( + format!("Created system table: {}", *CANONICAL_URLS_TABLE).into(), + ), // NOTE: Make sure to increase DATABASE_VERSION when adding new migrations. _ => anyhow::bail!("Version did not define a migration! {}", to_version), };