From 59aca5078a2c2f4095ee1100f87d9d89446373ac Mon Sep 17 00:00:00 2001 From: Laurent Wouters Date: Tue, 10 Sep 2024 11:53:38 +0200 Subject: [PATCH] feat: implement deprecation flag on packages --- ...84f8922ed0a33cafc445524ae7254295badac.json | 12 +++ ...592b32cdc3fed4a4c9b7bb4586e1a37057ee2.json | 12 +++ ...e50d816a77285fae5c86f728bb6313fa7ccfe.json | 20 ----- ...ddc7b09f8f7ff60c7938538f7410b7a5ab1e.json} | 16 ++-- ...0000071967cb91417dc752e48cfc9810696f8.json | 26 +++++++ ...c661d5e7dc4f2185de2074aa66cd39eeada1d.json | 12 --- ...9d7d1e17b200c02f0d2c7fea8f3352f390aff.json | 26 +++++++ src/application.rs | 32 ++++---- src/empty.db | Bin 81920 -> 81920 bytes src/main.rs | 3 +- src/migrations/mod.rs | 4 + src/migrations/v1.8.0.sql | 2 + src/model/cargo.rs | 3 + src/model/mod.rs | 3 + src/model/packages.rs | 3 + src/routes.rs | 18 ++++- src/schema.sql | 5 +- src/services/database/packages.rs | 70 ++++++++++++++---- src/webapp/api.js | 9 +++ src/webapp/crate.html | 24 ++++++ 20 files changed, 230 insertions(+), 70 deletions(-) create mode 100644 .sqlx/query-2b9ac703cd0cb1de2693770592184f8922ed0a33cafc445524ae7254295badac.json create mode 100644 .sqlx/query-4095ed6e6c8c884650d3f2a0b5c592b32cdc3fed4a4c9b7bb4586e1a37057ee2.json delete mode 100644 .sqlx/query-416d7277895859c59eb394345f4e50d816a77285fae5c86f728bb6313fa7ccfe.json rename .sqlx/{query-5a3925df81fe66e7c0e7ebe187d1cb77b99252d8163addd83ccc297dd7ecb3ad.json => query-86acb81b6a944e6ba67c61778885ddc7b09f8f7ff60c7938538f7410b7a5ab1e.json} (59%) create mode 100644 .sqlx/query-dfc23b2be60dac2eca00490edcb0000071967cb91417dc752e48cfc9810696f8.json delete mode 100644 .sqlx/query-e0945b958309dee635c7b465156c661d5e7dc4f2185de2074aa66cd39eeada1d.json create mode 100644 .sqlx/query-ed3036952b6035ee5405564b9b39d7d1e17b200c02f0d2c7fea8f3352f390aff.json create mode 100644 src/migrations/v1.8.0.sql diff --git a/.sqlx/query-2b9ac703cd0cb1de2693770592184f8922ed0a33cafc445524ae7254295badac.json b/.sqlx/query-2b9ac703cd0cb1de2693770592184f8922ed0a33cafc445524ae7254295badac.json new file mode 100644 index 0000000..124310b --- /dev/null +++ b/.sqlx/query-2b9ac703cd0cb1de2693770592184f8922ed0a33cafc445524ae7254295badac.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE Package SET isDeprecated = $2 WHERE name = $1", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "2b9ac703cd0cb1de2693770592184f8922ed0a33cafc445524ae7254295badac" +} diff --git a/.sqlx/query-4095ed6e6c8c884650d3f2a0b5c592b32cdc3fed4a4c9b7bb4586e1a37057ee2.json b/.sqlx/query-4095ed6e6c8c884650d3f2a0b5c592b32cdc3fed4a4c9b7bb4586e1a37057ee2.json new file mode 100644 index 0000000..0329e56 --- /dev/null +++ b/.sqlx/query-4095ed6e6c8c884650d3f2a0b5c592b32cdc3fed4a4c9b7bb4586e1a37057ee2.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO Package (name, lowercase, targets, isDeprecated) VALUES ($1, $2, '', FALSE)", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "4095ed6e6c8c884650d3f2a0b5c592b32cdc3fed4a4c9b7bb4586e1a37057ee2" +} diff --git a/.sqlx/query-416d7277895859c59eb394345f4e50d816a77285fae5c86f728bb6313fa7ccfe.json b/.sqlx/query-416d7277895859c59eb394345f4e50d816a77285fae5c86f728bb6313fa7ccfe.json deleted file mode 100644 index 5bd869e..0000000 --- a/.sqlx/query-416d7277895859c59eb394345f4e50d816a77285fae5c86f728bb6313fa7ccfe.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT name From Package WHERE name LIKE $1", - "describe": { - "columns": [ - { - "name": "name", - "ordinal": 0, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false - ] - }, - "hash": "416d7277895859c59eb394345f4e50d816a77285fae5c86f728bb6313fa7ccfe" -} diff --git a/.sqlx/query-5a3925df81fe66e7c0e7ebe187d1cb77b99252d8163addd83ccc297dd7ecb3ad.json b/.sqlx/query-86acb81b6a944e6ba67c61778885ddc7b09f8f7ff60c7938538f7410b7a5ab1e.json similarity index 59% rename from .sqlx/query-5a3925df81fe66e7c0e7ebe187d1cb77b99252d8163addd83ccc297dd7ecb3ad.json rename to .sqlx/query-86acb81b6a944e6ba67c61778885ddc7b09f8f7ff60c7938538f7410b7a5ab1e.json index bebd82b..ff03566 100644 --- a/.sqlx/query-5a3925df81fe66e7c0e7ebe187d1cb77b99252d8163addd83ccc297dd7ecb3ad.json +++ b/.sqlx/query-86acb81b6a944e6ba67c61778885ddc7b09f8f7ff60c7938538f7410b7a5ab1e.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT package, version, depsHasOutdated AS has_outdated, depsLastCheck AS last_check, targets\n FROM PackageVersion\n INNER JOIN Package ON PackageVersion.package = Package.name\n WHERE yanked = FALSE", + "query": "SELECT package, version, isDeprecated AS is_deprecated, depsHasOutdated AS has_outdated, depsLastCheck AS last_check, targets\n FROM PackageVersion\n INNER JOIN Package ON PackageVersion.package = Package.name\n WHERE yanked = FALSE", "describe": { "columns": [ { @@ -14,18 +14,23 @@ "type_info": "Text" }, { - "name": "has_outdated", + "name": "is_deprecated", "ordinal": 2, "type_info": "Bool" }, { - "name": "last_check", + "name": "has_outdated", "ordinal": 3, + "type_info": "Bool" + }, + { + "name": "last_check", + "ordinal": 4, "type_info": "Datetime" }, { "name": "targets", - "ordinal": 4, + "ordinal": 5, "type_info": "Text" } ], @@ -37,8 +42,9 @@ false, false, false, + false, false ] }, - "hash": "5a3925df81fe66e7c0e7ebe187d1cb77b99252d8163addd83ccc297dd7ecb3ad" + "hash": "86acb81b6a944e6ba67c61778885ddc7b09f8f7ff60c7938538f7410b7a5ab1e" } diff --git a/.sqlx/query-dfc23b2be60dac2eca00490edcb0000071967cb91417dc752e48cfc9810696f8.json b/.sqlx/query-dfc23b2be60dac2eca00490edcb0000071967cb91417dc752e48cfc9810696f8.json new file mode 100644 index 0000000..e30dfb1 --- /dev/null +++ b/.sqlx/query-dfc23b2be60dac2eca00490edcb0000071967cb91417dc752e48cfc9810696f8.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT name, isDeprecated AS is_deprecated From Package WHERE name LIKE $1 AND (isDeprecated = $2 OR $3)", + "describe": { + "columns": [ + { + "name": "name", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "is_deprecated", + "ordinal": 1, + "type_info": "Bool" + } + ], + "parameters": { + "Right": 3 + }, + "nullable": [ + false, + false + ] + }, + "hash": "dfc23b2be60dac2eca00490edcb0000071967cb91417dc752e48cfc9810696f8" +} diff --git a/.sqlx/query-e0945b958309dee635c7b465156c661d5e7dc4f2185de2074aa66cd39eeada1d.json b/.sqlx/query-e0945b958309dee635c7b465156c661d5e7dc4f2185de2074aa66cd39eeada1d.json deleted file mode 100644 index 4376669..0000000 --- a/.sqlx/query-e0945b958309dee635c7b465156c661d5e7dc4f2185de2074aa66cd39eeada1d.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO Package (name, lowercase, targets) VALUES ($1, $2, '')", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "e0945b958309dee635c7b465156c661d5e7dc4f2185de2074aa66cd39eeada1d" -} diff --git a/.sqlx/query-ed3036952b6035ee5405564b9b39d7d1e17b200c02f0d2c7fea8f3352f390aff.json b/.sqlx/query-ed3036952b6035ee5405564b9b39d7d1e17b200c02f0d2c7fea8f3352f390aff.json new file mode 100644 index 0000000..7013a0c --- /dev/null +++ b/.sqlx/query-ed3036952b6035ee5405564b9b39d7d1e17b200c02f0d2c7fea8f3352f390aff.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT isDeprecated AS is_deprecated, targets FROM Package WHERE name = $1 LIMIT 1", + "describe": { + "columns": [ + { + "name": "is_deprecated", + "ordinal": 0, + "type_info": "Bool" + }, + { + "name": "targets", + "ordinal": 1, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false + ] + }, + "hash": "ed3036952b6035ee5405564b9b39d7d1e17b200c02f0d2c7fea8f3352f390aff" +} diff --git a/src/application.rs b/src/application.rs index 506e9c8..0f3d3af 100644 --- a/src/application.rs +++ b/src/application.rs @@ -418,26 +418,19 @@ impl Application { /// Gets all the data about a crate pub async fn get_crate_info(&self, auth_data: &AuthData, package: &str) -> Result { - let (versions, targets) = self + let info = self .db_transaction_read(|app| async move { let _authentication = app.authenticate(auth_data).await?; - let versions = app - .database - .get_crate_versions(package, self.service_index.get_crate_data(package).await?) - .await?; - let targets = app.database.get_crate_targets(package).await?; - Ok::<_, ApiError>((versions, targets)) + app.database + .get_crate_info(package, self.service_index.get_crate_data(package).await?) + .await }) .await?; let metadata = self .service_storage - .download_crate_metadata(package, &versions.last().unwrap().index.vers) + .download_crate_metadata(package, &info.versions.last().unwrap().index.vers) .await?; - Ok(CrateInfo { - metadata, - versions, - targets, - }) + Ok(CrateInfo { metadata, ..info }) } /// Downloads the last README for a crate @@ -649,6 +642,16 @@ impl Application { .await } + /// Sets the deprecation status on a crate + pub async fn set_crate_deprecation(&self, auth_data: &AuthData, package: &str, deprecated: bool) -> Result<(), ApiError> { + self.db_transaction_write("set_crate_deprecation", |app| async move { + let authentication = app.authenticate(auth_data).await?; + app.check_can_manage_crate(&authentication, package).await?; + app.database.set_crate_deprecation(package, deprecated).await + }) + .await + } + /// Gets the global statistics for the registry pub async fn get_crates_stats(&self, auth_data: &AuthData) -> Result { self.db_transaction_read(|app| async move { @@ -664,10 +667,11 @@ impl Application { auth_data: &AuthData, query: &str, per_page: Option, + deprecated: Option, ) -> Result { self.db_transaction_read(|app| async move { let _authentication = app.authenticate(auth_data).await?; - app.database.search_crates(query, per_page).await + app.database.search_crates(query, per_page, deprecated).await }) .await } diff --git a/src/empty.db b/src/empty.db index a199ce63d8cdea9fb44b00723857d36da5a2ba5d..125617008ea4e6eeb56d0dd248387e0bf28b82bc 100644 GIT binary patch delta 100 zcmV-q0Gt1SfCYen1(0eejsOod54#S{4b%;S4H68Z3snl>3IYiq3DXGE2k-}t1wRDT z0_6gZv7!6{v#|n_1_?0>2NxR~0fIS`ObLaP-U9{-X>&wnaB^j1VRVzR1`~tP3AfP+ G0WUT}s33a) delta 82 zcmV-Y0ImOkfCYen1(0eeu>cP=54#S{4b%;S4H68Z3snl>3IYj23DXGn2onde1!4sE o0|)}Kvk?#<0<(?+k_HJ53kMe)8v%iHlPL*>gV71M(Fp-BHsF>QNdN!< diff --git a/src/main.rs b/src/main.rs index 0aac022..392509b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -131,7 +131,8 @@ async fn main_serve_app(application: Arc, cookie_key: Key) -> Resul .route("/:package/owners", put(routes::api_v1_cargo_add_crate_owners)) .route("/:package/owners", delete(routes::api_v1_cargo_remove_crate_owners)) .route("/:package/targets", get(routes::api_v1_get_crate_targets)) - .route("/:package/targets", patch(routes::api_v1_set_crate_targets)), + .route("/:package/targets", patch(routes::api_v1_set_crate_targets)) + .route("/:package/deprecated", patch(routes::api_v1_set_crate_deprecation)), ), ) // fall back to serving the index diff --git a/src/migrations/mod.rs b/src/migrations/mod.rs index a8599db..5c0ea04 100644 --- a/src/migrations/mod.rs +++ b/src/migrations/mod.rs @@ -46,6 +46,10 @@ const MIGRATIONS: &[Migration<'static>] = &[ target: "1.7.1", content: MigrationContent::Sql(include_bytes!("v1.7.1.sql")), }, + Migration { + target: "1.8.0", + content: MigrationContent::Sql(include_bytes!("v1.8.0.sql")), + }, ]; /// Gets the value for the metadata item diff --git a/src/migrations/v1.8.0.sql b/src/migrations/v1.8.0.sql new file mode 100644 index 0000000..e6ebf17 --- /dev/null +++ b/src/migrations/v1.8.0.sql @@ -0,0 +1,2 @@ +ALTER TABLE Package + ADD COLUMN isDeprecated BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/src/model/cargo.rs b/src/model/cargo.rs index 64ccf41..c061406 100644 --- a/src/model/cargo.rs +++ b/src/model/cargo.rs @@ -21,6 +21,9 @@ pub struct SearchResultCrate { pub name: String, /// The highest version available pub max_version: String, + /// Whether the entire package is deprecated + #[serde(rename = "isDeprecated")] + pub is_deprecated: bool, /// Textual description of the crate pub description: String, } diff --git a/src/model/mod.rs b/src/model/mod.rs index 871d77f..efaa9a9 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -93,6 +93,9 @@ pub struct CrateVersionDepsCheckState { pub name: String, /// The crate's version pub version: String, + /// Whether the entire package is deprecated + #[serde(rename = "isDeprecated")] + pub is_deprecated: bool, /// Whether the version has outdated dependencies #[serde(rename = "depsHasOutdated")] pub deps_has_outdated: bool, diff --git a/src/model/packages.rs b/src/model/packages.rs index b20bea0..e93f000 100644 --- a/src/model/packages.rs +++ b/src/model/packages.rs @@ -14,6 +14,9 @@ use super::cargo::{CrateMetadata, IndexCrateMetadata, RegistryUser}; pub struct CrateInfo { /// The last metadata, if any pub metadata: Option, + /// Whether the entire package is deprecated + #[serde(rename = "isDeprecated")] + pub is_deprecated: bool, /// Gets the versions in the index pub versions: Vec, /// The build targets to use (for docs generation and deps analysis) diff --git a/src/routes.rs b/src/routes.rs index 01baac7..36a6663 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -465,6 +465,7 @@ pub async fn api_v1_reactivate_user( pub struct SearchForm { q: String, per_page: Option, + deprecated: Option, } pub async fn api_v1_cargo_search( @@ -472,7 +473,12 @@ pub async fn api_v1_cargo_search( State(state): State>, form: Query, ) -> ApiResult { - response(state.application.search_crates(&auth_data, &form.q, form.per_page).await) + response( + state + .application + .search_crates(&auth_data, &form.q, form.per_page, form.deprecated) + .await, + ) } /// Gets the global statistics for the registry @@ -670,6 +676,16 @@ pub async fn api_v1_set_crate_targets( response(state.application.set_crate_targets(&auth_data, &package, &input).await) } +/// Sets the deprecation status on a crate +pub async fn api_v1_set_crate_deprecation( + auth_data: AuthData, + State(state): State>, + Path(PathInfoCrate { package }): Path, + input: Json, +) -> ApiResult<()> { + response(state.application.set_crate_deprecation(&auth_data, &package, input.0).await) +} + pub async fn index_serve_inner( index: &(dyn Index + Send + Sync), path: &str, diff --git a/src/schema.sql b/src/schema.sql index 3d16165..3869c38 100644 --- a/src/schema.sql +++ b/src/schema.sql @@ -5,7 +5,7 @@ CREATE TABLE IF NOT EXISTS SchemaMetadata ( CREATE INDEX IF NOT EXISTS SchemaMetadataIndex ON SchemaMetadata(name); -INSERT INTO SchemaMetadata VALUES ('version', '1.7.1'); +INSERT INTO SchemaMetadata VALUES ('version', '1.8.0'); CREATE TABLE RegistryUser ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, @@ -41,7 +41,8 @@ CREATE TABLE RegistryGlobalToken ( CREATE TABLE Package ( name TEXT NOT NULL PRIMARY KEY, lowercase TEXT NOT NULL, - targets TEXT NOT NULL + targets TEXT NOT NULL, + isDeprecated BOOLEAN NOT NULL ); CREATE INDEX IndexPackage ON Package (name); diff --git a/src/services/database/packages.rs b/src/services/database/packages.rs index 59ad82e..bf2438c 100644 --- a/src/services/database/packages.rs +++ b/src/services/database/packages.rs @@ -18,7 +18,7 @@ use crate::model::cargo::{ CrateUploadData, CrateUploadResult, IndexCrateMetadata, OwnersQueryResult, RegistryUser, SearchResultCrate, SearchResults, SearchResultsMeta, YesNoMsgResult, YesNoResult, }; -use crate::model::packages::CrateInfoVersion; +use crate::model::packages::{CrateInfo, CrateInfoVersion}; use crate::model::stats::{DownloadStats, SERIES_LENGTH}; use crate::model::{CrateVersion, CrateVersionDepsCheckState, JobCrate}; use crate::utils::apierror::{error_invalid_request, error_not_found, specialize, ApiError}; @@ -26,16 +26,28 @@ use crate::utils::comma_sep_to_vec; impl Database { /// Search for crates - pub async fn search_crates(&self, query: &str, per_page: Option) -> Result { + pub async fn search_crates( + &self, + query: &str, + per_page: Option, + deprecated: Option, + ) -> Result { let per_page = match per_page { None => 10, Some(value) if value > 100 => 100, Some(value) => value, }; let pattern = format!("%{query}%"); - let rows = sqlx::query!("SELECT name From Package WHERE name LIKE $1", pattern) - .fetch_all(&mut *self.transaction.borrow().await) - .await?; + let deprecated_value = deprecated.unwrap_or_default(); + let deprecated_short_circuit = deprecated.is_none(); // short-cirtcuit to true if no input + let rows = sqlx::query!( + "SELECT name, isDeprecated AS is_deprecated From Package WHERE name LIKE $1 AND (isDeprecated = $2 OR $3)", + pattern, + deprecated_value, + deprecated_short_circuit + ) + .fetch_all(&mut *self.transaction.borrow().await) + .await?; let mut crates = Vec::new(); for row_name in rows { let row = sqlx::query!("SELECT version, description FROM PackageVersion WHERE package = $1 AND yanked = FALSE ORDER BY id DESC LIMIT 1", row_name.name).fetch_optional(&mut *self.transaction.borrow().await).await?; @@ -43,6 +55,7 @@ impl Database { crates.push(SearchResultCrate { name: row_name.name, max_version: row.version, + is_deprecated: row_name.is_deprecated, description: row.description, }); } @@ -70,12 +83,22 @@ impl Database { Ok(row.version) } - /// Gets all the data about versions of a crate - pub async fn get_crate_versions( + /// Gets all the data about a crate + pub async fn get_crate_info( &self, package: &str, versions_in_index: Vec, - ) -> Result, ApiError> { + ) -> Result { + let row = sqlx::query!( + "SELECT isDeprecated AS is_deprecated, targets FROM Package WHERE name = $1 LIMIT 1", + package + ) + .fetch_optional(&mut *self.transaction.borrow().await) + .await? + .ok_or_else(error_not_found)?; + let is_deprecated = row.is_deprecated; + let targets = comma_sep_to_vec(&row.targets); + let rows = sqlx::query!( "SELECT version, upload, uploadedBy AS uploaded_by, hasDocs AS has_docs, docGenAttempted AS doc_gen_attempted, @@ -86,11 +109,11 @@ impl Database { ) .fetch_all(&mut *self.transaction.borrow().await) .await?; - let mut result = Vec::new(); + let mut versions = Vec::new(); for index_data in versions_in_index { if let Some(row) = rows.iter().find(|row| row.version == index_data.vers) { let uploaded_by = self.get_user_profile(row.uploaded_by).await?; - result.push(CrateInfoVersion { + versions.push(CrateInfoVersion { index: index_data, upload: row.upload, uploaded_by, @@ -103,7 +126,12 @@ impl Database { }); } } - Ok(result) + Ok(CrateInfo { + metadata: None, + is_deprecated, + versions, + targets, + }) } /// Publish a crate @@ -144,7 +172,7 @@ impl Database { } else { // create the package sqlx::query!( - "INSERT INTO Package (name, lowercase, targets) VALUES ($1, $2, '')", + "INSERT INTO Package (name, lowercase, targets, isDeprecated) VALUES ($1, $2, '', FALSE)", package.metadata.name, lowercase ) @@ -313,7 +341,7 @@ impl Database { Ok(heads .into_iter() .filter_map(|element| { - if element.deps_last_check < from { + if !element.is_deprecated && element.deps_last_check < from { Some(element.into()) } else { None @@ -328,7 +356,7 @@ impl Database { Ok(heads .into_iter() .filter_map(|element| { - if element.deps_has_outdated { + if !element.is_deprecated && element.deps_has_outdated { Some(element.into()) } else { None @@ -342,6 +370,7 @@ impl Database { struct Elem { semver: Version, version: String, + is_deprecated: bool, deps_has_outdated: bool, deps_last_check: NaiveDateTime, targets: String, @@ -349,7 +378,7 @@ impl Database { let mut cache = HashMap::::new(); let transaction = &mut *self.transaction.borrow().await; let mut stream = sqlx::query!( - "SELECT package, version, depsHasOutdated AS has_outdated, depsLastCheck AS last_check, targets + "SELECT package, version, isDeprecated AS is_deprecated, depsHasOutdated AS has_outdated, depsLastCheck AS last_check, targets FROM PackageVersion INNER JOIN Package ON PackageVersion.package = Package.name WHERE yanked = FALSE" @@ -366,6 +395,7 @@ impl Database { entry.insert(Elem { semver, version: row.version, + is_deprecated: row.is_deprecated, deps_has_outdated: row.has_outdated, deps_last_check: row.last_check, targets: row.targets, @@ -376,6 +406,7 @@ impl Database { entry.insert(Elem { semver, version: row.version, + is_deprecated: row.is_deprecated, deps_has_outdated: row.has_outdated, deps_last_check: row.last_check, targets: row.targets, @@ -390,6 +421,7 @@ impl Database { .map(|(name, elem)| CrateVersionDepsCheckState { name, version: elem.version, + is_deprecated: elem.is_deprecated, deps_has_outdated: elem.deps_has_outdated, deps_last_check: elem.deps_last_check, targets: elem.targets, @@ -548,4 +580,12 @@ impl Database { .await?; Ok(()) } + + /// Sets the deprecation status on a crate + pub async fn set_crate_deprecation(&self, package: &str, deprecated: bool) -> Result<(), ApiError> { + sqlx::query!("UPDATE Package SET isDeprecated = $2 WHERE name = $1", package, deprecated) + .execute(&mut *self.transaction.borrow().await) + .await?; + Ok(()) + } } diff --git a/src/webapp/api.js b/src/webapp/api.js index b62df74..23da2d1 100644 --- a/src/webapp/api.js +++ b/src/webapp/api.js @@ -206,6 +206,15 @@ async function apiSetCrateTargets(crate, targets) { return await onResponseJson(response); } +async function apiSetCrateDeprecation(crate, isDeprecated) { + const response = await fetch(`/api/v1/crates/${crate}/deprecated`, { + method: "PATCH", + body: JSON.stringify(isDeprecated), + headers: [["content-type", "application/json"]], + }); + return await onResponseJson(response); +} + async function apiRegenCrateDoc(crate, version) { const response = await fetch(`/api/v1/crates/${crate}/${version}/docsregen`, { method: "POST", diff --git a/src/webapp/crate.html b/src/webapp/crate.html index 7f76f09..5e54d0d 100644 --- a/src/webapp/crate.html +++ b/src/webapp/crate.html @@ -37,6 +37,9 @@

+

    @@ -157,6 +160,15 @@
    Targets
    +
    +
    Deprecation
    +

    Deprecated crates will not be checked by the dependency analyzer and will be marked with a warning in the web interface.

    + +
    @@ -243,6 +255,10 @@

    Remove this ow document.getElementById("header-dependencies-warn-icon").setAttribute("stroke", "red"); } } + + if (crate.isDeprecated) { + document.getElementById("meta-deprecation").style.display = null; + } document.getElementById("meta-name").appendChild(document.createTextNode(currentVersion.index.name)); document.getElementById("meta-name-link").setAttribute("href", `/crates/${currentVersion.index.name}`); document.getElementById("meta-version").appendChild(document.createTextNode(`v${currentVersion.index.vers}`)); @@ -287,6 +303,7 @@

    Remove this ow renderDependencies(currentVersion.index.deps, null); renderDocs(crate); + document.getElementById("tab-admin-deprecation-toggle").checked = crate.isDeprecated; const canAdmin = currentUser.roles.includes("admin") || owners.users.find(u => u.id === currentUser.id) !== undefined; if (canAdmin) { document.getElementById("header-admin").parentElement.style.display = null; @@ -301,6 +318,13 @@

    Remove this ow for (const target of crate.targets) { // TODO render targets here } + + document.getElementById("tab-admin-deprecation-toggle").onchange = () => { + apiSetCrateDeprecation(currentVersion.index.name, !crate.isDeprecated).then(() => { + crate.isDeprecated = !crate.isDeprecated; + document.getElementById("meta-deprecation").style.display = crate.isDeprecated ? null : "none"; + }); + } } hljs.highlightAll();