Set up some routes using new Rust API
This commit is contained in:
parent
fdc58b428b
commit
fc8e2a4360
81
backend/Cargo.lock
generated
81
backend/Cargo.lock
generated
|
|
@ -30,6 +30,15 @@ dependencies = [
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aliasable"
|
name = "aliasable"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
|
|
@ -492,6 +501,14 @@ dependencies = [
|
||||||
"unicode-width",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "common"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"chrono",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "concurrent-queue"
|
name = "concurrent-queue"
|
||||||
version = "2.2.0"
|
version = "2.2.0"
|
||||||
|
|
@ -537,11 +554,18 @@ name = "crabfit_backend"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"data",
|
"chrono",
|
||||||
|
"common",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
|
"punycode",
|
||||||
|
"rand",
|
||||||
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"sql-adaptor",
|
"sql-adaptor",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -637,14 +661,6 @@ dependencies = [
|
||||||
"syn 2.0.15",
|
"syn 2.0.15",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "data"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"async-trait",
|
|
||||||
"chrono",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "der"
|
name = "der"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
|
|
@ -1305,6 +1321,16 @@ dependencies = [
|
||||||
"minimal-lexical",
|
"minimal-lexical",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nu-ansi-term"
|
||||||
|
version = "0.46.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
|
||||||
|
dependencies = [
|
||||||
|
"overload",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-bigint"
|
name = "num-bigint"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
|
|
@ -1453,6 +1479,12 @@ dependencies = [
|
||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "overload"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking"
|
name = "parking"
|
||||||
version = "2.1.0"
|
version = "2.1.0"
|
||||||
|
|
@ -1649,6 +1681,12 @@ dependencies = [
|
||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "punycode"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e9e1dcb320d6839f6edb64f7a4a59d39b30480d4d1765b56873f7c858538a5fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.27"
|
version = "1.0.27"
|
||||||
|
|
@ -1723,6 +1761,8 @@ version = "1.8.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370"
|
checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
"regex-syntax 0.7.1",
|
"regex-syntax 0.7.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2204,7 +2244,7 @@ dependencies = [
|
||||||
"async-std",
|
"async-std",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"chrono",
|
"chrono",
|
||||||
"data",
|
"common",
|
||||||
"sea-orm",
|
"sea-orm",
|
||||||
"sea-orm-migration",
|
"sea-orm-migration",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -2596,6 +2636,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a"
|
checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"valuable",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-log"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
"log",
|
||||||
|
"tracing-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2605,12 +2657,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
|
checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"matchers",
|
"matchers",
|
||||||
|
"nu-ansi-term",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"regex",
|
"regex",
|
||||||
"sharded-slab",
|
"sharded-slab",
|
||||||
|
"smallvec",
|
||||||
"thread_local",
|
"thread_local",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
|
"tracing-log",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2684,6 +2739,12 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "valuable"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "value-bag"
|
name = "value-bag"
|
||||||
version = "1.0.0-alpha.9"
|
version = "1.0.0-alpha.9"
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,19 @@ version = "1.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["data", "adaptors/*"]
|
members = ["common", "adaptors/*"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = "0.6.18"
|
axum = "0.6.18"
|
||||||
serde = { version = "1.0.162", features = ["derive"] }
|
serde = { version = "1.0.162", features = ["derive"] }
|
||||||
tokio = { version = "1.28.0", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1.28.0", features = ["macros", "rt-multi-thread"] }
|
||||||
data = { path = "data" }
|
common = { path = "common" }
|
||||||
sql-adaptor = { path = "adaptors/sql" }
|
sql-adaptor = { path = "adaptors/sql" }
|
||||||
dotenv = "0.15.0"
|
dotenv = "0.15.0"
|
||||||
|
serde_json = "1.0.96"
|
||||||
|
rand = "0.8.5"
|
||||||
|
punycode = "0.4.1"
|
||||||
|
regex = "1.8.1"
|
||||||
|
tracing = "0.1.37"
|
||||||
|
tracing-subscriber = "0.3.17"
|
||||||
|
chrono = "0.4.24"
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = "0.1.68"
|
async-trait = "0.1.68"
|
||||||
data = { path = "../../data" }
|
common = { path = "../../common" }
|
||||||
sea-orm = { version = "0.11.3", features = [ "macros", "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", "runtime-tokio-native-tls" ] }
|
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" ] }
|
serde = { version = "1.0.162", features = [ "derive" ] }
|
||||||
async-std = { version = "1", features = ["attributes", "tokio1"] }
|
async-std = { version = "1", features = ["attributes", "tokio1"] }
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
|
//! `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::*;
|
use sea_orm::entity::prelude::*;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||||
|
|
@ -11,6 +9,7 @@ pub struct Model {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub created_at: DateTime,
|
pub created_at: DateTime,
|
||||||
|
pub visited_at: DateTime,
|
||||||
pub times: Json,
|
pub times: Json,
|
||||||
pub timezone: String,
|
pub timezone: String,
|
||||||
}
|
}
|
||||||
|
|
@ -28,15 +27,3 @@ impl Related<super::person::Entity> for Entity {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
|
//! `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::*;
|
use sea_orm::entity::prelude::*;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||||
|
|
@ -35,14 +33,3 @@ impl Related<super::event::Entity> for Entity {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
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![]),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
|
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
|
||||||
|
|
||||||
use data::stats::Stats;
|
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||||
|
|
@ -16,12 +15,3 @@ pub struct Model {
|
||||||
pub enum Relation {}
|
pub enum Relation {}
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
use std::{env, error::Error};
|
use std::{env, error::Error};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use data::{
|
use chrono::{DateTime as ChronoDateTime, Utc};
|
||||||
|
use common::{
|
||||||
adaptor::Adaptor,
|
adaptor::Adaptor,
|
||||||
event::{Event, EventDeletion},
|
event::{Event, EventDeletion},
|
||||||
person::Person,
|
person::Person,
|
||||||
|
|
@ -10,54 +11,48 @@ use data::{
|
||||||
use entity::{event, person, stats};
|
use entity::{event, person, stats};
|
||||||
use migration::{Migrator, MigratorTrait};
|
use migration::{Migrator, MigratorTrait};
|
||||||
use sea_orm::{
|
use sea_orm::{
|
||||||
|
strum::Display,
|
||||||
ActiveModelTrait,
|
ActiveModelTrait,
|
||||||
ActiveValue::{NotSet, Set},
|
ActiveValue::{NotSet, Set},
|
||||||
ColumnTrait, Database, DatabaseConnection, DbErr, EntityTrait, ModelTrait, QueryFilter,
|
ColumnTrait, Database, DatabaseConnection, DbErr, EntityTrait, ModelTrait, QueryFilter,
|
||||||
TransactionTrait, TryIntoModel,
|
TransactionError, TransactionTrait, TryIntoModel,
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
mod entity;
|
mod entity;
|
||||||
mod migration;
|
mod migration;
|
||||||
|
|
||||||
pub struct PostgresAdaptor {
|
pub struct SqlAdaptor {
|
||||||
db: DatabaseConnection,
|
db: DatabaseConnection,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Adaptor for PostgresAdaptor {
|
impl Adaptor for SqlAdaptor {
|
||||||
async fn new() -> Self {
|
type Error = SqlAdaptorError;
|
||||||
let connection_string = env::var("DATABASE_URL").unwrap();
|
|
||||||
|
|
||||||
// Connect to the database
|
async fn get_stats(&self) -> Result<Stats, Self::Error> {
|
||||||
let db = Database::connect(&connection_string).await.unwrap();
|
let stats_row = get_stats_row(&self.db).await?;
|
||||||
println!("Connected to database at {}", connection_string);
|
Ok(Stats {
|
||||||
|
event_count: stats_row.event_count.unwrap(),
|
||||||
// Setup tables
|
person_count: stats_row.person_count.unwrap(),
|
||||||
Migrator::up(&db, None).await.unwrap();
|
})
|
||||||
|
|
||||||
Self { db }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_stats(&self) -> Result<Stats, Box<dyn Error>> {
|
async fn increment_stat_event_count(&self) -> Result<i32, Self::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?;
|
let mut current_stats = get_stats_row(&self.db).await?;
|
||||||
current_stats.event_count = Set(current_stats.event_count.unwrap() + 1);
|
current_stats.event_count = Set(current_stats.event_count.unwrap() + 1);
|
||||||
|
|
||||||
Ok(current_stats.save(&self.db).await?.event_count.unwrap())
|
Ok(current_stats.save(&self.db).await?.event_count.unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn increment_stat_person_count(&self) -> Result<i32, Box<dyn Error>> {
|
async fn increment_stat_person_count(&self) -> Result<i32, Self::Error> {
|
||||||
let mut current_stats = get_stats_row(&self.db).await?;
|
let mut current_stats = get_stats_row(&self.db).await?;
|
||||||
current_stats.person_count = Set(current_stats.person_count.unwrap() + 1);
|
current_stats.person_count = Set(current_stats.person_count.unwrap() + 1);
|
||||||
|
|
||||||
Ok(current_stats.save(&self.db).await?.person_count.unwrap())
|
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>> {
|
async fn get_people(&self, event_id: String) -> Result<Option<Vec<Person>>, Self::Error> {
|
||||||
// TODO: optimize into one query
|
// TODO: optimize into one query
|
||||||
let event_row = event::Entity::find_by_id(event_id).one(&self.db).await?;
|
let event_row = event::Entity::find_by_id(event_id).one(&self.db).await?;
|
||||||
|
|
||||||
|
|
@ -75,11 +70,7 @@ impl Adaptor for PostgresAdaptor {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn upsert_person(
|
async fn upsert_person(&self, event_id: String, person: Person) -> Result<Person, Self::Error> {
|
||||||
&self,
|
|
||||||
event_id: String,
|
|
||||||
person: Person,
|
|
||||||
) -> Result<Person, Box<dyn Error>> {
|
|
||||||
Ok(person::ActiveModel {
|
Ok(person::ActiveModel {
|
||||||
name: Set(person.name),
|
name: Set(person.name),
|
||||||
password_hash: Set(person.password_hash),
|
password_hash: Set(person.password_hash),
|
||||||
|
|
@ -93,28 +84,29 @@ impl Adaptor for PostgresAdaptor {
|
||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_event(&self, id: String) -> Result<Option<Event>, Box<dyn Error>> {
|
async fn get_event(&self, id: String) -> Result<Option<Event>, Self::Error> {
|
||||||
Ok(event::Entity::find_by_id(id)
|
Ok(event::Entity::find_by_id(id)
|
||||||
.one(&self.db)
|
.one(&self.db)
|
||||||
.await?
|
.await?
|
||||||
.map(|model| model.into()))
|
.map(|model| model.into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_event(&self, event: Event) -> Result<Event, Box<dyn Error>> {
|
async fn create_event(&self, event: Event) -> Result<Event, Self::Error> {
|
||||||
Ok(event::ActiveModel {
|
Ok(event::ActiveModel {
|
||||||
id: Set(event.id),
|
id: Set(event.id),
|
||||||
name: Set(event.name),
|
name: Set(event.name),
|
||||||
created_at: Set(event.created_at.naive_utc()),
|
created_at: Set(event.created_at.naive_utc()),
|
||||||
|
visited_at: Set(event.visited_at.naive_utc()),
|
||||||
times: Set(serde_json::to_value(event.times).unwrap_or(json!([]))),
|
times: Set(serde_json::to_value(event.times).unwrap_or(json!([]))),
|
||||||
timezone: Set(event.timezone),
|
timezone: Set(event.timezone),
|
||||||
}
|
}
|
||||||
.save(&self.db)
|
.insert(&self.db)
|
||||||
.await?
|
.await?
|
||||||
.try_into_model()?
|
.try_into_model()?
|
||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_event(&self, id: String) -> Result<EventDeletion, Box<dyn Error>> {
|
async fn delete_event(&self, id: String) -> Result<EventDeletion, Self::Error> {
|
||||||
let event_id = id.clone();
|
let event_id = id.clone();
|
||||||
let person_count = self
|
let person_count = self
|
||||||
.db
|
.db
|
||||||
|
|
@ -141,6 +133,7 @@ impl Adaptor for PostgresAdaptor {
|
||||||
// Get the current stats as an ActiveModel
|
// Get the current stats as an ActiveModel
|
||||||
async fn get_stats_row(db: &DatabaseConnection) -> Result<stats::ActiveModel, DbErr> {
|
async fn get_stats_row(db: &DatabaseConnection) -> Result<stats::ActiveModel, DbErr> {
|
||||||
let current_stats = stats::Entity::find().one(db).await?;
|
let current_stats = stats::Entity::find().one(db).await?;
|
||||||
|
|
||||||
Ok(match current_stats {
|
Ok(match current_stats {
|
||||||
Some(model) => model.into(),
|
Some(model) => model.into(),
|
||||||
None => stats::ActiveModel {
|
None => stats::ActiveModel {
|
||||||
|
|
@ -150,3 +143,61 @@ async fn get_stats_row(db: &DatabaseConnection) -> Result<stats::ActiveModel, Db
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SqlAdaptor {
|
||||||
|
pub 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<event::Model> for Event {
|
||||||
|
fn from(value: event::Model) -> Self {
|
||||||
|
Self {
|
||||||
|
id: value.id,
|
||||||
|
name: value.name,
|
||||||
|
created_at: ChronoDateTime::<Utc>::from_utc(value.created_at, Utc),
|
||||||
|
visited_at: ChronoDateTime::<Utc>::from_utc(value.visited_at, Utc),
|
||||||
|
times: serde_json::from_value(value.times).unwrap_or(vec![]),
|
||||||
|
timezone: value.timezone,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<person::Model> for Person {
|
||||||
|
fn from(value: person::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![]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Display, Debug)]
|
||||||
|
pub enum SqlAdaptorError {
|
||||||
|
DbErr(DbErr),
|
||||||
|
TransactionError(TransactionError<DbErr>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for SqlAdaptorError {}
|
||||||
|
|
||||||
|
impl From<DbErr> for SqlAdaptorError {
|
||||||
|
fn from(value: DbErr) -> Self {
|
||||||
|
Self::DbErr(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<TransactionError<DbErr>> for SqlAdaptorError {
|
||||||
|
fn from(value: TransactionError<DbErr>) -> Self {
|
||||||
|
Self::TransactionError(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ impl MigrationTrait for Migration {
|
||||||
.col(ColumnDef::new(Event::Id).string().not_null().primary_key())
|
.col(ColumnDef::new(Event::Id).string().not_null().primary_key())
|
||||||
.col(ColumnDef::new(Event::Name).string().not_null())
|
.col(ColumnDef::new(Event::Name).string().not_null())
|
||||||
.col(ColumnDef::new(Event::CreatedAt).timestamp().not_null())
|
.col(ColumnDef::new(Event::CreatedAt).timestamp().not_null())
|
||||||
|
.col(ColumnDef::new(Event::VisitedAt).timestamp().not_null())
|
||||||
.col(ColumnDef::new(Event::Times).json().not_null())
|
.col(ColumnDef::new(Event::Times).json().not_null())
|
||||||
.col(ColumnDef::new(Event::Timezone).string().not_null())
|
.col(ColumnDef::new(Event::Timezone).string().not_null())
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
|
|
@ -105,6 +106,7 @@ enum Event {
|
||||||
Id,
|
Id,
|
||||||
Name,
|
Name,
|
||||||
CreatedAt,
|
CreatedAt,
|
||||||
|
VisitedAt,
|
||||||
Times,
|
Times,
|
||||||
Timezone,
|
Timezone,
|
||||||
}
|
}
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
pub use sea_orm_migration::prelude::*;
|
pub use sea_orm_migration::prelude::*;
|
||||||
|
|
||||||
mod setup_tables;
|
mod m01_setup_tables;
|
||||||
|
|
||||||
pub struct Migrator;
|
pub struct Migrator;
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl MigratorTrait for Migrator {
|
impl MigratorTrait for Migrator {
|
||||||
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||||
vec![Box::new(setup_tables::Migration)]
|
vec![Box::new(m01_setup_tables::Migration)]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
9
backend/common/Cargo.toml
Normal file
9
backend/common/Cargo.toml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
[package]
|
||||||
|
name = "common"
|
||||||
|
description = "Shared structs and traits for the data storage and transfer of Crab Fit"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
async-trait = "0.1.68"
|
||||||
|
chrono = "0.4.24"
|
||||||
29
backend/common/src/adaptor.rs
Normal file
29
backend/common/src/adaptor.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
event::{Event, EventDeletion},
|
||||||
|
person::Person,
|
||||||
|
stats::Stats,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Data storage adaptor, all methods on an adaptor can return an error if
|
||||||
|
/// something goes wrong, or potentially None if the data requested was not found.
|
||||||
|
#[async_trait]
|
||||||
|
pub trait Adaptor: Send + Sync {
|
||||||
|
type Error: Error;
|
||||||
|
|
||||||
|
async fn get_stats(&self) -> Result<Stats, Self::Error>;
|
||||||
|
async fn increment_stat_event_count(&self) -> Result<i32, Self::Error>;
|
||||||
|
async fn increment_stat_person_count(&self) -> Result<i32, Self::Error>;
|
||||||
|
|
||||||
|
async fn get_people(&self, event_id: String) -> Result<Option<Vec<Person>>, Self::Error>;
|
||||||
|
async fn upsert_person(&self, event_id: String, person: Person) -> Result<Person, Self::Error>;
|
||||||
|
|
||||||
|
async fn get_event(&self, id: String) -> Result<Option<Event>, Self::Error>;
|
||||||
|
async fn create_event(&self, event: Event) -> Result<Event, Self::Error>;
|
||||||
|
|
||||||
|
/// Delete an event as well as all related people
|
||||||
|
async fn delete_event(&self, id: String) -> Result<EventDeletion, Self::Error>;
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ pub struct Event {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub visited_at: DateTime<Utc>,
|
||||||
pub times: Vec<String>,
|
pub times: Vec<String>,
|
||||||
pub timezone: String,
|
pub timezone: String,
|
||||||
}
|
}
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "data"
|
|
||||||
description = "Structs and traits for the data storage and transfer of Crab Fit"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
async-trait = "0.1.68"
|
|
||||||
chrono = "0.4.24"
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
use std::error::Error;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
event::{Event, EventDeletion},
|
|
||||||
person::Person,
|
|
||||||
stats::Stats,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Data storage adaptor, all methods on an adaptor can return an error if
|
|
||||||
/// something goes wrong, or potentially None if the data requested was not found.
|
|
||||||
#[async_trait]
|
|
||||||
pub trait Adaptor {
|
|
||||||
/// Creates a new adaptor and performs all setup required
|
|
||||||
///
|
|
||||||
/// # Panics
|
|
||||||
/// If an error occurs while setting up the adaptor
|
|
||||||
async fn new() -> Self;
|
|
||||||
|
|
||||||
async fn get_stats(&self) -> Result<Stats, Box<dyn Error>>;
|
|
||||||
async fn increment_stat_event_count(&self) -> Result<i32, Box<dyn Error>>;
|
|
||||||
async fn increment_stat_person_count(&self) -> Result<i32, Box<dyn Error>>;
|
|
||||||
|
|
||||||
async fn get_people(&self, event_id: String) -> Result<Option<Vec<Person>>, Box<dyn Error>>;
|
|
||||||
async fn upsert_person(
|
|
||||||
&self,
|
|
||||||
event_id: String,
|
|
||||||
person: Person,
|
|
||||||
) -> Result<Person, Box<dyn Error>>;
|
|
||||||
|
|
||||||
async fn get_event(&self, id: String) -> Result<Option<Event>, Box<dyn Error>>;
|
|
||||||
async fn create_event(&self, event: Event) -> Result<Event, Box<dyn Error>>;
|
|
||||||
|
|
||||||
/// Delete an event as well as all related people
|
|
||||||
async fn delete_event(&self, id: String) -> Result<EventDeletion, Box<dyn Error>>;
|
|
||||||
}
|
|
||||||
22
backend/src/errors.rs
Normal file
22
backend/src/errors.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
use axum::{http::StatusCode, response::IntoResponse};
|
||||||
|
use common::adaptor::Adaptor;
|
||||||
|
|
||||||
|
pub enum ApiError<A: Adaptor> {
|
||||||
|
AdaptorError(A::Error),
|
||||||
|
NotFound,
|
||||||
|
// NotAuthorized,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define what the error types above should return
|
||||||
|
impl<A: Adaptor> IntoResponse for ApiError<A> {
|
||||||
|
fn into_response(self) -> axum::response::Response {
|
||||||
|
match self {
|
||||||
|
ApiError::AdaptorError(e) => {
|
||||||
|
tracing::error!(?e);
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||||
|
}
|
||||||
|
ApiError::NotFound => StatusCode::NOT_FOUND.into_response(),
|
||||||
|
// ApiError::NotAuthorized => StatusCode::UNAUTHORIZED.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,17 @@
|
||||||
use std::net::SocketAddr;
|
use std::{net::SocketAddr, sync::Arc};
|
||||||
|
|
||||||
use axum::{routing::get, Router, Server};
|
use axum::{
|
||||||
use data::adaptor::Adaptor;
|
extract,
|
||||||
use sql_adaptor::PostgresAdaptor;
|
routing::{get, post},
|
||||||
|
Router, Server,
|
||||||
|
};
|
||||||
|
use routes::*;
|
||||||
|
use sql_adaptor::SqlAdaptor;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
mod errors;
|
||||||
|
mod payloads;
|
||||||
|
mod routes;
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
const MODE: &str = "debug";
|
const MODE: &str = "debug";
|
||||||
|
|
@ -10,14 +19,29 @@ const MODE: &str = "debug";
|
||||||
#[cfg(not(debug_assertions))]
|
#[cfg(not(debug_assertions))]
|
||||||
const MODE: &str = "release";
|
const MODE: &str = "release";
|
||||||
|
|
||||||
|
pub struct ApiState<A> {
|
||||||
|
adaptor: A,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type State<A> = extract::State<Arc<Mutex<ApiState<A>>>>;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
// Load env
|
// Load env
|
||||||
dotenv::dotenv().ok();
|
dotenv::dotenv().ok();
|
||||||
|
|
||||||
PostgresAdaptor::new().await;
|
let shared_state = Arc::new(Mutex::new(ApiState {
|
||||||
|
adaptor: SqlAdaptor::new().await,
|
||||||
|
}));
|
||||||
|
|
||||||
let app = Router::new().route("/", get(get_root));
|
let app = Router::new()
|
||||||
|
.route("/", get(get_root))
|
||||||
|
.route("/stats", get(get_stats))
|
||||||
|
.route("/event/:event_id", get(get_event))
|
||||||
|
.route("/event", post(create_event))
|
||||||
|
.with_state(shared_state);
|
||||||
|
|
||||||
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
||||||
|
|
||||||
|
|
|
||||||
43
backend/src/payloads.rs
Normal file
43
backend/src/payloads.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
use axum::Json;
|
||||||
|
use common::event::Event;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::errors::ApiError;
|
||||||
|
|
||||||
|
pub type ApiResult<T, A> = Result<Json<T>, ApiError<A>>;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct EventInput {
|
||||||
|
pub name: String,
|
||||||
|
pub times: Vec<String>,
|
||||||
|
pub timezone: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct EventResponse {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub times: Vec<String>,
|
||||||
|
pub timezone: String,
|
||||||
|
pub created: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Event> for EventResponse {
|
||||||
|
fn from(value: Event) -> Self {
|
||||||
|
Self {
|
||||||
|
id: value.id,
|
||||||
|
name: value.name,
|
||||||
|
times: value.times,
|
||||||
|
timezone: value.timezone,
|
||||||
|
created: value.created_at.timestamp(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all(serialize = "camelCase"))]
|
||||||
|
pub struct StatsResponse {
|
||||||
|
pub event_count: i32,
|
||||||
|
pub person_count: i32,
|
||||||
|
pub version: String,
|
||||||
|
}
|
||||||
201
backend/src/res/adjectives.json
Normal file
201
backend/src/res/adjectives.json
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
[
|
||||||
|
"Adorable",
|
||||||
|
"Adventurous",
|
||||||
|
"Aggressive",
|
||||||
|
"Agreeable",
|
||||||
|
"Alert",
|
||||||
|
"Alive",
|
||||||
|
"Amused",
|
||||||
|
"Angry",
|
||||||
|
"Annoyed",
|
||||||
|
"Annoying",
|
||||||
|
"Anxious",
|
||||||
|
"Arrogant",
|
||||||
|
"Ashamed",
|
||||||
|
"Attractive",
|
||||||
|
"Average",
|
||||||
|
"Beautiful",
|
||||||
|
"Better",
|
||||||
|
"Bewildered",
|
||||||
|
"Blue",
|
||||||
|
"Blushing",
|
||||||
|
"Bored",
|
||||||
|
"Brainy",
|
||||||
|
"Brave",
|
||||||
|
"Breakable",
|
||||||
|
"Bright",
|
||||||
|
"Busy",
|
||||||
|
"Calm",
|
||||||
|
"Careful",
|
||||||
|
"Cautious",
|
||||||
|
"Charming",
|
||||||
|
"Cheerful",
|
||||||
|
"Clean",
|
||||||
|
"Clear",
|
||||||
|
"Clever",
|
||||||
|
"Cloudy",
|
||||||
|
"Clumsy",
|
||||||
|
"Colorful",
|
||||||
|
"Comfortable",
|
||||||
|
"Concerned",
|
||||||
|
"Confused",
|
||||||
|
"Cooperative",
|
||||||
|
"Courageous",
|
||||||
|
"Crazy",
|
||||||
|
"Creepy",
|
||||||
|
"Crowded",
|
||||||
|
"Curious",
|
||||||
|
"Cute",
|
||||||
|
"Dangerous",
|
||||||
|
"Dark",
|
||||||
|
"Defiant",
|
||||||
|
"Delightful",
|
||||||
|
"Depressed",
|
||||||
|
"Determined",
|
||||||
|
"Different",
|
||||||
|
"Difficult",
|
||||||
|
"Disgusted",
|
||||||
|
"Distinct",
|
||||||
|
"Disturbed",
|
||||||
|
"Dizzy",
|
||||||
|
"Doubtful",
|
||||||
|
"Drab",
|
||||||
|
"Dull",
|
||||||
|
"Eager",
|
||||||
|
"Easy",
|
||||||
|
"Elated",
|
||||||
|
"Elegant",
|
||||||
|
"Embarrassed",
|
||||||
|
"Enchanting",
|
||||||
|
"Encouraging",
|
||||||
|
"Energetic",
|
||||||
|
"Enthusiastic",
|
||||||
|
"Envious",
|
||||||
|
"Evil",
|
||||||
|
"Excited",
|
||||||
|
"Expensive",
|
||||||
|
"Exuberant",
|
||||||
|
"Fair",
|
||||||
|
"Faithful",
|
||||||
|
"Famous",
|
||||||
|
"Fancy",
|
||||||
|
"Fantastic",
|
||||||
|
"Fierce",
|
||||||
|
"Fine",
|
||||||
|
"Foolish",
|
||||||
|
"Fragile",
|
||||||
|
"Frail",
|
||||||
|
"Frantic",
|
||||||
|
"Friendly",
|
||||||
|
"Frightened",
|
||||||
|
"Funny",
|
||||||
|
"Gentle",
|
||||||
|
"Gifted",
|
||||||
|
"Glamorous",
|
||||||
|
"Gleaming",
|
||||||
|
"Glorious",
|
||||||
|
"Good",
|
||||||
|
"Gorgeous",
|
||||||
|
"Graceful",
|
||||||
|
"Grumpy",
|
||||||
|
"Handsome",
|
||||||
|
"Happy",
|
||||||
|
"Healthy",
|
||||||
|
"Helpful",
|
||||||
|
"Hilarious",
|
||||||
|
"Homely",
|
||||||
|
"Hungry",
|
||||||
|
"Important",
|
||||||
|
"Impossible",
|
||||||
|
"Inexpensive",
|
||||||
|
"Innocent",
|
||||||
|
"Inquisitive",
|
||||||
|
"Itchy",
|
||||||
|
"Jealous",
|
||||||
|
"Jittery",
|
||||||
|
"Jolly",
|
||||||
|
"Joyous",
|
||||||
|
"Kind",
|
||||||
|
"Lazy",
|
||||||
|
"Light",
|
||||||
|
"Lively",
|
||||||
|
"Lonely",
|
||||||
|
"Long",
|
||||||
|
"Lovely",
|
||||||
|
"Lucky",
|
||||||
|
"Magnificent",
|
||||||
|
"Misty",
|
||||||
|
"Modern",
|
||||||
|
"Motionless",
|
||||||
|
"Muddy",
|
||||||
|
"Mushy",
|
||||||
|
"Mysterious",
|
||||||
|
"Naughty",
|
||||||
|
"Nervous",
|
||||||
|
"Nice",
|
||||||
|
"Nutty",
|
||||||
|
"Obedient",
|
||||||
|
"Obnoxious",
|
||||||
|
"Odd",
|
||||||
|
"Old-fashioned",
|
||||||
|
"Open",
|
||||||
|
"Outrageous",
|
||||||
|
"Outstanding",
|
||||||
|
"Panicky",
|
||||||
|
"Perfect",
|
||||||
|
"Plain",
|
||||||
|
"Pleasant",
|
||||||
|
"Poised",
|
||||||
|
"Powerful",
|
||||||
|
"Precious",
|
||||||
|
"Prickly",
|
||||||
|
"Proud",
|
||||||
|
"Puzzled",
|
||||||
|
"Quaint",
|
||||||
|
"Real",
|
||||||
|
"Relieved",
|
||||||
|
"Scary",
|
||||||
|
"Selfish",
|
||||||
|
"Shiny",
|
||||||
|
"Shy",
|
||||||
|
"Silly",
|
||||||
|
"Sleepy",
|
||||||
|
"Smiling",
|
||||||
|
"Smoggy",
|
||||||
|
"Sparkling",
|
||||||
|
"Splendid",
|
||||||
|
"Spotless",
|
||||||
|
"Stormy",
|
||||||
|
"Strange",
|
||||||
|
"Successful",
|
||||||
|
"Super",
|
||||||
|
"Talented",
|
||||||
|
"Tame",
|
||||||
|
"Tasty",
|
||||||
|
"Tender",
|
||||||
|
"Tense",
|
||||||
|
"Terrible",
|
||||||
|
"Thankful",
|
||||||
|
"Thoughtful",
|
||||||
|
"Thoughtless",
|
||||||
|
"Tired",
|
||||||
|
"Tough",
|
||||||
|
"Uninterested",
|
||||||
|
"Unsightly",
|
||||||
|
"Unusual",
|
||||||
|
"Upset",
|
||||||
|
"Uptight",
|
||||||
|
"Vast",
|
||||||
|
"Victorious",
|
||||||
|
"Vivacious",
|
||||||
|
"Wandering",
|
||||||
|
"Weary",
|
||||||
|
"Wicked",
|
||||||
|
"Wide-eyed",
|
||||||
|
"Wild",
|
||||||
|
"Witty",
|
||||||
|
"Worried",
|
||||||
|
"Worrisome",
|
||||||
|
"Zany",
|
||||||
|
"Zealous"
|
||||||
|
]
|
||||||
47
backend/src/res/crabs.json
Normal file
47
backend/src/res/crabs.json
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
[
|
||||||
|
"American Horseshoe",
|
||||||
|
"Atlantic Ghost",
|
||||||
|
"Baja Elbow",
|
||||||
|
"Big Claw Purple Hermit",
|
||||||
|
"Coldwater Mole",
|
||||||
|
"Cuata Swim",
|
||||||
|
"Deepwater Frog",
|
||||||
|
"Dwarf Teardrop",
|
||||||
|
"Elegant Hermit",
|
||||||
|
"Flat Spider",
|
||||||
|
"Ghost",
|
||||||
|
"Globe Purse",
|
||||||
|
"Green",
|
||||||
|
"Halloween",
|
||||||
|
"Harbor Spider",
|
||||||
|
"Inflated Spider",
|
||||||
|
"Left Clawed Hermit",
|
||||||
|
"Lumpy Claw",
|
||||||
|
"Magnificent Hermit",
|
||||||
|
"Mexican Spider",
|
||||||
|
"Mouthless Land",
|
||||||
|
"Northern Lemon Rock",
|
||||||
|
"Pacific Arrow",
|
||||||
|
"Pacific Mole",
|
||||||
|
"Paco Box",
|
||||||
|
"Panamic Spider",
|
||||||
|
"Purple Shore",
|
||||||
|
"Red Rock",
|
||||||
|
"Red Swim",
|
||||||
|
"Red-leg Hermit",
|
||||||
|
"Robust Swim",
|
||||||
|
"Rough Swim",
|
||||||
|
"Sand Swim",
|
||||||
|
"Sally Lightfoot",
|
||||||
|
"Shamed-face Box",
|
||||||
|
"Shamed-face Heart Box",
|
||||||
|
"Shell",
|
||||||
|
"Small Arched Box",
|
||||||
|
"Southern Kelp",
|
||||||
|
"Spotted Box",
|
||||||
|
"Striated Mole",
|
||||||
|
"Striped Shore",
|
||||||
|
"Tropical Mole",
|
||||||
|
"Walking Rock",
|
||||||
|
"Yellow Shore"
|
||||||
|
]
|
||||||
92
backend/src/routes/create_event.rs
Normal file
92
backend/src/routes/create_event.rs
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
use axum::{extract, Json};
|
||||||
|
use common::{adaptor::Adaptor, event::Event};
|
||||||
|
use rand::{seq::SliceRandom, thread_rng, Rng};
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
errors::ApiError,
|
||||||
|
payloads::{ApiResult, EventInput, EventResponse},
|
||||||
|
State,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn create_event<A: Adaptor>(
|
||||||
|
extract::State(state): State<A>,
|
||||||
|
Json(input): Json<EventInput>,
|
||||||
|
) -> ApiResult<EventResponse, A> {
|
||||||
|
let adaptor = &state.lock().await.adaptor;
|
||||||
|
|
||||||
|
// Get the current timestamp
|
||||||
|
let now = chrono::offset::Utc::now();
|
||||||
|
|
||||||
|
// Generate a name if none provided
|
||||||
|
let name = match input.name.trim() {
|
||||||
|
"" => generate_name(),
|
||||||
|
x => x.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate an ID
|
||||||
|
let mut id = generate_id(&name);
|
||||||
|
|
||||||
|
// Check the ID doesn't already exist
|
||||||
|
while (adaptor
|
||||||
|
.get_event(id.clone())
|
||||||
|
.await
|
||||||
|
.map_err(ApiError::AdaptorError)?)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
id = generate_id(&name);
|
||||||
|
}
|
||||||
|
|
||||||
|
let event = adaptor
|
||||||
|
.create_event(Event {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
created_at: now,
|
||||||
|
visited_at: now,
|
||||||
|
times: input.times,
|
||||||
|
timezone: input.timezone,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(ApiError::AdaptorError)?;
|
||||||
|
|
||||||
|
// Update stats
|
||||||
|
adaptor
|
||||||
|
.increment_stat_event_count()
|
||||||
|
.await
|
||||||
|
.map_err(ApiError::AdaptorError)?;
|
||||||
|
|
||||||
|
Ok(Json(event.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a random name based on an adjective and a crab species
|
||||||
|
fn generate_name() -> String {
|
||||||
|
let adjectives: Vec<String> =
|
||||||
|
serde_json::from_slice(include_bytes!("../res/adjectives.json")).unwrap();
|
||||||
|
let crabs: Vec<String> = serde_json::from_slice(include_bytes!("../res/crabs.json")).unwrap();
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"{} {} Crab",
|
||||||
|
adjectives.choose(&mut thread_rng()).unwrap(),
|
||||||
|
crabs.choose(&mut thread_rng()).unwrap()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a slug for the crab fit
|
||||||
|
fn generate_id(name: &str) -> String {
|
||||||
|
let mut id = encode_name(name.to_string());
|
||||||
|
if id.replace('-', "").is_empty() {
|
||||||
|
id = encode_name(generate_name());
|
||||||
|
}
|
||||||
|
let number = thread_rng().gen_range(100000..=999999);
|
||||||
|
format!("{}-{}", id, number)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use punycode to encode the name
|
||||||
|
fn encode_name(name: String) -> String {
|
||||||
|
let pc = punycode::encode(&name.trim().to_lowercase())
|
||||||
|
.unwrap_or(String::from(""))
|
||||||
|
.trim()
|
||||||
|
.replace(|c: char| !c.is_ascii_alphanumeric() && c != ' ', "");
|
||||||
|
let re = Regex::new(r"\s+").unwrap();
|
||||||
|
re.replace_all(&pc, "-").to_string()
|
||||||
|
}
|
||||||
34
backend/src/routes/get_event.rs
Normal file
34
backend/src/routes/get_event.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
use axum::{
|
||||||
|
extract::{self, Path},
|
||||||
|
Json,
|
||||||
|
};
|
||||||
|
use common::adaptor::Adaptor;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
errors::ApiError,
|
||||||
|
payloads::{ApiResult, EventResponse},
|
||||||
|
State,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn get_event<A: Adaptor>(
|
||||||
|
extract::State(state): State<A>,
|
||||||
|
Path(event_id): Path<String>,
|
||||||
|
) -> ApiResult<EventResponse, A> {
|
||||||
|
let adaptor = &state.lock().await.adaptor;
|
||||||
|
|
||||||
|
let event = adaptor
|
||||||
|
.get_event(event_id)
|
||||||
|
.await
|
||||||
|
.map_err(ApiError::AdaptorError)?;
|
||||||
|
|
||||||
|
match event {
|
||||||
|
Some(event) => Ok(Json(EventResponse {
|
||||||
|
id: event.id,
|
||||||
|
name: event.name,
|
||||||
|
times: event.times,
|
||||||
|
timezone: event.timezone,
|
||||||
|
created: event.created_at.timestamp(),
|
||||||
|
})),
|
||||||
|
None => Err(ApiError::NotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
20
backend/src/routes/get_stats.rs
Normal file
20
backend/src/routes/get_stats.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
use axum::{extract, Json};
|
||||||
|
use common::adaptor::Adaptor;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
errors::ApiError,
|
||||||
|
payloads::{ApiResult, StatsResponse},
|
||||||
|
State,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn get_stats<A: Adaptor>(extract::State(state): State<A>) -> ApiResult<StatsResponse, A> {
|
||||||
|
let adaptor = &state.lock().await.adaptor;
|
||||||
|
|
||||||
|
let stats = adaptor.get_stats().await.map_err(ApiError::AdaptorError)?;
|
||||||
|
|
||||||
|
Ok(Json(StatsResponse {
|
||||||
|
event_count: stats.event_count,
|
||||||
|
person_count: stats.person_count,
|
||||||
|
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
8
backend/src/routes/mod.rs
Normal file
8
backend/src/routes/mod.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
mod get_event;
|
||||||
|
pub use get_event::get_event;
|
||||||
|
|
||||||
|
mod get_stats;
|
||||||
|
pub use get_stats::get_stats;
|
||||||
|
|
||||||
|
mod create_event;
|
||||||
|
pub use create_event::create_event;
|
||||||
Loading…
Reference in a new issue