Rename main folders and write sql backend adaptor
This commit is contained in:
parent
1d34f8e06d
commit
fdc58b428b
212 changed files with 3577 additions and 4775 deletions
14
backend/adaptors/sql/Cargo.toml
Normal file
14
backend/adaptors/sql/Cargo.toml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "sql-adaptor"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.68"
|
||||
data = { path = "../../data" }
|
||||
sea-orm = { version = "0.11.3", features = [ "macros", "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", "runtime-tokio-native-tls" ] }
|
||||
serde = { version = "1.0.162", features = [ "derive" ] }
|
||||
async-std = { version = "1", features = ["attributes", "tokio1"] }
|
||||
sea-orm-migration = "0.11.0"
|
||||
serde_json = "1.0.96"
|
||||
chrono = "0.4.24"
|
||||
42
backend/adaptors/sql/src/entity/event.rs
Normal file
42
backend/adaptors/sql/src/entity/event.rs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
|
||||
|
||||
use chrono::{DateTime as ChronoDateTime, Utc};
|
||||
use data::event::Event;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[sea_orm(table_name = "event")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub created_at: DateTime,
|
||||
pub times: Json,
|
||||
pub timezone: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::person::Entity")]
|
||||
Person,
|
||||
}
|
||||
|
||||
impl Related<super::person::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Person.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
impl From<Model> for Event {
|
||||
fn from(value: Model) -> Self {
|
||||
Self {
|
||||
id: value.id,
|
||||
name: value.name,
|
||||
created_at: ChronoDateTime::<Utc>::from_utc(value.created_at, Utc),
|
||||
times: serde_json::from_value(value.times).unwrap_or(vec![]),
|
||||
timezone: value.timezone,
|
||||
}
|
||||
}
|
||||
}
|
||||
7
backend/adaptors/sql/src/entity/mod.rs
Normal file
7
backend/adaptors/sql/src/entity/mod.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
|
||||
|
||||
pub mod prelude;
|
||||
|
||||
pub mod event;
|
||||
pub mod person;
|
||||
pub mod stats;
|
||||
48
backend/adaptors/sql/src/entity/person.rs
Normal file
48
backend/adaptors/sql/src/entity/person.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
|
||||
|
||||
use chrono::{DateTime as ChronoDateTime, Utc};
|
||||
use data::person::Person;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[sea_orm(table_name = "person")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub name: String,
|
||||
pub password_hash: Option<String>,
|
||||
pub created_at: DateTime,
|
||||
pub availability: Json,
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub event_id: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::event::Entity",
|
||||
from = "Column::EventId",
|
||||
to = "super::event::Column::Id",
|
||||
on_update = "Cascade",
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
Event,
|
||||
}
|
||||
|
||||
impl Related<super::event::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Event.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
impl From<Model> for Person {
|
||||
fn from(value: Model) -> Self {
|
||||
Self {
|
||||
name: value.name,
|
||||
password_hash: value.password_hash,
|
||||
created_at: ChronoDateTime::<Utc>::from_utc(value.created_at, Utc),
|
||||
availability: serde_json::from_value(value.availability).unwrap_or(vec![]),
|
||||
}
|
||||
}
|
||||
}
|
||||
5
backend/adaptors/sql/src/entity/prelude.rs
Normal file
5
backend/adaptors/sql/src/entity/prelude.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
|
||||
|
||||
pub use super::event::Entity as Event;
|
||||
pub use super::person::Entity as Person;
|
||||
pub use super::stats::Entity as Stats;
|
||||
27
backend/adaptors/sql/src/entity/stats.rs
Normal file
27
backend/adaptors/sql/src/entity/stats.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
|
||||
|
||||
use data::stats::Stats;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[sea_orm(table_name = "stats")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub event_count: i32,
|
||||
pub person_count: i32,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
impl From<Model> for Stats {
|
||||
fn from(value: Model) -> Self {
|
||||
Self {
|
||||
event_count: value.event_count,
|
||||
person_count: value.person_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
152
backend/adaptors/sql/src/lib.rs
Normal file
152
backend/adaptors/sql/src/lib.rs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
use std::{env, error::Error};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use data::{
|
||||
adaptor::Adaptor,
|
||||
event::{Event, EventDeletion},
|
||||
person::Person,
|
||||
stats::Stats,
|
||||
};
|
||||
use entity::{event, person, stats};
|
||||
use migration::{Migrator, MigratorTrait};
|
||||
use sea_orm::{
|
||||
ActiveModelTrait,
|
||||
ActiveValue::{NotSet, Set},
|
||||
ColumnTrait, Database, DatabaseConnection, DbErr, EntityTrait, ModelTrait, QueryFilter,
|
||||
TransactionTrait, TryIntoModel,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
mod entity;
|
||||
mod migration;
|
||||
|
||||
pub struct PostgresAdaptor {
|
||||
db: DatabaseConnection,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Adaptor for PostgresAdaptor {
|
||||
async fn new() -> Self {
|
||||
let connection_string = env::var("DATABASE_URL").unwrap();
|
||||
|
||||
// Connect to the database
|
||||
let db = Database::connect(&connection_string).await.unwrap();
|
||||
println!("Connected to database at {}", connection_string);
|
||||
|
||||
// Setup tables
|
||||
Migrator::up(&db, None).await.unwrap();
|
||||
|
||||
Self { db }
|
||||
}
|
||||
|
||||
async fn get_stats(&self) -> Result<Stats, Box<dyn Error>> {
|
||||
Ok(get_stats_row(&self.db).await?.try_into_model()?.into())
|
||||
}
|
||||
|
||||
async fn increment_stat_event_count(&self) -> Result<i32, Box<dyn Error>> {
|
||||
let mut current_stats = get_stats_row(&self.db).await?;
|
||||
current_stats.event_count = Set(current_stats.event_count.unwrap() + 1);
|
||||
|
||||
Ok(current_stats.save(&self.db).await?.event_count.unwrap())
|
||||
}
|
||||
|
||||
async fn increment_stat_person_count(&self) -> Result<i32, Box<dyn Error>> {
|
||||
let mut current_stats = get_stats_row(&self.db).await?;
|
||||
current_stats.person_count = Set(current_stats.person_count.unwrap() + 1);
|
||||
|
||||
Ok(current_stats.save(&self.db).await?.person_count.unwrap())
|
||||
}
|
||||
|
||||
async fn get_people(&self, event_id: String) -> Result<Option<Vec<Person>>, Box<dyn Error>> {
|
||||
// TODO: optimize into one query
|
||||
let event_row = event::Entity::find_by_id(event_id).one(&self.db).await?;
|
||||
|
||||
Ok(match event_row {
|
||||
Some(event) => Some(
|
||||
event
|
||||
.find_related(person::Entity)
|
||||
.all(&self.db)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|model| model.into())
|
||||
.collect(),
|
||||
),
|
||||
None => None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn upsert_person(
|
||||
&self,
|
||||
event_id: String,
|
||||
person: Person,
|
||||
) -> Result<Person, Box<dyn Error>> {
|
||||
Ok(person::ActiveModel {
|
||||
name: Set(person.name),
|
||||
password_hash: Set(person.password_hash),
|
||||
created_at: Set(person.created_at.naive_utc()),
|
||||
availability: Set(serde_json::to_value(person.availability).unwrap_or(json!([]))),
|
||||
event_id: Set(event_id),
|
||||
}
|
||||
.save(&self.db)
|
||||
.await?
|
||||
.try_into_model()?
|
||||
.into())
|
||||
}
|
||||
|
||||
async fn get_event(&self, id: String) -> Result<Option<Event>, Box<dyn Error>> {
|
||||
Ok(event::Entity::find_by_id(id)
|
||||
.one(&self.db)
|
||||
.await?
|
||||
.map(|model| model.into()))
|
||||
}
|
||||
|
||||
async fn create_event(&self, event: Event) -> Result<Event, Box<dyn Error>> {
|
||||
Ok(event::ActiveModel {
|
||||
id: Set(event.id),
|
||||
name: Set(event.name),
|
||||
created_at: Set(event.created_at.naive_utc()),
|
||||
times: Set(serde_json::to_value(event.times).unwrap_or(json!([]))),
|
||||
timezone: Set(event.timezone),
|
||||
}
|
||||
.save(&self.db)
|
||||
.await?
|
||||
.try_into_model()?
|
||||
.into())
|
||||
}
|
||||
|
||||
async fn delete_event(&self, id: String) -> Result<EventDeletion, Box<dyn Error>> {
|
||||
let event_id = id.clone();
|
||||
let person_count = self
|
||||
.db
|
||||
.transaction::<_, u64, DbErr>(|t| {
|
||||
Box::pin(async move {
|
||||
// Delete people
|
||||
let people_delete_result = person::Entity::delete_many()
|
||||
.filter(person::Column::EventId.eq(&event_id))
|
||||
.exec(t)
|
||||
.await?;
|
||||
|
||||
// Delete event
|
||||
event::Entity::delete_by_id(event_id).exec(t).await?;
|
||||
|
||||
Ok(people_delete_result.rows_affected)
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(EventDeletion { id, person_count })
|
||||
}
|
||||
}
|
||||
|
||||
// Get the current stats as an ActiveModel
|
||||
async fn get_stats_row(db: &DatabaseConnection) -> Result<stats::ActiveModel, DbErr> {
|
||||
let current_stats = stats::Entity::find().one(db).await?;
|
||||
Ok(match current_stats {
|
||||
Some(model) => model.into(),
|
||||
None => stats::ActiveModel {
|
||||
id: NotSet,
|
||||
event_count: Set(0),
|
||||
person_count: Set(0),
|
||||
},
|
||||
})
|
||||
}
|
||||
12
backend/adaptors/sql/src/migration/mod.rs
Normal file
12
backend/adaptors/sql/src/migration/mod.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
pub use sea_orm_migration::prelude::*;
|
||||
|
||||
mod setup_tables;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigratorTrait for Migrator {
|
||||
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||
vec![Box::new(setup_tables::Migration)]
|
||||
}
|
||||
}
|
||||
120
backend/adaptors/sql/src/migration/setup_tables.rs
Normal file
120
backend/adaptors/sql/src/migration/setup_tables.rs
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
print!("Setting up database...");
|
||||
|
||||
// Stats table
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Stats::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(Stats::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(Stats::EventCount).integer().not_null())
|
||||
.col(ColumnDef::new(Stats::PersonCount).integer().not_null())
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Events table
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Event::Table)
|
||||
.if_not_exists()
|
||||
.col(ColumnDef::new(Event::Id).string().not_null().primary_key())
|
||||
.col(ColumnDef::new(Event::Name).string().not_null())
|
||||
.col(ColumnDef::new(Event::CreatedAt).timestamp().not_null())
|
||||
.col(ColumnDef::new(Event::Times).json().not_null())
|
||||
.col(ColumnDef::new(Event::Timezone).string().not_null())
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// People table
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Person::Table)
|
||||
.if_not_exists()
|
||||
.col(ColumnDef::new(Person::Name).string().not_null())
|
||||
.col(ColumnDef::new(Person::PasswordHash).string())
|
||||
.col(ColumnDef::new(Person::CreatedAt).timestamp().not_null())
|
||||
.col(ColumnDef::new(Person::Availability).json().not_null())
|
||||
.col(ColumnDef::new(Person::EventId).string().not_null())
|
||||
.primary_key(Index::create().col(Person::EventId).col(Person::Name))
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Relation
|
||||
manager
|
||||
.create_foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("FK_person_event")
|
||||
.from(Person::Table, Person::EventId)
|
||||
.to(Event::Table, Event::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
.on_update(ForeignKeyAction::Cascade)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!(" done");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(Stats::Table).to_owned())
|
||||
.await?;
|
||||
manager
|
||||
.drop_table(Table::drop().table(Person::Table).to_owned())
|
||||
.await?;
|
||||
manager
|
||||
.drop_table(Table::drop().table(Event::Table).to_owned())
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Learn more at https://docs.rs/sea-query#iden
|
||||
#[derive(Iden)]
|
||||
enum Stats {
|
||||
Table,
|
||||
Id,
|
||||
EventCount,
|
||||
PersonCount,
|
||||
}
|
||||
|
||||
#[derive(Iden)]
|
||||
enum Event {
|
||||
Table,
|
||||
Id,
|
||||
Name,
|
||||
CreatedAt,
|
||||
Times,
|
||||
Timezone,
|
||||
}
|
||||
|
||||
#[derive(Iden)]
|
||||
enum Person {
|
||||
Table,
|
||||
Name,
|
||||
PasswordHash,
|
||||
CreatedAt,
|
||||
Availability,
|
||||
EventId,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue