329 lines
9.6 KiB
Rust
329 lines
9.6 KiB
Rust
use std::{env, error::Error, fmt::Display};
|
|
|
|
use async_trait::async_trait;
|
|
use chrono::{DateTime, NaiveDateTime, Utc};
|
|
use common::{Adaptor, Event, Person, Stats};
|
|
use google_cloud::{
|
|
authorize::ApplicationCredentials,
|
|
datastore::{Client, Filter, FromValue, IntoValue, Key, KeyID, Query},
|
|
};
|
|
use tokio::sync::Mutex;
|
|
|
|
pub struct DatastoreAdaptor {
|
|
client: Mutex<Client>,
|
|
}
|
|
|
|
// Keys
|
|
const STATS_KIND: &str = "Stats";
|
|
const EVENT_KIND: &str = "Event";
|
|
const PERSON_KIND: &str = "Person";
|
|
const STATS_EVENTS_ID: &str = "eventCount";
|
|
const STATS_PEOPLE_ID: &str = "personCount";
|
|
|
|
#[async_trait]
|
|
impl Adaptor for DatastoreAdaptor {
|
|
type Error = DatastoreAdaptorError;
|
|
|
|
async fn get_stats(&self) -> Result<Stats, Self::Error> {
|
|
let mut client = self.client.lock().await;
|
|
|
|
let event_key = Key::new(STATS_KIND).id(STATS_EVENTS_ID);
|
|
let event_stats: DatastoreStats = client.get(event_key).await?.unwrap_or_default();
|
|
|
|
let person_key = Key::new(STATS_KIND).id(STATS_PEOPLE_ID);
|
|
let person_stats: DatastoreStats = client.get(person_key).await?.unwrap_or_default();
|
|
|
|
Ok(Stats {
|
|
event_count: event_stats.value,
|
|
person_count: person_stats.value,
|
|
})
|
|
}
|
|
|
|
async fn increment_stat_event_count(&self) -> Result<i64, Self::Error> {
|
|
let mut client = self.client.lock().await;
|
|
|
|
let key = Key::new(STATS_KIND).id(STATS_EVENTS_ID);
|
|
let mut event_stats: DatastoreStats = client.get(key.clone()).await?.unwrap_or_default();
|
|
|
|
event_stats.value += 1;
|
|
client.put((key, event_stats.clone())).await?;
|
|
Ok(event_stats.value)
|
|
}
|
|
|
|
async fn increment_stat_person_count(&self) -> Result<i64, Self::Error> {
|
|
let mut client = self.client.lock().await;
|
|
|
|
let key = Key::new(STATS_KIND).id(STATS_PEOPLE_ID);
|
|
let mut person_stats: DatastoreStats = client.get(key.clone()).await?.unwrap_or_default();
|
|
|
|
person_stats.value += 1;
|
|
client.put((key, person_stats.clone())).await?;
|
|
Ok(person_stats.value)
|
|
}
|
|
|
|
async fn get_people(&self, event_id: String) -> Result<Option<Vec<Person>>, Self::Error> {
|
|
let mut client = self.client.lock().await;
|
|
|
|
// Check the event exists
|
|
if client
|
|
.get::<DatastoreEvent, _>(Key::new(EVENT_KIND).id(event_id.clone()))
|
|
.await?
|
|
.is_none()
|
|
{
|
|
return Ok(None);
|
|
}
|
|
|
|
Ok(Some(
|
|
client
|
|
.query(
|
|
Query::new(PERSON_KIND)
|
|
.filter(Filter::Equal("eventId".into(), event_id.into_value())),
|
|
)
|
|
.await?
|
|
.into_iter()
|
|
.filter_map(|entity| {
|
|
DatastorePerson::from_value(entity.properties().clone())
|
|
.ok()
|
|
.map(|ds_person| ds_person.into())
|
|
})
|
|
.collect(),
|
|
))
|
|
}
|
|
|
|
async fn upsert_person(
|
|
&self,
|
|
event_id: String,
|
|
person: Person,
|
|
) -> Result<Option<Person>, Self::Error> {
|
|
let mut client = self.client.lock().await;
|
|
|
|
// Check the event exists
|
|
if client
|
|
.get::<DatastoreEvent, _>(Key::new(EVENT_KIND).id(event_id.clone()))
|
|
.await?
|
|
.is_none()
|
|
{
|
|
return Ok(None);
|
|
}
|
|
|
|
// Check if person exists
|
|
let existing_person = client
|
|
.query(
|
|
Query::new(PERSON_KIND)
|
|
.filter(Filter::Equal(
|
|
"eventId".into(),
|
|
event_id.clone().into_value(),
|
|
))
|
|
.filter(Filter::Equal(
|
|
"name".into(),
|
|
person.name.clone().into_value(),
|
|
)),
|
|
)
|
|
.await?;
|
|
|
|
let mut key = Key::new(PERSON_KIND);
|
|
if let Some(entity) = existing_person.first() {
|
|
key = entity.key().clone();
|
|
}
|
|
|
|
client
|
|
.put((key, DatastorePerson::from_person(person.clone(), event_id)))
|
|
.await?;
|
|
|
|
Ok(Some(person))
|
|
}
|
|
|
|
async fn get_event(&self, id: String) -> Result<Option<Event>, Self::Error> {
|
|
let mut client = self.client.lock().await;
|
|
|
|
let key = Key::new(EVENT_KIND).id(id.clone());
|
|
let existing_event = client.get::<DatastoreEvent, _>(key.clone()).await?;
|
|
|
|
// Mark as visited if it exists
|
|
if let Some(mut event) = existing_event.clone() {
|
|
event.visited = Utc::now().timestamp();
|
|
client.put((key, event)).await?;
|
|
}
|
|
|
|
Ok(existing_event.map(|e| e.to_event(id)))
|
|
}
|
|
|
|
async fn create_event(&self, event: Event) -> Result<Event, Self::Error> {
|
|
let mut client = self.client.lock().await;
|
|
|
|
let key = Key::new(EVENT_KIND).id(event.id.clone());
|
|
|
|
let ds_event: DatastoreEvent = event.clone().into();
|
|
client.put((key, ds_event)).await?;
|
|
|
|
Ok(event)
|
|
}
|
|
|
|
async fn delete_events(&self, cutoff: DateTime<Utc>) -> Result<Stats, Self::Error> {
|
|
let mut client = self.client.lock().await;
|
|
|
|
let mut keys_to_delete: Vec<Key> = client
|
|
.query(Query::new(EVENT_KIND).filter(Filter::LesserThan(
|
|
"visited".into(),
|
|
cutoff.timestamp().into_value(),
|
|
)))
|
|
.await?
|
|
.iter()
|
|
.map(|entity| entity.key().clone())
|
|
.collect();
|
|
|
|
let event_count = keys_to_delete.len() as i64;
|
|
|
|
let events_to_delete = keys_to_delete.clone();
|
|
for e in events_to_delete.iter() {
|
|
if let KeyID::StringID(id) = e.get_id() {
|
|
let mut event_people_to_delete: Vec<Key> = client
|
|
.query(
|
|
Query::new(PERSON_KIND)
|
|
.filter(Filter::Equal("eventId".into(), id.clone().into_value())),
|
|
)
|
|
.await?
|
|
.iter()
|
|
.map(|entity| entity.key().clone())
|
|
.collect();
|
|
keys_to_delete.append(&mut event_people_to_delete);
|
|
}
|
|
}
|
|
|
|
let person_count = keys_to_delete.len() as i64 - event_count;
|
|
|
|
client.delete_all(keys_to_delete).await?;
|
|
|
|
Ok(Stats {
|
|
event_count,
|
|
person_count,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl DatastoreAdaptor {
|
|
pub async fn new() -> Self {
|
|
// Load credentials
|
|
let credentials: ApplicationCredentials = serde_json::from_str(
|
|
&env::var("GCP_CREDENTIALS").expect("Expected GCP_CREDENTIALS environment variable"),
|
|
)
|
|
.expect("GCP_CREDENTIALS environment variable is not valid JSON");
|
|
|
|
// Connect to datastore
|
|
let client = Client::from_credentials(credentials.project_id.clone(), credentials.clone())
|
|
.await
|
|
.expect("Failed to setup datastore client");
|
|
let client = Mutex::new(client);
|
|
|
|
println!(
|
|
"🎛️ Connected to datastore in project {}",
|
|
credentials.project_id
|
|
);
|
|
|
|
Self { client }
|
|
}
|
|
}
|
|
|
|
#[derive(FromValue, IntoValue, Default, Clone)]
|
|
struct DatastoreStats {
|
|
value: i64,
|
|
}
|
|
|
|
#[derive(FromValue, IntoValue, Clone)]
|
|
struct DatastoreEvent {
|
|
name: String,
|
|
created: i64,
|
|
visited: i64,
|
|
times: Vec<String>,
|
|
timezone: String,
|
|
}
|
|
|
|
#[derive(FromValue, IntoValue)]
|
|
#[allow(non_snake_case)]
|
|
struct DatastorePerson {
|
|
name: String,
|
|
password: Option<String>,
|
|
created: i64,
|
|
eventId: String,
|
|
availability: Vec<String>,
|
|
}
|
|
|
|
impl From<DatastorePerson> for Person {
|
|
fn from(value: DatastorePerson) -> Self {
|
|
Self {
|
|
name: value.name,
|
|
password_hash: value.password,
|
|
created_at: unix_to_date(value.created),
|
|
availability: value.availability,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl DatastorePerson {
|
|
fn from_person(person: Person, event_id: String) -> Self {
|
|
Self {
|
|
name: person.name,
|
|
password: person.password_hash,
|
|
created: person.created_at.timestamp(),
|
|
eventId: event_id,
|
|
availability: person.availability,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<Event> for DatastoreEvent {
|
|
fn from(value: Event) -> Self {
|
|
Self {
|
|
name: value.name,
|
|
created: value.created_at.timestamp(),
|
|
visited: value.visited_at.timestamp(),
|
|
times: value.times,
|
|
timezone: value.timezone,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl DatastoreEvent {
|
|
fn to_event(&self, event_id: String) -> Event {
|
|
Event {
|
|
id: event_id,
|
|
name: self.name.clone(),
|
|
created_at: unix_to_date(self.created),
|
|
visited_at: unix_to_date(self.visited),
|
|
times: self.times.clone(),
|
|
timezone: self.timezone.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn unix_to_date(unix: i64) -> DateTime<Utc> {
|
|
DateTime::from_utc(NaiveDateTime::from_timestamp_opt(unix, 0).unwrap(), Utc)
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum DatastoreAdaptorError {
|
|
DatastoreError(google_cloud::error::Error),
|
|
}
|
|
|
|
impl Display for DatastoreAdaptorError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
DatastoreAdaptorError::DatastoreError(e) => write!(f, "Datastore Error: {}", e),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Error for DatastoreAdaptorError {}
|
|
|
|
impl From<google_cloud::error::Error> for DatastoreAdaptorError {
|
|
fn from(value: google_cloud::error::Error) -> Self {
|
|
Self::DatastoreError(value)
|
|
}
|
|
}
|
|
|
|
impl From<google_cloud::error::ConvertError> for DatastoreAdaptorError {
|
|
fn from(value: google_cloud::error::ConvertError) -> Self {
|
|
Self::DatastoreError(google_cloud::error::Error::Convert(value))
|
|
}
|
|
}
|