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(rust): integrate space's subscription data in command #8296

Merged
merged 1 commit into from
Jul 16, 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
20 changes: 18 additions & 2 deletions implementations/rust/ockam/ockam_api/src/cli_state/spaces.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use ockam_core::Error;

use crate::cli_state::CliState;
use crate::cloud::space::Space;
use crate::cloud::subscription::Subscription;

use super::Result;

Expand All @@ -13,12 +14,14 @@ impl CliState {
space_id: &str,
space_name: &str,
users: Vec<&str>,
subscription: Option<&Subscription>,
) -> Result<Space> {
let repository = self.spaces_repository();
let space = Space {
id: space_id.to_string(),
name: space_name.to_string(),
users: users.iter().map(|u| u.to_string()).collect(),
subscription: subscription.cloned(),
};

repository.store_space(&space).await?;
Expand Down Expand Up @@ -96,13 +99,26 @@ mod test {

// the first created space becomes the default
let space1 = cli
.store_space("1", "name1", vec!["[email protected]", "[email protected]"])
.store_space(
"1",
"name1",
vec!["[email protected]", "[email protected]"],
Some(&Subscription::new(
"name1".to_string(),
false,
None,
None,
None,
)),
)
.await?;
let result = cli.get_default_space().await?;
assert_eq!(result, space1);

// the store method can be used to update a space
let updated_space1 = cli.store_space("1", "name1", vec!["[email protected]"]).await?;
let updated_space1 = cli
.store_space("1", "name1", vec!["[email protected]"], None)
.await?;
let result = cli.get_default_space().await?;
assert_eq!(result, updated_space1);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
use sqlx::any::AnyRow;
use sqlx::*;

use super::SpacesRepository;
use crate::cloud::space::Space;
use crate::cloud::subscription::Subscription;
use ockam_core::async_trait;
use ockam_core::Result;
use ockam_node::database::{Boolean, FromSqlxError, SqlxDatabase, ToVoid};

use crate::cloud::space::Space;

use super::SpacesRepository;
use ockam_node::database::{Boolean, FromSqlxError, Nullable, SqlxDatabase, ToVoid};
use sqlx::any::AnyRow;
use sqlx::*;
use time::OffsetDateTime;

#[derive(Clone)]
pub struct SpacesSqlxDatabase {
Expand All @@ -25,6 +24,15 @@ impl SpacesSqlxDatabase {
pub async fn create() -> Result<Self> {
Ok(Self::new(SqlxDatabase::in_memory("spaces").await?))
}

async fn query_subscription(&self, space_id: &str) -> Result<Option<Subscription>> {
let query = query_as("SELECT space_id, name, is_free_trial, marketplace, start_date, end_date FROM subscription WHERE space_id = $1").bind(space_id);
let row: Option<SubscriptionRow> = query
.fetch_optional(&*self.database.pool)
.await
.into_core()?;
Ok(row.map(|r| r.subscription()))
}
}

#[async_trait]
Expand Down Expand Up @@ -69,6 +77,31 @@ impl SpacesRepository for SpacesSqlxDatabase {
query4.execute(&mut *transaction).await.void()?;
}

// store the subscription if any
if let Some(subscription) = &space.subscription {
let start_date = subscription.start_date();
let end_date = subscription.end_date();
let query = query(
r"
INSERT INTO subscription (space_id, name, is_free_trial, marketplace, start_date, end_date)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (space_id)
DO UPDATE SET space_id = $1, name = $2, is_free_trial = $3, marketplace = $4, start_date = $5, end_date = $6",
)
.bind(&space.id)
.bind(&subscription.name)
.bind(subscription.is_free_trial)
.bind(&subscription.marketplace)
.bind(start_date.map(|d| d.unix_timestamp()))
.bind(end_date.map(|d| d.unix_timestamp()));
query.execute(&mut *transaction).await.void()?;
}
// remove the subscription
else {
let query = query("DELETE FROM subscription WHERE space_id = $1").bind(&space.id);
query.execute(&mut *transaction).await.void()?;
}

transaction.commit().await.void()
}

Expand All @@ -95,13 +128,18 @@ impl SpacesRepository for SpacesSqlxDatabase {
let row: Option<SpaceRow> = query1.fetch_optional(&mut *transaction).await.into_core()?;
let space = match row.map(|r| r.space()) {
Some(mut space) => {
// retrieve the users
let query2 =
query_as("SELECT space_id, user_email FROM user_space WHERE space_id = $1")
.bind(&space.id);
let rows: Vec<UserSpaceRow> =
query2.fetch_all(&mut *transaction).await.into_core()?;
let users = rows.into_iter().map(|r| r.user_email).collect();
space.users = users;

// retrieve the subscription
space.subscription = self.query_subscription(&space.id).await?;

Some(space)
}
None => None,
Expand All @@ -114,16 +152,20 @@ impl SpacesRepository for SpacesSqlxDatabase {
let mut transaction = self.database.begin().await.into_core()?;

let query = query_as("SELECT space_id, space_name FROM space");
let row: Vec<SpaceRow> = query.fetch_all(&mut *transaction).await.into_core()?;
let rows: Vec<SpaceRow> = query.fetch_all(&mut *transaction).await.into_core()?;

let mut spaces = vec![];
for space_row in row {
for row in rows {
let query2 =
query_as("SELECT space_id, user_email FROM user_space WHERE space_id = $1")
.bind(&space_row.space_id);
.bind(&row.space_id);
let rows: Vec<UserSpaceRow> = query2.fetch_all(&mut *transaction).await.into_core()?;
let users = rows.into_iter().map(|r| r.user_email).collect();
spaces.push(space_row.space_with_user_emails(users))
let subscription = self.query_subscription(&row.space_id).await?;
let mut space = row.space();
space.users = users;
space.subscription = subscription;
spaces.push(space);
}

transaction.commit().await.void()?;
Expand Down Expand Up @@ -169,6 +211,9 @@ impl SpacesRepository for SpacesSqlxDatabase {
let query2 = query("DELETE FROM user_space WHERE space_id = $1").bind(space_id);
query2.execute(&mut *transaction).await.void()?;

let query3 = query("DELETE FROM subscription WHERE space_id = $1").bind(space_id);
query3.execute(&mut *transaction).await.void()?;

transaction.commit().await.void()
}
}
Expand All @@ -184,14 +229,11 @@ struct SpaceRow {

impl SpaceRow {
pub(crate) fn space(&self) -> Space {
self.space_with_user_emails(vec![])
}

pub(crate) fn space_with_user_emails(&self, user_emails: Vec<String>) -> Space {
Space {
id: self.space_id.clone(),
name: self.space_name.clone(),
users: user_emails,
users: vec![],
subscription: None,
}
}
}
Expand All @@ -204,22 +246,52 @@ struct UserSpaceRow {
user_email: String,
}

/// Low-level representation of a row in the subscription table
#[derive(sqlx::FromRow)]
pub(super) struct SubscriptionRow {
#[allow(unused)]
space_id: String,
name: String,
is_free_trial: Boolean,
marketplace: Nullable<String>,
start_date: Nullable<i64>,
end_date: Nullable<i64>,
}

impl SubscriptionRow {
pub(crate) fn subscription(&self) -> Subscription {
Subscription::new(
self.name.clone(),
self.is_free_trial.to_bool(),
self.marketplace.to_option(),
self.start_date
.to_option()
.and_then(|t| OffsetDateTime::from_unix_timestamp(t).ok()),
self.end_date
.to_option()
.and_then(|t| OffsetDateTime::from_unix_timestamp(t).ok()),
)
}
}

#[cfg(test)]
mod test {
use super::*;
use ockam_node::database::with_dbs;
use std::sync::Arc;
use std::ops::Add;
use time::ext::NumericalDuration;

#[tokio::test]
async fn test_repository() -> Result<()> {
with_dbs(|db| async move {
let repository: Arc<dyn SpacesRepository> = Arc::new(SpacesSqlxDatabase::new(db));
let repository = SpacesSqlxDatabase::new(db);

// create and store 2 spaces
let space1 = Space {
id: "1".to_string(),
name: "name1".to_string(),
users: vec!["[email protected]".to_string(), "[email protected]".to_string()],
subscription: None,
};
let mut space2 = Space {
id: "2".to_string(),
Expand All @@ -229,11 +301,24 @@ mod test {
"[email protected]".to_string(),
"[email protected]".to_string(),
],
subscription: Some(Subscription::new(
"premium".to_string(),
false,
Some("aws".to_string()),
Some(OffsetDateTime::now_utc()),
Some(OffsetDateTime::now_utc().add(2.days())),
)),
};

repository.store_space(&space1).await?;
repository.store_space(&space2).await?;

// subscription is stored
let result = repository.query_subscription("1").await?;
assert_eq!(result, None);
let result = repository.query_subscription("2").await?;
assert_eq!(result, Some(space2.subscription.clone().unwrap()));

// retrieve them as a vector or by name
let result = repository.get_spaces().await?;
assert_eq!(result, vec![space1.clone(), space2.clone()]);
Expand Down Expand Up @@ -263,6 +348,13 @@ mod test {

let result = repository.get_spaces().await?;
assert_eq!(result, vec![space1.clone()]);

// subscription is deleted
let result = repository.query_subscription("1").await?;
assert_eq!(result, None);
let result = repository.query_subscription("2").await?;
assert_eq!(result, None);

Ok(())
})
.await
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
use crate::address::extract_address_value;
use crate::cli_state::EnrollmentTicket;
use crate::cloud::email_address::EmailAddress;
use crate::date::is_expired;
use crate::error::ApiError;
use crate::output::Output;
use minicbor::{CborLen, Decode, Encode};
use ockam::identity::Identifier;
use serde::{Deserialize, Serialize};
use std::{fmt::Display, str::FromStr};
use time::format_description::well_known::iso8601::Iso8601;
use time::OffsetDateTime;

#[derive(Clone, Debug, Eq, PartialEq, Decode, Encode, CborLen, Deserialize, Serialize)]
#[cbor(index_only)]
Expand Down Expand Up @@ -149,20 +148,6 @@ impl Output for SentInvitation {
}
}

/// Check if a string that represents an Iso8601 date is expired, using the `time` crate
fn is_expired(date: &str) -> ockam_core::Result<bool> {
// Add the Z timezone to the date, as the `time` crate requires it
let date = if date.ends_with('Z') {
date.to_string()
} else {
format!("{}Z", date)
};
let now = OffsetDateTime::now_utc();
let date = OffsetDateTime::parse(&date, &Iso8601::DEFAULT)
.map_err(|e| ApiError::core(e.to_string()))?;
Ok(date < now)
}

#[derive(Clone, Debug, Encode, Decode, CborLen, Deserialize, Serialize, PartialEq)]
#[cbor(map)]
#[rustfmt::skip]
Expand Down Expand Up @@ -192,7 +177,9 @@ impl ServiceAccessDetails {

#[cfg(test)]
mod test {
use super::*;
use crate::date::is_expired;
use time::format_description::well_known::Iso8601;
use time::OffsetDateTime;

#[test]
fn test_is_expired() {
Expand Down
Loading
Loading