move to monorepo dir

This commit is contained in:
Jai A
2024-10-16 14:11:42 -07:00
parent ff7975773e
commit e3a3379615
756 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
pub mod models;
mod postgres_database;
pub mod redis;
pub use models::Image;
pub use models::Project;
pub use models::Version;
pub use postgres_database::check_for_migrations;
pub use postgres_database::connect;

View File

@@ -0,0 +1,282 @@
use std::collections::HashMap;
use crate::database::redis::RedisPool;
use super::ids::*;
use super::DatabaseError;
use futures::TryStreamExt;
use serde::{Deserialize, Serialize};
const TAGS_NAMESPACE: &str = "tags";
pub struct ProjectType {
pub id: ProjectTypeId,
pub name: String,
}
#[derive(Serialize, Deserialize)]
pub struct Category {
pub id: CategoryId,
pub category: String,
pub project_type: String,
pub icon: String,
pub header: String,
}
pub struct ReportType {
pub id: ReportTypeId,
pub report_type: String,
}
#[derive(Serialize, Deserialize)]
pub struct LinkPlatform {
pub id: LinkPlatformId,
pub name: String,
pub donation: bool,
}
impl Category {
// Gets hashmap of category ids matching a name
// Multiple categories can have the same name, but different project types, so we need to return a hashmap
// ProjectTypeId -> CategoryId
pub async fn get_ids<'a, E>(
name: &str,
exec: E,
) -> Result<HashMap<ProjectTypeId, CategoryId>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let result = sqlx::query!(
"
SELECT id, project_type FROM categories
WHERE category = $1
",
name,
)
.fetch_all(exec)
.await?;
let mut map = HashMap::new();
for r in result {
map.insert(ProjectTypeId(r.project_type), CategoryId(r.id));
}
Ok(map)
}
pub async fn get_id_project<'a, E>(
name: &str,
project_type: ProjectTypeId,
exec: E,
) -> Result<Option<CategoryId>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let result = sqlx::query!(
"
SELECT id FROM categories
WHERE category = $1 AND project_type = $2
",
name,
project_type as ProjectTypeId
)
.fetch_optional(exec)
.await?;
Ok(result.map(|r| CategoryId(r.id)))
}
pub async fn list<'a, E>(exec: E, redis: &RedisPool) -> Result<Vec<Category>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let mut redis = redis.connect().await?;
let res: Option<Vec<Category>> = redis
.get_deserialized_from_json(TAGS_NAMESPACE, "category")
.await?;
if let Some(res) = res {
return Ok(res);
}
let result = sqlx::query!(
"
SELECT c.id id, c.category category, c.icon icon, c.header category_header, pt.name project_type
FROM categories c
INNER JOIN project_types pt ON c.project_type = pt.id
ORDER BY c.ordering, c.category
"
)
.fetch(exec)
.map_ok(|c| Category {
id: CategoryId(c.id),
category: c.category,
project_type: c.project_type,
icon: c.icon,
header: c.category_header
})
.try_collect::<Vec<Category>>()
.await?;
redis
.set_serialized_to_json(TAGS_NAMESPACE, "category", &result, None)
.await?;
Ok(result)
}
}
impl LinkPlatform {
pub async fn get_id<'a, E>(id: &str, exec: E) -> Result<Option<LinkPlatformId>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let result = sqlx::query!(
"
SELECT id FROM link_platforms
WHERE name = $1
",
id
)
.fetch_optional(exec)
.await?;
Ok(result.map(|r| LinkPlatformId(r.id)))
}
pub async fn list<'a, E>(exec: E, redis: &RedisPool) -> Result<Vec<LinkPlatform>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let mut redis = redis.connect().await?;
let res: Option<Vec<LinkPlatform>> = redis
.get_deserialized_from_json(TAGS_NAMESPACE, "link_platform")
.await?;
if let Some(res) = res {
return Ok(res);
}
let result = sqlx::query!(
"
SELECT id, name, donation FROM link_platforms
"
)
.fetch(exec)
.map_ok(|c| LinkPlatform {
id: LinkPlatformId(c.id),
name: c.name,
donation: c.donation,
})
.try_collect::<Vec<LinkPlatform>>()
.await?;
redis
.set_serialized_to_json(TAGS_NAMESPACE, "link_platform", &result, None)
.await?;
Ok(result)
}
}
impl ReportType {
pub async fn get_id<'a, E>(name: &str, exec: E) -> Result<Option<ReportTypeId>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let result = sqlx::query!(
"
SELECT id FROM report_types
WHERE name = $1
",
name
)
.fetch_optional(exec)
.await?;
Ok(result.map(|r| ReportTypeId(r.id)))
}
pub async fn list<'a, E>(exec: E, redis: &RedisPool) -> Result<Vec<String>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let mut redis = redis.connect().await?;
let res: Option<Vec<String>> = redis
.get_deserialized_from_json(TAGS_NAMESPACE, "report_type")
.await?;
if let Some(res) = res {
return Ok(res);
}
let result = sqlx::query!(
"
SELECT name FROM report_types
"
)
.fetch(exec)
.map_ok(|c| c.name)
.try_collect::<Vec<String>>()
.await?;
redis
.set_serialized_to_json(TAGS_NAMESPACE, "report_type", &result, None)
.await?;
Ok(result)
}
}
impl ProjectType {
pub async fn get_id<'a, E>(name: &str, exec: E) -> Result<Option<ProjectTypeId>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let result = sqlx::query!(
"
SELECT id FROM project_types
WHERE name = $1
",
name
)
.fetch_optional(exec)
.await?;
Ok(result.map(|r| ProjectTypeId(r.id)))
}
pub async fn list<'a, E>(exec: E, redis: &RedisPool) -> Result<Vec<String>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let mut redis = redis.connect().await?;
let res: Option<Vec<String>> = redis
.get_deserialized_from_json(TAGS_NAMESPACE, "project_type")
.await?;
if let Some(res) = res {
return Ok(res);
}
let result = sqlx::query!(
"
SELECT name FROM project_types
"
)
.fetch(exec)
.map_ok(|c| c.name)
.try_collect::<Vec<String>>()
.await?;
redis
.set_serialized_to_json(TAGS_NAMESPACE, "project_type", &result, None)
.await?;
Ok(result)
}
}

View File

@@ -0,0 +1,197 @@
use crate::database::models::{
ChargeId, DatabaseError, ProductPriceId, UserId, UserSubscriptionId,
};
use crate::models::billing::{ChargeStatus, ChargeType, PriceDuration};
use chrono::{DateTime, Utc};
use std::convert::{TryFrom, TryInto};
pub struct ChargeItem {
pub id: ChargeId,
pub user_id: UserId,
pub price_id: ProductPriceId,
pub amount: i64,
pub currency_code: String,
pub status: ChargeStatus,
pub due: DateTime<Utc>,
pub last_attempt: Option<DateTime<Utc>>,
pub type_: ChargeType,
pub subscription_id: Option<UserSubscriptionId>,
pub subscription_interval: Option<PriceDuration>,
}
struct ChargeResult {
id: i64,
user_id: i64,
price_id: i64,
amount: i64,
currency_code: String,
status: String,
due: DateTime<Utc>,
last_attempt: Option<DateTime<Utc>>,
charge_type: String,
subscription_id: Option<i64>,
subscription_interval: Option<String>,
}
impl TryFrom<ChargeResult> for ChargeItem {
type Error = serde_json::Error;
fn try_from(r: ChargeResult) -> Result<Self, Self::Error> {
Ok(ChargeItem {
id: ChargeId(r.id),
user_id: UserId(r.user_id),
price_id: ProductPriceId(r.price_id),
amount: r.amount,
currency_code: r.currency_code,
status: ChargeStatus::from_string(&r.status),
due: r.due,
last_attempt: r.last_attempt,
type_: ChargeType::from_string(&r.charge_type),
subscription_id: r.subscription_id.map(UserSubscriptionId),
subscription_interval: r
.subscription_interval
.map(|x| PriceDuration::from_string(&x)),
})
}
}
macro_rules! select_charges_with_predicate {
($predicate:tt, $param:ident) => {
sqlx::query_as!(
ChargeResult,
r#"
SELECT id, user_id, price_id, amount, currency_code, status, due, last_attempt, charge_type, subscription_id, subscription_interval
FROM charges
"#
+ $predicate,
$param
)
};
}
impl ChargeItem {
pub async fn upsert(
&self,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<ChargeId, DatabaseError> {
sqlx::query!(
r#"
INSERT INTO charges (id, user_id, price_id, amount, currency_code, charge_type, status, due, last_attempt, subscription_id, subscription_interval)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
ON CONFLICT (id)
DO UPDATE
SET status = EXCLUDED.status,
last_attempt = EXCLUDED.last_attempt,
due = EXCLUDED.due,
subscription_id = EXCLUDED.subscription_id,
subscription_interval = EXCLUDED.subscription_interval
"#,
self.id.0,
self.user_id.0,
self.price_id.0,
self.amount,
self.currency_code,
self.type_.as_str(),
self.status.as_str(),
self.due,
self.last_attempt,
self.subscription_id.map(|x| x.0),
self.subscription_interval.map(|x| x.as_str()),
)
.execute(&mut **transaction)
.await?;
Ok(self.id)
}
pub async fn get(
id: ChargeId,
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<Option<ChargeItem>, DatabaseError> {
let id = id.0;
let res = select_charges_with_predicate!("WHERE id = $1", id)
.fetch_optional(exec)
.await?;
Ok(res.and_then(|r| r.try_into().ok()))
}
pub async fn get_from_user(
user_id: UserId,
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<Vec<ChargeItem>, DatabaseError> {
let user_id = user_id.0;
let res = select_charges_with_predicate!("WHERE user_id = $1 ORDER BY due DESC", user_id)
.fetch_all(exec)
.await?;
Ok(res
.into_iter()
.map(|r| r.try_into())
.collect::<Result<Vec<_>, serde_json::Error>>()?)
}
pub async fn get_open_subscription(
user_subscription_id: UserSubscriptionId,
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<Option<ChargeItem>, DatabaseError> {
let user_subscription_id = user_subscription_id.0;
let res = select_charges_with_predicate!(
"WHERE subscription_id = $1 AND (status = 'open' OR status = 'cancelled')",
user_subscription_id
)
.fetch_optional(exec)
.await?;
Ok(res.and_then(|r| r.try_into().ok()))
}
pub async fn get_chargeable(
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<Vec<ChargeItem>, DatabaseError> {
let now = Utc::now();
let res = select_charges_with_predicate!("WHERE (status = 'open' AND due < $1) OR (status = 'failed' AND last_attempt < $1 - INTERVAL '2 days')", now)
.fetch_all(exec)
.await?;
Ok(res
.into_iter()
.map(|r| r.try_into())
.collect::<Result<Vec<_>, serde_json::Error>>()?)
}
pub async fn get_unprovision(
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<Vec<ChargeItem>, DatabaseError> {
let now = Utc::now();
let res =
select_charges_with_predicate!("WHERE (status = 'cancelled' AND due < $1) OR (status = 'failed' AND last_attempt < $1 - INTERVAL '2 days')", now)
.fetch_all(exec)
.await?;
Ok(res
.into_iter()
.map(|r| r.try_into())
.collect::<Result<Vec<_>, serde_json::Error>>()?)
}
pub async fn remove(
id: ChargeId,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<(), DatabaseError> {
sqlx::query!(
"
DELETE FROM charges
WHERE id = $1
",
id.0 as i64
)
.execute(&mut **transaction)
.await?;
Ok(())
}
}

View File

@@ -0,0 +1,221 @@
use super::ids::*;
use crate::database::models;
use crate::database::models::DatabaseError;
use crate::database::redis::RedisPool;
use crate::models::collections::CollectionStatus;
use chrono::{DateTime, Utc};
use dashmap::DashMap;
use futures::TryStreamExt;
use serde::{Deserialize, Serialize};
const COLLECTIONS_NAMESPACE: &str = "collections";
#[derive(Clone)]
pub struct CollectionBuilder {
pub collection_id: CollectionId,
pub user_id: UserId,
pub name: String,
pub description: Option<String>,
pub status: CollectionStatus,
pub projects: Vec<ProjectId>,
}
impl CollectionBuilder {
pub async fn insert(
self,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<CollectionId, DatabaseError> {
let collection_struct = Collection {
id: self.collection_id,
name: self.name,
user_id: self.user_id,
description: self.description,
created: Utc::now(),
updated: Utc::now(),
icon_url: None,
raw_icon_url: None,
color: None,
status: self.status,
projects: self.projects,
};
collection_struct.insert(transaction).await?;
Ok(self.collection_id)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Collection {
pub id: CollectionId,
pub user_id: UserId,
pub name: String,
pub description: Option<String>,
pub created: DateTime<Utc>,
pub updated: DateTime<Utc>,
pub icon_url: Option<String>,
pub raw_icon_url: Option<String>,
pub color: Option<u32>,
pub status: CollectionStatus,
pub projects: Vec<ProjectId>,
}
impl Collection {
pub async fn insert(
&self,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<(), DatabaseError> {
sqlx::query!(
"
INSERT INTO collections (
id, user_id, name, description,
created, icon_url, raw_icon_url, status
)
VALUES (
$1, $2, $3, $4,
$5, $6, $7, $8
)
",
self.id as CollectionId,
self.user_id as UserId,
&self.name,
self.description.as_ref(),
self.created,
self.icon_url.as_ref(),
self.raw_icon_url.as_ref(),
self.status.to_string(),
)
.execute(&mut **transaction)
.await?;
let (collection_ids, project_ids): (Vec<_>, Vec<_>) =
self.projects.iter().map(|p| (self.id.0, p.0)).unzip();
sqlx::query!(
"
INSERT INTO collections_mods (collection_id, mod_id)
SELECT * FROM UNNEST($1::bigint[], $2::bigint[])
ON CONFLICT DO NOTHING
",
&collection_ids[..],
&project_ids[..],
)
.execute(&mut **transaction)
.await?;
Ok(())
}
pub async fn remove(
id: CollectionId,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
redis: &RedisPool,
) -> Result<Option<()>, DatabaseError> {
let collection = Self::get(id, &mut **transaction, redis).await?;
if let Some(collection) = collection {
sqlx::query!(
"
DELETE FROM collections_mods
WHERE collection_id = $1
",
id as CollectionId,
)
.execute(&mut **transaction)
.await?;
sqlx::query!(
"
DELETE FROM collections
WHERE id = $1
",
id as CollectionId,
)
.execute(&mut **transaction)
.await?;
models::Collection::clear_cache(collection.id, redis).await?;
Ok(Some(()))
} else {
Ok(None)
}
}
pub async fn get<'a, 'b, E>(
id: CollectionId,
executor: E,
redis: &RedisPool,
) -> Result<Option<Collection>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
Collection::get_many(&[id], executor, redis)
.await
.map(|x| x.into_iter().next())
}
pub async fn get_many<'a, E>(
collection_ids: &[CollectionId],
exec: E,
redis: &RedisPool,
) -> Result<Vec<Collection>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let val = redis
.get_cached_keys(
COLLECTIONS_NAMESPACE,
&collection_ids.iter().map(|x| x.0).collect::<Vec<_>>(),
|collection_ids| async move {
let collections = sqlx::query!(
"
SELECT c.id id, c.name name, c.description description,
c.icon_url icon_url, c.raw_icon_url raw_icon_url, c.color color, c.created created, c.user_id user_id,
c.updated updated, c.status status,
ARRAY_AGG(DISTINCT cm.mod_id) filter (where cm.mod_id is not null) mods
FROM collections c
LEFT JOIN collections_mods cm ON cm.collection_id = c.id
WHERE c.id = ANY($1)
GROUP BY c.id;
",
&collection_ids,
)
.fetch(exec)
.try_fold(DashMap::new(), |acc, m| {
let collection = Collection {
id: CollectionId(m.id),
user_id: UserId(m.user_id),
name: m.name.clone(),
description: m.description.clone(),
icon_url: m.icon_url.clone(),
raw_icon_url: m.raw_icon_url.clone(),
color: m.color.map(|x| x as u32),
created: m.created,
updated: m.updated,
status: CollectionStatus::from_string(&m.status),
projects: m
.mods
.unwrap_or_default()
.into_iter()
.map(ProjectId)
.collect(),
};
acc.insert(m.id, collection);
async move { Ok(acc) }
})
.await?;
Ok(collections)
},
)
.await?;
Ok(val)
}
pub async fn clear_cache(id: CollectionId, redis: &RedisPool) -> Result<(), DatabaseError> {
let mut redis = redis.connect().await?;
redis.delete(COLLECTIONS_NAMESPACE, id.0).await?;
Ok(())
}
}

View File

@@ -0,0 +1,104 @@
use super::ids::*;
use crate::auth::oauth::uris::OAuthRedirectUris;
use crate::auth::AuthProvider;
use crate::database::models::DatabaseError;
use crate::database::redis::RedisPool;
use crate::models::pats::Scopes;
use chrono::Duration;
use rand::distributions::Alphanumeric;
use rand::Rng;
use rand_chacha::rand_core::SeedableRng;
use rand_chacha::ChaCha20Rng;
use serde::{Deserialize, Serialize};
const FLOWS_NAMESPACE: &str = "flows";
#[derive(Deserialize, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Flow {
OAuth {
user_id: Option<UserId>,
url: Option<String>,
provider: AuthProvider,
},
Login2FA {
user_id: UserId,
},
Initialize2FA {
user_id: UserId,
secret: String,
},
ForgotPassword {
user_id: UserId,
},
ConfirmEmail {
user_id: UserId,
confirm_email: String,
},
MinecraftAuth,
InitOAuthAppApproval {
user_id: UserId,
client_id: OAuthClientId,
existing_authorization_id: Option<OAuthClientAuthorizationId>,
scopes: Scopes,
redirect_uris: OAuthRedirectUris,
state: Option<String>,
},
OAuthAuthorizationCodeSupplied {
user_id: UserId,
client_id: OAuthClientId,
authorization_id: OAuthClientAuthorizationId,
scopes: Scopes,
original_redirect_uri: Option<String>, // Needed for https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
},
}
impl Flow {
pub async fn insert(
&self,
expires: Duration,
redis: &RedisPool,
) -> Result<String, DatabaseError> {
let mut redis = redis.connect().await?;
let flow = ChaCha20Rng::from_entropy()
.sample_iter(&Alphanumeric)
.take(32)
.map(char::from)
.collect::<String>();
redis
.set_serialized_to_json(FLOWS_NAMESPACE, &flow, &self, Some(expires.num_seconds()))
.await?;
Ok(flow)
}
pub async fn get(id: &str, redis: &RedisPool) -> Result<Option<Flow>, DatabaseError> {
let mut redis = redis.connect().await?;
redis.get_deserialized_from_json(FLOWS_NAMESPACE, id).await
}
/// Gets the flow and removes it from the cache, but only removes if the flow was present and the predicate returned true
/// The predicate should validate that the flow being removed is the correct one, as a security measure
pub async fn take_if(
id: &str,
predicate: impl FnOnce(&Flow) -> bool,
redis: &RedisPool,
) -> Result<Option<Flow>, DatabaseError> {
let flow = Self::get(id, redis).await?;
if let Some(flow) = flow.as_ref() {
if predicate(flow) {
Self::remove(id, redis).await?;
}
}
Ok(flow)
}
pub async fn remove(id: &str, redis: &RedisPool) -> Result<Option<()>, DatabaseError> {
let mut redis = redis.connect().await?;
redis.delete(FLOWS_NAMESPACE, id).await?;
Ok(Some(()))
}
}

View File

@@ -0,0 +1,596 @@
use super::DatabaseError;
use crate::models::ids::base62_impl::to_base62;
use crate::models::ids::{random_base62_rng, random_base62_rng_range};
use censor::Censor;
use rand::SeedableRng;
use rand_chacha::ChaCha20Rng;
use serde::{Deserialize, Serialize};
use sqlx::sqlx_macros::Type;
const ID_RETRY_COUNT: usize = 20;
macro_rules! generate_ids {
($vis:vis $function_name:ident, $return_type:ty, $id_length:expr, $select_stmnt:literal, $id_function:expr) => {
$vis async fn $function_name(
con: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<$return_type, DatabaseError> {
let mut rng = ChaCha20Rng::from_entropy();
let length = $id_length;
let mut id = random_base62_rng(&mut rng, length);
let mut retry_count = 0;
let censor = Censor::Standard + Censor::Sex;
// Check if ID is unique
loop {
let results = sqlx::query!($select_stmnt, id as i64)
.fetch_one(&mut **con)
.await?;
if results.exists.unwrap_or(true) || censor.check(&*to_base62(id)) {
id = random_base62_rng(&mut rng, length);
} else {
break;
}
retry_count += 1;
if retry_count > ID_RETRY_COUNT {
return Err(DatabaseError::RandomId);
}
}
Ok($id_function(id as i64))
}
};
}
macro_rules! generate_bulk_ids {
($vis:vis $function_name:ident, $return_type:ty, $select_stmnt:literal, $id_function:expr) => {
$vis async fn $function_name(
count: usize,
con: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Vec<$return_type>, DatabaseError> {
let mut rng = rand::thread_rng();
let mut retry_count = 0;
// Check if ID is unique
loop {
let base = random_base62_rng_range(&mut rng, 1, 10) as i64;
let ids = (0..count).map(|x| base + x as i64).collect::<Vec<_>>();
let results = sqlx::query!($select_stmnt, &ids)
.fetch_one(&mut **con)
.await?;
if !results.exists.unwrap_or(true) {
return Ok(ids.into_iter().map(|x| $id_function(x)).collect());
}
retry_count += 1;
if retry_count > ID_RETRY_COUNT {
return Err(DatabaseError::RandomId);
}
}
}
};
}
generate_ids!(
pub generate_project_id,
ProjectId,
8,
"SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)",
ProjectId
);
generate_ids!(
pub generate_version_id,
VersionId,
8,
"SELECT EXISTS(SELECT 1 FROM versions WHERE id=$1)",
VersionId
);
generate_ids!(
pub generate_team_id,
TeamId,
8,
"SELECT EXISTS(SELECT 1 FROM teams WHERE id=$1)",
TeamId
);
generate_ids!(
pub generate_organization_id,
OrganizationId,
8,
"SELECT EXISTS(SELECT 1 FROM organizations WHERE id=$1)",
OrganizationId
);
generate_ids!(
pub generate_collection_id,
CollectionId,
8,
"SELECT EXISTS(SELECT 1 FROM collections WHERE id=$1)",
CollectionId
);
generate_ids!(
pub generate_file_id,
FileId,
8,
"SELECT EXISTS(SELECT 1 FROM files WHERE id=$1)",
FileId
);
generate_ids!(
pub generate_team_member_id,
TeamMemberId,
8,
"SELECT EXISTS(SELECT 1 FROM team_members WHERE id=$1)",
TeamMemberId
);
generate_ids!(
pub generate_pat_id,
PatId,
8,
"SELECT EXISTS(SELECT 1 FROM pats WHERE id=$1)",
PatId
);
generate_ids!(
pub generate_user_id,
UserId,
8,
"SELECT EXISTS(SELECT 1 FROM users WHERE id=$1)",
UserId
);
generate_ids!(
pub generate_report_id,
ReportId,
8,
"SELECT EXISTS(SELECT 1 FROM reports WHERE id=$1)",
ReportId
);
generate_ids!(
pub generate_notification_id,
NotificationId,
8,
"SELECT EXISTS(SELECT 1 FROM notifications WHERE id=$1)",
NotificationId
);
generate_bulk_ids!(
pub generate_many_notification_ids,
NotificationId,
"SELECT EXISTS(SELECT 1 FROM notifications WHERE id = ANY($1))",
NotificationId
);
generate_ids!(
pub generate_thread_id,
ThreadId,
8,
"SELECT EXISTS(SELECT 1 FROM threads WHERE id=$1)",
ThreadId
);
generate_ids!(
pub generate_thread_message_id,
ThreadMessageId,
8,
"SELECT EXISTS(SELECT 1 FROM threads_messages WHERE id=$1)",
ThreadMessageId
);
generate_ids!(
pub generate_session_id,
SessionId,
8,
"SELECT EXISTS(SELECT 1 FROM sessions WHERE id=$1)",
SessionId
);
generate_ids!(
pub generate_image_id,
ImageId,
8,
"SELECT EXISTS(SELECT 1 FROM uploaded_images WHERE id=$1)",
ImageId
);
generate_ids!(
pub generate_oauth_client_authorization_id,
OAuthClientAuthorizationId,
8,
"SELECT EXISTS(SELECT 1 FROM oauth_client_authorizations WHERE id=$1)",
OAuthClientAuthorizationId
);
generate_ids!(
pub generate_oauth_client_id,
OAuthClientId,
8,
"SELECT EXISTS(SELECT 1 FROM oauth_clients WHERE id=$1)",
OAuthClientId
);
generate_ids!(
pub generate_oauth_redirect_id,
OAuthRedirectUriId,
8,
"SELECT EXISTS(SELECT 1 FROM oauth_client_redirect_uris WHERE id=$1)",
OAuthRedirectUriId
);
generate_ids!(
pub generate_oauth_access_token_id,
OAuthAccessTokenId,
8,
"SELECT EXISTS(SELECT 1 FROM oauth_access_tokens WHERE id=$1)",
OAuthAccessTokenId
);
generate_ids!(
pub generate_payout_id,
PayoutId,
8,
"SELECT EXISTS(SELECT 1 FROM oauth_access_tokens WHERE id=$1)",
PayoutId
);
generate_ids!(
pub generate_product_id,
ProductId,
8,
"SELECT EXISTS(SELECT 1 FROM products WHERE id=$1)",
ProductId
);
generate_ids!(
pub generate_product_price_id,
ProductPriceId,
8,
"SELECT EXISTS(SELECT 1 FROM products_prices WHERE id=$1)",
ProductPriceId
);
generate_ids!(
pub generate_user_subscription_id,
UserSubscriptionId,
8,
"SELECT EXISTS(SELECT 1 FROM users_subscriptions WHERE id=$1)",
UserSubscriptionId
);
generate_ids!(
pub generate_charge_id,
ChargeId,
8,
"SELECT EXISTS(SELECT 1 FROM charges WHERE id=$1)",
ChargeId
);
#[derive(Copy, Clone, Debug, PartialEq, Eq, Type, Hash, Serialize, Deserialize)]
#[sqlx(transparent)]
pub struct UserId(pub i64);
#[derive(Copy, Clone, Debug, Type, Eq, Hash, PartialEq, Serialize, Deserialize)]
#[sqlx(transparent)]
pub struct TeamId(pub i64);
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize)]
#[sqlx(transparent)]
pub struct TeamMemberId(pub i64);
#[derive(Copy, Clone, Debug, Type, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[sqlx(transparent)]
pub struct OrganizationId(pub i64);
#[derive(Copy, Clone, Debug, Type, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[sqlx(transparent)]
pub struct ProjectId(pub i64);
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[sqlx(transparent)]
pub struct ProjectTypeId(pub i32);
#[derive(Copy, Clone, Debug, Type)]
#[sqlx(transparent)]
pub struct StatusId(pub i32);
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize)]
#[sqlx(transparent)]
pub struct GameId(pub i32);
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[sqlx(transparent)]
pub struct LinkPlatformId(pub i32);
#[derive(
Copy, Clone, Debug, Type, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord,
)]
#[sqlx(transparent)]
pub struct VersionId(pub i64);
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[sqlx(transparent)]
pub struct LoaderId(pub i32);
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize)]
#[sqlx(transparent)]
pub struct CategoryId(pub i32);
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize)]
#[sqlx(transparent)]
pub struct CollectionId(pub i64);
#[derive(Copy, Clone, Debug, Type, Deserialize, Serialize)]
#[sqlx(transparent)]
pub struct ReportId(pub i64);
#[derive(Copy, Clone, Debug, Type)]
#[sqlx(transparent)]
pub struct ReportTypeId(pub i32);
#[derive(Copy, Clone, Debug, Type, Hash, Eq, PartialEq, Deserialize, Serialize)]
#[sqlx(transparent)]
pub struct FileId(pub i64);
#[derive(Copy, Clone, Debug, Type, Deserialize, Serialize, Eq, PartialEq, Hash)]
#[sqlx(transparent)]
pub struct PatId(pub i64);
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize)]
#[sqlx(transparent)]
pub struct NotificationId(pub i64);
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize)]
#[sqlx(transparent)]
pub struct NotificationActionId(pub i32);
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq)]
#[sqlx(transparent)]
pub struct ThreadId(pub i64);
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash)]
#[sqlx(transparent)]
pub struct ThreadMessageId(pub i64);
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash)]
#[sqlx(transparent)]
pub struct SessionId(pub i64);
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash)]
#[sqlx(transparent)]
pub struct ImageId(pub i64);
#[derive(
Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash, PartialOrd, Ord,
)]
#[sqlx(transparent)]
pub struct LoaderFieldId(pub i32);
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash)]
#[sqlx(transparent)]
pub struct LoaderFieldEnumId(pub i32);
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash)]
#[sqlx(transparent)]
pub struct LoaderFieldEnumValueId(pub i32);
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash)]
#[sqlx(transparent)]
pub struct OAuthClientId(pub i64);
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash)]
#[sqlx(transparent)]
pub struct OAuthClientAuthorizationId(pub i64);
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash)]
#[sqlx(transparent)]
pub struct OAuthRedirectUriId(pub i64);
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash)]
#[sqlx(transparent)]
pub struct OAuthAccessTokenId(pub i64);
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash)]
#[sqlx(transparent)]
pub struct PayoutId(pub i64);
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash)]
#[sqlx(transparent)]
pub struct ProductId(pub i64);
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash)]
#[sqlx(transparent)]
pub struct ProductPriceId(pub i64);
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash)]
#[sqlx(transparent)]
pub struct UserSubscriptionId(pub i64);
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash)]
#[sqlx(transparent)]
pub struct ChargeId(pub i64);
use crate::models::ids;
impl From<ids::ProjectId> for ProjectId {
fn from(id: ids::ProjectId) -> Self {
ProjectId(id.0 as i64)
}
}
impl From<ProjectId> for ids::ProjectId {
fn from(id: ProjectId) -> Self {
ids::ProjectId(id.0 as u64)
}
}
impl From<ids::UserId> for UserId {
fn from(id: ids::UserId) -> Self {
UserId(id.0 as i64)
}
}
impl From<UserId> for ids::UserId {
fn from(id: UserId) -> Self {
ids::UserId(id.0 as u64)
}
}
impl From<ids::TeamId> for TeamId {
fn from(id: ids::TeamId) -> Self {
TeamId(id.0 as i64)
}
}
impl From<TeamId> for ids::TeamId {
fn from(id: TeamId) -> Self {
ids::TeamId(id.0 as u64)
}
}
impl From<ids::OrganizationId> for OrganizationId {
fn from(id: ids::OrganizationId) -> Self {
OrganizationId(id.0 as i64)
}
}
impl From<OrganizationId> for ids::OrganizationId {
fn from(id: OrganizationId) -> Self {
ids::OrganizationId(id.0 as u64)
}
}
impl From<ids::VersionId> for VersionId {
fn from(id: ids::VersionId) -> Self {
VersionId(id.0 as i64)
}
}
impl From<VersionId> for ids::VersionId {
fn from(id: VersionId) -> Self {
ids::VersionId(id.0 as u64)
}
}
impl From<ids::CollectionId> for CollectionId {
fn from(id: ids::CollectionId) -> Self {
CollectionId(id.0 as i64)
}
}
impl From<CollectionId> for ids::CollectionId {
fn from(id: CollectionId) -> Self {
ids::CollectionId(id.0 as u64)
}
}
impl From<ids::ReportId> for ReportId {
fn from(id: ids::ReportId) -> Self {
ReportId(id.0 as i64)
}
}
impl From<ReportId> for ids::ReportId {
fn from(id: ReportId) -> Self {
ids::ReportId(id.0 as u64)
}
}
impl From<ImageId> for ids::ImageId {
fn from(id: ImageId) -> Self {
ids::ImageId(id.0 as u64)
}
}
impl From<ids::ImageId> for ImageId {
fn from(id: ids::ImageId) -> Self {
ImageId(id.0 as i64)
}
}
impl From<ids::NotificationId> for NotificationId {
fn from(id: ids::NotificationId) -> Self {
NotificationId(id.0 as i64)
}
}
impl From<NotificationId> for ids::NotificationId {
fn from(id: NotificationId) -> Self {
ids::NotificationId(id.0 as u64)
}
}
impl From<ids::ThreadId> for ThreadId {
fn from(id: ids::ThreadId) -> Self {
ThreadId(id.0 as i64)
}
}
impl From<ThreadId> for ids::ThreadId {
fn from(id: ThreadId) -> Self {
ids::ThreadId(id.0 as u64)
}
}
impl From<ids::ThreadMessageId> for ThreadMessageId {
fn from(id: ids::ThreadMessageId) -> Self {
ThreadMessageId(id.0 as i64)
}
}
impl From<ThreadMessageId> for ids::ThreadMessageId {
fn from(id: ThreadMessageId) -> Self {
ids::ThreadMessageId(id.0 as u64)
}
}
impl From<SessionId> for ids::SessionId {
fn from(id: SessionId) -> Self {
ids::SessionId(id.0 as u64)
}
}
impl From<PatId> for ids::PatId {
fn from(id: PatId) -> Self {
ids::PatId(id.0 as u64)
}
}
impl From<OAuthClientId> for ids::OAuthClientId {
fn from(id: OAuthClientId) -> Self {
ids::OAuthClientId(id.0 as u64)
}
}
impl From<ids::OAuthClientId> for OAuthClientId {
fn from(id: ids::OAuthClientId) -> Self {
Self(id.0 as i64)
}
}
impl From<OAuthRedirectUriId> for ids::OAuthRedirectUriId {
fn from(id: OAuthRedirectUriId) -> Self {
ids::OAuthRedirectUriId(id.0 as u64)
}
}
impl From<OAuthClientAuthorizationId> for ids::OAuthClientAuthorizationId {
fn from(id: OAuthClientAuthorizationId) -> Self {
ids::OAuthClientAuthorizationId(id.0 as u64)
}
}
impl From<ids::PayoutId> for PayoutId {
fn from(id: ids::PayoutId) -> Self {
PayoutId(id.0 as i64)
}
}
impl From<PayoutId> for ids::PayoutId {
fn from(id: PayoutId) -> Self {
ids::PayoutId(id.0 as u64)
}
}
impl From<ids::ProductId> for ProductId {
fn from(id: ids::ProductId) -> Self {
ProductId(id.0 as i64)
}
}
impl From<ProductId> for ids::ProductId {
fn from(id: ProductId) -> Self {
ids::ProductId(id.0 as u64)
}
}
impl From<ids::ProductPriceId> for ProductPriceId {
fn from(id: ids::ProductPriceId) -> Self {
ProductPriceId(id.0 as i64)
}
}
impl From<ProductPriceId> for ids::ProductPriceId {
fn from(id: ProductPriceId) -> Self {
ids::ProductPriceId(id.0 as u64)
}
}
impl From<ids::UserSubscriptionId> for UserSubscriptionId {
fn from(id: ids::UserSubscriptionId) -> Self {
UserSubscriptionId(id.0 as i64)
}
}
impl From<UserSubscriptionId> for ids::UserSubscriptionId {
fn from(id: UserSubscriptionId) -> Self {
ids::UserSubscriptionId(id.0 as u64)
}
}
impl From<ids::ChargeId> for ChargeId {
fn from(id: ids::ChargeId) -> Self {
ChargeId(id.0 as i64)
}
}
impl From<ChargeId> for ids::ChargeId {
fn from(id: ChargeId) -> Self {
ids::ChargeId(id.0 as u64)
}
}

