Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: state management for creators #179

Merged
merged 8 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions crates/db/src/core/definitions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ pub trait QuestsDatabase: Send + Sync + CloneDatabase {
async fn deactivate_quest(&self, id: &str) -> DBResult<String>;
async fn get_quest(&self, id: &str) -> DBResult<StoredQuest>;
async fn get_active_quests(&self, offset: i64, limit: i64) -> DBResult<Vec<StoredQuest>>;
async fn get_quests_by_creator_id(
async fn count_active_quests(&self) -> DBResult<i64>;
async fn get_quests_by_creator_address(
&self,
creator_id: &str,
creator_address: &str,
offset: i64,
limit: i64,
) -> DBResult<Vec<StoredQuest>>;
async fn count_quests_by_creator_address(&self, creator_address: &str) -> DBResult<i64>;
async fn is_active_quest(&self, quest_id: &str) -> DBResult<bool>;
async fn has_active_quest_instance(&self, user_address: &str, quest_id: &str)
-> DBResult<bool>;
Expand Down Expand Up @@ -59,10 +61,12 @@ pub trait QuestsDatabase: Send + Sync + CloneDatabase {
offset: i64,
limit: i64,
) -> DBResult<Vec<QuestInstance>>;
async fn count_active_quest_instances_by_quest_id(&self, quest_id: &str) -> DBResult<i64>;

async fn add_event(&self, event: &AddEvent, quest_instance_id: &str) -> DBResult<()>;
async fn get_events(&self, quest_instance_id: &str) -> DBResult<Vec<Event>>;
async fn remove_events(&self, quest_instance_id: &str) -> DBResult<()>;
async fn remove_events_from_quest_instance(&self, quest_instance_id: &str) -> DBResult<()>;
async fn remove_event(&self, event_id: &str) -> DBResult<()>;

async fn add_reward_hook_to_quest(
&self,
Expand Down
12 changes: 12 additions & 0 deletions crates/db/src/core/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ pub enum DBError {
#[error("Unable to retrieve all instances related to Quest ID {0}: {1}")]
GetQuestInstancesByQuestIdFailed(String, BoxDynError),

#[error("Unable to retrieve all active instances related to Quest ID {0}: {1}")]
GetActiveQuestInstancesByQuestIdFailed(String, BoxDynError),

#[error("Unable to check if a quest instance is still active: {0}")]
GetActiveQuestInstanceFailed(BoxDynError),

Expand Down Expand Up @@ -87,6 +90,15 @@ pub enum DBError {
#[error("Unable to check if a quest instance is completed: {0}")]
IsCompletedInstanceFailed(BoxDynError),

#[error("Unable to count active quest instances: {0}")]
UnableToCountActiveQuestInstances(BoxDynError),

#[error("Unable to count active quests: {0}")]
UnableToCountActiveQuests(BoxDynError),

#[error("Unable to check quest creator: {0}")]
UnableToCheckQuestCreator(BoxDynError),

#[error("Row has incorrect data: {0}")]
RowCorrupted(BoxDynError),

Expand Down
50 changes: 47 additions & 3 deletions crates/db/src/core/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ pub async fn quest_database_works<DB: QuestsDatabase>(db: &DB, quest: CreateQues
assert!(db.ping().await);
let quest_id = db.create_quest(&quest, "0xA").await.unwrap();

let count_active_quest = db.count_active_quests().await.unwrap();

assert_eq!(count_active_quest, 1);

let is_creator = db.is_quest_creator(&quest_id, "0xA").await.unwrap();
assert!(is_creator);

Expand Down Expand Up @@ -89,24 +93,35 @@ pub async fn quest_database_works<DB: QuestsDatabase>(db: &DB, quest: CreateQues
assert!(!db.can_activate_quest(&quest_id).await.unwrap());

// creators quests should be ONE because query returns current versions (activated and deactivated) and not old versions
let quests_by_creator = db.get_quests_by_creator_id("0xA", 0, 50).await.unwrap();
let quests_by_creator = db
.get_quests_by_creator_address("0xA", 0, 50)
.await
.unwrap();
assert_eq!(quests_by_creator.len(), 1);
assert_eq!(quests_by_creator.first().unwrap().id, new_quest_id);
assert!(quests_by_creator.first().unwrap().active);

let count_quest_by_creator = db.count_quests_by_creator_address("0xA").await.unwrap();
assert_eq!(count_quest_by_creator, 1);

let create_deactivated_quest = CreateQuest {
name: "DEACTIVATED_CREATOR_QUEST",
description: quest.description,
definition: quest.definition.clone(),
image_url: quest.image_url,
reward: None,
};

let deactivated_quest = db
.create_quest(&create_deactivated_quest, "0xA")
.await
.unwrap();
db.deactivate_quest(&deactivated_quest).await.unwrap();
// creators quests should be TWO because query returns current versions (activated and deactivated) and not old versions
let quests_by_creator = db.get_quests_by_creator_id("0xA", 0, 50).await.unwrap();
let quests_by_creator = db
.get_quests_by_creator_address("0xA", 0, 50)
.await
.unwrap();
assert_eq!(quests_by_creator.len(), 2);
// order by desc
assert_eq!(quests_by_creator.first().unwrap().id, deactivated_quest);
Expand Down Expand Up @@ -145,6 +160,12 @@ pub async fn quest_database_works<DB: QuestsDatabase>(db: &DB, quest: CreateQues

assert_eq!(quest_instances.len(), 1);

let count_quest_instances = db
.count_active_quest_instances_by_quest_id(&quest_id)
.await
.unwrap();
assert_eq!(count_quest_instances, 1);

let instances_by_quest_id = db
.get_all_quest_instances_by_quest_id(&quest_id)
.await
Expand Down Expand Up @@ -281,9 +302,32 @@ pub async fn quest_database_works<DB: QuestsDatabase>(db: &DB, quest: CreateQues
assert!(is_completed);

// test remove events
db.remove_events(&new_quest_instance_id).await.unwrap();
db.remove_events_from_quest_instance(&new_quest_instance_id)
.await
.unwrap();
let events = db.get_events(&new_quest_instance_id).await.unwrap();
assert_eq!(events.len(), 0);

let event_id = uuid::Uuid::new_v4().to_string();
db.add_event(
&AddEvent {
id: event_id.clone(),
user_address: "0xD",
event: vec![0],
},
&new_quest_instance_id,
)
.await
.unwrap();

let events = db.get_events(&new_quest_instance_id).await.unwrap();
assert_eq!(events.len(), 1);

db.remove_event(&event_id).await.unwrap();

let events = db.get_events(&new_quest_instance_id).await.unwrap();
assert_eq!(events.len(), 0);

// test remove from completed quest instances
db.remove_instance_from_completed_instances(&new_quest_instance_id)
.await
Expand Down
67 changes: 63 additions & 4 deletions crates/db/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,22 @@ impl QuestsDatabase for Database {
Ok(quests)
}

async fn get_quests_by_creator_id(
async fn count_active_quests(&self) -> DBResult<i64> {
let count: i64 = sqlx::query_scalar(
"
SELECT count(id) FROM quests
WHERE id NOT IN
(SELECT quest_id as id FROM deactivated_quests)
",
)
.fetch_one(&self.pool)
.await
.map_err(|err| DBError::UnableToCountActiveQuestInstances(Box::new(err)))?;

Ok(count)
}

async fn get_quests_by_creator_address(
&self,
creator_address: &str,
offset: i64,
Expand Down Expand Up @@ -196,6 +211,22 @@ impl QuestsDatabase for Database {
Ok(quests)
}

async fn count_quests_by_creator_address(&self, creator_address: &str) -> DBResult<i64> {
let count: i64 = sqlx::query_scalar(
"SELECT count(q.id)
FROM quests q
LEFT JOIN deactivated_quests dq ON q.id = dq.quest_id
LEFT JOIN quest_updates uq ON q.id = uq.previous_quest_id
WHERE q.creator_address = $1 AND uq.id IS NULL",
)
.bind(creator_address)
.fetch_one(&self.pool)
.await
.map_err(|err| DBError::UnableToCountActiveQuestInstances(Box::new(err)))?;

Ok(count)
}

async fn create_quest(&self, quest: &CreateQuest, creator_address: &str) -> DBResult<String> {
let quest_id = if let Some(reward) = &quest.reward {
let mut tx = self
Expand Down Expand Up @@ -526,7 +557,7 @@ impl QuestsDatabase for Database {
Ok(events)
}

async fn remove_events(&self, quest_instance_id: &str) -> DBResult<()> {
async fn remove_events_from_quest_instance(&self, quest_instance_id: &str) -> DBResult<()> {
sqlx::query("DELETE FROM events WHERE quest_instance_id = $1")
.bind(parse_str_to_uuid(quest_instance_id)?)
.execute(&self.pool)
Expand All @@ -536,6 +567,20 @@ impl QuestsDatabase for Database {
Ok(())
}

async fn remove_event(&self, event_id: &str) -> DBResult<()> {
let query_result = sqlx::query("DELETE FROM events WHERE id = $1")
.bind(parse_str_to_uuid(event_id)?)
.execute(&self.pool)
.await
.map_err(|err| DBError::GetQuestEventsFailed(Box::new(err)))?;

if query_result.rows_affected() == 0 {
return Err(DBError::RowNotFound);
}

Ok(())
}

async fn add_reward_hook_to_quest(
&self,
quest_id: &str,
Expand Down Expand Up @@ -619,7 +664,7 @@ impl QuestsDatabase for Database {
.fetch_all(&self.pool)
.await
.map_err(|err| {
DBError::GetQuestInstancesByQuestIdFailed(quest_id.to_string(), Box::new(err))
DBError::GetActiveQuestInstancesByQuestIdFailed(quest_id.to_string(), Box::new(err))
})?;

let result: Result<Vec<_>, _> =
Expand All @@ -628,6 +673,20 @@ impl QuestsDatabase for Database {
result.map_err(|err| DBError::RowCorrupted(Box::new(err)))
}

async fn count_active_quest_instances_by_quest_id(&self, quest_id: &str) -> DBResult<i64> {
let count: i64 = sqlx::query_scalar(
"SELECT count(id) FROM quest_instances
WHERE quest_id = $1
AND id NOT IN (SELECT quest_instance_id as id FROM abandoned_quest_instances)",
)
.bind(parse_str_to_uuid(quest_id)?)
.fetch_one(&self.pool)
.await
.map_err(|err| DBError::UnableToCountActiveQuestInstances(Box::new(err)))?;

Ok(count)
}

async fn is_quest_creator(&self, quest_id: &str, creator_address: &str) -> DBResult<bool> {
let quest_exists: bool = sqlx::query_scalar(
"
Expand All @@ -639,7 +698,7 @@ impl QuestsDatabase for Database {
.bind(creator_address)
.fetch_one(&self.pool)
.await
.map_err(|err| DBError::CanActivateQuestFailed(Box::new(err)))?;
.map_err(|err| DBError::UnableToCheckQuestCreator(Box::new(err)))?;

Ok(quest_exists)
}
Expand Down
8 changes: 4 additions & 4 deletions crates/server/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ pub async fn run_server(

pub fn get_app_router(
config: &Data<Config>,
db: &Data<Database>,
redis: &Data<RedisMessagesQueue>,
database: &Data<Database>,
events_queue: &Data<RedisMessagesQueue>,
metrics_collector: &Data<HttpMetricsCollector>,
) -> App<
impl ServiceFactory<
Expand All @@ -57,8 +57,8 @@ pub fn get_app_router(
.app_data(json_extractor_config())
.app_data(path_extractor_config())
.app_data(config.clone())
.app_data(db.clone())
.app_data(redis.clone())
.app_data(database.clone())
.app_data(events_queue.clone())
.app_data(metrics_collector.clone())
.wrap(dcl_http_prom_metrics::metrics())
.wrap(middlewares::metrics_token(&config.wkc_metrics_bearer_token))
Expand Down
11 changes: 9 additions & 2 deletions crates/server/src/api/routes/api_doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ use utoipa_redoc::Servable;
quests::get_quest_instances,
creators::get_quests_by_creator_id,
quest_instances::reset_quest_instance,
quest_instances::get_quest_instance_state
quest_instances::get_quest_instance_state,
quest_instances::add_event_to_instance,
quest_instances::get_quest_instance,
quest_instances::remove_event_from_instance,
),
components(
schemas(
Expand All @@ -53,6 +56,7 @@ use utoipa_redoc::Servable;
quests_protocol::definitions::Connection,
quests_protocol::definitions::QuestState,
quests_protocol::definitions::StepContent,
quests_protocol::definitions::EventRequest,
quests_protocol::definitions::Task,
quests_db::core::definitions::QuestReward,
quests_db::core::definitions::QuestRewardHook,
Expand All @@ -61,7 +65,10 @@ use utoipa_redoc::Servable;
quests_db::core::definitions::QuestInstance,
quest_instances::state::GetInstanceStateResponse,
quests::get_instances::GetQuestInstancesResponse,
quests::get_instances::GetQuestInstancesQuery
quests::get_instances::GetQuestInstancesQuery,
quest_instances::add_event::AddEventToInstancePayload,
quest_instances::add_event::AddEventToInstanceResponse,
quest_instances::get::GetQuestInstanceResponse,
)
),
tags(
Expand Down
28 changes: 19 additions & 9 deletions crates/server/src/api/routes/creators/get_quests_by_creator_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub struct GetQuestsQuery {
#[derive(Serialize, Deserialize, ToSchema)]
pub struct GetCreatorQuestsResponse {
pub quests: Vec<Quest>,
pub total: i64,
}

/// Get quests by creator id
Expand Down Expand Up @@ -47,23 +48,32 @@ pub async fn get_quests_by_creator_id(
};

match db
.get_quests_by_creator_id(
.get_quests_by_creator_address(
&user_address.to_ascii_lowercase(),
query.offset.unwrap_or(0),
query.limit.unwrap_or(50),
)
.await
{
Ok(stored_quests) => {
let mut quests = vec![];
for stored_quest in stored_quests {
match stored_quest.to_quest(is_owner) {
Ok(quest) => quests.push(quest),
Err(err) => return HttpResponse::from_error(err),
Ok(stored_quests) => match db
.count_quests_by_creator_address(&user_address.to_ascii_lowercase())
.await
{
Ok(total) => {
let mut quests = vec![];
for stored_quest in stored_quests {
match stored_quest.to_quest(is_owner) {
Ok(quest) => quests.push(quest),
Err(err) => return HttpResponse::from_error(err),
}
}
HttpResponse::Ok().json(GetCreatorQuestsResponse { quests, total })
}
HttpResponse::Ok().json(GetCreatorQuestsResponse { quests })
}
Err(err) => {
log::error!("Error counting quests: {:?}", err);
HttpResponse::from_error(QuestError::from(err))
}
},
Err(err) => {
log::error!("Error getting quests: {:?}", err);
HttpResponse::from_error(QuestError::from(err))
Expand Down
Loading
Loading