Skip to content

Commit fcc2ebb

Browse files
ldanilekConvex, Inc.
authored andcommitted
create _canonical_urls system table (#34431)
create a new system table `_canonical_urls` which will be the source of truth for the canonical urls of an instance. This data is related to but stored independently from the custom/vanity domains, which are used at the routing layer. In contrast, the canonical urls are owned by the backend/instance, so they can be used in `process.env.CONVEX_CLOUD_URL` and `process.env.CONVEX_SITE_URL`. Note the storage of urls instead of domains, since the urls can potentially be `http://` instead of `https://`, or have a `/http` path at the end. The overall plan is for the backend to own this "canonical url" information, and big brain doesn't care if those get out of sync with the verified custom domains (that big brain owns). These canonical urls will be used as the system env variables and also will be publically accessible so they can be used for `VITE_CONVEX_URL` by the CLI and such. The table is empty by default, in which case the urls used are the .convex.cloud and .convex.site ones. GitOrigin-RevId: aaa0bc6e0824d10f47d09c45bbc141d99d771d17
1 parent 16dcf0a commit fcc2ebb

File tree

5 files changed

+165
-2
lines changed

5 files changed

+165
-2
lines changed

crates/common/src/http/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -852,6 +852,7 @@ pub async fn stats_middleware<RM: RouteMapper>(
852852
pub struct InstanceNameExt(pub String);
853853

854854
#[derive(ToSchema, Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Ord, PartialOrd)]
855+
#[cfg_attr(any(test, feature = "testing"), derive(proptest_derive::Arbitrary))]
855856
#[serde(rename_all = "camelCase")]
856857
pub enum RequestDestination {
857858
ConvexCloud,
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
use std::{
2+
collections::BTreeMap,
3+
sync::LazyLock,
4+
};
5+
6+
use common::{
7+
document::{
8+
ParsedDocument,
9+
ResolvedDocument,
10+
},
11+
http::RequestDestination,
12+
query::{
13+
Order,
14+
Query,
15+
},
16+
runtime::Runtime,
17+
types::TableName,
18+
};
19+
use database::{
20+
ResolvedQuery,
21+
SystemMetadataModel,
22+
Transaction,
23+
};
24+
use value::TableNamespace;
25+
26+
use self::types::CanonicalUrl;
27+
use crate::SystemTable;
28+
29+
pub mod types;
30+
31+
pub static CANONICAL_URLS_TABLE: LazyLock<TableName> = LazyLock::new(|| {
32+
"_canonical_urls"
33+
.parse()
34+
.expect("Invalid built-in table name")
35+
});
36+
37+
pub struct CanonicalUrlsTable;
38+
39+
impl SystemTable for CanonicalUrlsTable {
40+
fn table_name(&self) -> &'static TableName {
41+
&CANONICAL_URLS_TABLE
42+
}
43+
44+
fn indexes(&self) -> Vec<crate::SystemIndex> {
45+
vec![]
46+
}
47+
48+
fn validate_document(&self, document: ResolvedDocument) -> anyhow::Result<()> {
49+
ParsedDocument::<CanonicalUrl>::try_from(document).map(|_| ())
50+
}
51+
}
52+
53+
pub struct CanonicalUrlsModel<'a, RT: Runtime> {
54+
tx: &'a mut Transaction<RT>,
55+
}
56+
57+
impl<'a, RT: Runtime> CanonicalUrlsModel<'a, RT> {
58+
pub fn new(tx: &'a mut Transaction<RT>) -> Self {
59+
Self { tx }
60+
}
61+
62+
pub async fn get_canonical_urls(
63+
&mut self,
64+
) -> anyhow::Result<BTreeMap<RequestDestination, ParsedDocument<CanonicalUrl>>> {
65+
let query = Query::full_table_scan(CANONICAL_URLS_TABLE.clone(), Order::Asc);
66+
let mut query_stream = ResolvedQuery::new(self.tx, TableNamespace::Global, query)?;
67+
let mut canonical_urls = BTreeMap::new();
68+
while let Some(document) = query_stream.next(self.tx, None).await? {
69+
let canonical_url = ParsedDocument::<CanonicalUrl>::try_from(document)?;
70+
canonical_urls.insert(canonical_url.request_destination, canonical_url);
71+
}
72+
Ok(canonical_urls)
73+
}
74+
75+
pub async fn set_canonical_url(
76+
&mut self,
77+
request_destination: RequestDestination,
78+
url: String,
79+
) -> anyhow::Result<()> {
80+
if let Some(existing_canonical_url) =
81+
self.get_canonical_urls().await?.get(&request_destination)
82+
{
83+
if existing_canonical_url.url == url {
84+
// Url isn't changing, so no-op.
85+
return Ok(());
86+
} else {
87+
// Delete the existing canonical url.
88+
SystemMetadataModel::new_global(self.tx)
89+
.delete(existing_canonical_url.id())
90+
.await?;
91+
}
92+
}
93+
let canonical_url = CanonicalUrl {
94+
request_destination,
95+
url,
96+
};
97+
SystemMetadataModel::new_global(self.tx)
98+
.insert(&CANONICAL_URLS_TABLE, canonical_url.try_into()?)
99+
.await?;
100+
Ok(())
101+
}
102+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
use common::http::RequestDestination;
2+
use serde::{
3+
Deserialize,
4+
Serialize,
5+
};
6+
use value::codegen_convex_serialization;
7+
8+
#[derive(Clone, Debug, Eq, PartialEq)]
9+
#[cfg_attr(any(test, feature = "testing"), derive(proptest_derive::Arbitrary))]
10+
pub struct CanonicalUrl {
11+
pub request_destination: RequestDestination,
12+
pub url: String,
13+
}
14+
15+
#[derive(Serialize, Deserialize)]
16+
#[serde(rename_all = "camelCase")]
17+
struct SerializedCanonicalUrl {
18+
request_destination: String,
19+
url: String,
20+
}
21+
22+
impl From<CanonicalUrl> for SerializedCanonicalUrl {
23+
fn from(value: CanonicalUrl) -> Self {
24+
Self {
25+
request_destination: match value.request_destination {
26+
RequestDestination::ConvexCloud => "convexCloud".to_string(),
27+
RequestDestination::ConvexSite => "convexSite".to_string(),
28+
},
29+
url: value.url,
30+
}
31+
}
32+
}
33+
34+
impl TryFrom<SerializedCanonicalUrl> for CanonicalUrl {
35+
type Error = anyhow::Error;
36+
37+
fn try_from(value: SerializedCanonicalUrl) -> Result<Self, Self::Error> {
38+
Ok(Self {
39+
request_destination: match value.request_destination.as_str() {
40+
"convexCloud" => RequestDestination::ConvexCloud,
41+
"convexSite" => RequestDestination::ConvexSite,
42+
_ => anyhow::bail!("Invalid request destination: {}", value.request_destination),
43+
},
44+
url: value.url,
45+
})
46+
}
47+
}
48+
49+
codegen_convex_serialization!(CanonicalUrl, SerializedCanonicalUrl);

crates/model/src/lib.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ use backend_state::{
4343
BackendStateTable,
4444
BACKEND_STATE_TABLE,
4545
};
46+
use canonical_urls::CANONICAL_URLS_TABLE;
4647
use common::{
4748
bootstrap_model::index::{
4849
IndexConfig,
@@ -119,6 +120,7 @@ use value::{
119120
use crate::{
120121
auth::AuthTable,
121122
backend_state::BackendStateModel,
123+
canonical_urls::CanonicalUrlsTable,
122124
cron_jobs::{
123125
CronJobLogsTable,
124126
CronJobsTable,
@@ -138,6 +140,7 @@ use crate::{
138140

139141
pub mod auth;
140142
pub mod backend_state;
143+
pub mod canonical_urls;
141144
pub mod components;
142145
pub mod config;
143146
pub mod cron_jobs;
@@ -188,9 +191,10 @@ enum DefaultTableNumber {
188191
ComponentDefinitionsTable = 31,
189192
ComponentsTable = 32,
190193
FunctionHandlesTable = 33,
194+
CanonicalUrls = 34,
191195
// Keep this number and your user name up to date. The number makes it easy to know
192196
// what to use next. The username on the same line detects merge conflicts
193-
// Next Number - 34 - sujayakar
197+
// Next Number - 35 - lee
194198
}
195199

196200
impl From<DefaultTableNumber> for TableNumber {
@@ -227,6 +231,7 @@ impl From<DefaultTableNumber> for &'static dyn SystemTable {
227231
DefaultTableNumber::ComponentDefinitionsTable => &ComponentDefinitionsTable,
228232
DefaultTableNumber::ComponentsTable => &ComponentsTable,
229233
DefaultTableNumber::FunctionHandlesTable => &FunctionHandlesTable,
234+
DefaultTableNumber::CanonicalUrls => &CanonicalUrlsTable,
230235
}
231236
}
232237
}
@@ -427,6 +432,7 @@ pub fn app_system_tables() -> Vec<&'static dyn SystemTable> {
427432
&ExportsTable,
428433
&SnapshotImportsTable,
429434
&FunctionHandlesTable,
435+
&CanonicalUrlsTable,
430436
];
431437
system_tables.extend(component_system_tables());
432438
system_tables
@@ -455,6 +461,7 @@ static APP_TABLES_TO_LOAD_IN_MEMORY: LazyLock<BTreeSet<TableName>> = LazyLock::n
455461
ENVIRONMENT_VARIABLES_TABLE.clone(),
456462
CRON_JOBS_TABLE.clone(),
457463
BACKEND_STATE_TABLE.clone(),
464+
CANONICAL_URLS_TABLE.clone(),
458465
}
459466
});
460467

crates/model/src/migrations.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ use value::{
3434
};
3535

3636
use crate::{
37+
canonical_urls::CANONICAL_URLS_TABLE,
3738
database_globals::{
3839
types::DatabaseVersion,
3940
DatabaseGlobalsModel,
@@ -77,7 +78,7 @@ impl fmt::Display for MigrationCompletionCriterion {
7778
// migrations unless explicitly dropping support.
7879
// Add a user name next to the version when you make a change to highlight merge
7980
// conflicts.
80-
pub const DATABASE_VERSION: DatabaseVersion = 115; // nipunn
81+
pub const DATABASE_VERSION: DatabaseVersion = 116; // lee
8182

8283
pub struct MigrationWorker<RT: Runtime> {
8384
rt: RT,
@@ -372,6 +373,9 @@ impl<RT: Runtime> MigrationWorker<RT> {
372373
}
373374
MigrationCompletionCriterion::MigrationComplete(to_version)
374375
},
376+
116 => MigrationCompletionCriterion::LogLine(
377+
format!("Created system table: {}", *CANONICAL_URLS_TABLE).into(),
378+
),
375379
// NOTE: Make sure to increase DATABASE_VERSION when adding new migrations.
376380
_ => anyhow::bail!("Version did not define a migration! {}", to_version),
377381
};

0 commit comments

Comments
 (0)