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:
Aeledfyr
2020-08-12 14:54:03 -05:00
committed by GitHub
parent e2bf474332
commit 781f0c843e
20 changed files with 2146 additions and 125 deletions

View 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))
}
}

View File

@@ -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;
}

View File

@@ -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),
}

View File

@@ -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>,
}

View File

@@ -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>>,
}