Add a schema to extensions, to prevent installing extensions on too old of a Zed version (#9599)

Release Notes:

- N/A

---------

Co-authored-by: Marshall <marshall@zed.dev>
This commit is contained in:
Max Brunsfeld 2024-03-20 14:33:26 -07:00 committed by GitHub
parent b1feeb9f29
commit 585e8671e3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 165 additions and 44 deletions

View file

@ -373,6 +373,7 @@ CREATE TABLE extension_versions (
authors TEXT NOT NULL,
repository TEXT NOT NULL,
description TEXT NOT NULL,
schema_version INTEGER NOT NULL DEFAULT 0,
download_count INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (extension_id, version)
);

View file

@ -0,0 +1,2 @@
-- Add migration script here
ALTER TABLE extension_versions ADD COLUMN schema_version INTEGER NOT NULL DEFAULT 0;

View file

@ -12,6 +12,7 @@ use axum::{
Extension, Json, Router,
};
use collections::HashMap;
use rpc::ExtensionApiManifest;
use serde::{Deserialize, Serialize};
use std::{sync::Arc, time::Duration};
use time::PrimitiveDateTime;
@ -33,6 +34,8 @@ pub fn router() -> Router {
#[derive(Debug, Deserialize)]
struct GetExtensionsParams {
filter: Option<String>,
#[serde(default)]
max_schema_version: i32,
}
#[derive(Debug, Deserialize)]
@ -51,20 +54,14 @@ struct GetExtensionsResponse {
pub data: Vec<ExtensionMetadata>,
}
#[derive(Deserialize)]
struct ExtensionManifest {
name: String,
version: String,
description: Option<String>,
authors: Vec<String>,
repository: String,
}
async fn get_extensions(
Extension(app): Extension<Arc<AppState>>,
Query(params): Query<GetExtensionsParams>,
) -> Result<Json<GetExtensionsResponse>> {
let extensions = app.db.get_extensions(params.filter.as_deref(), 500).await?;
let extensions = app
.db
.get_extensions(params.filter.as_deref(), params.max_schema_version, 500)
.await?;
Ok(Json(GetExtensionsResponse { data: extensions }))
}
@ -267,7 +264,7 @@ async fn fetch_extension_manifest(
})?
.to_vec();
let manifest =
serde_json::from_slice::<ExtensionManifest>(&manifest_bytes).with_context(|| {
serde_json::from_slice::<ExtensionApiManifest>(&manifest_bytes).with_context(|| {
format!(
"invalid manifest for extension {extension_id} version {version}: {}",
String::from_utf8_lossy(&manifest_bytes)
@ -287,6 +284,7 @@ async fn fetch_extension_manifest(
description: manifest.description.unwrap_or_default(),
authors: manifest.authors,
repository: manifest.repository,
schema_version: manifest.schema_version.unwrap_or(0),
published_at,
})
}

View file

@ -731,6 +731,7 @@ pub struct NewExtensionVersion {
pub description: String,
pub authors: Vec<String>,
pub repository: String,
pub schema_version: i32,
pub published_at: PrimitiveDateTime,
}

View file

@ -4,27 +4,28 @@ impl Database {
pub async fn get_extensions(
&self,
filter: Option<&str>,
max_schema_version: i32,
limit: usize,
) -> Result<Vec<ExtensionMetadata>> {
self.transaction(|tx| async move {
let mut condition = Condition::all();
let mut condition = Condition::all().add(
extension::Column::LatestVersion
.into_expr()
.eq(extension_version::Column::Version.into_expr()),
);
if let Some(filter) = filter {
let fuzzy_name_filter = Self::fuzzy_like_string(filter);
condition = condition.add(Expr::cust_with_expr("name ILIKE $1", fuzzy_name_filter));
}
let extensions = extension::Entity::find()
.inner_join(extension_version::Entity)
.select_also(extension_version::Entity)
.filter(condition)
.filter(extension_version::Column::SchemaVersion.lte(max_schema_version))
.order_by_desc(extension::Column::TotalDownloadCount)
.order_by_asc(extension::Column::Name)
.limit(Some(limit as u64))
.filter(
extension::Column::LatestVersion
.into_expr()
.eq(extension_version::Column::Version.into_expr()),
)
.inner_join(extension_version::Entity)
.select_also(extension_version::Entity)
.all(&*tx)
.await?;
@ -170,6 +171,7 @@ impl Database {
authors: ActiveValue::Set(version.authors.join(", ")),
repository: ActiveValue::Set(version.repository.clone()),
description: ActiveValue::Set(version.description.clone()),
schema_version: ActiveValue::Set(version.schema_version),
download_count: ActiveValue::NotSet,
}
}))

View file

@ -13,6 +13,7 @@ pub struct Model {
pub authors: String,
pub repository: String,
pub description: String,
pub schema_version: i32,
pub download_count: i64,
}

View file

@ -16,7 +16,7 @@ async fn test_extensions(db: &Arc<Database>) {
let versions = db.get_known_extension_versions().await.unwrap();
assert!(versions.is_empty());
let extensions = db.get_extensions(None, 5).await.unwrap();
let extensions = db.get_extensions(None, 1, 5).await.unwrap();
assert!(extensions.is_empty());
let t0 = OffsetDateTime::from_unix_timestamp_nanos(0).unwrap();
@ -33,6 +33,7 @@ async fn test_extensions(db: &Arc<Database>) {
description: "an extension".into(),
authors: vec!["max".into()],
repository: "ext1/repo".into(),
schema_version: 1,
published_at: t0,
},
NewExtensionVersion {
@ -41,6 +42,7 @@ async fn test_extensions(db: &Arc<Database>) {
description: "a good extension".into(),
authors: vec!["max".into(), "marshall".into()],
repository: "ext1/repo".into(),
schema_version: 1,
published_at: t0,
},
],
@ -53,6 +55,7 @@ async fn test_extensions(db: &Arc<Database>) {
description: "a great extension".into(),
authors: vec!["marshall".into()],
repository: "ext2/repo".into(),
schema_version: 0,
published_at: t0,
}],
),
@ -75,7 +78,7 @@ async fn test_extensions(db: &Arc<Database>) {
);
// The latest version of each extension is returned.
let extensions = db.get_extensions(None, 5).await.unwrap();
let extensions = db.get_extensions(None, 1, 5).await.unwrap();
assert_eq!(
extensions,
&[
@ -102,6 +105,22 @@ async fn test_extensions(db: &Arc<Database>) {
]
);
// Extensions with too new of a schema version are excluded.
let extensions = db.get_extensions(None, 0, 5).await.unwrap();
assert_eq!(
extensions,
&[ExtensionMetadata {
id: "ext2".into(),
name: "Extension Two".into(),
version: "0.2.0".into(),
authors: vec!["marshall".into()],
description: "a great extension".into(),
repository: "ext2/repo".into(),
published_at: t0,
download_count: 0
},]
);
// Record extensions being downloaded.
for _ in 0..7 {
assert!(db.record_extension_download("ext2", "0.0.2").await.unwrap());
@ -122,7 +141,7 @@ async fn test_extensions(db: &Arc<Database>) {
.unwrap());
// Extensions are returned in descending order of total downloads.
let extensions = db.get_extensions(None, 5).await.unwrap();
let extensions = db.get_extensions(None, 1, 5).await.unwrap();
assert_eq!(
extensions,
&[
@ -161,6 +180,7 @@ async fn test_extensions(db: &Arc<Database>) {
description: "a real good extension".into(),
authors: vec!["max".into(), "marshall".into()],
repository: "ext1/repo".into(),
schema_version: 1,
published_at: t0,
}],
),
@ -172,6 +192,7 @@ async fn test_extensions(db: &Arc<Database>) {
description: "an old extension".into(),
authors: vec!["marshall".into()],
repository: "ext2/repo".into(),
schema_version: 0,
published_at: t0,
}],
),
@ -196,7 +217,7 @@ async fn test_extensions(db: &Arc<Database>) {
.collect()
);
let extensions = db.get_extensions(None, 5).await.unwrap();
let extensions = db.get_extensions(None, 1, 5).await.unwrap();
assert_eq!(
extensions,
&[