View File

@@ -0,0 +1,232 @@
use super::ids::*;
use crate::database::redis::RedisPool;
use crate::{database::models::DatabaseError, models::images::ImageContext};
use chrono::{DateTime, Utc};
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
const IMAGES_NAMESPACE: &str = "images";
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Image {
pub id: ImageId,
pub url: String,
pub raw_url: String,
pub size: u64,
pub created: DateTime<Utc>,
pub owner_id: UserId,
// context it is associated with
pub context: String,
pub project_id: Option<ProjectId>,
pub version_id: Option<VersionId>,
pub thread_message_id: Option<ThreadMessageId>,
pub report_id: Option<ReportId>,
}
impl Image {
pub async fn insert(
&self,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<(), DatabaseError> {
sqlx::query!(
"
INSERT INTO uploaded_images (
id, url, raw_url, size, created, owner_id, context, mod_id, version_id, thread_message_id, report_id
)
VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
);
",
self.id as ImageId,
self.url,
self.raw_url,
self.size as i64,
self.created,
self.owner_id as UserId,
self.context,
self.project_id.map(|x| x.0),
self.version_id.map(|x| x.0),
self.thread_message_id.map(|x| x.0),
self.report_id.map(|x| x.0),
)
.execute(&mut **transaction)
.await?;
Ok(())
}
pub async fn remove(
id: ImageId,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
redis: &RedisPool,
) -> Result<Option<()>, DatabaseError> {
let image = Self::get(id, &mut **transaction, redis).await?;
if let Some(image) = image {
sqlx::query!(
"
DELETE FROM uploaded_images
WHERE id = $1
",
id as ImageId,
)
.execute(&mut **transaction)
.await?;
Image::clear_cache(image.id, redis).await?;
Ok(Some(()))
} else {
Ok(None)
}
}
pub async fn get_many_contexted(
context: ImageContext,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Vec<Image>, sqlx::Error> {
// Set all of project_id, version_id, thread_message_id, report_id to None
// Then set the one that is relevant to Some
let mut project_id = None;
let mut version_id = None;
let mut thread_message_id = None;
let mut report_id = None;
match context {
ImageContext::Project {
project_id: Some(id),
} => {
project_id = Some(ProjectId::from(id));
}
ImageContext::Version {
version_id: Some(id),
} => {
version_id = Some(VersionId::from(id));
}
ImageContext::ThreadMessage {
thread_message_id: Some(id),
} => {
thread_message_id = Some(ThreadMessageId::from(id));
}
ImageContext::Report {
report_id: Some(id),
} => {
report_id = Some(ReportId::from(id));
}
_ => {}
}
use futures::stream::TryStreamExt;
sqlx::query!(
"
SELECT id, url, raw_url, size, created, owner_id, context, mod_id, version_id, thread_message_id, report_id
FROM uploaded_images
WHERE context = $1
AND (mod_id = $2 OR ($2 IS NULL AND mod_id IS NULL))
AND (version_id = $3 OR ($3 IS NULL AND version_id IS NULL))
AND (thread_message_id = $4 OR ($4 IS NULL AND thread_message_id IS NULL))
AND (report_id = $5 OR ($5 IS NULL AND report_id IS NULL))
GROUP BY id
",
context.context_as_str(),
project_id.map(|x| x.0),
version_id.map(|x| x.0),
thread_message_id.map(|x| x.0),
report_id.map(|x| x.0),
)
.fetch(&mut **transaction)
.map_ok(|row| {
let id = ImageId(row.id);
Image {
id,
url: row.url,
raw_url: row.raw_url,
size: row.size as u64,
created: row.created,
owner_id: UserId(row.owner_id),
context: row.context,
project_id: row.mod_id.map(ProjectId),
version_id: row.version_id.map(VersionId),
thread_message_id: row.thread_message_id.map(ThreadMessageId),
report_id: row.report_id.map(ReportId),
}
})
.try_collect::<Vec<Image>>()
.await
}
pub async fn get<'a, 'b, E>(
id: ImageId,
executor: E,
redis: &RedisPool,
) -> Result<Option<Image>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
Image::get_many(&[id], executor, redis)
.await
.map(|x| x.into_iter().next())
}
pub async fn get_many<'a, E>(
image_ids: &[ImageId],
exec: E,
redis: &RedisPool,
) -> Result<Vec<Image>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
use futures::TryStreamExt;
let val = redis.get_cached_keys(
IMAGES_NAMESPACE,
&image_ids.iter().map(|x| x.0).collect::<Vec<_>>(),
|image_ids| async move {
let images = sqlx::query!(
"
SELECT id, url, raw_url, size, created, owner_id, context, mod_id, version_id, thread_message_id, report_id
FROM uploaded_images
WHERE id = ANY($1)
GROUP BY id;
",
&image_ids,
)
.fetch(exec)
.try_fold(DashMap::new(), |acc, i| {
let img = Image {
id: ImageId(i.id),
url: i.url,
raw_url: i.raw_url,
size: i.size as u64,
created: i.created,
owner_id: UserId(i.owner_id),
context: i.context,
project_id: i.mod_id.map(ProjectId),
version_id: i.version_id.map(VersionId),
thread_message_id: i.thread_message_id.map(ThreadMessageId),
report_id: i.report_id.map(ReportId),
};
acc.insert(i.id, img);
async move { Ok(acc) }
})
.await?;
Ok(images)
},
).await?;
Ok(val)
}
pub async fn clear_cache(id: ImageId, redis: &RedisPool) -> Result<(), DatabaseError> {
let mut redis = redis.connect().await?;
redis.delete(IMAGES_NAMESPACE, id.0).await?;
Ok(())
}
}

View File

