Staging bug fixes (#819)

* Staging bug fixes

* Finish fixes

* fix tests

* Update migration

* Update migrations

* fix side types being added for ineligible loaders

* fix tests

* Fix tests

* Finish fixes

* Add slug display names
This commit is contained in:
Geometrically
2024-01-04 16:24:33 -05:00
committed by GitHub
parent cf9c8cbb4f
commit f5802fee31
36 changed files with 322 additions and 246 deletions

View File

@@ -15,7 +15,10 @@ pub struct Organization {
/// The id of the organization
pub id: OrganizationId,
/// The title (and slug) of the organization
/// The slug of the organization
pub slug: String,
/// The title of the organization
pub name: String,
/// The associated team of the organization
@@ -36,10 +39,11 @@ impl Organization {
) -> Result<(), super::DatabaseError> {
sqlx::query!(
"
INSERT INTO organizations (id, name, team_id, description, icon_url, color)
VALUES ($1, $2, $3, $4, $5, $6)
INSERT INTO organizations (id, slug, name, team_id, description, icon_url, color)
VALUES ($1, $2, $3, $4, $5, $6, $7)
",
self.id.0,
self.slug,
self.name,
self.team_id as TeamId,
self.description,
@@ -149,7 +153,7 @@ impl Organization {
{
remaining_strings.retain(|x| {
&to_base62(organization.id.0 as u64) != x
&& organization.name.to_lowercase() != x.to_lowercase()
&& organization.slug.to_lowercase() != x.to_lowercase()
});
found_organizations.push(organization);
continue;
@@ -166,9 +170,9 @@ impl Organization {
let organizations: Vec<Organization> = sqlx::query!(
"
SELECT o.id, o.name, o.team_id, o.description, o.icon_url, o.color
SELECT o.id, o.slug, o.name, o.team_id, o.description, o.icon_url, o.color
FROM organizations o
WHERE o.id = ANY($1) OR LOWER(o.name) = ANY($2)
WHERE o.id = ANY($1) OR LOWER(o.slug) = ANY($2)
GROUP BY o.id;
",
&organization_ids_parsed,
@@ -181,6 +185,7 @@ impl Organization {
.try_filter_map(|e| async {
Ok(e.right().map(|m| Organization {
id: OrganizationId(m.id),
slug: m.slug,
name: m.name,
team_id: TeamId(m.team_id),
description: m.description,
@@ -203,7 +208,7 @@ impl Organization {
redis
.set(
ORGANIZATIONS_TITLES_NAMESPACE,
&organization.name.to_lowercase(),
&organization.slug.to_lowercase(),
&organization.id.0.to_string(),
None,
)
@@ -226,7 +231,7 @@ impl Organization {
{
let result = sqlx::query!(
"
SELECT o.id, o.name, o.team_id, o.description, o.icon_url, o.color
SELECT o.id, o.slug, o.name, o.team_id, o.description, o.icon_url, o.color
FROM organizations o
LEFT JOIN mods m ON m.organization_id = o.id
WHERE m.id = $1
@@ -240,6 +245,7 @@ impl Organization {
if let Some(result) = result {
Ok(Some(Organization {
id: OrganizationId(result.id),
slug: result.slug,
name: result.name,
team_id: TeamId(result.team_id),
description: result.description,
@@ -299,7 +305,7 @@ impl Organization {
pub async fn clear_cache(
id: OrganizationId,
title: Option<String>,
slug: Option<String>,
redis: &RedisPool,
) -> Result<(), super::DatabaseError> {
let mut redis = redis.connect().await?;
@@ -309,7 +315,7 @@ impl Organization {
(ORGANIZATIONS_NAMESPACE, Some(id.0.to_string())),
(
ORGANIZATIONS_TITLES_NAMESPACE,
title.map(|x| x.to_lowercase()),
slug.map(|x| x.to_lowercase()),
),
])
.await?;

View File

@@ -273,13 +273,13 @@ impl Project {
id, team_id, name, summary, description,
published, downloads, icon_url, status, requested_status,
license_url, license,
slug, color, monetization_status
slug, color, monetization_status, organization_id
)
VALUES (
$1, $2, $3, $4, $5, $6,
$7, $8, $9, $10,
$11, $12,
LOWER($13), $14, $15
LOWER($13), $14, $15, $16
)
",
self.id as ProjectId,
@@ -297,6 +297,7 @@ impl Project {
self.slug.as_ref(),
self.color.map(|x| x as i32),
self.monetization_status.as_str(),
self.organization_id.map(|x| x.0 as i64),
)
.execute(&mut **transaction)
.await?;

View File

@@ -412,10 +412,10 @@ impl TeamMember {
sqlx::query!(
"
INSERT INTO team_members (
id, team_id, user_id, role, permissions, organization_permissions, is_owner, accepted
id, team_id, user_id, role, permissions, organization_permissions, is_owner, accepted, payouts_split
)
VALUES (
$1, $2, $3, $4, $5, $6, $7, $8
$1, $2, $3, $4, $5, $6, $7, $8, $9
)
",
self.id as TeamMemberId,
@@ -426,6 +426,7 @@ impl TeamMember {
self.organization_permissions.map(|p| p.bits() as i64),
self.is_owner,
self.accepted,
self.payouts_split
)
.execute(&mut **transaction)
.await?;

View File

@@ -126,70 +126,42 @@ pub struct VersionFileBuilder {
}
impl VersionFileBuilder {
pub async fn insert_many(
version_files: Vec<Self>,
pub async fn insert(
self,
version_id: VersionId,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<FileId, DatabaseError> {
let file_id = generate_file_id(transaction).await?;
let file_id = generate_file_id(&mut *transaction).await?;
let (file_ids, version_ids, urls, filenames, primary, sizes, file_types): (
Vec<_>,
Vec<_>,
Vec<_>,
Vec<_>,
Vec<_>,
Vec<_>,
Vec<_>,
) = version_files
.iter()
.map(|f| {
(
file_id.0,
version_id.0,
f.url.clone(),
f.filename.clone(),
f.primary,
f.size as i32,
f.file_type.map(|x| x.to_string()),
)
})
.multiunzip();
sqlx::query!(
"
INSERT INTO files (id, version_id, url, filename, is_primary, size, file_type)
SELECT * FROM UNNEST($1::bigint[], $2::bigint[], $3::varchar[], $4::varchar[], $5::bool[], $6::integer[], $7::varchar[])
VALUES ($1, $2, $3, $4, $5, $6, $7)
",
&file_ids[..],
&version_ids[..],
&urls[..],
&filenames[..],
&primary[..],
&sizes[..],
&file_types[..] as &[Option<String>],
file_id as FileId,
version_id as VersionId,
self.url,
self.filename,
self.primary,
self.size as i32,
self.file_type.map(|x| x.as_str()),
)
.execute(&mut **transaction)
.await?;
let (file_ids, algorithms, hashes): (Vec<_>, Vec<_>, Vec<_>) = version_files
.into_iter()
.flat_map(|f| {
f.hashes
.into_iter()
.map(|h| (file_id.0, h.algorithm, h.hash))
})
.multiunzip();
sqlx::query!(
"
INSERT INTO hashes (file_id, algorithm, hash)
SELECT * FROM UNNEST($1::bigint[], $2::varchar[], $3::bytea[])
",
&file_ids[..],
&algorithms[..],
&hashes[..],
)
.execute(&mut **transaction)
.await?;
for hash in self.hashes {
sqlx::query!(
"
INSERT INTO hashes (file_id, algorithm, hash)
VALUES ($1, $2, $3)
",
file_id as FileId,
hash.algorithm,
hash.hash,
)
.execute(&mut **transaction)
.await?;
}
Ok(file_id)
}
@@ -242,7 +214,10 @@ impl VersionBuilder {
version_id,
..
} = self;
VersionFileBuilder::insert_many(files, self.version_id, transaction).await?;
for file in files {
file.insert(version_id, transaction).await?;
}
DependencyBuilder::insert_many(dependencies, self.version_id, transaction).await?;

View File

@@ -83,24 +83,23 @@ impl LegacyProject {
let mut game_versions = Vec::new();
// V2 versions only have one project type- v3 versions can rarely have multiple.
// We'll prioritize 'modpack' first, then 'mod', and if neither are found, use the first one.
// We'll prioritize 'modpack' first, and if neither are found, use the first one.
// If there are no project types, default to 'project'
let mut project_types = data.project_types;
if project_types.contains(&"modpack".to_string()) {
project_types = vec!["modpack".to_string()];
} else if project_types.contains(&"mod".to_string()) {
project_types = vec!["mod".to_string()];
}
let project_type = project_types
let og_project_type = project_types
.first()
.cloned()
.unwrap_or("project".to_string()); // Default to 'project' if none are found
let mut project_type = if project_type == "datapack" || project_type == "plugin" {
let mut project_type = if og_project_type == "datapack" || og_project_type == "plugin" {
// These are not supported in V2, so we'll just use 'mod' instead
"mod".to_string()
} else {
project_type
og_project_type.clone()
};
let mut loaders = data.loaders;
@@ -120,7 +119,8 @@ impl LegacyProject {
.iter()
.map(|f| (f.field_name.clone(), f.value.clone().serialize_internal()))
.collect::<HashMap<_, _>>();
(client_side, server_side) = v2_reroute::convert_side_types_v2(&fields);
(client_side, server_side) =
v2_reroute::convert_side_types_v2(&fields, Some(&*og_project_type));
// - if loader is mrpack, this is a modpack
// the loaders are whatever the corresponding loader fields are

View File

@@ -75,24 +75,22 @@ impl LegacyResultSearchProject {
display_categories.dedup();
// V2 versions only have one project type- v3 versions can rarely have multiple.
// We'll prioritize 'modpack' first, then 'mod', and if neither are found, use the first one.
// We'll prioritize 'modpack' first, and if neither are found, use the first one.
// If there are no project types, default to 'project'
let mut project_types = result_search_project.project_types;
if project_types.contains(&"modpack".to_string()) {
project_types = vec!["modpack".to_string()];
} else if project_types.contains(&"mod".to_string()) {
project_types = vec!["mod".to_string()];
}
let project_type = project_types
let og_project_type = project_types
.first()
.cloned()
.unwrap_or("project".to_string()); // Default to 'project' if none are found
let project_type = if project_type == "datapack" || project_type == "plugin" {
let project_type = if og_project_type == "datapack" || og_project_type == "plugin" {
// These are not supported in V2, so we'll just use 'mod' instead
"mod".to_string()
} else {
project_type
og_project_type.clone()
};
let loader_fields = result_search_project.loader_fields.clone();
@@ -115,6 +113,7 @@ impl LegacyResultSearchProject {
client_only,
server_only,
client_and_server,
Some(&*og_project_type),
);
let client_side = client_side.to_string();
let server_side = server_side.to_string();

View File

@@ -15,6 +15,8 @@ pub struct OrganizationId(pub u64);
pub struct Organization {
/// The id of the organization
pub id: OrganizationId,
/// The slug of the organization
pub slug: String,
/// The title (and slug) of the organization
pub name: String,
/// The associated team of the organization
@@ -38,6 +40,7 @@ impl Organization {
) -> Self {
Self {
id: data.id.into(),
slug: data.slug,
name: data.name,
team_id: data.team_id.into(),
description: data.description,

View File

@@ -497,7 +497,7 @@ pub async fn project_edit(
let version = Version::from(version);
let mut fields = version.fields;
let (current_client_side, current_server_side) =
v2_reroute::convert_side_types_v2(&fields);
v2_reroute::convert_side_types_v2(&fields, None);
let client_side = client_side.unwrap_or(current_client_side);
let server_side = server_side.unwrap_or(current_server_side);
fields.extend(v2_reroute::convert_side_types_v3(client_side, server_side));

View File

@@ -79,16 +79,22 @@ pub async fn loader_list(
Ok(loaders) => {
let loaders = loaders
.into_iter()
.map(|l| LoaderData {
icon: l.icon,
name: l.name,
.filter(|l| &*l.name != "mrpack")
.map(|l| {
let mut supported_project_types = l.supported_project_types;
// Add generic 'project' type to all loaders, which is the v2 representation of
// a project type before any versions are set.
supported_project_types: l
.supported_project_types
.into_iter()
.chain(std::iter::once("project".to_string()))
.collect(),
supported_project_types.push("project".to_string());
if ["forge", "fabric", "quilt", "neoforge"].contains(&&*l.name) {
supported_project_types.push("modpack".to_string());
}
LoaderData {
icon: l.icon,
name: l.name,
supported_project_types,
}
})
.collect::<Vec<_>>();
Ok(HttpResponse::Ok().json(loaders))

View File

@@ -242,6 +242,7 @@ pub fn convert_side_type_facets_v3(facets: Vec<Vec<Vec<String>>>) -> Vec<Vec<Vec
// this is not lossless. (See tests)
pub fn convert_side_types_v2(
side_types: &HashMap<String, Value>,
project_type: Option<&str>,
) -> (LegacySideType, LegacySideType) {
let client_and_server = side_types
.get("client_and_server")
@@ -265,6 +266,7 @@ pub fn convert_side_types_v2(
client_only,
server_only,
Some(client_and_server),
project_type,
)
}
@@ -274,29 +276,38 @@ pub fn convert_side_types_v2_bools(
client_only: bool,
server_only: bool,
client_and_server: Option<bool>,
project_type: Option<&str>,
) -> (LegacySideType, LegacySideType) {
use LegacySideType::{Optional, Required, Unsupported};
use LegacySideType::{Optional, Required, Unknown, Unsupported};
let singleplayer = singleplayer.or(client_and_server).unwrap_or(false);
match project_type {
Some("plugin") => (Unsupported, Required),
Some("datapack") => (Optional, Required),
Some("shader") => (Required, Unsupported),
Some("resourcepack") => (Required, Unsupported),
_ => {
let singleplayer = singleplayer.or(client_and_server).unwrap_or(false);
match (singleplayer, client_only, server_only) {
// Only singleplayer
(true, false, false) => (Required, Required),
match (singleplayer, client_only, server_only) {
// Only singleplayer
(true, false, false) => (Required, Required),
// Client only and not server only
(false, true, false) => (Required, Unsupported),
(true, true, false) => (Required, Unsupported),
// Client only and not server only
(false, true, false) => (Required, Unsupported),
(true, true, false) => (Required, Unsupported),
// Server only and not client only
(false, false, true) => (Unsupported, Required),
(true, false, true) => (Unsupported, Required),
// Server only and not client only
(false, false, true) => (Unsupported, Required),
(true, false, true) => (Unsupported, Required),
// Both server only and client only
(true, true, true) => (Optional, Optional),
(false, true, true) => (Optional, Optional),
// Both server only and client only
(true, true, true) => (Optional, Optional),
(false, true, true) => (Optional, Optional),
// Bad type
(false, false, false) => (Unsupported, Unsupported),
// Bad type
(false, false, false) => (Unknown, Unknown),
}
}
}
}
@@ -321,6 +332,7 @@ mod tests {
(Unsupported, Optional),
(Required, Optional),
(Optional, Required),
(Unsupported, Unsupported),
];
for client_side in [Required, Optional, Unsupported] {
@@ -329,7 +341,7 @@ mod tests {
continue;
}
let side_types = convert_side_types_v3(client_side, server_side);
let (client_side2, server_side2) = convert_side_types_v2(&side_types);
let (client_side2, server_side2) = convert_side_types_v2(&side_types, None);
assert_eq!(client_side, client_side2);
assert_eq!(server_side, server_side2);
}

View File

@@ -72,7 +72,7 @@ pub async fn organization_projects_get(
"
SELECT m.id FROM organizations o
INNER JOIN mods m ON m.organization_id = o.id
WHERE (o.id = $1 AND $1 IS NOT NULL) OR (o.name = $2 AND $2 IS NOT NULL)
WHERE (o.id = $1 AND $1 IS NOT NULL) OR (o.slug = $2 AND $2 IS NOT NULL)
",
possible_organization_id.map(|x| x as i64),
info
@@ -95,7 +95,9 @@ pub struct NewOrganization {
length(min = 3, max = 64),
regex = "crate::util::validate::RE_URL_SAFE"
)]
// Title of the organization, also used as slug
pub slug: String,
// Title of the organization
#[validate(length(min = 3, max = 64))]
pub name: String,
#[validate(length(min = 3, max = 256))]
pub description: String,
@@ -126,12 +128,12 @@ pub async fn organization_create(
// Try title
let name_organization_id_option: Option<OrganizationId> =
serde_json::from_str(&format!("\"{}\"", new_organization.name)).ok();
serde_json::from_str(&format!("\"{}\"", new_organization.slug)).ok();
let mut organization_strings = vec![];
if let Some(name_organization_id) = name_organization_id_option {
organization_strings.push(name_organization_id.to_string());
}
organization_strings.push(new_organization.name.clone());
organization_strings.push(new_organization.slug.clone());
let results = Organization::get_many(&organization_strings, &mut *transaction, &redis).await?;
if !results.is_empty() {
return Err(CreateError::SlugCollision);
@@ -157,6 +159,7 @@ pub async fn organization_create(
// Create organization
let organization = Organization {
id: organization_id,
slug: new_organization.slug.clone(),
name: new_organization.name.clone(),
description: new_organization.description.clone(),
team_id,
@@ -336,7 +339,8 @@ pub struct OrganizationEdit {
length(min = 3, max = 64),
regex = "crate::util::validate::RE_URL_SAFE"
)]
// Title of the organization, also used as slug
pub slug: Option<String>,
#[validate(length(min = 3, max = 64))]
pub name: Option<String>,
}
@@ -406,8 +410,28 @@ pub async fn organizations_edit(
.to_string(),
));
}
sqlx::query!(
"
UPDATE organizations
SET name = $1
WHERE (id = $2)
",
name,
id as database::models::ids::OrganizationId,
)
.execute(&mut *transaction)
.await?;
}
let name_organization_id_option: Option<u64> = parse_base62(name).ok();
if let Some(slug) = &new_organization.slug {
if !perms.contains(OrganizationPermissions::EDIT_DETAILS) {
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the slug of this organization!"
.to_string(),
));
}
let name_organization_id_option: Option<u64> = parse_base62(slug).ok();
if let Some(name_organization_id) = name_organization_id_option {
let results = sqlx::query!(
"
@@ -420,26 +444,26 @@ pub async fn organizations_edit(
if results.exists.unwrap_or(true) {
return Err(ApiError::InvalidInput(
"name collides with other organization's id!".to_string(),
"slug collides with other organization's id!".to_string(),
));
}
}
// Make sure the new name is different from the old one
// We are able to unwrap here because the name is always set
if !name.eq(&organization_item.name.clone()) {
if !slug.eq(&organization_item.slug.clone()) {
let results = sqlx::query!(
"
SELECT EXISTS(SELECT 1 FROM organizations WHERE name = LOWER($1))
",
name
SELECT EXISTS(SELECT 1 FROM organizations WHERE LOWER(slug) = LOWER($1))
",
slug
)
.fetch_one(&mut *transaction)
.await?;
if results.exists.unwrap_or(true) {
return Err(ApiError::InvalidInput(
"Name collides with other organization's id!".to_string(),
"slug collides with other organization's id!".to_string(),
));
}
}
@@ -447,10 +471,10 @@ pub async fn organizations_edit(
sqlx::query!(
"
UPDATE organizations
SET name = LOWER($1)
SET slug = $1
WHERE (id = $2)
",
Some(name),
Some(slug),
id as database::models::ids::OrganizationId,
)
.execute(&mut *transaction)
@@ -460,7 +484,7 @@ pub async fn organizations_edit(
transaction.commit().await?;
database::models::Organization::clear_cache(
organization_item.id,
Some(organization_item.name),
Some(organization_item.slug),
&redis,
)
.await?;
@@ -578,7 +602,7 @@ pub async fn organization_delete(
transaction.commit().await?;
database::models::Organization::clear_cache(organization.id, Some(organization.name), &redis)
database::models::Organization::clear_cache(organization.id, Some(organization.slug), &redis)
.await?;
for team_id in organization_project_teams {
@@ -994,7 +1018,7 @@ pub async fn organization_icon_edit(
transaction.commit().await?;
database::models::Organization::clear_cache(
organization_item.id,
Some(organization_item.name),
Some(organization_item.slug),
&redis,
)
.await?;
@@ -1079,7 +1103,7 @@ pub async fn delete_organization_icon(
database::models::Organization::clear_cache(
organization_item.id,
Some(organization_item.name),
Some(organization_item.slug),
&redis,
)
.await?;

View File

@@ -73,7 +73,7 @@ pub enum CreateError {
InvalidCategory(String),
#[error("Invalid file type for version file: {0}")]
InvalidFileType(String),
#[error("Slug collides with other project's id!")]
#[error("Slug is already taken!")]
SlugCollision,
#[error("Authentication Error: {0}")]
Unauthorized(#[from] AuthenticationError),
@@ -612,22 +612,22 @@ async fn project_create_inner(
additional_categories.extend(ids.values());
}
// Should only be owner if not attached to an organization
let is_owner = project_create_data.organization_id.is_none();
let mut members = vec![];
let team = models::team_item::TeamBuilder {
members: vec![models::team_item::TeamMemberBuilder {
if project_create_data.organization_id.is_none() {
members.push(models::team_item::TeamMemberBuilder {
user_id: current_user.id.into(),
role: crate::models::teams::OWNER_ROLE.to_owned(),
is_owner,
// Allow all permissions for project creator, even if attached to a project
is_owner: true,
permissions: ProjectPermissions::all(),
organization_permissions: None,
accepted: true,
payouts_split: Decimal::ONE_HUNDRED,
ordering: 0,
}],
};
})
}
let team = models::team_item::TeamBuilder { members };
let team_id = team.insert(&mut *transaction).await?;

View File

@@ -65,7 +65,7 @@ pub async fn team_members_get_project(
}
let members_data =
TeamMember::get_from_team_full(project.inner.team_id, &**pool, &redis).await?;
let users = crate::database::models::User::get_many_ids(
let users = User::get_many_ids(
&members_data.iter().map(|x| x.user_id).collect::<Vec<_>>(),
&**pool,
&redis,
@@ -73,14 +73,14 @@ pub async fn team_members_get_project(
.await?;
let user_id = current_user.as_ref().map(|x| x.id.into());
let logged_in = if let Some(user_id) = user_id {
let (team_member, organization_team_member) =
TeamMember::get_for_project_permissions(&project.inner, user_id, &**pool).await?;
let logged_in = current_user
.and_then(|user| {
members_data
.iter()
.find(|x| x.user_id == user.id.into() && x.accepted)
})
.is_some();
team_member.is_some() || organization_team_member.is_some()
} else {
false
};
let team_members: Vec<_> = members_data
.into_iter()

View File

@@ -731,7 +731,9 @@ async fn upload_file_to_version_inner(
"At least one file must be specified".to_string(),
));
} else {
VersionFileBuilder::insert_many(file_builders, version_id, &mut *transaction).await?;
for file in file_builders {
file.insert(version_id, &mut *transaction).await?;
}
}
// Clear version cache

View File

@@ -163,6 +163,9 @@ pub async fn index_local(
})
.unwrap_or_default();
categories.extend(mrpack_loaders);
if loader_fields.contains_key("mrpack_loaders") {
categories.retain(|x| *x != "mrpack");
}
let gallery = m
.gallery_items