You've already forked AstralRinth
forked from didirus/AstralRinth
Implement more database methods and basic API routes (#50)
* feat: Implement more database methods & add mod and version routes * feat: Implement deleting mods/versions & implement categories * feat: Implement routes for categories, game versions & loaders * feat: Reorganize API routes in a (hopefully) usable way
This commit is contained in:
396
src/database/models/categories.rs
Normal file
396
src/database/models/categories.rs
Normal file
@@ -0,0 +1,396 @@
|
||||
use super::ids::*;
|
||||
use super::DatabaseError;
|
||||
use futures::TryStreamExt;
|
||||
|
||||
pub struct Loader {
|
||||
pub id: LoaderId,
|
||||
pub loader: String,
|
||||
}
|
||||
|
||||
pub struct GameVersion {
|
||||
pub id: GameVersionId,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
pub struct Category {
|
||||
pub id: CategoryId,
|
||||
pub category: String,
|
||||
}
|
||||
|
||||
pub struct CategoryBuilder<'a> {
|
||||
pub name: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl Category {
|
||||
pub fn builder() -> CategoryBuilder<'static> {
|
||||
CategoryBuilder { name: None }
|
||||
}
|
||||
|
||||
pub async fn get_id<'a, E>(name: &str, exec: E) -> Result<Option<CategoryId>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
if !name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
|
||||
{
|
||||
return Err(DatabaseError::InvalidIdentifier(name.to_string()));
|
||||
}
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT id FROM categories
|
||||
WHERE category = $1
|
||||
",
|
||||
name
|
||||
)
|
||||
.fetch_optional(exec)
|
||||
.await?;
|
||||
|
||||
Ok(result.map(|r| CategoryId(r.id)))
|
||||
}
|
||||
|
||||
pub async fn get_name<'a, E>(id: CategoryId, exec: E) -> Result<String, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT category FROM categories
|
||||
WHERE id = $1
|
||||
",
|
||||
id as CategoryId
|
||||
)
|
||||
.fetch_one(exec)
|
||||
.await?;
|
||||
|
||||
Ok(result.category)
|
||||
}
|
||||
|
||||
pub async fn list<'a, E>(exec: E) -> Result<Vec<String>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT category FROM categories
|
||||
"
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.category)) })
|
||||
.try_collect::<Vec<String>>()
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn remove<'a, E>(name: &str, exec: E) -> Result<Option<()>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
use sqlx::Done;
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
DELETE FROM categories
|
||||
WHERE category = $1
|
||||
",
|
||||
name
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
// Nothing was deleted
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> CategoryBuilder<'a> {
|
||||
/// The name of the category. Must be ASCII alphanumeric or `-`/`_`
|
||||
pub fn name(mut self, name: &'a str) -> Result<CategoryBuilder<'a>, DatabaseError> {
|
||||
if name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
|
||||
{
|
||||
Ok(Self { name: Some(name) })
|
||||
} else {
|
||||
Err(DatabaseError::InvalidIdentifier(name.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn insert<'b, E>(self, exec: E) -> Result<CategoryId, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'b, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
INSERT INTO categories (category)
|
||||
VALUES ($1)
|
||||
RETURNING id
|
||||
",
|
||||
self.name
|
||||
)
|
||||
.fetch_one(exec)
|
||||
.await?;
|
||||
|
||||
Ok(CategoryId(result.id))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LoaderBuilder<'a> {
|
||||
pub name: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl Loader {
|
||||
pub fn builder() -> LoaderBuilder<'static> {
|
||||
LoaderBuilder { name: None }
|
||||
}
|
||||
|
||||
pub async fn get_id<'a, E>(name: &str, exec: E) -> Result<Option<LoaderId>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
if !name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
|
||||
{
|
||||
return Err(DatabaseError::InvalidIdentifier(name.to_string()));
|
||||
}
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT id FROM loaders
|
||||
WHERE loader = $1
|
||||
",
|
||||
name
|
||||
)
|
||||
.fetch_optional(exec)
|
||||
.await?;
|
||||
|
||||
Ok(result.map(|r| LoaderId(r.id)))
|
||||
}
|
||||
|
||||
pub async fn get_name<'a, E>(id: LoaderId, exec: E) -> Result<String, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT loader FROM loaders
|
||||
WHERE id = $1
|
||||
",
|
||||
id as LoaderId
|
||||
)
|
||||
.fetch_one(exec)
|
||||
.await?;
|
||||
|
||||
Ok(result.loader)
|
||||
}
|
||||
|
||||
pub async fn list<'a, E>(exec: E) -> Result<Vec<String>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT loader FROM loaders
|
||||
"
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.loader)) })
|
||||
.try_collect::<Vec<String>>()
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
// TODO: remove loaders with mods using them
|
||||
pub async fn remove<'a, E>(name: &str, exec: E) -> Result<Option<()>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
use sqlx::Done;
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
DELETE FROM loaders
|
||||
WHERE loader = $1
|
||||
",
|
||||
name
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
// Nothing was deleted
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> LoaderBuilder<'a> {
|
||||
/// The name of the loader. Must be ASCII alphanumeric or `-`/`_`
|
||||
pub fn name(mut self, name: &'a str) -> Result<LoaderBuilder<'a>, DatabaseError> {
|
||||
if name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
|
||||
{
|
||||
Ok(Self { name: Some(name) })
|
||||
} else {
|
||||
Err(DatabaseError::InvalidIdentifier(name.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn insert<'b, E>(self, exec: E) -> Result<LoaderId, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'b, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
INSERT INTO loaders (loader)
|
||||
VALUES ($1)
|
||||
RETURNING id
|
||||
",
|
||||
self.name
|
||||
)
|
||||
.fetch_one(exec)
|
||||
.await?;
|
||||
|
||||
Ok(LoaderId(result.id))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GameVersionBuilder<'a> {
|
||||
pub version: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl GameVersion {
|
||||
pub fn builder() -> GameVersionBuilder<'static> {
|
||||
GameVersionBuilder { version: None }
|
||||
}
|
||||
|
||||
pub async fn get_id<'a, E>(
|
||||
version: &str,
|
||||
exec: E,
|
||||
) -> Result<Option<GameVersionId>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
if !version
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || "-_.".contains(c))
|
||||
{
|
||||
return Err(DatabaseError::InvalidIdentifier(version.to_string()));
|
||||
}
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT id FROM game_versions
|
||||
WHERE version = $1
|
||||
",
|
||||
version
|
||||
)
|
||||
.fetch_optional(exec)
|
||||
.await?;
|
||||
|
||||
Ok(result.map(|r| GameVersionId(r.id)))
|
||||
}
|
||||
|
||||
pub async fn get_name<'a, E>(id: VersionId, exec: E) -> Result<String, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT version FROM game_versions
|
||||
WHERE id = $1
|
||||
",
|
||||
id as VersionId
|
||||
)
|
||||
.fetch_one(exec)
|
||||
.await?;
|
||||
|
||||
Ok(result.version)
|
||||
}
|
||||
|
||||
pub async fn list<'a, E>(exec: E) -> Result<Vec<String>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT version FROM game_versions
|
||||
"
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.version)) })
|
||||
.try_collect::<Vec<String>>()
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn remove<'a, E>(name: &str, exec: E) -> Result<Option<()>, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
use sqlx::Done;
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
DELETE FROM game_versions
|
||||
WHERE version = $1
|
||||
",
|
||||
name
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
// Nothing was deleted
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> GameVersionBuilder<'a> {
|
||||
/// The game version. Spaces must be replaced with '_' for it to be valid
|
||||
pub fn version(mut self, version: &'a str) -> Result<GameVersionBuilder<'a>, DatabaseError> {
|
||||
if version
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || "-_.".contains(c))
|
||||
{
|
||||
Ok(Self {
|
||||
version: Some(version),
|
||||
})
|
||||
} else {
|
||||
Err(DatabaseError::InvalidIdentifier(version.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn insert<'b, E>(self, exec: E) -> Result<GameVersionId, DatabaseError>
|
||||
where
|
||||
E: sqlx::Executor<'b, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
INSERT INTO game_versions (version)
|
||||
VALUES ($1)
|
||||
RETURNING id
|
||||
",
|
||||
self.version
|
||||
)
|
||||
.fetch_one(exec)
|
||||
.await?;
|
||||
|
||||
Ok(GameVersionId(result.id))
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::DatabaseError;
|
||||
use crate::models::ids::random_base62;
|
||||
use crate::models::ids::random_base62_rng;
|
||||
use sqlx_macros::Type;
|
||||
|
||||
const ID_RETRY_COUNT: usize = 20;
|
||||
@@ -9,8 +9,10 @@ macro_rules! generate_ids {
|
||||
$vis async fn $function_name(
|
||||
con: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
) -> Result<$return_type, DatabaseError> {
|
||||
use rand::Rng;
|
||||
let mut rng = rand::thread_rng();
|
||||
let length = $id_length;
|
||||
let mut id = random_base62(length);
|
||||
let mut id = random_base62_rng(&mut rng, length);
|
||||
let mut retry_count = 0;
|
||||
|
||||
// Check if ID is unique
|
||||
@@ -20,7 +22,7 @@ macro_rules! generate_ids {
|
||||
.await?;
|
||||
|
||||
if results.exists.unwrap_or(true) {
|
||||
id = random_base62(length);
|
||||
id = random_base62_rng(&mut rng, length);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod categories;
|
||||
pub mod ids;
|
||||
pub mod mod_item;
|
||||
pub mod team_item;
|
||||
@@ -22,4 +23,9 @@ pub enum DatabaseError {
|
||||
DatabaseError(#[from] sqlx::error::Error),
|
||||
#[error("Error while trying to generate random ID")]
|
||||
RandomIdError,
|
||||
#[error(
|
||||
"Invalid identifier: Category/version names must contain only ASCII \
|
||||
alphanumeric characters or '_-'."
|
||||
)]
|
||||
InvalidIdentifier(String),
|
||||
}
|
||||
|
||||
@@ -105,4 +105,175 @@ impl Mod {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get<'a, 'b, E>(id: ModId, executor: E) -> Result<Option<Self>, sqlx::error::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT title, description, downloads,
|
||||
icon_url, body_url, published,
|
||||
issues_url, source_url, wiki_url,
|
||||
team_id
|
||||
FROM mods
|
||||
WHERE id = $1
|
||||
",
|
||||
id as ModId,
|
||||
)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
if let Some(row) = result {
|
||||
Ok(Some(Mod {
|
||||
id,
|
||||
team_id: TeamId(row.team_id),
|
||||
title: row.title,
|
||||
description: row.description,
|
||||
downloads: row.downloads,
|
||||
body_url: row.body_url,
|
||||
icon_url: row.icon_url,
|
||||
published: row.published,
|
||||
issues_url: row.issues_url,
|
||||
source_url: row.source_url,
|
||||
wiki_url: row.wiki_url,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn remove_full<'a, 'b, E>(
|
||||
id: ModId,
|
||||
exec: E,
|
||||
) -> Result<Option<()>, sqlx::error::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT team_id FROM mods WHERE id = $1
|
||||
",
|
||||
id as ModId,
|
||||
)
|
||||
.fetch_optional(exec)
|
||||
.await?;
|
||||
|
||||
let team_id: TeamId = if let Some(id) = result {
|
||||
TeamId(id.team_id)
|
||||
} else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM mods_categories
|
||||
WHERE joining_mod_id = $1
|
||||
",
|
||||
id as ModId,
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
use futures::TryStreamExt;
|
||||
let versions: Vec<VersionId> = sqlx::query!(
|
||||
"
|
||||
SELECT id FROM versions
|
||||
WHERE mod_id = $1
|
||||
",
|
||||
id as ModId,
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| VersionId(c.id))) })
|
||||
.try_collect::<Vec<VersionId>>()
|
||||
.await?;
|
||||
|
||||
for version in versions {
|
||||
super::Version::remove_full(version, exec).await?;
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM mods
|
||||
WHERE id = $1
|
||||
",
|
||||
id as ModId,
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM team_members
|
||||
WHERE team_id = $1
|
||||
",
|
||||
team_id as TeamId,
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM teams
|
||||
WHERE id = $1
|
||||
",
|
||||
team_id as TeamId,
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
Ok(Some(()))
|
||||
}
|
||||
|
||||
pub async fn get_full<'a, 'b, E>(
|
||||
id: ModId,
|
||||
executor: E,
|
||||
) -> Result<Option<QueryMod>, sqlx::error::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
let result = Self::get(id, executor).await?;
|
||||
if let Some(inner) = result {
|
||||
use futures::TryStreamExt;
|
||||
let categories: Vec<String> = sqlx::query!(
|
||||
"
|
||||
SELECT category FROM mods_categories
|
||||
INNER JOIN categories ON joining_category_id = id
|
||||
WHERE joining_mod_id = $1
|
||||
",
|
||||
id as ModId,
|
||||
)
|
||||
.fetch_many(executor)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.category)) })
|
||||
.try_collect::<Vec<String>>()
|
||||
.await?;
|
||||
|
||||
let versions: Vec<VersionId> = sqlx::query!(
|
||||
"
|
||||
SELECT id FROM versions
|
||||
WHERE mod_id = $1
|
||||
",
|
||||
id as ModId,
|
||||
)
|
||||
.fetch_many(executor)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| VersionId(c.id))) })
|
||||
.try_collect::<Vec<VersionId>>()
|
||||
.await?;
|
||||
|
||||
Ok(Some(QueryMod {
|
||||
inner,
|
||||
categories,
|
||||
versions,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct QueryMod {
|
||||
pub inner: Mod,
|
||||
|
||||
pub categories: Vec<String>,
|
||||
pub versions: Vec<VersionId>,
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use super::categories::{GameVersion, Loader};
|
||||
use super::ids::*;
|
||||
use super::DatabaseError;
|
||||
|
||||
@@ -173,7 +174,118 @@ impl Version {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_dependencies<'a, E>(&self, exec: E) -> Result<Vec<VersionId>, sqlx::Error>
|
||||
// TODO: someone verify this
|
||||
pub async fn remove_full<'a, E>(id: VersionId, exec: E) -> Result<Option<()>, sqlx::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
use sqlx::Done;
|
||||
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT EXISTS(SELECT 1 FROM versions WHERE id = $1)
|
||||
",
|
||||
id as VersionId,
|
||||
)
|
||||
.fetch_one(exec)
|
||||
.await?;
|
||||
|
||||
if !result.exists.unwrap_or(false) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM game_versions_versions gvv
|
||||
WHERE gvv.joining_version_id = $1
|
||||
",
|
||||
id as VersionId,
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM loaders_versions
|
||||
WHERE loaders_versions.version_id = $1
|
||||
",
|
||||
id as VersionId,
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
use futures::TryStreamExt;
|
||||
|
||||
let mut files = sqlx::query!(
|
||||
"
|
||||
SELECT files.id, files.url, files.filename FROM files
|
||||
WHERE files.version_id = $1
|
||||
",
|
||||
id as VersionId,
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async {
|
||||
Ok(e.right().map(|c| VersionFile {
|
||||
id: FileId(c.id),
|
||||
version_id: id,
|
||||
url: c.url,
|
||||
filename: c.filename,
|
||||
}))
|
||||
})
|
||||
.try_collect::<Vec<VersionFile>>()
|
||||
.await?;
|
||||
|
||||
for file in files {
|
||||
// TODO: store backblaze id in database so that we can delete the files here
|
||||
// For now, we can't delete the files since we don't have the backblaze id
|
||||
log::warn!(
|
||||
"Can't delete version file id: {} (url: {}, name: {})",
|
||||
file.id.0,
|
||||
file.url,
|
||||
file.filename
|
||||
)
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM hashes
|
||||
WHERE EXISTS(
|
||||
SELECT 1 FROM files WHERE
|
||||
(files.version_id = $1) AND
|
||||
(hashes.file_id = files.id)
|
||||
)
|
||||
",
|
||||
id as VersionId
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM files
|
||||
WHERE files.version_id = $1
|
||||
",
|
||||
id as VersionId,
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM versions WHERE id = $1
|
||||
",
|
||||
id as VersionId,
|
||||
)
|
||||
.execute(exec)
|
||||
.await?;
|
||||
|
||||
Ok(Some(()))
|
||||
}
|
||||
|
||||
pub async fn get_dependencies<'a, E>(
|
||||
id: VersionId,
|
||||
exec: E,
|
||||
) -> Result<Vec<VersionId>, sqlx::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
@@ -184,7 +296,7 @@ impl Version {
|
||||
SELECT dependency_id id FROM dependencies
|
||||
WHERE dependent_id = $1
|
||||
",
|
||||
self.id as VersionId,
|
||||
id as VersionId,
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|v| VersionId(v.id))) })
|
||||
@@ -193,20 +305,177 @@ impl Version {
|
||||
|
||||
Ok(vec)
|
||||
}
|
||||
|
||||
pub async fn get_mod_versions<'a, E>(
|
||||
mod_id: ModId,
|
||||
exec: E,
|
||||
) -> Result<Vec<VersionId>, sqlx::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
use futures::stream::TryStreamExt;
|
||||
|
||||
let vec = sqlx::query!(
|
||||
"
|
||||
SELECT id FROM versions
|
||||
WHERE mod_id = $1
|
||||
",
|
||||
mod_id as ModId,
|
||||
)
|
||||
.fetch_many(exec)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|v| VersionId(v.id))) })
|
||||
.try_collect::<Vec<VersionId>>()
|
||||
.await?;
|
||||
|
||||
Ok(vec)
|
||||
}
|
||||
|
||||
pub async fn get<'a, 'b, E>(
|
||||
id: VersionId,
|
||||
executor: E,
|
||||
) -> Result<Option<Self>, sqlx::error::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT v.mod_id, v.name, v.version_number,
|
||||
v.changelog_url, v.date_published, v.downloads,
|
||||
v.release_channel
|
||||
FROM versions v
|
||||
WHERE v.id = $1
|
||||
",
|
||||
id as VersionId,
|
||||
)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
if let Some(row) = result {
|
||||
Ok(Some(Version {
|
||||
id,
|
||||
mod_id: ModId(row.mod_id),
|
||||
name: row.name,
|
||||
version_number: row.version_number,
|
||||
changelog_url: row.changelog_url,
|
||||
date_published: row.date_published,
|
||||
downloads: row.downloads,
|
||||
release_channel: ChannelId(row.release_channel),
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_full<'a, 'b, E>(
|
||||
id: VersionId,
|
||||
executor: E,
|
||||
) -> Result<Option<QueryVersion>, sqlx::error::Error>
|
||||
where
|
||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT v.mod_id, v.name, v.version_number,
|
||||
v.changelog_url, v.date_published, v.downloads,
|
||||
release_channels.channel
|
||||
FROM versions v
|
||||
INNER JOIN release_channels ON v.release_channel = release_channels.id
|
||||
WHERE v.id = $1
|
||||
",
|
||||
id as VersionId,
|
||||
)
|
||||
.fetch_optional(executor)
|
||||
.await?;
|
||||
|
||||
if let Some(row) = result {
|
||||
use futures::TryStreamExt;
|
||||
use sqlx::Row;
|
||||
|
||||
let game_versions: Vec<String> = sqlx::query!(
|
||||
"
|
||||
SELECT gv.version FROM game_versions_versions gvv
|
||||
INNER JOIN game_versions gv ON gvv.game_version_id=gv.id
|
||||
WHERE gvv.joining_version_id = $1
|
||||
",
|
||||
id as VersionId,
|
||||
)
|
||||
.fetch_many(executor)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.version)) })
|
||||
.try_collect::<Vec<String>>()
|
||||
.await?;
|
||||
|
||||
let loaders: Vec<String> = sqlx::query!(
|
||||
"
|
||||
SELECT loaders.loader FROM loaders
|
||||
INNER JOIN loaders_versions ON loaders.id = loaders_versions.loader_id
|
||||
WHERE loaders_versions.version_id = $1
|
||||
",
|
||||
id as VersionId,
|
||||
)
|
||||
.fetch_many(executor)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| c.loader)) })
|
||||
.try_collect::<Vec<String>>()
|
||||
.await?;
|
||||
|
||||
let mut files = sqlx::query!(
|
||||
"
|
||||
SELECT files.id, files.url, files.filename FROM files
|
||||
WHERE files.version_id = $1
|
||||
",
|
||||
id as VersionId,
|
||||
)
|
||||
.fetch_many(executor)
|
||||
.try_filter_map(|e| async {
|
||||
Ok(e.right().map(|c| QueryFile {
|
||||
id: FileId(c.id),
|
||||
url: c.url,
|
||||
filename: c.filename,
|
||||
hashes: std::collections::HashMap::new(),
|
||||
}))
|
||||
})
|
||||
.try_collect::<Vec<QueryFile>>()
|
||||
.await?;
|
||||
|
||||
for file in files.iter_mut() {
|
||||
let mut files = sqlx::query!(
|
||||
"
|
||||
SELECT hashes.algorithm, hashes.hash FROM hashes
|
||||
WHERE hashes.file_id = $1
|
||||
",
|
||||
file.id as FileId
|
||||
)
|
||||
.fetch_many(executor)
|
||||
.try_filter_map(|e| async { Ok(e.right().map(|c| (c.algorithm, c.hash))) })
|
||||
.try_collect::<Vec<(String, Vec<u8>)>>()
|
||||
.await?;
|
||||
|
||||
file.hashes.extend(files);
|
||||
}
|
||||
|
||||
Ok(Some(QueryVersion {
|
||||
id,
|
||||
mod_id: ModId(row.mod_id),
|
||||
name: row.name,
|
||||
version_number: row.version_number,
|
||||
changelog_url: row.changelog_url,
|
||||
date_published: row.date_published,
|
||||
downloads: row.downloads,
|
||||
|
||||
release_channel: row.channel,
|
||||
files: Vec::<QueryFile>::new(),
|
||||
loaders,
|
||||
game_versions,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ReleaseChannel {
|
||||
pub id: ChannelId,
|
||||
pub channel: String,
|
||||
}
|
||||
pub struct Loader {
|
||||
pub id: LoaderId,
|
||||
pub loader: String,
|
||||
}
|
||||
pub struct GameVersion {
|
||||
pub id: GameVersionId,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
pub struct VersionFile {
|
||||
pub id: FileId,
|
||||
@@ -221,7 +490,24 @@ pub struct FileHash {
|
||||
pub hash: Vec<u8>,
|
||||
}
|
||||
|
||||
pub struct Category {
|
||||
pub id: CategoryId,
|
||||
pub category: String,
|
||||
pub struct QueryVersion {
|
||||
pub id: VersionId,
|
||||
pub mod_id: ModId,
|
||||
pub name: String,
|
||||
pub version_number: String,
|
||||
pub changelog_url: Option<String>,
|
||||
pub date_published: chrono::DateTime<chrono::Utc>,
|
||||
pub downloads: i32,
|
||||
|
||||
pub release_channel: String,
|
||||
pub files: Vec<QueryFile>,
|
||||
pub game_versions: Vec<String>,
|
||||
pub loaders: Vec<String>,
|
||||
}
|
||||
|
||||
pub struct QueryFile {
|
||||
pub id: FileId,
|
||||
pub url: String,
|
||||
pub filename: String,
|
||||
pub hashes: std::collections::HashMap<String, Vec<u8>>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user