@@ -0,0 +1,220 @@
// In V3, we switched to dynamic loader fields for a better support for more loaders, games, and potential metadata.
// This file contains the legacy loader fields, which are still used by V2 projects.
// They are still useful to have in several places where minecraft-java functionality is hardcoded- for example,
// for fetching data from forge, maven, etc.
// These fields only apply to minecraft-java, and are hardcoded to the minecraft-java game.
use chrono::{DateTime, Utc};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use serde_json::json;
use crate::database::redis::RedisPool;
use super::{
loader_fields::{LoaderFieldEnum, LoaderFieldEnumValue, VersionField, VersionFieldValue},
DatabaseError, LoaderFieldEnumValueId,
};
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct MinecraftGameVersion {
pub id: LoaderFieldEnumValueId,
pub version: String,
#[serde(rename = "type")]
pub type_: String,
pub created: DateTime<Utc>,
pub major: bool,
}
impl MinecraftGameVersion {
// The name under which this legacy field is stored as a LoaderField
pub const FIELD_NAME: &'static str = "game_versions";
pub fn builder() -> MinecraftGameVersionBuilder<'static> {
MinecraftGameVersionBuilder::default()
}
pub async fn list<'a, E>(
version_type_option: Option<&str>,
major_option: Option<bool>,
exec: E,
redis: &RedisPool,
) -> Result<Vec<MinecraftGameVersion>, DatabaseError>
where
E: sqlx::Acquire<'a, Database = sqlx::Postgres>,
{
let mut exec = exec.acquire().await?;
let game_version_enum = LoaderFieldEnum::get(Self::FIELD_NAME, &mut *exec, redis)
.await?
.ok_or_else(|| {
DatabaseError::SchemaError("Could not find game version enum.".to_string())
})?;
let game_version_enum_values =
LoaderFieldEnumValue::list(game_version_enum.id, &mut *exec, redis).await?;
let game_versions = game_version_enum_values
.into_iter()
.map(MinecraftGameVersion::from_enum_value)
.filter(|x| {
let mut bool = true;
if let Some(version_type) = version_type_option {
bool &= &*x.type_ == version_type;
}
if let Some(major) = major_option {
bool &= x.major == major;
}
bool
})
.collect_vec();
Ok(game_versions)
}
// Tries to create a MinecraftGameVersion from a VersionField
// Clones on success
pub fn try_from_version_field(
version_field: &VersionField,
) -> Result<Vec<Self>, DatabaseError> {
if version_field.field_name != Self::FIELD_NAME {
return Err(DatabaseError::SchemaError(format!(
"Field name {} is not {}",
version_field.field_name,
Self::FIELD_NAME
)));
}
let game_versions = match version_field.clone() {
VersionField {
value: VersionFieldValue::ArrayEnum(_, values),
..
} => values.into_iter().map(Self::from_enum_value).collect(),
VersionField {
value: VersionFieldValue::Enum(_, value),
..
} => {
vec![Self::from_enum_value(value)]
}
_ => {
return Err(DatabaseError::SchemaError(format!(
"Game version requires field value to be an enum: {:?}",
version_field
)));
}
};
Ok(game_versions)
}
pub fn from_enum_value(loader_field_enum_value: LoaderFieldEnumValue) -> MinecraftGameVersion {
MinecraftGameVersion {
id: loader_field_enum_value.id,
version: loader_field_enum_value.value,
created: loader_field_enum_value.created,
type_: loader_field_enum_value
.metadata
.get("type")
.and_then(|x| x.as_str())
.map(|x| x.to_string())
.unwrap_or_default(),
major: loader_field_enum_value
.metadata
.get("major")
.and_then(|x| x.as_bool())
.unwrap_or_default(),
}
}
}
#[derive(Default)]
pub struct MinecraftGameVersionBuilder<'a> {
pub version: Option<&'a str>,
pub version_type: Option<&'a str>,
pub date: Option<&'a DateTime<Utc>>,
}
impl<'a> MinecraftGameVersionBuilder<'a> {
pub fn new() -> Self {
Self::default()
}
/// The game version. Spaces must be replaced with '_' for it to be valid
pub fn version(
self,
version: &'a str,
) -> Result<MinecraftGameVersionBuilder<'a>, DatabaseError> {
Ok(Self {
version: Some(version),
..self
})
}
pub fn version_type(
self,
version_type: &'a str,
) -> Result<MinecraftGameVersionBuilder<'a>, DatabaseError> {
Ok(Self {
version_type: Some(version_type),
..self
})
}
pub fn created(self, created: &'a DateTime<Utc>) -> MinecraftGameVersionBuilder<'a> {
Self {
date: Some(created),
..self
}
}
pub async fn insert<'b, E>(
self,
exec: E,
redis: &RedisPool,
) -> Result<LoaderFieldEnumValueId, DatabaseError>
where
E: sqlx::Executor<'b, Database = sqlx::Postgres> + Copy,
{
let game_versions_enum = LoaderFieldEnum::get("game_versions", exec, redis)
.await?
.ok_or(DatabaseError::SchemaError(
"Missing loaders field: 'game_versions'".to_string(),
))?;
// Get enum id for game versions
let metadata = json!({
"type": self.version_type,
"major": false
});
// This looks like a mess, but it *should* work
// This allows game versions to be partially updated without
// replacing the unspecified fields with defaults.
let result = sqlx::query!(
"
INSERT INTO loader_field_enum_values (enum_id, value, created, metadata)
VALUES ($1, $2, COALESCE($3, timezone('utc', now())), $4)
ON CONFLICT (enum_id, value) DO UPDATE
SET metadata = jsonb_set(
COALESCE(loader_field_enum_values.metadata, $4),
'{type}',
COALESCE($4->'type', loader_field_enum_values.metadata->'type')
),
created = COALESCE($3, loader_field_enum_values.created)
RETURNING id
",
game_versions_enum.id.0,
self.version,
self.date.map(chrono::DateTime::naive_utc),
metadata
)
.fetch_one(exec)
.await?;
let mut conn = redis.connect().await?;
conn.delete(
crate::database::models::loader_fields::LOADER_FIELD_ENUM_VALUES_NAMESPACE,
game_versions_enum.id.0,
)
.await?;
Ok(LoaderFieldEnumValueId(result.id))
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,56 @@
use thiserror::Error;
pub mod categories;
pub mod charge_item;
pub mod collection_item;
pub mod flow_item;
pub mod ids;
pub mod image_item;
pub mod legacy_loader_fields;
pub mod loader_fields;
pub mod notification_item;
pub mod oauth_client_authorization_item;
pub mod oauth_client_item;
pub mod oauth_token_item;
pub mod organization_item;
pub mod pat_item;
pub mod payout_item;
pub mod product_item;
pub mod project_item;
pub mod report_item;
pub mod session_item;
pub mod team_item;
pub mod thread_item;
pub mod user_item;
pub mod user_subscription_item;
pub mod version_item;
pub use collection_item::Collection;
pub use ids::*;
pub use image_item::Image;
pub use oauth_client_item::OAuthClient;
pub use organization_item::Organization;
pub use project_item::Project;
pub use team_item::Team;
pub use team_item::TeamMember;
pub use thread_item::{Thread, ThreadMessage};
pub use user_item::User;
pub use version_item::Version;
#[derive(Error, Debug)]
pub enum DatabaseError {
#[error("Error while interacting with the database: {0}")]
Database(#[from] sqlx::Error),
#[error("Error while trying to generate random ID")]
RandomId,
#[error("Error while interacting with the cache: {0}")]
CacheError(#[from] redis::RedisError),
#[error("Redis Pool Error: {0}")]
RedisPool(#[from] deadpool_redis::PoolError),
#[error("Error while serializing with the cache: {0}")]
SerdeCacheError(#[from] serde_json::Error),
#[error("Schema error: {0}")]
SchemaError(String),
#[error("Timeout when waiting for cache subscriber")]
CacheTimeout,
}

View File

@@ -0,0 +1,310 @@
use super::ids::*;
use crate::database::{models::DatabaseError, redis::RedisPool};
use crate::models::notifications::NotificationBody;
use chrono::{DateTime, Utc};
use futures::TryStreamExt;
use serde::{Deserialize, Serialize};
const USER_NOTIFICATIONS_NAMESPACE: &str = "user_notifications";
pub struct NotificationBuilder {
pub body: NotificationBody,
}
#[derive(Serialize, Deserialize)]
pub struct Notification {
pub id: NotificationId,
pub user_id: UserId,
pub body: NotificationBody,
pub read: bool,
pub created: DateTime<Utc>,
}
#[derive(Serialize, Deserialize)]
pub struct NotificationAction {
pub id: NotificationActionId,
pub notification_id: NotificationId,
pub name: String,
pub action_route_method: String,
pub action_route: String,
}
impl NotificationBuilder {
pub async fn insert(
&self,
user: UserId,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
redis: &RedisPool,
) -> Result<(), DatabaseError> {
self.insert_many(vec![user], transaction, redis).await
}
pub async fn insert_many(
&self,
users: Vec<UserId>,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
redis: &RedisPool,
) -> Result<(), DatabaseError> {
let notification_ids =
generate_many_notification_ids(users.len(), &mut *transaction).await?;
let body = serde_json::value::to_value(&self.body)?;
let bodies = notification_ids
.iter()
.map(|_| body.clone())
.collect::<Vec<_>>();
sqlx::query!(
"
INSERT INTO notifications (
id, user_id, body
)
SELECT * FROM UNNEST($1::bigint[], $2::bigint[], $3::jsonb[])
",
&notification_ids
.into_iter()
.map(|x| x.0)
.collect::<Vec<_>>()[..],
&users.iter().map(|x| x.0).collect::<Vec<_>>()[..],
&bodies[..],
)
.execute(&mut **transaction)
.await?;
Notification::clear_user_notifications_cache(&users, redis).await?;
Ok(())
}
}
impl Notification {
pub async fn get<'a, 'b, E>(
id: NotificationId,
executor: E,
) -> Result<Option<Self>, sqlx::error::Error>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{
Self::get_many(&[id], executor)
.await
.map(|x| x.into_iter().next())
}
pub async fn get_many<'a, E>(
notification_ids: &[NotificationId],
exec: E,
) -> Result<Vec<Notification>, sqlx::Error>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{
let notification_ids_parsed: Vec<i64> = notification_ids.iter().map(|x| x.0).collect();
sqlx::query!(
"
SELECT n.id, n.user_id, n.name, n.text, n.link, n.created, n.read, n.type notification_type, n.body,
JSONB_AGG(DISTINCT jsonb_build_object('id', na.id, 'notification_id', na.notification_id, 'name', na.name, 'action_route_method', na.action_route_method, 'action_route', na.action_route)) filter (where na.id is not null) actions
FROM notifications n
LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id
WHERE n.id = ANY($1)
GROUP BY n.id, n.user_id
ORDER BY n.created DESC;
",
&notification_ids_parsed
)
.fetch(exec)
.map_ok(|row| {
let id = NotificationId(row.id);
Notification {
id,
user_id: UserId(row.user_id),
read: row.read,
created: row.created,
body: row.body.clone().and_then(|x| serde_json::from_value(x).ok()).unwrap_or_else(|| {
if let Some(name) = row.name {
NotificationBody::LegacyMarkdown {
notification_type: row.notification_type,
name,
text: row.text.unwrap_or_default(),
link: row.link.unwrap_or_default(),
actions: serde_json::from_value(
row.actions.unwrap_or_default(),
)
.ok()
.unwrap_or_default(),
}
} else {
NotificationBody::Unknown
}
}),
}
})
.try_collect::<Vec<Notification>>()
.await
}
pub async fn get_many_user<'a, E>(
user_id: UserId,
exec: E,
redis: &RedisPool,
) -> Result<Vec<Notification>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{
let mut redis = redis.connect().await?;
let cached_notifications: Option<Vec<Notification>> = redis
.get_deserialized_from_json(USER_NOTIFICATIONS_NAMESPACE, &user_id.0.to_string())
.await?;
if let Some(notifications) = cached_notifications {
return Ok(notifications);
}
let db_notifications = sqlx::query!(
"
SELECT n.id, n.user_id, n.name, n.text, n.link, n.created, n.read, n.type notification_type, n.body,
JSONB_AGG(DISTINCT jsonb_build_object('id', na.id, 'notification_id', na.notification_id, 'name', na.name, 'action_route_method', na.action_route_method, 'action_route', na.action_route)) filter (where na.id is not null) actions
FROM notifications n
LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id
WHERE n.user_id = $1
GROUP BY n.id, n.user_id;
",
user_id as UserId
)
.fetch(exec)
.map_ok(|row| {
let id = NotificationId(row.id);
Notification {
id,
user_id: UserId(row.user_id),
read: row.read,
created: row.created,
body: row.body.clone().and_then(|x| serde_json::from_value(x).ok()).unwrap_or_else(|| {
if let Some(name) = row.name {
NotificationBody::LegacyMarkdown {
notification_type: row.notification_type,
name,
text: row.text.unwrap_or_default(),
link: row.link.unwrap_or_default(),
actions: serde_json::from_value(
row.actions.unwrap_or_default(),
)
.ok()
.unwrap_or_default(),
}
} else {
NotificationBody::Unknown
}
}),
}
})
.try_collect::<Vec<Notification>>()
.await?;
redis
.set_serialized_to_json(
USER_NOTIFICATIONS_NAMESPACE,
user_id.0,
&db_notifications,
None,
)
.await?;
Ok(db_notifications)
}
pub async fn read(
id: NotificationId,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
redis: &RedisPool,
) -> Result<Option<()>, DatabaseError> {
Self::read_many(&[id], transaction, redis).await
}
pub async fn read_many(
notification_ids: &[NotificationId],
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
redis: &RedisPool,
) -> Result<Option<()>, DatabaseError> {
let notification_ids_parsed: Vec<i64> = notification_ids.iter().map(|x| x.0).collect();
let affected_users = sqlx::query!(
"
UPDATE notifications
SET read = TRUE
WHERE id = ANY($1)
RETURNING user_id
",
&notification_ids_parsed
)
.fetch(&mut **transaction)
.map_ok(|x| UserId(x.user_id))
.try_collect::<Vec<_>>()
.await?;
Notification::clear_user_notifications_cache(affected_users.iter(), redis).await?;
Ok(Some(()))
}
pub async fn remove(
id: NotificationId,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
redis: &RedisPool,
) -> Result<Option<()>, DatabaseError> {
Self::remove_many(&[id], transaction, redis).await
}
pub async fn remove_many(
notification_ids: &[NotificationId],
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
redis: &RedisPool,
) -> Result<Option<()>, DatabaseError> {
let notification_ids_parsed: Vec<i64> = notification_ids.iter().map(|x| x.0).collect();
sqlx::query!(
"
DELETE FROM notifications_actions
WHERE notification_id = ANY($1)
",
&notification_ids_parsed
)
.execute(&mut **transaction)
.await?;
let affected_users = sqlx::query!(
"
DELETE FROM notifications
WHERE id = ANY($1)
RETURNING user_id
",
&notification_ids_parsed
)
.fetch(&mut **transaction)
.map_ok(|x| UserId(x.user_id))
.try_collect::<Vec<_>>()
.await?;
Notification::clear_user_notifications_cache(affected_users.iter(), redis).await?;
Ok(Some(()))
}
pub async fn clear_user_notifications_cache(
user_ids: impl IntoIterator<Item = &UserId>,
redis: &RedisPool,
) -> Result<(), DatabaseError> {
let mut redis = redis.connect().await?;
redis
.delete_many(
user_ids
.into_iter()
.map(|id| (USER_NOTIFICATIONS_NAMESPACE, Some(id.0.to_string()))),
)
.await?;
Ok(())
}
}

View File

@@ -0,0 +1,126 @@
use chrono::{DateTime, Utc};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use crate::models::pats::Scopes;
use super::{DatabaseError, OAuthClientAuthorizationId, OAuthClientId, UserId};
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct OAuthClientAuthorization {
pub id: OAuthClientAuthorizationId,
pub client_id: OAuthClientId,
pub user_id: UserId,
pub scopes: Scopes,
pub created: DateTime<Utc>,
}
struct AuthorizationQueryResult {
id: i64,
client_id: i64,
user_id: i64,
scopes: i64,
created: DateTime<Utc>,
}
impl From<AuthorizationQueryResult> for OAuthClientAuthorization {
fn from(value: AuthorizationQueryResult) -> Self {
OAuthClientAuthorization {
id: OAuthClientAuthorizationId(value.id),
client_id: OAuthClientId(value.client_id),
user_id: UserId(value.user_id),
scopes: Scopes::from_postgres(value.scopes),
created: value.created,
}
}
}
impl OAuthClientAuthorization {
pub async fn get(
client_id: OAuthClientId,
user_id: UserId,
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<Option<OAuthClientAuthorization>, DatabaseError> {
let value = sqlx::query_as!(
AuthorizationQueryResult,
"
SELECT id, client_id, user_id, scopes, created
FROM oauth_client_authorizations
WHERE client_id=$1 AND user_id=$2
",
client_id.0,
user_id.0,
)
.fetch_optional(exec)
.await?;
Ok(value.map(|r| r.into()))
}
pub async fn get_all_for_user(
user_id: UserId,
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<Vec<OAuthClientAuthorization>, DatabaseError> {
let results = sqlx::query_as!(
AuthorizationQueryResult,
"
SELECT id, client_id, user_id, scopes, created
FROM oauth_client_authorizations
WHERE user_id=$1
",
user_id.0
)
.fetch_all(exec)
.await?;
Ok(results.into_iter().map(|r| r.into()).collect_vec())
}
pub async fn upsert(
id: OAuthClientAuthorizationId,
client_id: OAuthClientId,
user_id: UserId,
scopes: Scopes,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<(), DatabaseError> {
sqlx::query!(
"
INSERT INTO oauth_client_authorizations (
id, client_id, user_id, scopes
)
VALUES (
$1, $2, $3, $4
)
ON CONFLICT (id)
DO UPDATE SET scopes = EXCLUDED.scopes
",
id.0,
client_id.0,
user_id.0,
scopes.bits() as i64,
)
.execute(&mut **transaction)
.await?;
Ok(())
}
pub async fn remove(
client_id: OAuthClientId,
user_id: UserId,
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<(), DatabaseError> {
sqlx::query!(
"
DELETE FROM oauth_client_authorizations
WHERE client_id=$1 AND user_id=$2
",
client_id.0,
user_id.0
)
.execute(exec)
.await?;
Ok(())
}
}

View File

@@ -0,0 +1,261 @@
use chrono::{DateTime, Utc};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use sha2::Digest;
use super::{DatabaseError, OAuthClientId, OAuthRedirectUriId, UserId};
use crate::models::pats::Scopes;
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct OAuthRedirectUri {
pub id: OAuthRedirectUriId,
pub client_id: OAuthClientId,
pub uri: String,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct OAuthClient {
pub id: OAuthClientId,
pub name: String,
pub icon_url: Option<String>,
pub raw_icon_url: Option<String>,
pub max_scopes: Scopes,
pub secret_hash: String,
pub redirect_uris: Vec<OAuthRedirectUri>,
pub created: DateTime<Utc>,
pub created_by: UserId,
pub url: Option<String>,
pub description: Option<String>,
}
struct ClientQueryResult {
id: i64,
name: String,
icon_url: Option<String>,
raw_icon_url: Option<String>,
max_scopes: i64,
secret_hash: String,
created: DateTime<Utc>,
created_by: i64,
url: Option<String>,
description: Option<String>,
uri_ids: Option<Vec<i64>>,
uri_vals: Option<Vec<String>>,
}
macro_rules! select_clients_with_predicate {
($predicate:tt, $param:ident) => {
// The columns in this query have nullability type hints, because for some reason
// the combination of the JOIN and filter using ANY makes sqlx think all columns are nullable
// https://docs.rs/sqlx/latest/sqlx/macro.query.html#force-nullable
sqlx::query_as!(
ClientQueryResult,
r#"
SELECT
clients.id as "id!",
clients.name as "name!",
clients.icon_url as "icon_url?",
clients.raw_icon_url as "raw_icon_url?",
clients.max_scopes as "max_scopes!",
clients.secret_hash as "secret_hash!",
clients.created as "created!",
clients.created_by as "created_by!",
clients.url as "url?",
clients.description as "description?",
uris.uri_ids as "uri_ids?",
uris.uri_vals as "uri_vals?"
FROM oauth_clients clients
LEFT JOIN (
SELECT client_id, array_agg(id) as uri_ids, array_agg(uri) as uri_vals
FROM oauth_client_redirect_uris
GROUP BY client_id
) uris ON clients.id = uris.client_id
"#
+ $predicate,
$param
)
};
}
impl OAuthClient {
pub async fn get(
id: OAuthClientId,
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<Option<OAuthClient>, DatabaseError> {
Ok(Self::get_many(&[id], exec).await?.into_iter().next())
}
pub async fn get_many(
ids: &[OAuthClientId],
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<Vec<OAuthClient>, DatabaseError> {
let ids = ids.iter().map(|id| id.0).collect_vec();
let ids_ref: &[i64] = &ids;
let results =
select_clients_with_predicate!("WHERE clients.id = ANY($1::bigint[])", ids_ref)
.fetch_all(exec)
.await?;
Ok(results.into_iter().map(|r| r.into()).collect_vec())
}
pub async fn get_all_user_clients(
user_id: UserId,
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<Vec<OAuthClient>, DatabaseError> {
let user_id_param = user_id.0;
let clients = select_clients_with_predicate!("WHERE created_by = $1", user_id_param)
.fetch_all(exec)
.await?;
Ok(clients.into_iter().map(|r| r.into()).collect())
}
pub async fn remove(
id: OAuthClientId,
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<(), DatabaseError> {
// Cascades to oauth_client_redirect_uris, oauth_client_authorizations
sqlx::query!(
"
DELETE FROM oauth_clients
WHERE id = $1
",
id.0
)
.execute(exec)
.await?;
Ok(())
}
pub async fn insert(
&self,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<(), DatabaseError> {
sqlx::query!(
"
INSERT INTO oauth_clients (
id, name, icon_url, raw_icon_url, max_scopes, secret_hash, created_by
)
VALUES (
$1, $2, $3, $4, $5, $6, $7
)
",
self.id.0,
self.name,
self.icon_url,
self.raw_icon_url,
self.max_scopes.to_postgres(),
self.secret_hash,
self.created_by.0
)
.execute(&mut **transaction)
.await?;
Self::insert_redirect_uris(&self.redirect_uris, &mut **transaction).await?;
Ok(())
}
pub async fn update_editable_fields(
&self,
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<(), DatabaseError> {
sqlx::query!(
"
UPDATE oauth_clients
SET name = $1, icon_url = $2, raw_icon_url = $3, max_scopes = $4, url = $5, description = $6
WHERE (id = $7)
",
self.name,
self.icon_url,
self.raw_icon_url,
self.max_scopes.to_postgres(),
self.url,
self.description,
self.id.0,
)
.execute(exec)
.await?;
Ok(())
}
pub async fn remove_redirect_uris(
ids: impl IntoIterator<Item = OAuthRedirectUriId>,
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<(), DatabaseError> {
let ids = ids.into_iter().map(|id| id.0).collect_vec();
sqlx::query!(
"
DELETE FROM oauth_client_redirect_uris
WHERE id IN
(SELECT * FROM UNNEST($1::bigint[]))
",
&ids[..]
)
.execute(exec)
.await?;
Ok(())
}
pub async fn insert_redirect_uris(
uris: &[OAuthRedirectUri],
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<(), DatabaseError> {
let (ids, client_ids, uris): (Vec<_>, Vec<_>, Vec<_>) = uris
.iter()
.map(|r| (r.id.0, r.client_id.0, r.uri.clone()))
.multiunzip();
sqlx::query!(
"
INSERT INTO oauth_client_redirect_uris (id, client_id, uri)
SELECT * FROM UNNEST($1::bigint[], $2::bigint[], $3::varchar[])
",
&ids[..],
&client_ids[..],
&uris[..],
)
.execute(exec)
.await?;
Ok(())
}
pub fn hash_secret(secret: &str) -> String {
format!("{:x}", sha2::Sha512::digest(secret.as_bytes()))
}
}
impl From<ClientQueryResult> for OAuthClient {
fn from(r: ClientQueryResult) -> Self {
let redirects = if let (Some(ids), Some(uris)) = (r.uri_ids.as_ref(), r.uri_vals.as_ref()) {
ids.iter()
.zip(uris.iter())
.map(|(id, uri)| OAuthRedirectUri {
id: OAuthRedirectUriId(*id),
client_id: OAuthClientId(r.id),
uri: uri.to_string(),
})
.collect()
} else {
vec![]
};
OAuthClient {
id: OAuthClientId(r.id),
name: r.name,
icon_url: r.icon_url,
raw_icon_url: r.raw_icon_url,
max_scopes: Scopes::from_postgres(r.max_scopes),
secret_hash: r.secret_hash,
redirect_uris: redirects,
created: r.created,
created_by: UserId(r.created_by),
url: r.url,
description: r.description,
}
}
}

View File

@@ -0,0 +1,95 @@
use super::{DatabaseError, OAuthAccessTokenId, OAuthClientAuthorizationId, OAuthClientId, UserId};
use crate::models::pats::Scopes;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sha2::Digest;
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct OAuthAccessToken {
pub id: OAuthAccessTokenId,
pub authorization_id: OAuthClientAuthorizationId,
pub token_hash: String,
pub scopes: Scopes,
pub created: DateTime<Utc>,
pub expires: DateTime<Utc>,
pub last_used: Option<DateTime<Utc>>,
// Stored separately inside oauth_client_authorizations table
pub client_id: OAuthClientId,
pub user_id: UserId,
}
impl OAuthAccessToken {
pub async fn get(
token_hash: String,
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<Option<OAuthAccessToken>, DatabaseError> {
let value = sqlx::query!(
"
SELECT
tokens.id,
tokens.authorization_id,
tokens.token_hash,
tokens.scopes,
tokens.created,
tokens.expires,
tokens.last_used,
auths.client_id,
auths.user_id
FROM oauth_access_tokens tokens
JOIN oauth_client_authorizations auths
ON tokens.authorization_id = auths.id
WHERE tokens.token_hash = $1
",
token_hash
)
.fetch_optional(exec)
.await?;
Ok(value.map(|r| OAuthAccessToken {
id: OAuthAccessTokenId(r.id),
authorization_id: OAuthClientAuthorizationId(r.authorization_id),
token_hash: r.token_hash,
scopes: Scopes::from_postgres(r.scopes),
created: r.created,
expires: r.expires,
last_used: r.last_used,
client_id: OAuthClientId(r.client_id),
user_id: UserId(r.user_id),
}))
}
/// Inserts and returns the time until the token expires
pub async fn insert(
&self,
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<chrono::Duration, DatabaseError> {
let r = sqlx::query!(
"
INSERT INTO oauth_access_tokens (
id, authorization_id, token_hash, scopes, last_used
)
VALUES (
$1, $2, $3, $4, $5
)
RETURNING created, expires
",
self.id.0,
self.authorization_id.0,
self.token_hash,
self.scopes.to_postgres(),
Option::<DateTime<Utc>>::None
)
.fetch_one(exec)
.await?;
let (created, expires) = (r.created, r.expires);
let time_until_expiration = expires - created;
Ok(time_until_expiration)
}
pub fn hash_token(token: &str) -> String {
format!("{:x}", sha2::Sha512::digest(token.as_bytes()))
}
}

View File

@@ -0,0 +1,265 @@
use crate::{database::redis::RedisPool, models::ids::base62_impl::parse_base62};
use dashmap::DashMap;
use futures::TryStreamExt;
use std::fmt::{Debug, Display};
use std::hash::Hash;
use super::{ids::*, TeamMember};
use serde::{Deserialize, Serialize};
const ORGANIZATIONS_NAMESPACE: &str = "organizations";
const ORGANIZATIONS_TITLES_NAMESPACE: &str = "organizations_titles";
#[derive(Deserialize, Serialize, Clone, Debug)]
/// An organization of users who together control one or more projects and organizations.
pub struct Organization {
/// The id of the organization
pub id: OrganizationId,
/// The slug of the organization
pub slug: String,
/// The title of the organization
pub name: String,
/// The associated team of the organization
pub team_id: TeamId,
/// The description of the organization
pub description: String,
/// The display icon for the organization
pub icon_url: Option<String>,
pub raw_icon_url: Option<String>,
pub color: Option<u32>,
}
impl Organization {
pub async fn insert(
self,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<(), super::DatabaseError> {
sqlx::query!(
"
INSERT INTO organizations (id, slug, name, team_id, description, icon_url, raw_icon_url, color)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
",
self.id.0,
self.slug,
self.name,
self.team_id as TeamId,
self.description,
self.icon_url,
self.raw_icon_url,
self.color.map(|x| x as i32),
)
.execute(&mut **transaction)
.await?;
Ok(())
}
pub async fn get<'a, E>(
string: &str,
exec: E,
redis: &RedisPool,
) -> Result<Option<Self>, super::DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
Self::get_many(&[string], exec, redis)
.await
.map(|x| x.into_iter().next())
}
pub async fn get_id<'a, 'b, E>(
id: OrganizationId,
exec: E,
redis: &RedisPool,
) -> Result<Option<Self>, super::DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
Self::get_many_ids(&[id], exec, redis)
.await
.map(|x| x.into_iter().next())
}
pub async fn get_many_ids<'a, 'b, E>(
organization_ids: &[OrganizationId],
exec: E,
redis: &RedisPool,
) -> Result<Vec<Self>, super::DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let ids = organization_ids
.iter()
.map(|x| crate::models::ids::OrganizationId::from(*x))
.collect::<Vec<_>>();
Self::get_many(&ids, exec, redis).await
}
pub async fn get_many<'a, E, T: Display + Hash + Eq + PartialEq + Clone + Debug>(
organization_strings: &[T],
exec: E,
redis: &RedisPool,
) -> Result<Vec<Self>, super::DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let val = redis
.get_cached_keys_with_slug(
ORGANIZATIONS_NAMESPACE,
ORGANIZATIONS_TITLES_NAMESPACE,
false,
organization_strings,
|ids| async move {
let org_ids: Vec<i64> = ids
.iter()
.flat_map(|x| parse_base62(&x.to_string()).ok())
.map(|x| x as i64)
.collect();
let slugs = ids
.into_iter()
.map(|x| x.to_string().to_lowercase())
.collect::<Vec<_>>();
let organizations = sqlx::query!(
"
SELECT o.id, o.slug, o.name, o.team_id, o.description, o.icon_url, o.raw_icon_url, o.color
FROM organizations o
WHERE o.id = ANY($1) OR LOWER(o.slug) = ANY($2)
GROUP BY o.id;
",
&org_ids,
&slugs,
)
.fetch(exec)
.try_fold(DashMap::new(), |acc, m| {
let org = Organization {
id: OrganizationId(m.id),
slug: m.slug.clone(),
name: m.name,
team_id: TeamId(m.team_id),
description: m.description,
icon_url: m.icon_url,
raw_icon_url: m.raw_icon_url,
color: m.color.map(|x| x as u32),
};
acc.insert(m.id, (Some(m.slug), org));
async move { Ok(acc) }
})
.await?;
Ok(organizations)
},
)
.await?;
Ok(val)
}
// Gets organization associated with a project ID, if it exists and there is one
pub async fn get_associated_organization_project_id<'a, 'b, E>(
project_id: ProjectId,
exec: E,
) -> Result<Option<Self>, super::DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let result = sqlx::query!(
"
SELECT o.id, o.slug, o.name, o.team_id, o.description, o.icon_url, o.raw_icon_url, o.color
FROM organizations o
LEFT JOIN mods m ON m.organization_id = o.id
WHERE m.id = $1
GROUP BY o.id;
",
project_id as ProjectId,
)
.fetch_optional(exec)
.await?;
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,
icon_url: result.icon_url,
raw_icon_url: result.raw_icon_url,
color: result.color.map(|x| x as u32),
}))
} else {
Ok(None)
}
}
pub async fn remove(
id: OrganizationId,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
redis: &RedisPool,
) -> Result<Option<()>, super::DatabaseError> {
let organization = Self::get_id(id, &mut **transaction, redis).await?;
if let Some(organization) = organization {
sqlx::query!(
"
DELETE FROM organizations
WHERE id = $1
",
id as OrganizationId,
)
.execute(&mut **transaction)
.await?;
TeamMember::clear_cache(organization.team_id, redis).await?;
sqlx::query!(
"
DELETE FROM team_members
WHERE team_id = $1
",
organization.team_id as TeamId,
)
.execute(&mut **transaction)
.await?;
sqlx::query!(
"
DELETE FROM teams
WHERE id = $1
",
organization.team_id as TeamId,
)
.execute(&mut **transaction)
.await?;
Ok(Some(()))
} else {
Ok(None)
}
}
pub async fn clear_cache(
id: OrganizationId,
slug: Option<String>,
redis: &RedisPool,
) -> Result<(), super::DatabaseError> {
let mut redis = redis.connect().await?;
redis
.delete_many([
(ORGANIZATIONS_NAMESPACE, Some(id.0.to_string())),
(
ORGANIZATIONS_TITLES_NAMESPACE,
slug.map(|x| x.to_lowercase()),
),
])
.await?;
Ok(())
}
}

View File

@@ -0,0 +1,224 @@
use super::ids::*;
use crate::database::models::DatabaseError;
use crate::database::redis::RedisPool;
use crate::models::ids::base62_impl::parse_base62;
use crate::models::pats::Scopes;
use chrono::{DateTime, Utc};
use dashmap::DashMap;
use futures::TryStreamExt;
use serde::{Deserialize, Serialize};
use std::fmt::{Debug, Display};
use std::hash::Hash;
const PATS_NAMESPACE: &str = "pats";
const PATS_TOKENS_NAMESPACE: &str = "pats_tokens";
const PATS_USERS_NAMESPACE: &str = "pats_users";
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct PersonalAccessToken {
pub id: PatId,
pub name: String,
pub access_token: String,
pub scopes: Scopes,
pub user_id: UserId,
pub created: DateTime<Utc>,
pub expires: DateTime<Utc>,
pub last_used: Option<DateTime<Utc>>,
}
impl PersonalAccessToken {
pub async fn insert(
&self,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<(), DatabaseError> {
sqlx::query!(
"
INSERT INTO pats (
id, name, access_token, scopes, user_id,
expires
)
VALUES (
$1, $2, $3, $4, $5,
$6
)
",
self.id as PatId,
self.name,
self.access_token,
self.scopes.bits() as i64,
self.user_id as UserId,
self.expires
)
.execute(&mut **transaction)
.await?;
Ok(())
}
pub async fn get<'a, E, T: Display + Hash + Eq + PartialEq + Clone + Debug>(
id: T,
exec: E,
redis: &RedisPool,
) -> Result<Option<PersonalAccessToken>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
Self::get_many(&[id], exec, redis)
.await
.map(|x| x.into_iter().next())
}
pub async fn get_many_ids<'a, E>(
pat_ids: &[PatId],
exec: E,
redis: &RedisPool,
) -> Result<Vec<PersonalAccessToken>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let ids = pat_ids
.iter()
.map(|x| crate::models::ids::PatId::from(*x))
.collect::<Vec<_>>();
PersonalAccessToken::get_many(&ids, exec, redis).await
}
pub async fn get_many<'a, E, T: Display + Hash + Eq + PartialEq + Clone + Debug>(
pat_strings: &[T],
exec: E,
redis: &RedisPool,
) -> Result<Vec<PersonalAccessToken>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let val = redis
.get_cached_keys_with_slug(
PATS_NAMESPACE,
PATS_TOKENS_NAMESPACE,
true,
pat_strings,
|ids| async move {
let pat_ids: Vec<i64> = ids
.iter()
.flat_map(|x| parse_base62(&x.to_string()).ok())
.map(|x| x as i64)
.collect();
let slugs = ids.into_iter().map(|x| x.to_string()).collect::<Vec<_>>();
let pats = sqlx::query!(
"
SELECT id, name, access_token, scopes, user_id, created, expires, last_used
FROM pats
WHERE id = ANY($1) OR access_token = ANY($2)
ORDER BY created DESC
",
&pat_ids,
&slugs,
)
.fetch(exec)
.try_fold(DashMap::new(), |acc, x| {
let pat = PersonalAccessToken {
id: PatId(x.id),
name: x.name,
access_token: x.access_token.clone(),
scopes: Scopes::from_bits(x.scopes as u64).unwrap_or(Scopes::NONE),
user_id: UserId(x.user_id),
created: x.created,
expires: x.expires,
last_used: x.last_used,
};
acc.insert(x.id, (Some(x.access_token), pat));
async move { Ok(acc) }
})
.await?;
Ok(pats)
},
)
.await?;
Ok(val)
}
pub async fn get_user_pats<'a, E>(
user_id: UserId,
exec: E,
redis: &RedisPool,
) -> Result<Vec<PatId>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let mut redis = redis.connect().await?;
let res = redis
.get_deserialized_from_json::<Vec<i64>>(PATS_USERS_NAMESPACE, &user_id.0.to_string())
.await?;
if let Some(res) = res {
return Ok(res.into_iter().map(PatId).collect());
}
let db_pats: Vec<PatId> = sqlx::query!(
"
SELECT id
FROM pats
WHERE user_id = $1
ORDER BY created DESC
",
user_id.0,
)
.fetch(exec)
.map_ok(|x| PatId(x.id))
.try_collect::<Vec<PatId>>()
.await?;
redis
.set(
PATS_USERS_NAMESPACE,
&user_id.0.to_string(),
&serde_json::to_string(&db_pats)?,
None,
)
.await?;
Ok(db_pats)
}
pub async fn clear_cache(
clear_pats: Vec<(Option<PatId>, Option<String>, Option<UserId>)>,
redis: &RedisPool,
) -> Result<(), DatabaseError> {
let mut redis = redis.connect().await?;
if clear_pats.is_empty() {
return Ok(());
}
redis
.delete_many(clear_pats.into_iter().flat_map(|(id, token, user_id)| {
[
(PATS_NAMESPACE, id.map(|i| i.0.to_string())),
(PATS_TOKENS_NAMESPACE, token),
(PATS_USERS_NAMESPACE, user_id.map(|i| i.0.to_string())),
]
}))
.await?;
Ok(())
}
pub async fn remove(
id: PatId,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Option<()>, sqlx::error::Error> {
sqlx::query!(
"
DELETE FROM pats WHERE id = $1
",
id as PatId,
)
.execute(&mut **transaction)
.await?;
Ok(Some(()))
}
}

View File

@@ -0,0 +1,115 @@
use crate::models::payouts::{PayoutMethodType, PayoutStatus};
use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use super::{DatabaseError, PayoutId, UserId};
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct Payout {
pub id: PayoutId,
pub user_id: UserId,
pub created: DateTime<Utc>,
pub status: PayoutStatus,
pub amount: Decimal,
pub fee: Option<Decimal>,
pub method: Option<PayoutMethodType>,
pub method_address: Option<String>,
pub platform_id: Option<String>,
}
impl Payout {
pub async fn insert(
&self,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<(), DatabaseError> {
sqlx::query!(
"
INSERT INTO payouts (
id, amount, fee, user_id, status, method, method_address, platform_id
)
VALUES (
$1, $2, $3, $4, $5, $6, $7, $8
)
",
self.id.0,
self.amount,
self.fee,
self.user_id.0,
self.status.as_str(),
self.method.map(|x| x.as_str()),
self.method_address,
self.platform_id,
)
.execute(&mut **transaction)
.await?;
Ok(())
}
pub async fn get<'a, 'b, E>(id: PayoutId, executor: E) -> Result<Option<Payout>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
Payout::get_many(&[id], executor)
.await
.map(|x| x.into_iter().next())
}
pub async fn get_many<'a, E>(
payout_ids: &[PayoutId],
exec: E,
) -> Result<Vec<Payout>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
use futures::TryStreamExt;
let results = sqlx::query!(
"
SELECT id, user_id, created, amount, status, method, method_address, platform_id, fee
FROM payouts
WHERE id = ANY($1)
",
&payout_ids.into_iter().map(|x| x.0).collect::<Vec<_>>()
)
.fetch(exec)
.map_ok(|r| Payout {
id: PayoutId(r.id),
user_id: UserId(r.user_id),
created: r.created,
status: PayoutStatus::from_string(&r.status),
amount: r.amount,
method: r.method.map(|x| PayoutMethodType::from_string(&x)),
method_address: r.method_address,
platform_id: r.platform_id,
fee: r.fee,
})
.try_collect::<Vec<Payout>>()
.await?;
Ok(results)
}
pub async fn get_all_for_user(
user_id: UserId,
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<Vec<PayoutId>, DatabaseError> {
let results = sqlx::query!(
"
SELECT id
FROM payouts
WHERE user_id = $1
",
user_id.0
)
.fetch_all(exec)
.await?;
Ok(results
.into_iter()
.map(|r| PayoutId(r.id))
.collect::<Vec<_>>())
}
}

View File

@@ -0,0 +1,248 @@
use crate::database::models::{product_item, DatabaseError, ProductId, ProductPriceId};
use crate::database::redis::RedisPool;
use crate::models::billing::{Price, ProductMetadata};
use dashmap::DashMap;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use std::convert::TryFrom;
use std::convert::TryInto;
const PRODUCTS_NAMESPACE: &str = "products";
pub struct ProductItem {
pub id: ProductId,
pub metadata: ProductMetadata,
pub unitary: bool,
}
struct ProductResult {
id: i64,
metadata: serde_json::Value,
unitary: bool,
}
macro_rules! select_products_with_predicate {
($predicate:tt, $param:ident) => {
sqlx::query_as!(
ProductResult,
r#"
SELECT id, metadata, unitary
FROM products
"#
+ $predicate,
$param
)
};
}
impl TryFrom<ProductResult> for ProductItem {
type Error = serde_json::Error;
fn try_from(r: ProductResult) -> Result<Self, Self::Error> {
Ok(ProductItem {
id: ProductId(r.id),
metadata: serde_json::from_value(r.metadata)?,
unitary: r.unitary,
})
}
}
impl ProductItem {
pub async fn get(
id: ProductId,
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<Option<ProductItem>, DatabaseError> {
Ok(Self::get_many(&[id], exec).await?.into_iter().next())
}
pub async fn get_many(
ids: &[ProductId],
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<Vec<ProductItem>, DatabaseError> {
let ids = ids.iter().map(|id| id.0).collect_vec();
let ids_ref: &[i64] = &ids;
let results = select_products_with_predicate!("WHERE id = ANY($1::bigint[])", ids_ref)
.fetch_all(exec)
.await?;
Ok(results
.into_iter()
.map(|r| r.try_into())
.collect::<Result<Vec<_>, serde_json::Error>>()?)
}
pub async fn get_all(
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<Vec<ProductItem>, DatabaseError> {
let one = 1;
let results = select_products_with_predicate!("WHERE 1 = $1", one)
.fetch_all(exec)
.await?;
Ok(results
.into_iter()
.map(|r| r.try_into())
.collect::<Result<Vec<_>, serde_json::Error>>()?)
}
}
#[derive(Deserialize, Serialize)]
pub struct QueryProduct {
pub id: ProductId,
pub metadata: ProductMetadata,
pub unitary: bool,
pub prices: Vec<ProductPriceItem>,
}
impl QueryProduct {
pub async fn list<'a, E>(exec: E, redis: &RedisPool) -> Result<Vec<QueryProduct>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{
let mut redis = redis.connect().await?;
let res: Option<Vec<QueryProduct>> = redis
.get_deserialized_from_json(PRODUCTS_NAMESPACE, "all")
.await?;
if let Some(res) = res {
return Ok(res);
}
let all_products = product_item::ProductItem::get_all(exec).await?;
let prices = product_item::ProductPriceItem::get_all_products_prices(
&all_products.iter().map(|x| x.id).collect::<Vec<_>>(),
exec,
)
.await?;
let products = all_products
.into_iter()
.map(|x| QueryProduct {
id: x.id,
metadata: x.metadata,
prices: prices
.remove(&x.id)
.map(|x| x.1)
.unwrap_or_default()
.into_iter()
.map(|x| ProductPriceItem {
id: x.id,
product_id: x.product_id,
prices: x.prices,
currency_code: x.currency_code,
})
.collect(),
unitary: x.unitary,
})
.collect::<Vec<_>>();
redis
.set_serialized_to_json(PRODUCTS_NAMESPACE, "all", &products, None)
.await?;
Ok(products)
}
}
#[derive(Deserialize, Serialize)]
pub struct ProductPriceItem {
pub id: ProductPriceId,
pub product_id: ProductId,
pub prices: Price,
pub currency_code: String,
}
struct ProductPriceResult {
id: i64,
product_id: i64,
prices: serde_json::Value,
currency_code: String,
}
macro_rules! select_prices_with_predicate {
($predicate:tt, $param:ident) => {
sqlx::query_as!(
ProductPriceResult,
r#"
SELECT id, product_id, prices, currency_code
FROM products_prices
"#
+ $predicate,
$param
)
};
}
impl TryFrom<ProductPriceResult> for ProductPriceItem {
type Error = serde_json::Error;
fn try_from(r: ProductPriceResult) -> Result<Self, Self::Error> {
Ok(ProductPriceItem {
id: ProductPriceId(r.id),
product_id: ProductId(r.product_id),
prices: serde_json::from_value(r.prices)?,
currency_code: r.currency_code,
})
}
}
impl ProductPriceItem {
pub async fn get(
id: ProductPriceId,
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<Option<ProductPriceItem>, DatabaseError> {
Ok(Self::get_many(&[id], exec).await?.into_iter().next())
}
pub async fn get_many(
ids: &[ProductPriceId],
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<Vec<ProductPriceItem>, DatabaseError> {
let ids = ids.iter().map(|id| id.0).collect_vec();
let ids_ref: &[i64] = &ids;
let results = select_prices_with_predicate!("WHERE id = ANY($1::bigint[])", ids_ref)
.fetch_all(exec)
.await?;
Ok(results
.into_iter()
.map(|r| r.try_into())
.collect::<Result<Vec<_>, serde_json::Error>>()?)
}
pub async fn get_all_product_prices(
product_id: ProductId,
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<Vec<ProductPriceItem>, DatabaseError> {
let res = Self::get_all_products_prices(&[product_id], exec).await?;
Ok(res.remove(&product_id).map(|x| x.1).unwrap_or_default())
}
pub async fn get_all_products_prices(
product_ids: &[ProductId],
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<DashMap<ProductId, Vec<ProductPriceItem>>, DatabaseError> {
let ids = product_ids.iter().map(|id| id.0).collect_vec();
let ids_ref: &[i64] = &ids;
use futures_util::TryStreamExt;
let prices = select_prices_with_predicate!("WHERE product_id = ANY($1::bigint[])", ids_ref)
.fetch(exec)
.try_fold(
DashMap::new(),
|acc: DashMap<ProductId, Vec<ProductPriceItem>>, x| {
if let Ok(item) = <ProductPriceResult as TryInto<ProductPriceItem>>::try_into(x)
{
acc.entry(item.product_id).or_default().push(item);
}
async move { Ok(acc) }
},
)
.await?;
Ok(prices)
}
}

View File

@@ -0,0 +1,927 @@
use super::loader_fields::{
QueryLoaderField, QueryLoaderFieldEnumValue, QueryVersionField, VersionField,
};
use super::{ids::*, User};
use crate::database::models;
use crate::database::models::DatabaseError;
use crate::database::redis::RedisPool;
use crate::models::ids::base62_impl::parse_base62;
use crate::models::projects::{MonetizationStatus, ProjectStatus};
use chrono::{DateTime, Utc};
use dashmap::{DashMap, DashSet};
use futures::TryStreamExt;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use std::fmt::{Debug, Display};
use std::hash::Hash;
pub const PROJECTS_NAMESPACE: &str = "projects";
pub const PROJECTS_SLUGS_NAMESPACE: &str = "projects_slugs";
const PROJECTS_DEPENDENCIES_NAMESPACE: &str = "projects_dependencies";
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LinkUrl {
pub platform_id: LinkPlatformId,
pub platform_name: String,
pub url: String,
pub donation: bool, // Is this a donation link
}
impl LinkUrl {
pub async fn insert_many_projects(
links: Vec<Self>,
project_id: ProjectId,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<(), sqlx::error::Error> {
let (project_ids, platform_ids, urls): (Vec<_>, Vec<_>, Vec<_>) = links
.into_iter()
.map(|url| (project_id.0, url.platform_id.0, url.url))
.multiunzip();
sqlx::query!(
"
INSERT INTO mods_links (
joining_mod_id, joining_platform_id, url
)
SELECT * FROM UNNEST($1::bigint[], $2::int[], $3::varchar[])
",
&project_ids[..],
&platform_ids[..],
&urls[..],
)
.execute(&mut **transaction)
.await?;
Ok(())
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct GalleryItem {
pub image_url: String,
pub raw_image_url: String,
pub featured: bool,
pub name: Option<String>,
pub description: Option<String>,
pub created: DateTime<Utc>,
pub ordering: i64,
}
impl GalleryItem {
pub async fn insert_many(
items: Vec<Self>,
project_id: ProjectId,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<(), sqlx::error::Error> {
let (project_ids, image_urls, raw_image_urls, featureds, names, descriptions, orderings): (
Vec<_>,
Vec<_>,
Vec<_>,
Vec<_>,
Vec<_>,
Vec<_>,
Vec<_>,
) = items
.into_iter()
.map(|gi| {
(
project_id.0,
gi.image_url,
gi.raw_image_url,
gi.featured,
gi.name,
gi.description,
gi.ordering,
)
})
.multiunzip();
sqlx::query!(
"
INSERT INTO mods_gallery (
mod_id, image_url, raw_image_url, featured, name, description, ordering
)
SELECT * FROM UNNEST ($1::bigint[], $2::varchar[], $3::varchar[], $4::bool[], $5::varchar[], $6::varchar[], $7::bigint[])
",
&project_ids[..],
&image_urls[..],
&raw_image_urls[..],
&featureds[..],
&names[..] as &[Option<String>],
&descriptions[..] as &[Option<String>],
&orderings[..]
)
.execute(&mut **transaction)
.await?;
Ok(())
}
}
#[derive(derive_new::new)]
pub struct ModCategory {
project_id: ProjectId,
category_id: CategoryId,
is_additional: bool,
}
impl ModCategory {
pub async fn insert_many(
items: Vec<Self>,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<(), DatabaseError> {
let (project_ids, category_ids, is_additionals): (Vec<_>, Vec<_>, Vec<_>) = items
.into_iter()
.map(|mc| (mc.project_id.0, mc.category_id.0, mc.is_additional))
.multiunzip();
sqlx::query!(
"
INSERT INTO mods_categories (joining_mod_id, joining_category_id, is_additional)
SELECT * FROM UNNEST ($1::bigint[], $2::int[], $3::bool[])
",
&project_ids[..],
&category_ids[..],
&is_additionals[..]
)
.execute(&mut **transaction)
.await?;
Ok(())
}
}
#[derive(Clone)]
pub struct ProjectBuilder {
pub project_id: ProjectId,
pub team_id: TeamId,
pub organization_id: Option<OrganizationId>,
pub name: String,
pub summary: String,
pub description: String,
pub icon_url: Option<String>,
pub raw_icon_url: Option<String>,
pub license_url: Option<String>,
pub categories: Vec<CategoryId>,
pub additional_categories: Vec<CategoryId>,
pub initial_versions: Vec<super::version_item::VersionBuilder>,
pub status: ProjectStatus,
pub requested_status: Option<ProjectStatus>,
pub license: String,
pub slug: Option<String>,
pub link_urls: Vec<LinkUrl>,
pub gallery_items: Vec<GalleryItem>,
pub color: Option<u32>,
pub monetization_status: MonetizationStatus,
}
impl ProjectBuilder {
pub async fn insert(
self,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<ProjectId, DatabaseError> {
let project_struct = Project {
id: self.project_id,
team_id: self.team_id,
organization_id: self.organization_id,
name: self.name,
summary: self.summary,
description: self.description,
published: Utc::now(),
updated: Utc::now(),
approved: None,
queued: if self.status == ProjectStatus::Processing {
Some(Utc::now())
} else {
None
},
status: self.status,
requested_status: self.requested_status,
downloads: 0,
follows: 0,
icon_url: self.icon_url,
raw_icon_url: self.raw_icon_url,
license_url: self.license_url,
license: self.license,
slug: self.slug,
moderation_message: None,
moderation_message_body: None,
webhook_sent: false,
color: self.color,
monetization_status: self.monetization_status,
loaders: vec![],
};
project_struct.insert(&mut *transaction).await?;
let ProjectBuilder {
link_urls,
gallery_items,
categories,
additional_categories,
..
} = self;
for mut version in self.initial_versions {
version.project_id = self.project_id;
version.insert(&mut *transaction).await?;
}
LinkUrl::insert_many_projects(link_urls, self.project_id, &mut *transaction).await?;
GalleryItem::insert_many(gallery_items, self.project_id, &mut *transaction).await?;
let project_id = self.project_id;
let mod_categories = categories
.into_iter()
.map(|c| ModCategory::new(project_id, c, false))
.chain(
additional_categories
.into_iter()
.map(|c| ModCategory::new(project_id, c, true)),
)
.collect_vec();
ModCategory::insert_many(mod_categories, &mut *transaction).await?;
Ok(self.project_id)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Project {
pub id: ProjectId,
pub team_id: TeamId,
pub organization_id: Option<OrganizationId>,
pub name: String,
pub summary: String,
pub description: String,
pub published: DateTime<Utc>,
pub updated: DateTime<Utc>,
pub approved: Option<DateTime<Utc>>,
pub queued: Option<DateTime<Utc>>,
pub status: ProjectStatus,
pub requested_status: Option<ProjectStatus>,
pub downloads: i32,
pub follows: i32,
pub icon_url: Option<String>,
pub raw_icon_url: Option<String>,
pub license_url: Option<String>,
pub license: String,
pub slug: Option<String>,
pub moderation_message: Option<String>,
pub moderation_message_body: Option<String>,
pub webhook_sent: bool,
pub color: Option<u32>,
pub monetization_status: MonetizationStatus,
pub loaders: Vec<String>,
}
impl Project {
pub async fn insert(
&self,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<(), DatabaseError> {
sqlx::query!(
"
INSERT INTO mods (
id, team_id, name, summary, description,
published, downloads, icon_url, raw_icon_url, status, requested_status,
license_url, license,
slug, color, monetization_status, organization_id
)
VALUES (
$1, $2, $3, $4, $5, $6,
$7, $8, $9, $10, $11,
$12, $13,
LOWER($14), $15, $16, $17
)
",
self.id as ProjectId,
self.team_id as TeamId,
&self.name,
&self.summary,
&self.description,
self.published,
self.downloads,
self.icon_url.as_ref(),
self.raw_icon_url.as_ref(),
self.status.as_str(),
self.requested_status.map(|x| x.as_str()),
self.license_url.as_ref(),
&self.license,
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?;
Ok(())
}
pub async fn remove(
id: ProjectId,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
redis: &RedisPool,
) -> Result<Option<()>, DatabaseError> {
let project = Self::get_id(id, &mut **transaction, redis).await?;
if let Some(project) = project {
Project::clear_cache(id, project.inner.slug, Some(true), redis).await?;
sqlx::query!(
"
DELETE FROM mod_follows
WHERE mod_id = $1
",
id as ProjectId
)
.execute(&mut **transaction)
.await?;
sqlx::query!(
"
DELETE FROM mods_gallery
WHERE mod_id = $1
",
id as ProjectId
)
.execute(&mut **transaction)
.await?;
sqlx::query!(
"
DELETE FROM mod_follows
WHERE mod_id = $1
",
id as ProjectId,
)
.execute(&mut **transaction)
.await?;
models::Thread::remove_full(project.thread_id, transaction).await?;
sqlx::query!(
"
UPDATE reports
SET mod_id = NULL
WHERE mod_id = $1
",
id as ProjectId,
)
.execute(&mut **transaction)
.await?;
sqlx::query!(
"
DELETE FROM mods_categories
WHERE joining_mod_id = $1
",
id as ProjectId,
)
.execute(&mut **transaction)
.await?;
sqlx::query!(
"
DELETE FROM mods_links
WHERE joining_mod_id = $1
",
id as ProjectId,
)
.execute(&mut **transaction)
.await?;
for version in project.versions {
super::Version::remove_full(version, redis, transaction).await?;
}
sqlx::query!(
"
DELETE FROM dependencies WHERE mod_dependency_id = $1
",
id as ProjectId,
)
.execute(&mut **transaction)
.await?;
sqlx::query!(
"
UPDATE payouts_values
SET mod_id = NULL
WHERE (mod_id = $1)
",
id as ProjectId,
)
.execute(&mut **transaction)
.await?;
sqlx::query!(
"
DELETE FROM mods
WHERE id = $1
",
id as ProjectId,
)
.execute(&mut **transaction)
.await?;
models::TeamMember::clear_cache(project.inner.team_id, redis).await?;
let affected_user_ids = sqlx::query!(
"
DELETE FROM team_members
WHERE team_id = $1
RETURNING user_id
",
project.inner.team_id as TeamId,
)
.fetch(&mut **transaction)
.map_ok(|x| UserId(x.user_id))
.try_collect::<Vec<_>>()
.await?;
User::clear_project_cache(&affected_user_ids, redis).await?;
sqlx::query!(
"
DELETE FROM teams
WHERE id = $1
",
project.inner.team_id as TeamId,
)
.execute(&mut **transaction)
.await?;
Ok(Some(()))
} else {
Ok(None)
}
}
pub async fn get<'a, 'b, E>(
string: &str,
executor: E,
redis: &RedisPool,
) -> Result<Option<QueryProject>, DatabaseError>
where
E: sqlx::Acquire<'a, Database = sqlx::Postgres>,
{
Project::get_many(&[string], executor, redis)
.await
.map(|x| x.into_iter().next())
}
pub async fn get_id<'a, 'b, E>(
id: ProjectId,
executor: E,
redis: &RedisPool,
) -> Result<Option<QueryProject>, DatabaseError>
where
E: sqlx::Acquire<'a, Database = sqlx::Postgres>,
{
Project::get_many(&[crate::models::ids::ProjectId::from(id)], executor, redis)
.await
.map(|x| x.into_iter().next())
}
pub async fn get_many_ids<'a, E>(
project_ids: &[ProjectId],
exec: E,
redis: &RedisPool,
) -> Result<Vec<QueryProject>, DatabaseError>
where
E: sqlx::Acquire<'a, Database = sqlx::Postgres>,
{
let ids = project_ids
.iter()
.map(|x| crate::models::ids::ProjectId::from(*x))
.collect::<Vec<_>>();
Project::get_many(&ids, exec, redis).await
}
pub async fn get_many<'a, E, T: Display + Hash + Eq + PartialEq + Clone + Debug>(
project_strings: &[T],
exec: E,
redis: &RedisPool,
) -> Result<Vec<QueryProject>, DatabaseError>
where
E: sqlx::Acquire<'a, Database = sqlx::Postgres>,
{
let val = redis.get_cached_keys_with_slug(
PROJECTS_NAMESPACE,
PROJECTS_SLUGS_NAMESPACE,
false,
project_strings,
|ids| async move {
let mut exec = exec.acquire().await?;
let project_ids_parsed: Vec<i64> = ids
.iter()
.flat_map(|x| parse_base62(&x.to_string()).ok())
.map(|x| x as i64)
.collect();
let slugs = ids
.into_iter()
.map(|x| x.to_string().to_lowercase())
.collect::<Vec<_>>();
let all_version_ids = DashSet::new();
let versions: DashMap<ProjectId, Vec<(VersionId, DateTime<Utc>)>> = sqlx::query!(
"
SELECT DISTINCT mod_id, v.id as id, date_published
FROM mods m
INNER JOIN versions v ON m.id = v.mod_id AND v.status = ANY($3)
WHERE m.id = ANY($1) OR m.slug = ANY($2)
",
&project_ids_parsed,
&slugs,
&*crate::models::projects::VersionStatus::iterator()
.filter(|x| x.is_listed())
.map(|x| x.to_string())
.collect::<Vec<String>>()
)
.fetch(&mut *exec)
.try_fold(
DashMap::new(),
|acc: DashMap<ProjectId, Vec<(VersionId, DateTime<Utc>)>>, m| {
let version_id = VersionId(m.id);
let date_published = m.date_published;
all_version_ids.insert(version_id);
acc.entry(ProjectId(m.mod_id))
.or_default()
.push((version_id, date_published));
async move { Ok(acc) }
},
)
.await?;
let loader_field_enum_value_ids = DashSet::new();
let version_fields: DashMap<ProjectId, Vec<QueryVersionField>> = sqlx::query!(
"
SELECT DISTINCT mod_id, version_id, field_id, int_value, enum_value, string_value
FROM versions v
INNER JOIN version_fields vf ON v.id = vf.version_id
WHERE v.id = ANY($1)
",
&all_version_ids.iter().map(|x| x.0).collect::<Vec<_>>()
)
.fetch(&mut *exec)
.try_fold(
DashMap::new(),
|acc: DashMap<ProjectId, Vec<QueryVersionField>>, m| {
let qvf = QueryVersionField {
version_id: VersionId(m.version_id),
field_id: LoaderFieldId(m.field_id),
int_value: m.int_value,
enum_value: m.enum_value.map(LoaderFieldEnumValueId),
string_value: m.string_value,
};
if let Some(enum_value) = m.enum_value {
loader_field_enum_value_ids.insert(LoaderFieldEnumValueId(enum_value));
}
acc.entry(ProjectId(m.mod_id)).or_default().push(qvf);
async move { Ok(acc) }
},
)
.await?;
let loader_field_enum_values: Vec<QueryLoaderFieldEnumValue> = sqlx::query!(
"
SELECT DISTINCT id, enum_id, value, ordering, created, metadata
FROM loader_field_enum_values lfev
WHERE id = ANY($1)
ORDER BY enum_id, ordering, created DESC
",
&loader_field_enum_value_ids
.iter()
.map(|x| x.0)
.collect::<Vec<_>>()
)
.fetch(&mut *exec)
.map_ok(|m| QueryLoaderFieldEnumValue {
id: LoaderFieldEnumValueId(m.id),
enum_id: LoaderFieldEnumId(m.enum_id),
value: m.value,
ordering: m.ordering,
created: m.created,
metadata: m.metadata,
})
.try_collect()
.await?;
let mods_gallery: DashMap<ProjectId, Vec<GalleryItem>> = sqlx::query!(
"
SELECT DISTINCT mod_id, mg.image_url, mg.raw_image_url, mg.featured, mg.name, mg.description, mg.created, mg.ordering
FROM mods_gallery mg
INNER JOIN mods m ON mg.mod_id = m.id
WHERE m.id = ANY($1) OR m.slug = ANY($2)
",
&project_ids_parsed,
&slugs
).fetch(&mut *exec)
.try_fold(DashMap::new(), |acc : DashMap<ProjectId, Vec<GalleryItem>>, m| {
acc.entry(ProjectId(m.mod_id))
.or_default()
.push(GalleryItem {
image_url: m.image_url,
raw_image_url: m.raw_image_url,
featured: m.featured.unwrap_or(false),
name: m.name,
description: m.description,
created: m.created,
ordering: m.ordering,
});
async move { Ok(acc) }
}
).await?;
let links: DashMap<ProjectId, Vec<LinkUrl>> = sqlx::query!(
"
SELECT DISTINCT joining_mod_id as mod_id, joining_platform_id as platform_id, lp.name as platform_name, url, lp.donation as donation
FROM mods_links ml
INNER JOIN mods m ON ml.joining_mod_id = m.id
INNER JOIN link_platforms lp ON ml.joining_platform_id = lp.id
WHERE m.id = ANY($1) OR m.slug = ANY($2)
",
&project_ids_parsed,
&slugs
).fetch(&mut *exec)
.try_fold(DashMap::new(), |acc : DashMap<ProjectId, Vec<LinkUrl>>, m| {
acc.entry(ProjectId(m.mod_id))
.or_default()
.push(LinkUrl {
platform_id: LinkPlatformId(m.platform_id),
platform_name: m.platform_name,
url: m.url,
donation: m.donation,
});
async move { Ok(acc) }
}
).await?;
#[derive(Default)]
struct VersionLoaderData {
loaders: Vec<String>,
project_types: Vec<String>,
games: Vec<String>,
loader_loader_field_ids: Vec<LoaderFieldId>,
}
let loader_field_ids = DashSet::new();
let loaders_ptypes_games: DashMap<ProjectId, VersionLoaderData> = sqlx::query!(
"
SELECT DISTINCT mod_id,
ARRAY_AGG(DISTINCT l.loader) filter (where l.loader is not null) loaders,
ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types,
ARRAY_AGG(DISTINCT g.slug) filter (where g.slug is not null) games,
ARRAY_AGG(DISTINCT lfl.loader_field_id) filter (where lfl.loader_field_id is not null) loader_fields
FROM versions v
INNER JOIN loaders_versions lv ON v.id = lv.version_id
INNER JOIN loaders l ON lv.loader_id = l.id
INNER JOIN loaders_project_types lpt ON lpt.joining_loader_id = l.id
INNER JOIN project_types pt ON pt.id = lpt.joining_project_type_id
INNER JOIN loaders_project_types_games lptg ON lptg.loader_id = l.id AND lptg.project_type_id = pt.id
INNER JOIN games g ON lptg.game_id = g.id
LEFT JOIN loader_fields_loaders lfl ON lfl.loader_id = l.id
WHERE v.id = ANY($1)
GROUP BY mod_id
",
&all_version_ids.iter().map(|x| x.0).collect::<Vec<_>>()
).fetch(&mut *exec)
.map_ok(|m| {
let project_id = ProjectId(m.mod_id);
// Add loader fields to the set we need to fetch
let loader_loader_field_ids = m.loader_fields.unwrap_or_default().into_iter().map(LoaderFieldId).collect::<Vec<_>>();
for loader_field_id in loader_loader_field_ids.iter() {
loader_field_ids.insert(*loader_field_id);
}
// Add loader + loader associated data to the map
let version_loader_data = VersionLoaderData {
loaders: m.loaders.unwrap_or_default(),
project_types: m.project_types.unwrap_or_default(),
games: m.games.unwrap_or_default(),
loader_loader_field_ids,
};
(project_id, version_loader_data)
}
).try_collect().await?;
let loader_fields: Vec<QueryLoaderField> = sqlx::query!(
"
SELECT DISTINCT id, field, field_type, enum_type, min_val, max_val, optional
FROM loader_fields lf
WHERE id = ANY($1)
",
&loader_field_ids.iter().map(|x| x.0).collect::<Vec<_>>()
)
.fetch(&mut *exec)
.map_ok(|m| QueryLoaderField {
id: LoaderFieldId(m.id),
field: m.field,
field_type: m.field_type,
enum_type: m.enum_type.map(LoaderFieldEnumId),
min_val: m.min_val,
max_val: m.max_val,
optional: m.optional,
})
.try_collect()
.await?;
let projects = sqlx::query!(
"
SELECT m.id id, m.name name, m.summary summary, m.downloads downloads, m.follows follows,
m.icon_url icon_url, m.raw_icon_url raw_icon_url, m.description description, m.published published,
m.updated updated, m.approved approved, m.queued, m.status status, m.requested_status requested_status,
m.license_url license_url,
m.team_id team_id, m.organization_id organization_id, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body,
m.webhook_sent, m.color,
t.id thread_id, m.monetization_status monetization_status,
ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories,
ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories
FROM mods m
INNER JOIN threads t ON t.mod_id = m.id
LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id
LEFT JOIN categories c ON mc.joining_category_id = c.id
WHERE m.id = ANY($1) OR m.slug = ANY($2)
GROUP BY t.id, m.id;
",
&project_ids_parsed,
&slugs,
)
.fetch(&mut *exec)
.try_fold(DashMap::new(), |acc, m| {
let id = m.id;
let project_id = ProjectId(id);
let VersionLoaderData {
loaders,
project_types,
games,
loader_loader_field_ids,
} = loaders_ptypes_games.remove(&project_id).map(|x|x.1).unwrap_or_default();
let mut versions = versions.remove(&project_id).map(|x| x.1).unwrap_or_default();
let mut gallery = mods_gallery.remove(&project_id).map(|x| x.1).unwrap_or_default();
let urls = links.remove(&project_id).map(|x| x.1).unwrap_or_default();
let version_fields = version_fields.remove(&project_id).map(|x| x.1).unwrap_or_default();
let loader_fields = loader_fields.iter()
.filter(|x| loader_loader_field_ids.contains(&x.id))
.collect::<Vec<_>>();
let project = QueryProject {
inner: Project {
id: ProjectId(id),
team_id: TeamId(m.team_id),
organization_id: m.organization_id.map(OrganizationId),
name: m.name.clone(),
summary: m.summary.clone(),
downloads: m.downloads,
icon_url: m.icon_url.clone(),
raw_icon_url: m.raw_icon_url.clone(),
published: m.published,
updated: m.updated,
license_url: m.license_url.clone(),
status: ProjectStatus::from_string(
&m.status,
),
requested_status: m.requested_status.map(|x| ProjectStatus::from_string(
&x,
)),
license: m.license.clone(),
slug: m.slug.clone(),
description: m.description.clone(),
follows: m.follows,
moderation_message: m.moderation_message,
moderation_message_body: m.moderation_message_body,
approved: m.approved,
webhook_sent: m.webhook_sent,
color: m.color.map(|x| x as u32),
queued: m.queued,
monetization_status: MonetizationStatus::from_string(
&m.monetization_status,
),
loaders,
},
categories: m.categories.unwrap_or_default(),
additional_categories: m.additional_categories.unwrap_or_default(),
project_types,
games,
versions: {
// Each version is a tuple of (VersionId, DateTime<Utc>)
versions.sort_by(|a, b| a.1.cmp(&b.1));
versions.into_iter().map(|x| x.0).collect()
},
gallery_items: {
gallery.sort_by(|a, b| a.ordering.cmp(&b.ordering));
gallery
},
urls,
aggregate_version_fields: VersionField::from_query_json(version_fields, &loader_fields, &loader_field_enum_values, true),
thread_id: ThreadId(m.thread_id),
};
acc.insert(m.id, (m.slug, project));
async move { Ok(acc) }
})
.await?;
Ok(projects)
},
).await?;
Ok(val)
}
pub async fn get_dependencies<'a, E>(
id: ProjectId,
exec: E,
redis: &RedisPool,
) -> Result<Vec<(Option<VersionId>, Option<ProjectId>, Option<ProjectId>)>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
type Dependencies = Vec<(Option<VersionId>, Option<ProjectId>, Option<ProjectId>)>;
let mut redis = redis.connect().await?;
let dependencies = redis
.get_deserialized_from_json::<Dependencies>(
PROJECTS_DEPENDENCIES_NAMESPACE,
&id.0.to_string(),
)
.await?;
if let Some(dependencies) = dependencies {
return Ok(dependencies);
}
let dependencies: Dependencies = sqlx::query!(
"
SELECT d.dependency_id, COALESCE(vd.mod_id, 0) mod_id, d.mod_dependency_id
FROM versions v
INNER JOIN dependencies d ON d.dependent_id = v.id
LEFT JOIN versions vd ON d.dependency_id = vd.id
WHERE v.mod_id = $1
",
id as ProjectId
)
.fetch(exec)
.map_ok(|x| {
(
x.dependency_id.map(VersionId),
if x.mod_id == Some(0) {
None
} else {
x.mod_id.map(ProjectId)
},
x.mod_dependency_id.map(ProjectId),
)
})
.try_collect::<Dependencies>()
.await?;
redis
.set_serialized_to_json(PROJECTS_DEPENDENCIES_NAMESPACE, id.0, &dependencies, None)
.await?;
Ok(dependencies)
}
pub async fn clear_cache(
id: ProjectId,
slug: Option<String>,
clear_dependencies: Option<bool>,
redis: &RedisPool,
) -> Result<(), DatabaseError> {
let mut redis = redis.connect().await?;
redis
.delete_many([
(PROJECTS_NAMESPACE, Some(id.0.to_string())),
(PROJECTS_SLUGS_NAMESPACE, slug.map(|x| x.to_lowercase())),
(
PROJECTS_DEPENDENCIES_NAMESPACE,
if clear_dependencies.unwrap_or(false) {
Some(id.0.to_string())
} else {
None
},
),
])
.await?;
Ok(())
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct QueryProject {
pub inner: Project,
pub categories: Vec<String>,
pub additional_categories: Vec<String>,
pub versions: Vec<VersionId>,
pub project_types: Vec<String>,
pub games: Vec<String>,
pub urls: Vec<LinkUrl>,
pub gallery_items: Vec<GalleryItem>,
pub thread_id: ThreadId,
pub aggregate_version_fields: Vec<VersionField>,
}

View File

@@ -0,0 +1,151 @@
use super::ids::*;
use chrono::{DateTime, Utc};
pub struct Report {
pub id: ReportId,
pub report_type_id: ReportTypeId,
pub project_id: Option<ProjectId>,
pub version_id: Option<VersionId>,
pub user_id: Option<UserId>,
pub body: String,
pub reporter: UserId,
pub created: DateTime<Utc>,
pub closed: bool,
}
pub struct QueryReport {
pub id: ReportId,
pub report_type: String,
pub project_id: Option<ProjectId>,
pub version_id: Option<VersionId>,
pub user_id: Option<UserId>,
pub body: String,
pub reporter: UserId,
pub created: DateTime<Utc>,
pub closed: bool,
pub thread_id: ThreadId,
}
impl Report {
pub async fn insert(
&self,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<(), sqlx::error::Error> {
sqlx::query!(
"
INSERT INTO reports (
id, report_type_id, mod_id, version_id, user_id,
body, reporter
)
VALUES (
$1, $2, $3, $4, $5,
$6, $7
)
",
self.id as ReportId,
self.report_type_id as ReportTypeId,
self.project_id.map(|x| x.0 as i64),
self.version_id.map(|x| x.0 as i64),
self.user_id.map(|x| x.0 as i64),
self.body,
self.reporter as UserId
)
.execute(&mut **transaction)
.await?;
Ok(())
}
pub async fn get<'a, E>(id: ReportId, exec: E) -> Result<Option<QueryReport>, sqlx::Error>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
Self::get_many(&[id], exec)
.await
.map(|x| x.into_iter().next())
}
pub async fn get_many<'a, E>(
report_ids: &[ReportId],
exec: E,
) -> Result<Vec<QueryReport>, sqlx::Error>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
use futures::stream::TryStreamExt;
let report_ids_parsed: Vec<i64> = report_ids.iter().map(|x| x.0).collect();
let reports = sqlx::query!(
"
SELECT r.id, rt.name, r.mod_id, r.version_id, r.user_id, r.body, r.reporter, r.created, t.id thread_id, r.closed
FROM reports r
INNER JOIN report_types rt ON rt.id = r.report_type_id
INNER JOIN threads t ON t.report_id = r.id
WHERE r.id = ANY($1)
ORDER BY r.created DESC
",
&report_ids_parsed
)
.fetch(exec)
.map_ok(|x| QueryReport {
id: ReportId(x.id),
report_type: x.name,
project_id: x.mod_id.map(ProjectId),
version_id: x.version_id.map(VersionId),
user_id: x.user_id.map(UserId),
body: x.body,
reporter: UserId(x.reporter),
created: x.created,
closed: x.closed,
thread_id: ThreadId(x.thread_id)
})
.try_collect::<Vec<QueryReport>>()
.await?;
Ok(reports)
}
pub async fn remove_full(
id: ReportId,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Option<()>, sqlx::error::Error> {
let result = sqlx::query!(
"
SELECT EXISTS(SELECT 1 FROM reports WHERE id = $1)
",
id as ReportId
)
.fetch_one(&mut **transaction)
.await?;
if !result.exists.unwrap_or(false) {
return Ok(None);
}
let thread_id = sqlx::query!(
"
SELECT id FROM threads
WHERE report_id = $1
",
id as ReportId
)
.fetch_optional(&mut **transaction)
.await?;
if let Some(thread_id) = thread_id {
crate::database::models::Thread::remove_full(ThreadId(thread_id.id), transaction)
.await?;
}
sqlx::query!(
"
DELETE FROM reports WHERE id = $1
",
id as ReportId,
)
.execute(&mut **transaction)
.await?;
Ok(Some(()))
}
}

View File

@@ -0,0 +1,276 @@
use super::ids::*;
use crate::database::models::DatabaseError;
use crate::database::redis::RedisPool;
use crate::models::ids::base62_impl::parse_base62;
use chrono::{DateTime, Utc};
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use std::fmt::{Debug, Display};
use std::hash::Hash;
const SESSIONS_NAMESPACE: &str = "sessions";
const SESSIONS_IDS_NAMESPACE: &str = "sessions_ids";
const SESSIONS_USERS_NAMESPACE: &str = "sessions_users";
pub struct SessionBuilder {
pub session: String,
pub user_id: UserId,
pub os: Option<String>,
pub platform: Option<String>,
pub city: Option<String>,
pub country: Option<String>,
pub ip: String,
pub user_agent: String,
}
impl SessionBuilder {
pub async fn insert(
&self,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<SessionId, DatabaseError> {
let id = generate_session_id(transaction).await?;
sqlx::query!(
"
INSERT INTO sessions (
id, session, user_id, os, platform,
city, country, ip, user_agent
)
VALUES (
$1, $2, $3, $4, $5,
$6, $7, $8, $9
)
",
id as SessionId,
self.session,
self.user_id as UserId,
self.os,
self.platform,
self.city,
self.country,
self.ip,
self.user_agent,
)
.execute(&mut **transaction)
.await?;
Ok(id)
}
}
#[derive(Deserialize, Serialize)]
pub struct Session {
pub id: SessionId,
pub session: String,
pub user_id: UserId,
pub created: DateTime<Utc>,
pub last_login: DateTime<Utc>,
pub expires: DateTime<Utc>,
pub refresh_expires: DateTime<Utc>,
pub os: Option<String>,
pub platform: Option<String>,
pub user_agent: String,
pub city: Option<String>,
pub country: Option<String>,
pub ip: String,
}
impl Session {
pub async fn get<'a, E, T: Display + Hash + Eq + PartialEq + Clone + Debug>(
id: T,
exec: E,
redis: &RedisPool,
) -> Result<Option<Session>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
Self::get_many(&[id], exec, redis)
.await
.map(|x| x.into_iter().next())
}
pub async fn get_id<'a, 'b, E>(
id: SessionId,
executor: E,
redis: &RedisPool,
) -> Result<Option<Session>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
Session::get_many(&[crate::models::ids::SessionId::from(id)], executor, redis)
.await
.map(|x| x.into_iter().next())
}
pub async fn get_many_ids<'a, E>(
session_ids: &[SessionId],
exec: E,
redis: &RedisPool,
) -> Result<Vec<Session>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let ids = session_ids
.iter()
.map(|x| crate::models::ids::SessionId::from(*x))
.collect::<Vec<_>>();
Session::get_many(&ids, exec, redis).await
}
pub async fn get_many<'a, E, T: Display + Hash + Eq + PartialEq + Clone + Debug>(
session_strings: &[T],
exec: E,
redis: &RedisPool,
) -> Result<Vec<Session>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
use futures::TryStreamExt;
let val = redis.get_cached_keys_with_slug(
SESSIONS_NAMESPACE,
SESSIONS_IDS_NAMESPACE,
true,
session_strings,
|ids| async move {
let session_ids: Vec<i64> = ids
.iter()
.flat_map(|x| parse_base62(&x.to_string()).ok())
.map(|x| x as i64)
.collect();
let slugs = ids
.into_iter()
.map(|x| x.to_string())
.collect::<Vec<_>>();
let db_sessions = sqlx::query!(
"
SELECT id, user_id, session, created, last_login, expires, refresh_expires, os, platform,
city, country, ip, user_agent
FROM sessions
WHERE id = ANY($1) OR session = ANY($2)
ORDER BY created DESC
",
&session_ids,
&slugs,
)
.fetch(exec)
.try_fold(DashMap::new(), |acc, x| {
let session = Session {
id: SessionId(x.id),
session: x.session.clone(),
user_id: UserId(x.user_id),
created: x.created,
last_login: x.last_login,
expires: x.expires,
refresh_expires: x.refresh_expires,
os: x.os,
platform: x.platform,
city: x.city,
country: x.country,
ip: x.ip,
user_agent: x.user_agent,
};
acc.insert(x.id, (Some(x.session), session));
async move { Ok(acc) }
})
.await?;
Ok(db_sessions)
}).await?;
Ok(val)
}
pub async fn get_user_sessions<'a, E>(
user_id: UserId,
exec: E,
redis: &RedisPool,
) -> Result<Vec<SessionId>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let mut redis = redis.connect().await?;
let res = redis
.get_deserialized_from_json::<Vec<i64>>(
SESSIONS_USERS_NAMESPACE,
&user_id.0.to_string(),
)
.await?;
if let Some(res) = res {
return Ok(res.into_iter().map(SessionId).collect());
}
use futures::TryStreamExt;
let db_sessions: Vec<SessionId> = sqlx::query!(
"
SELECT id
FROM sessions
WHERE user_id = $1
ORDER BY created DESC
",
user_id.0,
)
.fetch(exec)
.map_ok(|x| SessionId(x.id))
.try_collect::<Vec<SessionId>>()
.await?;
redis
.set_serialized_to_json(SESSIONS_USERS_NAMESPACE, user_id.0, &db_sessions, None)
.await?;
Ok(db_sessions)
}
pub async fn clear_cache(
clear_sessions: Vec<(Option<SessionId>, Option<String>, Option<UserId>)>,
redis: &RedisPool,
) -> Result<(), DatabaseError> {
let mut redis = redis.connect().await?;
if clear_sessions.is_empty() {
return Ok(());
}
redis
.delete_many(
clear_sessions
.into_iter()
.flat_map(|(id, session, user_id)| {
[
(SESSIONS_NAMESPACE, id.map(|i| i.0.to_string())),
(SESSIONS_IDS_NAMESPACE, session),
(SESSIONS_USERS_NAMESPACE, user_id.map(|i| i.0.to_string())),
]
}),
)
.await?;
Ok(())
}
pub async fn remove(
id: SessionId,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Option<()>, sqlx::error::Error> {
sqlx::query!(
"
DELETE FROM sessions WHERE id = $1
",
id as SessionId,
)
.execute(&mut **transaction)
.await?;
Ok(Some(()))
}
}

View File

@@ -0,0 +1,708 @@
use super::{ids::*, Organization, Project};
use crate::{
database::redis::RedisPool,
models::teams::{OrganizationPermissions, ProjectPermissions},
};
use dashmap::DashMap;
use futures::TryStreamExt;
use itertools::Itertools;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
const TEAMS_NAMESPACE: &str = "teams";
pub struct TeamBuilder {
pub members: Vec<TeamMemberBuilder>,
}
pub struct TeamMemberBuilder {
pub user_id: UserId,
pub role: String,
pub is_owner: bool,
pub permissions: ProjectPermissions,
pub organization_permissions: Option<OrganizationPermissions>,
pub accepted: bool,
pub payouts_split: Decimal,
pub ordering: i64,
}
impl TeamBuilder {
pub async fn insert(
self,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<TeamId, super::DatabaseError> {
let team_id = generate_team_id(transaction).await?;
let team = Team { id: team_id };
sqlx::query!(
"
INSERT INTO teams (id)
VALUES ($1)
",
team.id as TeamId,
)
.execute(&mut **transaction)
.await?;
let mut team_member_ids = Vec::new();
for _ in self.members.iter() {
team_member_ids.push(generate_team_member_id(transaction).await?.0);
}
let TeamBuilder { members } = self;
let (
team_ids,
user_ids,
roles,
is_owners,
permissions,
organization_permissions,
accepteds,
payouts_splits,
orderings,
): (
Vec<_>,
Vec<_>,
Vec<_>,
Vec<_>,
Vec<_>,
Vec<_>,
Vec<_>,
Vec<_>,
Vec<_>,
) = members
.into_iter()
.map(|m| {
(
team.id.0,
m.user_id.0,
m.role,
m.is_owner,
m.permissions.bits() as i64,
m.organization_permissions.map(|p| p.bits() as i64),
m.accepted,
m.payouts_split,
m.ordering,
)
})
.multiunzip();
sqlx::query!(
"
INSERT INTO team_members (id, team_id, user_id, role, is_owner, permissions, organization_permissions, accepted, payouts_split, ordering)
SELECT * FROM UNNEST ($1::int8[], $2::int8[], $3::int8[], $4::varchar[], $5::bool[], $6::int8[], $7::int8[], $8::bool[], $9::numeric[], $10::int8[])
",
&team_member_ids[..],
&team_ids[..],
&user_ids[..],
&roles[..],
&is_owners[..],
&permissions[..],
&organization_permissions[..] as &[Option<i64>],
&accepteds[..],
&payouts_splits[..],
&orderings[..],
)
.execute(&mut **transaction)
.await?;
Ok(team_id)
}
}
/// A team of users who control a project
pub struct Team {
/// The id of the team
pub id: TeamId,
}
#[derive(Deserialize, Serialize, Clone, Debug, Copy)]
pub enum TeamAssociationId {
Project(ProjectId),
Organization(OrganizationId),
}
impl Team {
pub async fn get_association<'a, 'b, E>(
id: TeamId,
executor: E,
) -> Result<Option<TeamAssociationId>, super::DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let result = sqlx::query!(
"
SELECT m.id AS pid, NULL AS oid
FROM mods m
WHERE m.team_id = $1
UNION ALL
SELECT NULL AS pid, o.id AS oid
FROM organizations o
WHERE o.team_id = $1
",
id as TeamId
)
.fetch_optional(executor)
.await?;
if let Some(t) = result {
// Only one of project_id or organization_id will be set
let mut team_association_id = None;
if let Some(pid) = t.pid {
team_association_id = Some(TeamAssociationId::Project(ProjectId(pid)));
}
if let Some(oid) = t.oid {
team_association_id = Some(TeamAssociationId::Organization(OrganizationId(oid)));
}
return Ok(team_association_id);
}
Ok(None)
}
}
/// A member of a team
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct TeamMember {
pub id: TeamMemberId,
pub team_id: TeamId,
/// The ID of the user associated with the member
pub user_id: UserId,
pub role: String,
pub is_owner: bool,
// The permissions of the user in this project team
// For an organization team, these are the fallback permissions for any project in the organization
pub permissions: ProjectPermissions,
// The permissions of the user in this organization team
// For a project team, this is None
pub organization_permissions: Option<OrganizationPermissions>,
pub accepted: bool,
pub payouts_split: Decimal,
pub ordering: i64,
}
impl TeamMember {
// Lists the full members of a team
pub async fn get_from_team_full<'a, 'b, E>(
id: TeamId,
executor: E,
redis: &RedisPool,
) -> Result<Vec<TeamMember>, super::DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{
Self::get_from_team_full_many(&[id], executor, redis).await
}
pub async fn get_from_team_full_many<'a, E>(
team_ids: &[TeamId],
exec: E,
redis: &RedisPool,
) -> Result<Vec<TeamMember>, super::DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{
if team_ids.is_empty() {
return Ok(Vec::new());
}
let val = redis.get_cached_keys(
TEAMS_NAMESPACE,
&team_ids.iter().map(|x| x.0).collect::<Vec<_>>(),
|team_ids| async move {
let teams = sqlx::query!(
"
SELECT id, team_id, role AS member_role, is_owner, permissions, organization_permissions,
accepted, payouts_split,
ordering, user_id
FROM team_members
WHERE team_id = ANY($1)
ORDER BY team_id, ordering;
",
&team_ids
)
.fetch(exec)
.try_fold(DashMap::new(), |acc: DashMap<i64, Vec<TeamMember>>, m| {
let member = TeamMember {
id: TeamMemberId(m.id),
team_id: TeamId(m.team_id),
role: m.member_role,
is_owner: m.is_owner,
permissions: ProjectPermissions::from_bits(m.permissions as u64)
.unwrap_or_default(),
organization_permissions: m
.organization_permissions
.map(|p| OrganizationPermissions::from_bits(p as u64).unwrap_or_default()),
accepted: m.accepted,
user_id: UserId(m.user_id),
payouts_split: m.payouts_split,
ordering: m.ordering,
};
acc.entry(m.team_id)
.or_default()
.push(member);
async move { Ok(acc) }
})
.await?;
Ok(teams)
},
).await?;
Ok(val.into_iter().flatten().collect())
}
pub async fn clear_cache(id: TeamId, redis: &RedisPool) -> Result<(), super::DatabaseError> {
let mut redis = redis.connect().await?;
redis.delete(TEAMS_NAMESPACE, id.0).await?;
Ok(())
}
/// Gets a team member from a user id and team id. Does not return pending members.
pub async fn get_from_user_id<'a, 'b, E>(
id: TeamId,
user_id: UserId,
executor: E,
) -> Result<Option<Self>, super::DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
Self::get_from_user_id_many(&[id], user_id, executor)
.await
.map(|x| x.into_iter().next())
}
/// Gets team members from user ids and team ids. Does not return pending members.
pub async fn get_from_user_id_many<'a, 'b, E>(
team_ids: &[TeamId],
user_id: UserId,
executor: E,
) -> Result<Vec<Self>, super::DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let team_ids_parsed: Vec<i64> = team_ids.iter().map(|x| x.0).collect();
let team_members = sqlx::query!(
"
SELECT id, team_id, role AS member_role, is_owner, permissions, organization_permissions,
accepted, payouts_split, role,
ordering, user_id
FROM team_members
WHERE (team_id = ANY($1) AND user_id = $2 AND accepted = TRUE)
ORDER BY ordering
",
&team_ids_parsed,
user_id as UserId
)
.fetch(executor)
.map_ok(|m| TeamMember {
id: TeamMemberId(m.id),
team_id: TeamId(m.team_id),
user_id,
role: m.role,
is_owner: m.is_owner,
permissions: ProjectPermissions::from_bits(m.permissions as u64)
.unwrap_or_default(),
organization_permissions: m
.organization_permissions
.map(|p| OrganizationPermissions::from_bits(p as u64).unwrap_or_default()),
accepted: m.accepted,
payouts_split: m.payouts_split,
ordering: m.ordering,
})
.try_collect::<Vec<TeamMember>>()
.await?;
Ok(team_members)
}
/// Gets a team member from a user id and team id, including pending members.
pub async fn get_from_user_id_pending<'a, 'b, E>(
id: TeamId,
user_id: UserId,
executor: E,
) -> Result<Option<Self>, super::DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let result = sqlx::query!(
"
SELECT id, team_id, role AS member_role, is_owner, permissions, organization_permissions,
accepted, payouts_split, role,
ordering, user_id
FROM team_members
WHERE (team_id = $1 AND user_id = $2)
ORDER BY ordering
",
id as TeamId,
user_id as UserId
)
.fetch_optional(executor)
.await?;
if let Some(m) = result {
Ok(Some(TeamMember {
id: TeamMemberId(m.id),
team_id: id,
user_id,
role: m.role,
is_owner: m.is_owner,
permissions: ProjectPermissions::from_bits(m.permissions as u64)
.unwrap_or_default(),
organization_permissions: m
.organization_permissions
.map(|p| OrganizationPermissions::from_bits(p as u64).unwrap_or_default()),
accepted: m.accepted,
payouts_split: m.payouts_split,
ordering: m.ordering,
}))
} else {
Ok(None)
}
}
pub async fn insert(
&self,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<(), sqlx::error::Error> {
sqlx::query!(
"
INSERT INTO team_members (
id, team_id, user_id, role, permissions, organization_permissions, is_owner, accepted, payouts_split
)
VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9
)
",
self.id as TeamMemberId,
self.team_id as TeamId,
self.user_id as UserId,
self.role,
self.permissions.bits() as i64,
self.organization_permissions.map(|p| p.bits() as i64),
self.is_owner,
self.accepted,
self.payouts_split
)
.execute(&mut **transaction)
.await?;
Ok(())
}
pub async fn delete<'a, 'b>(
id: TeamId,
user_id: UserId,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<(), super::DatabaseError> {
sqlx::query!(
"
DELETE FROM team_members
WHERE (team_id = $1 AND user_id = $2 AND NOT is_owner = TRUE)
",
id as TeamId,
user_id as UserId,
)
.execute(&mut **transaction)
.await?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub async fn edit_team_member(
id: TeamId,
user_id: UserId,
new_permissions: Option<ProjectPermissions>,
new_organization_permissions: Option<OrganizationPermissions>,
new_role: Option<String>,
new_accepted: Option<bool>,
new_payouts_split: Option<Decimal>,
new_ordering: Option<i64>,
new_is_owner: Option<bool>,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<(), super::DatabaseError> {
if let Some(permissions) = new_permissions {
sqlx::query!(
"
UPDATE team_members
SET permissions = $1
WHERE (team_id = $2 AND user_id = $3)
",
permissions.bits() as i64,
id as TeamId,
user_id as UserId,
)
.execute(&mut **transaction)
.await?;
}
if let Some(organization_permissions) = new_organization_permissions {
sqlx::query!(
"
UPDATE team_members
SET organization_permissions = $1
WHERE (team_id = $2 AND user_id = $3)
",
organization_permissions.bits() as i64,
id as TeamId,
user_id as UserId,
)
.execute(&mut **transaction)
.await?;
}
if let Some(role) = new_role {
sqlx::query!(
"
UPDATE team_members
SET role = $1
WHERE (team_id = $2 AND user_id = $3)
",
role,
id as TeamId,
user_id as UserId,
)
.execute(&mut **transaction)
.await?;
}
if let Some(accepted) = new_accepted {
if accepted {
sqlx::query!(
"
UPDATE team_members
SET accepted = TRUE
WHERE (team_id = $1 AND user_id = $2)
",
id as TeamId,
user_id as UserId,
)
.execute(&mut **transaction)
.await?;
}
}
if let Some(payouts_split) = new_payouts_split {
sqlx::query!(
"
UPDATE team_members
SET payouts_split = $1
WHERE (team_id = $2 AND user_id = $3)
",
payouts_split,
id as TeamId,
user_id as UserId,
)
.execute(&mut **transaction)
.await?;
}
if let Some(ordering) = new_ordering {
sqlx::query!(
"
UPDATE team_members
SET ordering = $1
WHERE (team_id = $2 AND user_id = $3)
",
ordering,
id as TeamId,
user_id as UserId,
)
.execute(&mut **transaction)
.await?;
}
if let Some(is_owner) = new_is_owner {
sqlx::query!(
"
UPDATE team_members
SET is_owner = $1
WHERE (team_id = $2 AND user_id = $3)
",
is_owner,
id as TeamId,
user_id as UserId,
)
.execute(&mut **transaction)
.await?;
}
Ok(())
}
pub async fn get_from_user_id_project<'a, 'b, E>(
id: ProjectId,
user_id: UserId,
allow_pending: bool,
executor: E,
) -> Result<Option<Self>, super::DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let accepted = if allow_pending {
vec![true, false]
} else {
vec![true]
};
let result = sqlx::query!(
"
SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.is_owner, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering
FROM mods m
INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 AND accepted = ANY($3)
WHERE m.id = $1
",
id as ProjectId,
user_id as UserId,
&accepted
)
.fetch_optional(executor)
.await?;
if let Some(m) = result {
Ok(Some(TeamMember {
id: TeamMemberId(m.id),
team_id: TeamId(m.team_id),
user_id,
role: m.role,
is_owner: m.is_owner,
permissions: ProjectPermissions::from_bits(m.permissions as u64)
.unwrap_or_default(),
organization_permissions: m
.organization_permissions
.map(|p| OrganizationPermissions::from_bits(p as u64).unwrap_or_default()),
accepted: m.accepted,
payouts_split: m.payouts_split,
ordering: m.ordering,
}))
} else {
Ok(None)
}
}
pub async fn get_from_user_id_organization<'a, 'b, E>(
id: OrganizationId,
user_id: UserId,
allow_pending: bool,
executor: E,
) -> Result<Option<Self>, super::DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let accepted = if allow_pending {
vec![true, false]
} else {
vec![true]
};
let result = sqlx::query!(
"
SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.is_owner, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering
FROM organizations o
INNER JOIN team_members tm ON tm.team_id = o.team_id AND user_id = $2 AND accepted = ANY($3)
WHERE o.id = $1
",
id as OrganizationId,
user_id as UserId,
&accepted
)
.fetch_optional(executor)
.await?;
if let Some(m) = result {
Ok(Some(TeamMember {
id: TeamMemberId(m.id),
team_id: TeamId(m.team_id),
user_id,
role: m.role,
is_owner: m.is_owner,
permissions: ProjectPermissions::from_bits(m.permissions as u64)
.unwrap_or_default(),
organization_permissions: m
.organization_permissions
.map(|p| OrganizationPermissions::from_bits(p as u64).unwrap_or_default()),
accepted: m.accepted,
payouts_split: m.payouts_split,
ordering: m.ordering,
}))
} else {
Ok(None)
}
}
pub async fn get_from_user_id_version<'a, 'b, E>(
id: VersionId,
user_id: UserId,
executor: E,
) -> Result<Option<Self>, super::DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let result = sqlx::query!(
"
SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.is_owner, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering, v.mod_id
FROM versions v
INNER JOIN mods m ON m.id = v.mod_id
INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.user_id = $2 AND tm.accepted = TRUE
WHERE v.id = $1
",
id as VersionId,
user_id as UserId
)
.fetch_optional(executor)
.await?;
if let Some(m) = result {
Ok(Some(TeamMember {
id: TeamMemberId(m.id),
team_id: TeamId(m.team_id),
user_id,
role: m.role,
is_owner: m.is_owner,
permissions: ProjectPermissions::from_bits(m.permissions as u64)
.unwrap_or_default(),
organization_permissions: m
.organization_permissions
.map(|p| OrganizationPermissions::from_bits(p as u64).unwrap_or_default()),
accepted: m.accepted,
payouts_split: m.payouts_split,
ordering: m.ordering,
}))
} else {
Ok(None)
}
}
// Gets both required members for checking permissions of an action on a project
// - project team member (a user's membership to a given project)
// - organization team member (a user's membership to a given organization that owns a given project)
pub async fn get_for_project_permissions<'a, 'b, E>(
project: &Project,
user_id: UserId,
executor: E,
) -> Result<(Option<Self>, Option<Self>), super::DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{
let project_team_member =
Self::get_from_user_id(project.team_id, user_id, executor).await?;
let organization =
Organization::get_associated_organization_project_id(project.id, executor).await?;
let organization_team_member = if let Some(organization) = &organization {
Self::get_from_user_id(organization.team_id, user_id, executor).await?
} else {
None
};
Ok((project_team_member, organization_team_member))
}
}

View File

@@ -0,0 +1,271 @@
use super::ids::*;
use crate::database::models::DatabaseError;
use crate::models::threads::{MessageBody, ThreadType};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
pub struct ThreadBuilder {
pub type_: ThreadType,
pub members: Vec<UserId>,
pub project_id: Option<ProjectId>,
pub report_id: Option<ReportId>,
}
#[derive(Clone, Serialize)]
pub struct Thread {
pub id: ThreadId,
pub project_id: Option<ProjectId>,
pub report_id: Option<ReportId>,
pub type_: ThreadType,
pub messages: Vec<ThreadMessage>,
pub members: Vec<UserId>,
}
pub struct ThreadMessageBuilder {
pub author_id: Option<UserId>,
pub body: MessageBody,
pub thread_id: ThreadId,
pub hide_identity: bool,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct ThreadMessage {
pub id: ThreadMessageId,
pub thread_id: ThreadId,
pub author_id: Option<UserId>,
pub body: MessageBody,
pub created: DateTime<Utc>,
pub hide_identity: bool,
}
impl ThreadMessageBuilder {
pub async fn insert(
&self,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<ThreadMessageId, DatabaseError> {
let thread_message_id = generate_thread_message_id(transaction).await?;
sqlx::query!(
"
INSERT INTO threads_messages (
id, author_id, body, thread_id, hide_identity
)
VALUES (
$1, $2, $3, $4, $5
)
",
thread_message_id as ThreadMessageId,
self.author_id.map(|x| x.0),
serde_json::value::to_value(self.body.clone())?,
self.thread_id as ThreadId,
self.hide_identity
)
.execute(&mut **transaction)
.await?;
Ok(thread_message_id)
}
}
impl ThreadBuilder {
pub async fn insert(
&self,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<ThreadId, DatabaseError> {
let thread_id = generate_thread_id(&mut *transaction).await?;
sqlx::query!(
"
INSERT INTO threads (
id, thread_type, mod_id, report_id
)
VALUES (
$1, $2, $3, $4
)
",
thread_id as ThreadId,
self.type_.as_str(),
self.project_id.map(|x| x.0),
self.report_id.map(|x| x.0),
)
.execute(&mut **transaction)
.await?;
let (thread_ids, members): (Vec<_>, Vec<_>) =
self.members.iter().map(|m| (thread_id.0, m.0)).unzip();
sqlx::query!(
"
INSERT INTO threads_members (
thread_id, user_id
)
SELECT * FROM UNNEST ($1::int8[], $2::int8[])
",
&thread_ids[..],
&members[..],
)
.execute(&mut **transaction)
.await?;
Ok(thread_id)
}
}
impl Thread {
pub async fn get<'a, E>(id: ThreadId, exec: E) -> Result<Option<Thread>, sqlx::Error>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{
Self::get_many(&[id], exec)
.await
.map(|x| x.into_iter().next())
}
pub async fn get_many<'a, E>(
thread_ids: &[ThreadId],
exec: E,
) -> Result<Vec<Thread>, sqlx::Error>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{
use futures::stream::TryStreamExt;
let thread_ids_parsed: Vec<i64> = thread_ids.iter().map(|x| x.0).collect();
let threads = sqlx::query!(
"
SELECT t.id, t.thread_type, t.mod_id, t.report_id,
ARRAY_AGG(DISTINCT tm.user_id) filter (where tm.user_id is not null) members,
JSONB_AGG(DISTINCT jsonb_build_object('id', tmsg.id, 'author_id', tmsg.author_id, 'thread_id', tmsg.thread_id, 'body', tmsg.body, 'created', tmsg.created, 'hide_identity', tmsg.hide_identity)) filter (where tmsg.id is not null) messages
FROM threads t
LEFT OUTER JOIN threads_messages tmsg ON tmsg.thread_id = t.id
LEFT OUTER JOIN threads_members tm ON tm.thread_id = t.id
WHERE t.id = ANY($1)
GROUP BY t.id
",
&thread_ids_parsed
)
.fetch(exec)
.map_ok(|x| Thread {
id: ThreadId(x.id),
project_id: x.mod_id.map(ProjectId),
report_id: x.report_id.map(ReportId),
type_: ThreadType::from_string(&x.thread_type),
messages: {
let mut messages: Vec<ThreadMessage> = serde_json::from_value(
x.messages.unwrap_or_default(),
)
.ok()
.unwrap_or_default();
messages.sort_by(|a, b| a.created.cmp(&b.created));
messages
},
members: x.members.unwrap_or_default().into_iter().map(UserId).collect(),
})
.try_collect::<Vec<Thread>>()
.await?;
Ok(threads)
}
pub async fn remove_full(
id: ThreadId,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Option<()>, sqlx::error::Error> {
sqlx::query!(
"
DELETE FROM threads_messages
WHERE thread_id = $1
",
id as ThreadId,
)
.execute(&mut **transaction)
.await?;
sqlx::query!(
"
DELETE FROM threads_members
WHERE thread_id = $1
",
id as ThreadId
)
.execute(&mut **transaction)
.await?;
sqlx::query!(
"
DELETE FROM threads
WHERE id = $1
",
id as ThreadId,
)
.execute(&mut **transaction)
.await?;
Ok(Some(()))
}
}
impl ThreadMessage {
pub async fn get<'a, E>(
id: ThreadMessageId,
exec: E,
) -> Result<Option<ThreadMessage>, sqlx::Error>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
Self::get_many(&[id], exec)
.await
.map(|x| x.into_iter().next())
}
pub async fn get_many<'a, E>(
message_ids: &[ThreadMessageId],
exec: E,
) -> Result<Vec<ThreadMessage>, sqlx::Error>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
use futures::stream::TryStreamExt;
let message_ids_parsed: Vec<i64> = message_ids.iter().map(|x| x.0).collect();
let messages = sqlx::query!(
"
SELECT tm.id, tm.author_id, tm.thread_id, tm.body, tm.created, tm.hide_identity
FROM threads_messages tm
WHERE tm.id = ANY($1)
",
&message_ids_parsed
)
.fetch(exec)
.map_ok(|x| ThreadMessage {
id: ThreadMessageId(x.id),
thread_id: ThreadId(x.thread_id),
author_id: x.author_id.map(UserId),
body: serde_json::from_value(x.body).unwrap_or(MessageBody::Deleted { private: false }),
created: x.created,
hide_identity: x.hide_identity,
})
.try_collect::<Vec<ThreadMessage>>()
.await?;
Ok(messages)
}
pub async fn remove_full(
id: ThreadMessageId,
private: bool,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Option<()>, sqlx::error::Error> {
sqlx::query!(
"
UPDATE threads_messages
SET body = $2
WHERE id = $1
",
id as ThreadMessageId,
serde_json::to_value(MessageBody::Deleted { private }).unwrap_or(serde_json::json!({}))
)
.execute(&mut **transaction)
.await?;
Ok(Some(()))
}
}

View File

@@ -0,0 +1,650 @@
use super::ids::{ProjectId, UserId};
use super::{CollectionId, ReportId, ThreadId};
use crate::database::models;
use crate::database::models::{DatabaseError, OrganizationId};
use crate::database::redis::RedisPool;
use crate::models::ids::base62_impl::{parse_base62, to_base62};
use crate::models::users::Badges;
use chrono::{DateTime, Utc};
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use std::fmt::{Debug, Display};
use std::hash::Hash;
const USERS_NAMESPACE: &str = "users";
const USER_USERNAMES_NAMESPACE: &str = "users_usernames";
const USERS_PROJECTS_NAMESPACE: &str = "users_projects";
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct User {
pub id: UserId,
pub github_id: Option<i64>,
pub discord_id: Option<i64>,
pub gitlab_id: Option<i64>,
pub google_id: Option<String>,
pub steam_id: Option<i64>,
pub microsoft_id: Option<String>,
pub password: Option<String>,
pub paypal_id: Option<String>,
pub paypal_country: Option<String>,
pub paypal_email: Option<String>,
pub venmo_handle: Option<String>,
pub stripe_customer_id: Option<String>,
pub totp_secret: Option<String>,
pub username: String,
pub email: Option<String>,
pub email_verified: bool,
pub avatar_url: Option<String>,
pub raw_avatar_url: Option<String>,
pub bio: Option<String>,
pub created: DateTime<Utc>,
pub role: String,
pub badges: Badges,
}
impl User {
pub async fn insert(
&self,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<(), sqlx::error::Error> {
sqlx::query!(
"
INSERT INTO users (
id, username, email,
avatar_url, raw_avatar_url, bio, created,
github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,
email_verified, password, paypal_id, paypal_country, paypal_email,
venmo_handle, stripe_customer_id
)
VALUES (
$1, $2, $3, $4, $5,
$6, $7,
$8, $9, $10, $11, $12, $13,
$14, $15, $16, $17, $18, $19, $20
)
",
self.id as UserId,
&self.username,
self.email.as_ref(),
self.avatar_url.as_ref(),
self.raw_avatar_url.as_ref(),
self.bio.as_ref(),
self.created,
self.github_id,
self.discord_id,
self.gitlab_id,
self.google_id,
self.steam_id,
self.microsoft_id,
self.email_verified,
self.password,
self.paypal_id,
self.paypal_country,
self.paypal_email,
self.venmo_handle,
self.stripe_customer_id
)
.execute(&mut **transaction)
.await?;
Ok(())
}
pub async fn get<'a, 'b, E>(
string: &str,
executor: E,
redis: &RedisPool,
) -> Result<Option<User>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
User::get_many(&[string], executor, redis)
.await
.map(|x| x.into_iter().next())
}
pub async fn get_id<'a, 'b, E>(
id: UserId,
executor: E,
redis: &RedisPool,
) -> Result<Option<User>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
User::get_many(&[crate::models::ids::UserId::from(id)], executor, redis)
.await
.map(|x| x.into_iter().next())
}
pub async fn get_many_ids<'a, E>(
user_ids: &[UserId],
exec: E,
redis: &RedisPool,
) -> Result<Vec<User>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
let ids = user_ids
.iter()
.map(|x| crate::models::ids::UserId::from(*x))
.collect::<Vec<_>>();
User::get_many(&ids, exec, redis).await
}
pub async fn get_many<'a, E, T: Display + Hash + Eq + PartialEq + Clone + Debug>(
users_strings: &[T],
exec: E,
redis: &RedisPool,
) -> Result<Vec<User>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres>,
{
use futures::TryStreamExt;
let val = redis.get_cached_keys_with_slug(
USERS_NAMESPACE,
USER_USERNAMES_NAMESPACE,
false,
users_strings,
|ids| async move {
let user_ids: Vec<i64> = ids
.iter()
.flat_map(|x| parse_base62(&x.to_string()).ok())
.map(|x| x as i64)
.collect();
let slugs = ids
.into_iter()
.map(|x| x.to_string().to_lowercase())
.collect::<Vec<_>>();
let users = sqlx::query!(
"
SELECT id, email,
avatar_url, raw_avatar_url, username, bio,
created, role, badges,
github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,
email_verified, password, totp_secret, paypal_id, paypal_country, paypal_email,
venmo_handle, stripe_customer_id
FROM users
WHERE id = ANY($1) OR LOWER(username) = ANY($2)
",
&user_ids,
&slugs,
)
.fetch(exec)
.try_fold(DashMap::new(), |acc, u| {
let user = User {
id: UserId(u.id),
github_id: u.github_id,
discord_id: u.discord_id,
gitlab_id: u.gitlab_id,
google_id: u.google_id,
steam_id: u.steam_id,
microsoft_id: u.microsoft_id,
email: u.email,
email_verified: u.email_verified,
avatar_url: u.avatar_url,
raw_avatar_url: u.raw_avatar_url,
username: u.username.clone(),
bio: u.bio,
created: u.created,
role: u.role,
badges: Badges::from_bits(u.badges as u64).unwrap_or_default(),
password: u.password,
paypal_id: u.paypal_id,
paypal_country: u.paypal_country,
paypal_email: u.paypal_email,
venmo_handle: u.venmo_handle,
stripe_customer_id: u.stripe_customer_id,
totp_secret: u.totp_secret,
};
acc.insert(u.id, (Some(u.username), user));
async move { Ok(acc) }
})
.await?;
Ok(users)
}).await?;
Ok(val)
}
pub async fn get_email<'a, E>(email: &str, exec: E) -> Result<Option<UserId>, sqlx::Error>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{
let user_pass = sqlx::query!(
"
SELECT id FROM users
WHERE email = $1
",
email
)
.fetch_optional(exec)
.await?;
Ok(user_pass.map(|x| UserId(x.id)))
}
pub async fn get_projects<'a, E>(
user_id: UserId,
exec: E,
redis: &RedisPool,
) -> Result<Vec<ProjectId>, DatabaseError>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{
use futures::stream::TryStreamExt;
let mut redis = redis.connect().await?;
let cached_projects = redis
.get_deserialized_from_json::<Vec<ProjectId>>(
USERS_PROJECTS_NAMESPACE,
&user_id.0.to_string(),
)
.await?;
if let Some(projects) = cached_projects {
return Ok(projects);
}
let db_projects = sqlx::query!(
"
SELECT m.id FROM mods m
INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.accepted = TRUE
WHERE tm.user_id = $1
ORDER BY m.downloads DESC
",
user_id as UserId,
)
.fetch(exec)
.map_ok(|m| ProjectId(m.id))
.try_collect::<Vec<ProjectId>>()
.await?;
redis
.set_serialized_to_json(USERS_PROJECTS_NAMESPACE, user_id.0, &db_projects, None)
.await?;
Ok(db_projects)
}
pub async fn get_organizations<'a, E>(
user_id: UserId,
exec: E,
) -> Result<Vec<OrganizationId>, sqlx::Error>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{
use futures::stream::TryStreamExt;
let orgs = sqlx::query!(
"
SELECT o.id FROM organizations o
INNER JOIN team_members tm ON tm.team_id = o.team_id AND tm.accepted = TRUE
WHERE tm.user_id = $1
",
user_id as UserId,
)
.fetch(exec)
.map_ok(|m| OrganizationId(m.id))
.try_collect::<Vec<OrganizationId>>()
.await?;
Ok(orgs)
}
pub async fn get_collections<'a, E>(
user_id: UserId,
exec: E,
) -> Result<Vec<CollectionId>, sqlx::Error>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{
use futures::stream::TryStreamExt;
let projects = sqlx::query!(
"
SELECT c.id FROM collections c
WHERE c.user_id = $1
",
user_id as UserId,
)
.fetch(exec)
.map_ok(|m| CollectionId(m.id))
.try_collect::<Vec<CollectionId>>()
.await?;
Ok(projects)
}
pub async fn get_follows<'a, E>(user_id: UserId, exec: E) -> Result<Vec<ProjectId>, sqlx::Error>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{
use futures::stream::TryStreamExt;
let projects = sqlx::query!(
"
SELECT mf.mod_id FROM mod_follows mf
WHERE mf.follower_id = $1
",
user_id as UserId,
)
.fetch(exec)
.map_ok(|m| ProjectId(m.mod_id))
.try_collect::<Vec<ProjectId>>()
.await?;
Ok(projects)
}
pub async fn get_reports<'a, E>(user_id: UserId, exec: E) -> Result<Vec<ReportId>, sqlx::Error>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{
use futures::stream::TryStreamExt;
let reports = sqlx::query!(
"
SELECT r.id FROM reports r
WHERE r.user_id = $1
",
user_id as UserId,
)
.fetch(exec)
.map_ok(|m| ReportId(m.id))
.try_collect::<Vec<ReportId>>()
.await?;
Ok(reports)
}
pub async fn get_backup_codes<'a, E>(
user_id: UserId,
exec: E,
) -> Result<Vec<String>, sqlx::Error>
where
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
{
use futures::stream::TryStreamExt;
let codes = sqlx::query!(
"
SELECT code FROM user_backup_codes
WHERE user_id = $1
",
user_id as UserId,
)
.fetch(exec)
.map_ok(|m| to_base62(m.code as u64))
.try_collect::<Vec<String>>()
.await?;
Ok(codes)
}
pub async fn clear_caches(
user_ids: &[(UserId, Option<String>)],
redis: &RedisPool,
) -> Result<(), DatabaseError> {
let mut redis = redis.connect().await?;
redis
.delete_many(user_ids.iter().flat_map(|(id, username)| {
[
(USERS_NAMESPACE, Some(id.0.to_string())),
(
USER_USERNAMES_NAMESPACE,
username.clone().map(|i| i.to_lowercase()),
),
]
}))
.await?;
Ok(())
}
pub async fn clear_project_cache(
user_ids: &[UserId],
redis: &RedisPool,
) -> Result<(), DatabaseError> {
let mut redis = redis.connect().await?;
redis
.delete_many(
user_ids
.iter()
.map(|id| (USERS_PROJECTS_NAMESPACE, Some(id.0.to_string()))),
)
.await?;
Ok(())
}
pub async fn remove(
id: UserId,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
redis: &RedisPool,
) -> Result<Option<()>, DatabaseError> {
let user = Self::get_id(id, &mut **transaction, redis).await?;
if let Some(delete_user) = user {
User::clear_caches(&[(id, Some(delete_user.username))], redis).await?;
let deleted_user: UserId = crate::models::users::DELETED_USER.into();
sqlx::query!(
"
UPDATE team_members
SET user_id = $1
WHERE (user_id = $2 AND is_owner = TRUE)
",
deleted_user as UserId,
id as UserId,
)
.execute(&mut **transaction)
.await?;
sqlx::query!(
"
UPDATE versions
SET author_id = $1
WHERE (author_id = $2)
",
deleted_user as UserId,
id as UserId,
)
.execute(&mut **transaction)
.await?;
use futures::TryStreamExt;
let notifications: Vec<i64> = sqlx::query!(
"
SELECT n.id FROM notifications n
WHERE n.user_id = $1
",
id as UserId,
)
.fetch(&mut **transaction)
.map_ok(|m| m.id)
.try_collect::<Vec<i64>>()
.await?;
sqlx::query!(
"
DELETE FROM notifications
WHERE user_id = $1
",
id as UserId,
)
.execute(&mut **transaction)
.await?;
sqlx::query!(
"
DELETE FROM notifications_actions
WHERE notification_id = ANY($1)
",
&notifications
)
.execute(&mut **transaction)
.await?;
let user_collections = sqlx::query!(
"
SELECT id
FROM collections
WHERE user_id = $1
",
id as UserId,
)
.fetch(&mut **transaction)
.map_ok(|x| CollectionId(x.id))
.try_collect::<Vec<_>>()
.await?;
for collection_id in user_collections {
models::Collection::remove(collection_id, transaction, redis).await?;
}
let report_threads = sqlx::query!(
"
SELECT t.id
FROM threads t
INNER JOIN reports r ON t.report_id = r.id AND (r.user_id = $1 OR r.reporter = $1)
WHERE report_id IS NOT NULL
",
id as UserId,
)
.fetch(&mut **transaction)
.map_ok(|x| ThreadId(x.id))
.try_collect::<Vec<_>>()
.await?;
for thread_id in report_threads {
models::Thread::remove_full(thread_id, transaction).await?;
}
sqlx::query!(
"
DELETE FROM reports
WHERE user_id = $1 OR reporter = $1
",
id as UserId,
)
.execute(&mut **transaction)
.await?;
sqlx::query!(
"
DELETE FROM mod_follows
WHERE follower_id = $1
",
id as UserId,
)
.execute(&mut **transaction)
.await?;
sqlx::query!(
"
DELETE FROM team_members
WHERE user_id = $1
",
id as UserId,
)
.execute(&mut **transaction)
.await?;
sqlx::query!(
"
DELETE FROM payouts_values
WHERE user_id = $1
",
id as UserId,
)
.execute(&mut **transaction)
.await?;
sqlx::query!(
"
DELETE FROM payouts
WHERE user_id = $1
",
id as UserId,
)
.execute(&mut **transaction)
.await?;
sqlx::query!(
r#"
UPDATE threads_messages
SET body = '{"type": "deleted"}', author_id = $2
WHERE author_id = $1
"#,
id as UserId,
deleted_user as UserId,
)
.execute(&mut **transaction)
.await?;
sqlx::query!(
"
DELETE FROM threads_members
WHERE user_id = $1
",
id as UserId,
)
.execute(&mut **transaction)
.await?;
sqlx::query!(
"
DELETE FROM sessions
WHERE user_id = $1
",
id as UserId,
)
.execute(&mut **transaction)
.await?;
sqlx::query!(
"
DELETE FROM pats
WHERE user_id = $1
",
id as UserId,
)
.execute(&mut **transaction)
.await?;
sqlx::query!(
"
DELETE FROM user_backup_codes
WHERE user_id = $1
",
id as UserId,
)
.execute(&mut **transaction)
.await?;
sqlx::query!(
"
DELETE FROM users
WHERE id = $1
",
id as UserId,
)
.execute(&mut **transaction)
.await?;
Ok(Some(()))
} else {
Ok(None)
}
}
}

