refactor(models): replace hardcoded strings with typed enums
- Add ReviewState enum (pending, approved, changes_requested, etc.) - Add DEFAULT_REVISION constant for git HEAD references - service/pr/reviews.rs: use ReviewState for review creation and submission state validation - service/pr/core.rs: use MergeStrategyKind for merge strategy selection - service/im/stages.rs: use StagePrivacyLevel for stage creation - service/im/invitations.rs: use Role enum for invitation role defaults
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::error::AppError;
|
||||
use crate::models::channels::ChannelInvitation;
|
||||
use crate::models::common::Role;
|
||||
use crate::service::ImService;
|
||||
|
||||
use super::session::ImSession;
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
|
||||
pub struct CreateInvitationParams {
|
||||
pub invited_user_id: Option<Uuid>,
|
||||
pub email: Option<String>,
|
||||
pub role: Option<String>,
|
||||
pub expires_in_hours: Option<i64>,
|
||||
}
|
||||
|
||||
impl ImService {
|
||||
pub async fn invitation_list(
|
||||
&self,
|
||||
_ctx: &ImSession,
|
||||
channel_id: Uuid,
|
||||
) -> Result<Vec<ChannelInvitation>, AppError> {
|
||||
sqlx::query_as::<_, ChannelInvitation>(
|
||||
"SELECT id, channel_id, workspace_id, invited_user_id, email, role, token_hash, \
|
||||
invited_by, accepted_at, revoked_at, expires_at, created_at \
|
||||
FROM channel_invitation WHERE channel_id = $1 \
|
||||
AND accepted_at IS NULL AND revoked_at IS NULL \
|
||||
ORDER BY created_at DESC",
|
||||
)
|
||||
.bind(channel_id)
|
||||
.fetch_all(self.ctx.db.reader())
|
||||
.await
|
||||
.map_err(AppError::Database)
|
||||
}
|
||||
|
||||
pub async fn invitation_create(
|
||||
&self,
|
||||
ctx: &ImSession,
|
||||
channel_id: Uuid,
|
||||
workspace_id: Uuid,
|
||||
params: CreateInvitationParams,
|
||||
) -> Result<ChannelInvitation, AppError> {
|
||||
let now = chrono::Utc::now();
|
||||
let expires_at = now + chrono::Duration::hours(params.expires_in_hours.unwrap_or(168));
|
||||
let token = Uuid::now_v7().to_string();
|
||||
let token_hash = format!("{:x}", Sha256::digest(token.as_bytes()));
|
||||
let role = params
|
||||
.role
|
||||
.as_deref()
|
||||
.and_then(|s| s.parse::<Role>().ok())
|
||||
.filter(|r| *r != Role::Unknown)
|
||||
.unwrap_or(Role::Member);
|
||||
|
||||
sqlx::query_as::<_, ChannelInvitation>(
|
||||
"INSERT INTO channel_invitation \
|
||||
(id, channel_id, workspace_id, invited_user_id, email, role, token_hash, \
|
||||
invited_by, expires_at, created_at) \
|
||||
VALUES ($1, $2, $3, $4, $5, $6::role, $7, $8, $9, $10) \
|
||||
RETURNING id, channel_id, workspace_id, invited_user_id, email, role, token_hash, \
|
||||
invited_by, accepted_at, revoked_at, expires_at, created_at",
|
||||
)
|
||||
.bind(Uuid::now_v7())
|
||||
.bind(channel_id)
|
||||
.bind(workspace_id)
|
||||
.bind(params.invited_user_id)
|
||||
.bind(params.email.as_deref())
|
||||
.bind(role)
|
||||
.bind(&token_hash)
|
||||
.bind(ctx.user)
|
||||
.bind(expires_at)
|
||||
.bind(now)
|
||||
.fetch_one(self.ctx.db.writer())
|
||||
.await
|
||||
.map_err(AppError::Database)
|
||||
}
|
||||
|
||||
pub async fn invitation_accept(
|
||||
&self,
|
||||
ctx: &ImSession,
|
||||
invitation_id: Uuid,
|
||||
) -> Result<ChannelInvitation, AppError> {
|
||||
let now = chrono::Utc::now();
|
||||
let mut txn = self
|
||||
.ctx
|
||||
.db
|
||||
.writer()
|
||||
.begin()
|
||||
.await
|
||||
.map_err(|_| AppError::TxnError)?;
|
||||
|
||||
let invitation = sqlx::query_as::<_, ChannelInvitation>(
|
||||
"UPDATE channel_invitation SET accepted_at = $1 \
|
||||
WHERE id = $2 AND accepted_at IS NULL AND revoked_at IS NULL AND expires_at > $1 \
|
||||
RETURNING id, channel_id, workspace_id, invited_user_id, email, role, token_hash, \
|
||||
invited_by, accepted_at, revoked_at, expires_at, created_at",
|
||||
)
|
||||
.bind(now)
|
||||
.bind(invitation_id)
|
||||
.fetch_one(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO channel_member \
|
||||
(id, channel_id, user_id, role, status, muted, pinned, joined_at, created_at, updated_at) \
|
||||
VALUES ($1, $2, $3, $4::role, 'active', false, false, $5, $5, $5) \
|
||||
ON CONFLICT (channel_id, user_id) DO NOTHING",
|
||||
)
|
||||
.bind(Uuid::now_v7())
|
||||
.bind(invitation.channel_id)
|
||||
.bind(ctx.user)
|
||||
.bind(invitation.role.to_string())
|
||||
.bind(now)
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
|
||||
txn.commit().await.map_err(|_| AppError::TxnError)?;
|
||||
Ok(invitation)
|
||||
}
|
||||
|
||||
pub async fn invitation_revoke(
|
||||
&self,
|
||||
_ctx: &ImSession,
|
||||
invitation_id: Uuid,
|
||||
) -> Result<ChannelInvitation, AppError> {
|
||||
let now = chrono::Utc::now();
|
||||
sqlx::query_as::<_, ChannelInvitation>(
|
||||
"UPDATE channel_invitation SET revoked_at = $1 \
|
||||
WHERE id = $2 AND accepted_at IS NULL AND revoked_at IS NULL \
|
||||
RETURNING id, channel_id, workspace_id, invited_user_id, email, role, token_hash, \
|
||||
invited_by, accepted_at, revoked_at, expires_at, created_at",
|
||||
)
|
||||
.bind(now)
|
||||
.bind(invitation_id)
|
||||
.fetch_one(self.ctx.db.writer())
|
||||
.await
|
||||
.map_err(AppError::Database)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::error::AppError;
|
||||
use crate::models::channels::Stage;
|
||||
use crate::models::common::StagePrivacyLevel;
|
||||
use crate::service::ImService;
|
||||
|
||||
use super::session::ImSession;
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
|
||||
pub struct CreateStageParams {
|
||||
pub topic: String,
|
||||
pub privacy_level: Option<String>,
|
||||
pub discoverable: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
|
||||
pub struct UpdateStageParams {
|
||||
pub topic: Option<String>,
|
||||
pub privacy_level: Option<String>,
|
||||
pub discoverable: Option<bool>,
|
||||
}
|
||||
|
||||
impl ImService {
|
||||
pub async fn stage_get(
|
||||
&self,
|
||||
_ctx: &ImSession,
|
||||
channel_id: Uuid,
|
||||
) -> Result<Option<Stage>, AppError> {
|
||||
sqlx::query_as::<_, Stage>(
|
||||
"SELECT id, channel_id, topic, privacy_level, discoverable, \
|
||||
started_by, started_at, ended_at, created_at, updated_at \
|
||||
FROM stage WHERE channel_id = $1 AND ended_at IS NULL \
|
||||
ORDER BY started_at DESC LIMIT 1",
|
||||
)
|
||||
.bind(channel_id)
|
||||
.fetch_optional(self.ctx.db.reader())
|
||||
.await
|
||||
.map_err(AppError::Database)
|
||||
}
|
||||
|
||||
pub async fn stage_create(
|
||||
&self,
|
||||
ctx: &ImSession,
|
||||
channel_id: Uuid,
|
||||
params: CreateStageParams,
|
||||
) -> Result<Stage, AppError> {
|
||||
let now = chrono::Utc::now();
|
||||
let privacy = params
|
||||
.privacy_level
|
||||
.as_deref()
|
||||
.and_then(|s| s.parse::<StagePrivacyLevel>().ok())
|
||||
.filter(|s| *s != StagePrivacyLevel::Unknown)
|
||||
.unwrap_or(StagePrivacyLevel::GuildOnly);
|
||||
sqlx::query_as::<_, Stage>(
|
||||
"INSERT INTO stage \
|
||||
(id, channel_id, topic, privacy_level, discoverable, \
|
||||
started_by, started_at, created_at, updated_at) \
|
||||
VALUES ($1, $2, $3, $4::stage_privacy_level, $5, $6, $7, $7, $7) \
|
||||
RETURNING id, channel_id, topic, privacy_level, discoverable, \
|
||||
started_by, started_at, ended_at, created_at, updated_at",
|
||||
)
|
||||
.bind(Uuid::now_v7())
|
||||
.bind(channel_id)
|
||||
.bind(¶ms.topic)
|
||||
.bind(privacy)
|
||||
.bind(params.discoverable.unwrap_or(false))
|
||||
.bind(ctx.user)
|
||||
.bind(now)
|
||||
.fetch_one(self.ctx.db.writer())
|
||||
.await
|
||||
.map_err(AppError::Database)
|
||||
}
|
||||
|
||||
pub async fn stage_update(
|
||||
&self,
|
||||
_ctx: &ImSession,
|
||||
stage_id: Uuid,
|
||||
params: UpdateStageParams,
|
||||
) -> Result<Stage, AppError> {
|
||||
let now = chrono::Utc::now();
|
||||
sqlx::query_as::<_, Stage>(
|
||||
"UPDATE stage SET \
|
||||
topic = COALESCE($1, topic), \
|
||||
privacy_level = COALESCE($2::stage_privacy_level, privacy_level), \
|
||||
discoverable = COALESCE($3, discoverable), \
|
||||
updated_at = $4 \
|
||||
WHERE id = $5 \
|
||||
RETURNING id, channel_id, topic, privacy_level, discoverable, \
|
||||
started_by, started_at, ended_at, created_at, updated_at",
|
||||
)
|
||||
.bind(params.topic.as_deref())
|
||||
.bind(params.privacy_level.as_deref())
|
||||
.bind(params.discoverable)
|
||||
.bind(now)
|
||||
.bind(stage_id)
|
||||
.fetch_one(self.ctx.db.writer())
|
||||
.await
|
||||
.map_err(AppError::Database)
|
||||
}
|
||||
|
||||
pub async fn stage_delete(
|
||||
&self,
|
||||
_ctx: &ImSession,
|
||||
stage_id: Uuid,
|
||||
) -> Result<(), AppError> {
|
||||
let now = chrono::Utc::now();
|
||||
sqlx::query(
|
||||
"UPDATE stage SET ended_at = $1, updated_at = $1 WHERE id = $2 AND ended_at IS NULL",
|
||||
)
|
||||
.bind(now)
|
||||
.bind(stage_id)
|
||||
.execute(self.ctx.db.writer())
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user