View File

@@ -0,0 +1,130 @@
use crate::database::models::{DatabaseError, ProductPriceId, UserId, UserSubscriptionId};
use crate::models::billing::{PriceDuration, SubscriptionMetadata, SubscriptionStatus};
use chrono::{DateTime, Utc};
use itertools::Itertools;
use std::convert::{TryFrom, TryInto};
pub struct UserSubscriptionItem {
pub id: UserSubscriptionId,
pub user_id: UserId,
pub price_id: ProductPriceId,
pub interval: PriceDuration,
pub created: DateTime<Utc>,
pub status: SubscriptionStatus,
pub metadata: Option<SubscriptionMetadata>,
}
struct UserSubscriptionResult {
id: i64,
user_id: i64,
price_id: i64,
interval: String,
pub created: DateTime<Utc>,
pub status: String,
pub metadata: serde_json::Value,
}
macro_rules! select_user_subscriptions_with_predicate {
($predicate:tt, $param:ident) => {
sqlx::query_as!(
UserSubscriptionResult,
r#"
SELECT
us.id, us.user_id, us.price_id, us.interval, us.created, us.status, us.metadata
FROM users_subscriptions us
"#
+ $predicate,
$param
)
};
}
impl TryFrom<UserSubscriptionResult> for UserSubscriptionItem {
type Error = serde_json::Error;
fn try_from(r: UserSubscriptionResult) -> Result<Self, Self::Error> {
Ok(UserSubscriptionItem {
id: UserSubscriptionId(r.id),
user_id: UserId(r.user_id),
price_id: ProductPriceId(r.price_id),
interval: PriceDuration::from_string(&r.interval),
created: r.created,
status: SubscriptionStatus::from_string(&r.status),
metadata: serde_json::from_value(r.metadata)?,
})
}
}
impl UserSubscriptionItem {
pub async fn get(
id: UserSubscriptionId,
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<Option<UserSubscriptionItem>, DatabaseError> {
Ok(Self::get_many(&[id], exec).await?.into_iter().next())
}
pub async fn get_many(
ids: &[UserSubscriptionId],
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<Vec<UserSubscriptionItem>, DatabaseError> {
let ids = ids.iter().map(|id| id.0).collect_vec();
let ids_ref: &[i64] = &ids;
let results =
select_user_subscriptions_with_predicate!("WHERE us.id = ANY($1::bigint[])", ids_ref)
.fetch_all(exec)
.await?;
Ok(results
.into_iter()
.map(|r| r.try_into())
.collect::<Result<Vec<_>, serde_json::Error>>()?)
}
pub async fn get_all_user(
user_id: UserId,
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<Vec<UserSubscriptionItem>, DatabaseError> {
let user_id = user_id.0;
let results = select_user_subscriptions_with_predicate!("WHERE us.user_id = $1", user_id)
.fetch_all(exec)
.await?;
Ok(results
.into_iter()
.map(|r| r.try_into())
.collect::<Result<Vec<_>, serde_json::Error>>()?)
}
pub async fn upsert(
&self,
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<(), DatabaseError> {
sqlx::query!(
"
INSERT INTO users_subscriptions (
id, user_id, price_id, interval, created, status, metadata
)
VALUES (
$1, $2, $3, $4, $5, $6, $7
)
ON CONFLICT (id)
DO UPDATE
SET interval = EXCLUDED.interval,
status = EXCLUDED.status,
price_id = EXCLUDED.price_id,
metadata = EXCLUDED.metadata
",
self.id.0,
self.user_id.0,
self.price_id.0,
self.interval.as_str(),
self.created,
self.status.as_str(),
serde_json::to_value(&self.metadata)?,
)
.execute(&mut **transaction)
.await?;
Ok(())
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,46 @@
use log::info;
use sqlx::migrate::MigrateDatabase;
use sqlx::postgres::{PgPool, PgPoolOptions};
use sqlx::{Connection, PgConnection, Postgres};
use std::time::Duration;
pub async fn connect() -> Result<PgPool, sqlx::Error> {
info!("Initializing database connection");
let database_url = dotenvy::var("DATABASE_URL").expect("`DATABASE_URL` not in .env");
let pool = PgPoolOptions::new()
.min_connections(
dotenvy::var("DATABASE_MIN_CONNECTIONS")
.ok()
.and_then(|x| x.parse().ok())
.unwrap_or(0),
)
.max_connections(
dotenvy::var("DATABASE_MAX_CONNECTIONS")
.ok()
.and_then(|x| x.parse().ok())
.unwrap_or(16),
)
.max_lifetime(Some(Duration::from_secs(60 * 60)))
.connect(&database_url)
.await?;
Ok(pool)
}
pub async fn check_for_migrations() -> Result<(), sqlx::Error> {
let uri = dotenvy::var("DATABASE_URL").expect("`DATABASE_URL` not in .env");
let uri = uri.as_str();
if !Postgres::database_exists(uri).await? {
info!("Creating database...");
Postgres::create_database(uri).await?;
}
info!("Applying migrations...");
let mut conn: PgConnection = PgConnection::connect(uri).await?;
sqlx::migrate!()
.run(&mut conn)
.await
.expect("Error while running database migrations!");
Ok(())
}

View File

@@ -0,0 +1,552 @@
use super::models::DatabaseError;
use crate::models::ids::base62_impl::{parse_base62, to_base62};
use chrono::{TimeZone, Utc};
use dashmap::DashMap;
use deadpool_redis::{Config, Runtime};
use redis::{cmd, Cmd, ExistenceCheck, SetExpiry, SetOptions};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt::{Debug, Display};
use std::future::Future;
use std::hash::Hash;
use std::pin::Pin;
use std::time::Duration;
const DEFAULT_EXPIRY: i64 = 60 * 60 * 12; // 12 hours
const ACTUAL_EXPIRY: i64 = 60 * 30; // 30 minutes
#[derive(Clone)]
pub struct RedisPool {
pub pool: deadpool_redis::Pool,
meta_namespace: String,
}
pub struct RedisConnection {
pub connection: deadpool_redis::Connection,
meta_namespace: String,
}
impl RedisPool {
// initiate a new redis pool
// testing pool uses a hashmap to mimic redis behaviour for very small data sizes (ie: tests)
// PANICS: production pool will panic if redis url is not set
pub fn new(meta_namespace: Option<String>) -> Self {
let redis_pool = Config::from_url(dotenvy::var("REDIS_URL").expect("Redis URL not set"))
.builder()
.expect("Error building Redis pool")
.max_size(
dotenvy::var("DATABASE_MAX_CONNECTIONS")
.ok()
.and_then(|x| x.parse().ok())
.unwrap_or(10000),
)
.runtime(Runtime::Tokio1)
.build()
.expect("Redis connection failed");
RedisPool {
pool: redis_pool,
meta_namespace: meta_namespace.unwrap_or("".to_string()),
}
}
pub async fn connect(&self) -> Result<RedisConnection, DatabaseError> {
Ok(RedisConnection {
connection: self.pool.get().await?,
meta_namespace: self.meta_namespace.clone(),
})
}
pub async fn get_cached_keys<F, Fut, T, K>(
&self,
namespace: &str,
keys: &[K],
closure: F,
) -> Result<Vec<T>, DatabaseError>
where
F: FnOnce(Vec<K>) -> Fut,
Fut: Future<Output = Result<DashMap<K, T>, DatabaseError>>,
T: Serialize + DeserializeOwned,
K: Display + Hash + Eq + PartialEq + Clone + DeserializeOwned + Serialize + Debug,
{
Ok(self
.get_cached_keys_raw(namespace, keys, closure)
.await?
.into_iter()
.map(|x| x.1)
.collect())
}
pub async fn get_cached_keys_raw<F, Fut, T, K>(
&self,
namespace: &str,
keys: &[K],
closure: F,
) -> Result<HashMap<K, T>, DatabaseError>
where
F: FnOnce(Vec<K>) -> Fut,
Fut: Future<Output = Result<DashMap<K, T>, DatabaseError>>,
T: Serialize + DeserializeOwned,
K: Display + Hash + Eq + PartialEq + Clone + DeserializeOwned + Serialize + Debug,
{
self.get_cached_keys_raw_with_slug(namespace, None, false, keys, |ids| async move {
Ok(closure(ids)
.await?
.into_iter()
.map(|(key, val)| (key, (None::<String>, val)))
.collect())
})
.await
}
pub async fn get_cached_keys_with_slug<F, Fut, T, I, K, S>(
&self,
namespace: &str,
slug_namespace: &str,
case_sensitive: bool,
keys: &[I],
closure: F,
) -> Result<Vec<T>, DatabaseError>
where
F: FnOnce(Vec<I>) -> Fut,
Fut: Future<Output = Result<DashMap<K, (Option<S>, T)>, DatabaseError>>,
T: Serialize + DeserializeOwned,
I: Display + Hash + Eq + PartialEq + Clone + Debug,
K: Display + Hash + Eq + PartialEq + Clone + DeserializeOwned + Serialize,
S: Display + Clone + DeserializeOwned + Serialize + Debug,
{
Ok(self
.get_cached_keys_raw_with_slug(
namespace,
Some(slug_namespace),
case_sensitive,
keys,
closure,
)
.await?
.into_iter()
.map(|x| x.1)
.collect())
}
pub async fn get_cached_keys_raw_with_slug<F, Fut, T, I, K, S>(
&self,
namespace: &str,
slug_namespace: Option<&str>,
case_sensitive: bool,
keys: &[I],
closure: F,
) -> Result<HashMap<K, T>, DatabaseError>
where
F: FnOnce(Vec<I>) -> Fut,
Fut: Future<Output = Result<DashMap<K, (Option<S>, T)>, DatabaseError>>,
T: Serialize + DeserializeOwned,
I: Display + Hash + Eq + PartialEq + Clone + Debug,
K: Display + Hash + Eq + PartialEq + Clone + DeserializeOwned + Serialize,
S: Display + Clone + DeserializeOwned + Serialize + Debug,
{
let connection = self.connect().await?.connection;
let ids = keys
.iter()
.map(|x| (x.to_string(), x.clone()))
.collect::<DashMap<String, I>>();
if ids.is_empty() {
return Ok(HashMap::new());
}
let get_cached_values =
|ids: DashMap<String, I>, mut connection: deadpool_redis::Connection| async move {
let slug_ids = if let Some(slug_namespace) = slug_namespace {
cmd("MGET")
.arg(
ids.iter()
.map(|x| {
format!(
"{}_{slug_namespace}:{}",
self.meta_namespace,
if case_sensitive {
x.value().to_string()
} else {
x.value().to_string().to_lowercase()
}
)
})
.collect::<Vec<_>>(),
)
.query_async::<_, Vec<Option<String>>>(&mut connection)
.await?
.into_iter()
.flatten()
.collect::<Vec<_>>()
} else {
Vec::new()
};
let cached_values = cmd("MGET")
.arg(
ids.iter()
.map(|x| x.value().to_string())
.chain(ids.iter().filter_map(|x| {
parse_base62(&x.value().to_string())
.ok()
.map(|x| x.to_string())
}))
.chain(slug_ids)
.map(|x| format!("{}_{namespace}:{x}", self.meta_namespace))
.collect::<Vec<_>>(),
)
.query_async::<_, Vec<Option<String>>>(&mut connection)
.await?
.into_iter()
.filter_map(|x| {
x.and_then(|val| serde_json::from_str::<RedisValue<T, K, S>>(&val).ok())
.map(|val| (val.key.clone(), val))
})
.collect::<HashMap<_, _>>();
Ok::<_, DatabaseError>((cached_values, connection, ids))
};
let current_time = Utc::now();
let mut expired_values = HashMap::new();
let (cached_values_raw, mut connection, ids) = get_cached_values(ids, connection).await?;
let mut cached_values = cached_values_raw
.into_iter()
.filter_map(|(key, val)| {
if Utc.timestamp_opt(val.iat + ACTUAL_EXPIRY, 0).unwrap() < current_time {
expired_values.insert(val.key.to_string(), val);
None
} else {
let key_str = val.key.to_string();
ids.remove(&key_str);
if let Ok(value) = key_str.parse::<u64>() {
let base62 = to_base62(value);
ids.remove(&base62);
}
if let Some(ref alias) = val.alias {
ids.remove(&alias.to_string());
}
Some((key, val))
}
})
.collect::<HashMap<_, _>>();
let subscribe_ids = DashMap::new();
if !ids.is_empty() {
let mut pipe = redis::pipe();
let fetch_ids = ids.iter().map(|x| x.key().clone()).collect::<Vec<_>>();
fetch_ids.iter().for_each(|key| {
pipe.atomic().set_options(
format!("{}_{namespace}:{}/lock", self.meta_namespace, key),
100,
SetOptions::default()
.get(true)
.conditional_set(ExistenceCheck::NX)
.with_expiration(SetExpiry::EX(60)),
);
});
let results = pipe
.query_async::<_, Vec<Option<i32>>>(&mut connection)
.await?;
for (idx, key) in fetch_ids.into_iter().enumerate() {
if let Some(locked) = results.get(idx) {
if locked.is_none() {
continue;
}
}
if let Some((key, raw_key)) = ids.remove(&key) {
if let Some(val) = expired_values.remove(&key) {
if let Some(ref alias) = val.alias {
ids.remove(&alias.to_string());
}
if let Ok(value) = val.key.to_string().parse::<u64>() {
let base62 = to_base62(value);
ids.remove(&base62);
}
cached_values.insert(val.key.clone(), val);
} else {
subscribe_ids.insert(key, raw_key);
}
}
}
}
#[allow(clippy::type_complexity)]
let mut fetch_tasks: Vec<
Pin<Box<dyn Future<Output = Result<HashMap<K, RedisValue<T, K, S>>, DatabaseError>>>>,
> = Vec::new();
if !ids.is_empty() {
fetch_tasks.push(Box::pin(async {
let fetch_ids = ids.iter().map(|x| x.value().clone()).collect::<Vec<_>>();
let vals = closure(fetch_ids).await?;
let mut return_values = HashMap::new();
let mut pipe = redis::pipe();
if !vals.is_empty() {
for (key, (slug, value)) in vals {
let value = RedisValue {
key: key.clone(),
iat: Utc::now().timestamp(),
val: value,
alias: slug.clone(),
};
pipe.atomic().set_ex(
format!("{}_{namespace}:{key}", self.meta_namespace),
serde_json::to_string(&value)?,
DEFAULT_EXPIRY as u64,
);
if let Some(slug) = slug {
ids.remove(&slug.to_string());
if let Some(slug_namespace) = slug_namespace {
let actual_slug = if case_sensitive {
slug.to_string()
} else {
slug.to_string().to_lowercase()
};
pipe.atomic().set_ex(
format!(
"{}_{slug_namespace}:{}",
self.meta_namespace, actual_slug
),
key.to_string(),
DEFAULT_EXPIRY as u64,
);
pipe.atomic().del(format!(
"{}_{namespace}:{}/lock",
self.meta_namespace, actual_slug
));
}
}
let key_str = key.to_string();
ids.remove(&key_str);
if let Ok(value) = key_str.parse::<u64>() {
let base62 = to_base62(value);
ids.remove(&base62);
pipe.atomic()
.del(format!("{}_{namespace}:{base62}/lock", self.meta_namespace));
}
pipe.atomic()
.del(format!("{}_{namespace}:{key}/lock", self.meta_namespace));
return_values.insert(key, value);
}
}
for (key, _) in ids {
pipe.atomic()
.del(format!("{}_{namespace}:{key}/lock", self.meta_namespace));
}
pipe.query_async(&mut connection).await?;
Ok(return_values)
}));
}
if !subscribe_ids.is_empty() {
fetch_tasks.push(Box::pin(async {
let mut connection = self.pool.get().await?;
let mut interval = tokio::time::interval(Duration::from_millis(100));
let start = Utc::now();
loop {
let results = cmd("MGET")
.arg(
subscribe_ids
.iter()
.map(|x| {
format!("{}_{namespace}:{}/lock", self.meta_namespace, x.key())
})
.collect::<Vec<_>>(),
)
.query_async::<_, Vec<Option<String>>>(&mut connection)
.await?;
if results.into_iter().all(|x| x.is_none()) {
break;
}
if (Utc::now() - start) > chrono::Duration::seconds(5) {
return Err(DatabaseError::CacheTimeout);
}
interval.tick().await;
}
let (return_values, _, _) = get_cached_values(subscribe_ids, connection).await?;
Ok(return_values)
}));
}
if !fetch_tasks.is_empty() {
for map in futures::future::try_join_all(fetch_tasks).await? {
for (key, value) in map {
cached_values.insert(key, value);
}
}
}
Ok(cached_values.into_iter().map(|x| (x.0, x.1.val)).collect())
}
}
impl RedisConnection {
pub async fn set(
&mut self,
namespace: &str,
id: &str,
data: &str,
expiry: Option<i64>,
) -> Result<(), DatabaseError> {
let mut cmd = cmd("SET");
redis_args(
&mut cmd,
vec![
format!("{}_{}:{}", self.meta_namespace, namespace, id),
data.to_string(),
"EX".to_string(),
expiry.unwrap_or(DEFAULT_EXPIRY).to_string(),
]
.as_slice(),
);
redis_execute(&mut cmd, &mut self.connection).await?;
Ok(())
}
pub async fn set_serialized_to_json<Id, D>(
&mut self,
namespace: &str,
id: Id,
data: D,
expiry: Option<i64>,
) -> Result<(), DatabaseError>
where
Id: Display,
D: serde::Serialize,
{
self.set(
namespace,
&id.to_string(),
&serde_json::to_string(&data)?,
expiry,
)
.await
}
pub async fn get(
&mut self,
namespace: &str,
id: &str,
) -> Result<Option<String>, DatabaseError> {
let mut cmd = cmd("GET");
redis_args(
&mut cmd,
vec![format!("{}_{}:{}", self.meta_namespace, namespace, id)].as_slice(),
);
let res = redis_execute(&mut cmd, &mut self.connection).await?;
Ok(res)
}
pub async fn get_deserialized_from_json<R>(
&mut self,
namespace: &str,
id: &str,
) -> Result<Option<R>, DatabaseError>
where
R: for<'a> serde::Deserialize<'a>,
{
Ok(self
.get(namespace, id)
.await?
.and_then(|x| serde_json::from_str(&x).ok()))
}
pub async fn delete<T1>(&mut self, namespace: &str, id: T1) -> Result<(), DatabaseError>
where
T1: Display,
{
let mut cmd = cmd("DEL");
redis_args(
&mut cmd,
vec![format!("{}_{}:{}", self.meta_namespace, namespace, id)].as_slice(),
);
redis_execute(&mut cmd, &mut self.connection).await?;
Ok(())
}
pub async fn delete_many(
&mut self,
iter: impl IntoIterator<Item = (&str, Option<String>)>,
) -> Result<(), DatabaseError> {
let mut cmd = cmd("DEL");
let mut any = false;
for (namespace, id) in iter {
if let Some(id) = id {
redis_args(
&mut cmd,
[format!("{}_{}:{}", self.meta_namespace, namespace, id)].as_slice(),
);
any = true;
}
}
if any {
redis_execute(&mut cmd, &mut self.connection).await?;
}
Ok(())
}
}
#[derive(Serialize, Deserialize)]
pub struct RedisValue<T, K, S> {
key: K,
#[serde(skip_serializing_if = "Option::is_none")]
alias: Option<S>,
iat: i64,
val: T,
}
pub fn redis_args(cmd: &mut Cmd, args: &[String]) {
for arg in args {
cmd.arg(arg);
}
}
pub async fn redis_execute<T>(
cmd: &mut Cmd,
redis: &mut deadpool_redis::Connection,
) -> Result<T, deadpool_redis::PoolError>
where
T: redis::FromRedisValue,
{
let res = cmd.query_async::<_, T>(redis).await?;
Ok(res)
}