Rename main folders and write sql backend adaptor
6
.github/workflows/deploy_backend.yml
vendored
|
|
@ -3,7 +3,7 @@ name: Deploy Backend
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ['main']
|
branches: ['main']
|
||||||
paths: ['crabfit-backend/**']
|
paths: ['backend/**']
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
|
|
@ -11,7 +11,7 @@ jobs:
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: crabfit-backend
|
working-directory: backend
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
@ -33,5 +33,5 @@ jobs:
|
||||||
- id: deploy
|
- id: deploy
|
||||||
uses: google-github-actions/deploy-appengine@v0
|
uses: google-github-actions/deploy-appengine@v0
|
||||||
with:
|
with:
|
||||||
working_directory: crabfit-backend
|
working_directory: backend
|
||||||
version: v1
|
version: v1
|
||||||
|
|
|
||||||
6
.github/workflows/deploy_frontend.yml
vendored
|
|
@ -3,7 +3,7 @@ name: Deploy Frontend
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ['main']
|
branches: ['main']
|
||||||
paths: ['crabfit-frontend/**']
|
paths: ['frontend/**']
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
|
|
@ -11,7 +11,7 @@ jobs:
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: crabfit-frontend
|
working-directory: frontend
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
@ -33,5 +33,5 @@ jobs:
|
||||||
- id: deploy
|
- id: deploy
|
||||||
uses: google-github-actions/deploy-appengine@v0
|
uses: google-github-actions/deploy-appengine@v0
|
||||||
with:
|
with:
|
||||||
working_directory: crabfit-frontend
|
working_directory: frontend
|
||||||
version: v1
|
version: v1
|
||||||
|
|
|
||||||
3
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
||||||
/graphics
|
/graphics
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/crabfit-browser-extension/*.zip
|
/browser-extension/*.zip
|
||||||
|
js-backend
|
||||||
|
|
|
||||||
2
backend/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
target
|
||||||
|
.env
|
||||||
3014
backend/Cargo.lock
generated
Normal file
15
backend/Cargo.toml
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
[package]
|
||||||
|
name = "crabfit_backend"
|
||||||
|
version = "1.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = ["data", "adaptors/*"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
axum = "0.6.18"
|
||||||
|
serde = { version = "1.0.162", features = ["derive"] }
|
||||||
|
tokio = { version = "1.28.0", features = ["macros", "rt-multi-thread"] }
|
||||||
|
data = { path = "data" }
|
||||||
|
sql-adaptor = { path = "adaptors/sql" }
|
||||||
|
dotenv = "0.15.0"
|
||||||
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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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,
|
||||||
|
}
|
||||||
9
backend/data/Cargo.toml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
[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"
|
||||||
37
backend/data/src/adaptor.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
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>>;
|
||||||
|
}
|
||||||
16
backend/data/src/event.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
|
pub struct Event {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub times: Vec<String>,
|
||||||
|
pub timezone: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Info about a deleted event
|
||||||
|
pub struct EventDeletion {
|
||||||
|
pub id: String,
|
||||||
|
/// The amount of people that were in this event that were also deleted
|
||||||
|
pub person_count: u64,
|
||||||
|
}
|
||||||
4
backend/data/src/lib.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
pub mod adaptor;
|
||||||
|
pub mod event;
|
||||||
|
pub mod person;
|
||||||
|
pub mod stats;
|
||||||
8
backend/data/src/person.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
|
pub struct Person {
|
||||||
|
pub name: String,
|
||||||
|
pub password_hash: Option<String>,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub availability: Vec<String>,
|
||||||
|
}
|
||||||
4
backend/data/src/stats.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
pub struct Stats {
|
||||||
|
pub event_count: i32,
|
||||||
|
pub person_count: i32,
|
||||||
|
}
|
||||||
33
backend/src/main.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
|
use axum::{routing::get, Router, Server};
|
||||||
|
use data::adaptor::Adaptor;
|
||||||
|
use sql_adaptor::PostgresAdaptor;
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
const MODE: &str = "debug";
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
const MODE: &str = "release";
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
// Load env
|
||||||
|
dotenv::dotenv().ok();
|
||||||
|
|
||||||
|
PostgresAdaptor::new().await;
|
||||||
|
|
||||||
|
let app = Router::new().route("/", get(get_root));
|
||||||
|
|
||||||
|
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
||||||
|
|
||||||
|
println!("Crab Fit API listening at http://{} in {} mode", addr, MODE);
|
||||||
|
Server::bind(&addr)
|
||||||
|
.serve(app.into_make_service())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_root() -> String {
|
||||||
|
format!("Crab Fit API v{}", env!("CARGO_PKG_VERSION"))
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 416 B After Width: | Height: | Size: 416 B |
|
Before Width: | Height: | Size: 841 B After Width: | Height: | Size: 841 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
|
@ -1,50 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
'env': {
|
|
||||||
'es2021': true,
|
|
||||||
'node': true
|
|
||||||
},
|
|
||||||
'extends': 'eslint:recommended',
|
|
||||||
'overrides': [
|
|
||||||
],
|
|
||||||
'parserOptions': {
|
|
||||||
'ecmaVersion': 'latest',
|
|
||||||
'sourceType': 'module'
|
|
||||||
},
|
|
||||||
'rules': {
|
|
||||||
'indent': [
|
|
||||||
'error',
|
|
||||||
2
|
|
||||||
],
|
|
||||||
'linebreak-style': [
|
|
||||||
'error',
|
|
||||||
'unix'
|
|
||||||
],
|
|
||||||
'quotes': [
|
|
||||||
'error',
|
|
||||||
'single'
|
|
||||||
],
|
|
||||||
'semi': [
|
|
||||||
'error',
|
|
||||||
'never'
|
|
||||||
],
|
|
||||||
'eqeqeq': 2,
|
|
||||||
'no-return-await': 1,
|
|
||||||
'no-var': 2,
|
|
||||||
'prefer-const': 1,
|
|
||||||
'yoda': 2,
|
|
||||||
'no-trailing-spaces': 1,
|
|
||||||
'eol-last': [1, 'always'],
|
|
||||||
'no-unused-vars': [
|
|
||||||
1,
|
|
||||||
{
|
|
||||||
'args': 'all',
|
|
||||||
'argsIgnorePattern': '^_',
|
|
||||||
'ignoreRestSiblings': true
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'arrow-parens': [
|
|
||||||
'error',
|
|
||||||
'as-needed'
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
.gcloudignore
|
|
||||||
|
|
||||||
.git
|
|
||||||
.gitignore
|
|
||||||
|
|
||||||
.env
|
|
||||||
|
|
||||||
node_modules/
|
|
||||||
.parcel-cache
|
|
||||||
res
|
|
||||||
routes
|
|
||||||
swagger.yaml
|
|
||||||
8
crabfit-backend/.gitignore
vendored
|
|
@ -1,8 +0,0 @@
|
||||||
node_modules
|
|
||||||
dist
|
|
||||||
.parcel-cache
|
|
||||||
.env
|
|
||||||
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
runtime: nodejs16
|
|
||||||
service: api
|
|
||||||
handlers:
|
|
||||||
- url: /.*
|
|
||||||
secure: always
|
|
||||||
redirect_http_response_code: 301
|
|
||||||
script: auto
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
cron:
|
|
||||||
- description: "clean up old events"
|
|
||||||
url: /tasks/cleanup
|
|
||||||
schedule: every monday 09:00
|
|
||||||
target: api
|
|
||||||
- description: "remove people with an event id that no longer exists"
|
|
||||||
url: /tasks/removeOrphans
|
|
||||||
schedule: 1st wednesday of month 09:00
|
|
||||||
target: api
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
import { config } from 'dotenv'
|
|
||||||
import { Datastore } from '@google-cloud/datastore'
|
|
||||||
import express from 'express'
|
|
||||||
import cors from 'cors'
|
|
||||||
|
|
||||||
import packageJson from './package.json'
|
|
||||||
|
|
||||||
import {
|
|
||||||
stats,
|
|
||||||
getEvent,
|
|
||||||
createEvent,
|
|
||||||
getPeople,
|
|
||||||
createPerson,
|
|
||||||
login,
|
|
||||||
updatePerson,
|
|
||||||
taskCleanup,
|
|
||||||
taskRemoveOrphans,
|
|
||||||
} from './routes'
|
|
||||||
|
|
||||||
config()
|
|
||||||
|
|
||||||
const app = express()
|
|
||||||
const port = 8080
|
|
||||||
const corsOptions = {
|
|
||||||
origin: process.env.NODE_ENV === 'production' ? 'https://crab.fit' : 'http://localhost:5173',
|
|
||||||
}
|
|
||||||
|
|
||||||
const datastore = new Datastore({
|
|
||||||
keyFilename: process.env.GOOGLE_APPLICATION_CREDENTIALS,
|
|
||||||
})
|
|
||||||
|
|
||||||
app.use(express.json())
|
|
||||||
app.use((req, _res, next) => {
|
|
||||||
req.datastore = datastore
|
|
||||||
req.types = {
|
|
||||||
event: process.env.NODE_ENV === 'production' ? 'Event' : 'DevEvent',
|
|
||||||
person: process.env.NODE_ENV === 'production' ? 'Person' : 'DevPerson',
|
|
||||||
stats: process.env.NODE_ENV === 'production' ? 'Stats' : 'DevStats',
|
|
||||||
}
|
|
||||||
next()
|
|
||||||
})
|
|
||||||
app.options('*', cors(corsOptions))
|
|
||||||
app.use(cors(corsOptions))
|
|
||||||
|
|
||||||
app.get('/', (_req, res) => res.send(`Crabfit API v${packageJson.version}`))
|
|
||||||
|
|
||||||
app.get('/stats', stats)
|
|
||||||
app.get('/event/:eventId', getEvent)
|
|
||||||
app.post('/event', createEvent)
|
|
||||||
app.get('/event/:eventId/people', getPeople)
|
|
||||||
app.post('/event/:eventId/people', createPerson)
|
|
||||||
app.post('/event/:eventId/people/:personName', login)
|
|
||||||
app.patch('/event/:eventId/people/:personName', updatePerson)
|
|
||||||
|
|
||||||
// Tasks
|
|
||||||
app.get('/tasks/cleanup', taskCleanup)
|
|
||||||
app.get('/tasks/removeOrphans', taskRemoveOrphans)
|
|
||||||
|
|
||||||
app.listen(port, () => {
|
|
||||||
console.log(`Crabfit API listening at http://localhost:${port} in ${process.env.NODE_ENV === 'production' ? 'prod' : 'dev'} mode`)
|
|
||||||
})
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
{
|
|
||||||
"name": "crabfit-backend",
|
|
||||||
"version": "2.0.0",
|
|
||||||
"description": "API for Crabfit",
|
|
||||||
"author": "Ben Grant",
|
|
||||||
"license": "GPL-3.0-only",
|
|
||||||
"private": true,
|
|
||||||
"source": "index.js",
|
|
||||||
"main": "dist/index.js",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.0.0"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"build:dev": "NODE_ENV=development parcel build --no-cache",
|
|
||||||
"dev": "rm -rf .parcel-cache dist && NODE_ENV=development nodemon --exec \"yarn build:dev && yarn start\" --watch routes --watch res --watch index.js",
|
|
||||||
"build": "parcel build",
|
|
||||||
"start": "node ./dist/index.js",
|
|
||||||
"lint": "eslint index.js ./routes"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@google-cloud/datastore": "^7.0.0",
|
|
||||||
"bcrypt": "^5.0.1",
|
|
||||||
"cors": "^2.8.5",
|
|
||||||
"dayjs": "^1.11.5",
|
|
||||||
"dotenv": "^16.0.1",
|
|
||||||
"express": "^4.18.1",
|
|
||||||
"punycode": "^2.1.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"eslint": "^8.22.0",
|
|
||||||
"nodemon": "^2.0.19",
|
|
||||||
"parcel": "^2.7.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,201 +0,0 @@
|
||||||
[
|
|
||||||
"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"
|
|
||||||
]
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
[
|
|
||||||
"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"
|
|
||||||
]
|
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import punycode from 'punycode/'
|
|
||||||
|
|
||||||
import adjectives from '../res/adjectives.json'
|
|
||||||
import crabs from '../res/crabs.json'
|
|
||||||
|
|
||||||
const capitalize = string => string.charAt(0).toUpperCase() + string.slice(1)
|
|
||||||
|
|
||||||
// Generate a random name based on an adjective and a crab species
|
|
||||||
const generateName = () =>
|
|
||||||
`${capitalize(adjectives[Math.floor(Math.random() * adjectives.length)])} ${crabs[Math.floor(Math.random() * crabs.length)]} Crab`
|
|
||||||
|
|
||||||
// Generate a slug for the crab fit
|
|
||||||
const generateId = name => {
|
|
||||||
let id = punycode.encode(name.trim().toLowerCase()).trim().replace(/[^A-Za-z0-9 ]/g, '').replace(/\s+/g, '-')
|
|
||||||
if (id.replace(/-/g, '') === '') {
|
|
||||||
id = generateName().trim().toLowerCase().replace(/[^A-Za-z0-9 ]/g, '').replace(/\s+/g, '-')
|
|
||||||
}
|
|
||||||
const number = Math.floor(100000 + Math.random() * 900000)
|
|
||||||
return `${id}-${number}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const createEvent = async (req, res) => {
|
|
||||||
const { event } = req.body
|
|
||||||
|
|
||||||
try {
|
|
||||||
const name = event.name.trim() === '' ? generateName() : event.name.trim()
|
|
||||||
let eventId = generateId(name)
|
|
||||||
const currentTime = dayjs().unix()
|
|
||||||
|
|
||||||
// Check if the event ID already exists, and if so generate a new one
|
|
||||||
let eventResult
|
|
||||||
do {
|
|
||||||
const query = req.datastore.createQuery(req.types.event)
|
|
||||||
.select('__key__')
|
|
||||||
.filter('__key__', req.datastore.key([req.types.event, eventId]))
|
|
||||||
|
|
||||||
eventResult = (await req.datastore.runQuery(query))[0][0]
|
|
||||||
|
|
||||||
if (eventResult !== undefined) {
|
|
||||||
eventId = generateId(name)
|
|
||||||
}
|
|
||||||
} while (eventResult !== undefined)
|
|
||||||
|
|
||||||
const entity = {
|
|
||||||
key: req.datastore.key([req.types.event, eventId]),
|
|
||||||
data: {
|
|
||||||
name: name,
|
|
||||||
created: currentTime,
|
|
||||||
times: event.times,
|
|
||||||
timezone: event.timezone,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
await req.datastore.insert(entity)
|
|
||||||
|
|
||||||
res.status(201).send({
|
|
||||||
id: eventId,
|
|
||||||
name: name,
|
|
||||||
created: currentTime,
|
|
||||||
times: event.times,
|
|
||||||
timezone: event.timezone,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update stats
|
|
||||||
const eventCountResult = (await req.datastore.get(req.datastore.key([req.types.stats, 'eventCount'])))[0] || null
|
|
||||||
if (eventCountResult) {
|
|
||||||
await req.datastore.upsert({
|
|
||||||
...eventCountResult,
|
|
||||||
value: eventCountResult.value + 1,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
await req.datastore.insert({
|
|
||||||
key: req.datastore.key([req.types.stats, 'eventCount']),
|
|
||||||
data: { value: 1 },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
res.status(400).send({ error: 'An error occurred while creating the event' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default createEvent
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import bcrypt from 'bcrypt'
|
|
||||||
|
|
||||||
const createPerson = async (req, res) => {
|
|
||||||
const { eventId } = req.params
|
|
||||||
const { person } = req.body
|
|
||||||
|
|
||||||
try {
|
|
||||||
const event = (await req.datastore.get(req.datastore.key([req.types.event, eventId])))[0]
|
|
||||||
const query = req.datastore.createQuery(req.types.person)
|
|
||||||
.filter('eventId', eventId)
|
|
||||||
.filter('name', person.name)
|
|
||||||
const personResult = (await req.datastore.runQuery(query))[0][0]
|
|
||||||
|
|
||||||
if (event) {
|
|
||||||
if (person && personResult === undefined) {
|
|
||||||
const currentTime = dayjs().unix()
|
|
||||||
|
|
||||||
// If password
|
|
||||||
let hash = null
|
|
||||||
if (person.password) {
|
|
||||||
hash = await bcrypt.hash(person.password, 10)
|
|
||||||
}
|
|
||||||
|
|
||||||
const entity = {
|
|
||||||
key: req.datastore.key(req.types.person),
|
|
||||||
data: {
|
|
||||||
name: person.name.trim(),
|
|
||||||
password: hash,
|
|
||||||
eventId: eventId,
|
|
||||||
created: currentTime,
|
|
||||||
availability: [],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
await req.datastore.insert(entity)
|
|
||||||
|
|
||||||
res.status(201).send({ success: 'Created' })
|
|
||||||
|
|
||||||
// Update stats
|
|
||||||
const personCountResult = (await req.datastore.get(req.datastore.key([req.types.stats, 'personCount'])))[0] || null
|
|
||||||
if (personCountResult) {
|
|
||||||
await req.datastore.upsert({
|
|
||||||
...personCountResult,
|
|
||||||
value: personCountResult.value + 1,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
await req.datastore.insert({
|
|
||||||
key: req.datastore.key([req.types.stats, 'personCount']),
|
|
||||||
data: { value: 1 },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
res.status(400).send({ error: 'Unable to create person' })
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
res.status(404).send({ error: 'Event does not exist' })
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
res.status(400).send({ error: 'An error occurred while creating the person' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default createPerson
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
import dayjs from 'dayjs'
|
|
||||||
|
|
||||||
const getEvent = async (req, res) => {
|
|
||||||
const { eventId } = req.params
|
|
||||||
|
|
||||||
try {
|
|
||||||
const event = (await req.datastore.get(req.datastore.key([req.types.event, eventId])))[0]
|
|
||||||
|
|
||||||
if (event) {
|
|
||||||
res.send({
|
|
||||||
id: eventId,
|
|
||||||
...event,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update last visited time
|
|
||||||
await req.datastore.upsert({
|
|
||||||
...event,
|
|
||||||
visited: dayjs().unix()
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
res.status(404).send({ error: 'Event not found' })
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
res.status(404).send({ error: 'Event not found' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getEvent
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
const getPeople = async (req, res) => {
|
|
||||||
const { eventId } = req.params
|
|
||||||
|
|
||||||
try {
|
|
||||||
const query = req.datastore.createQuery(req.types.person).filter('eventId', eventId)
|
|
||||||
let people = (await req.datastore.runQuery(query))[0]
|
|
||||||
people = people.map(person => ({
|
|
||||||
name: person.name,
|
|
||||||
availability: person.availability,
|
|
||||||
created: person.created,
|
|
||||||
}))
|
|
||||||
|
|
||||||
res.send({ people })
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
res.status(404).send({ error: 'Person not found' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getPeople
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
export { default as stats } from './stats'
|
|
||||||
export { default as getEvent } from './getEvent'
|
|
||||||
export { default as createEvent } from './createEvent'
|
|
||||||
export { default as getPeople } from './getPeople'
|
|
||||||
export { default as createPerson } from './createPerson'
|
|
||||||
export { default as login } from './login'
|
|
||||||
export { default as updatePerson } from './updatePerson'
|
|
||||||
|
|
||||||
export { default as taskCleanup } from './taskCleanup'
|
|
||||||
export { default as taskRemoveOrphans } from './taskRemoveOrphans'
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import bcrypt from 'bcrypt'
|
|
||||||
|
|
||||||
const login = async (req, res) => {
|
|
||||||
const { eventId, personName } = req.params
|
|
||||||
const { person } = req.body
|
|
||||||
|
|
||||||
try {
|
|
||||||
const query = req.datastore.createQuery(req.types.person)
|
|
||||||
.filter('eventId', eventId)
|
|
||||||
.filter('name', personName)
|
|
||||||
const personResult = (await req.datastore.runQuery(query))[0][0]
|
|
||||||
|
|
||||||
if (personResult) {
|
|
||||||
if (personResult.password) {
|
|
||||||
const passwordsMatch = person && person.password && await bcrypt.compare(person.password, personResult.password)
|
|
||||||
if (!passwordsMatch) {
|
|
||||||
return res.status(401).send({ error: 'Incorrect password' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res.send({
|
|
||||||
name: personName,
|
|
||||||
availability: personResult.availability,
|
|
||||||
created: personResult.created,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
res.status(404).send({ error: 'Person does not exist' })
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
res.status(400).send({ error: 'An error occurred' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default login
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
import packageJson from '../package.json'
|
|
||||||
|
|
||||||
const stats = async (req, res) => {
|
|
||||||
let eventCount = null
|
|
||||||
let personCount = null
|
|
||||||
|
|
||||||
try {
|
|
||||||
const eventResult = (await req.datastore.get(req.datastore.key([req.types.stats, 'eventCount'])))[0] || null
|
|
||||||
const personResult = (await req.datastore.get(req.datastore.key([req.types.stats, 'personCount'])))[0] || null
|
|
||||||
|
|
||||||
if (eventResult) {
|
|
||||||
eventCount = eventResult.value
|
|
||||||
}
|
|
||||||
if (personResult) {
|
|
||||||
personCount = personResult.value
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
res.send({
|
|
||||||
eventCount,
|
|
||||||
personCount,
|
|
||||||
version: packageJson.version,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default stats
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
import dayjs from 'dayjs'
|
|
||||||
|
|
||||||
const taskCleanup = async (req, res) => {
|
|
||||||
if (req.header('X-Appengine-Cron') === undefined) {
|
|
||||||
return res.status(400).send({ error: 'This task can only be run from a cron job' })
|
|
||||||
}
|
|
||||||
|
|
||||||
const threeMonthsAgo = dayjs().subtract(3, 'month').unix()
|
|
||||||
|
|
||||||
console.log(`Running cleanup task at ${dayjs().format('h:mma D MMM YYYY')}`)
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Fetch events that haven't been visited in over 3 months
|
|
||||||
const eventQuery = req.datastore.createQuery(req.types.event).filter('visited', '<', threeMonthsAgo)
|
|
||||||
const oldEvents = (await req.datastore.runQuery(eventQuery))[0]
|
|
||||||
|
|
||||||
if (oldEvents && oldEvents.length > 0) {
|
|
||||||
const oldEventIds = oldEvents.map(e => e[req.datastore.KEY].name)
|
|
||||||
console.log(`Found ${oldEventIds.length} events to remove`)
|
|
||||||
|
|
||||||
// Fetch availabilities linked to the events discovered
|
|
||||||
let peopleDiscovered = 0
|
|
||||||
await Promise.all(oldEventIds.map(async eventId => {
|
|
||||||
const peopleQuery = req.datastore.createQuery(req.types.person).filter('eventId', eventId)
|
|
||||||
const oldPeople = (await req.datastore.runQuery(peopleQuery))[0]
|
|
||||||
|
|
||||||
if (oldPeople && oldPeople.length > 0) {
|
|
||||||
peopleDiscovered += oldPeople.length
|
|
||||||
await req.datastore.delete(oldPeople.map(person => person[req.datastore.KEY]))
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
await req.datastore.delete(oldEvents.map(event => event[req.datastore.KEY]))
|
|
||||||
|
|
||||||
console.log(`Cleanup successful: ${oldEventIds.length} events and ${peopleDiscovered} people removed`)
|
|
||||||
|
|
||||||
res.sendStatus(200)
|
|
||||||
} else {
|
|
||||||
console.log('Found 0 events to remove, ending cleanup')
|
|
||||||
res.sendStatus(404)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
res.sendStatus(404)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default taskCleanup
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
import dayjs from 'dayjs'
|
|
||||||
|
|
||||||
const taskRemoveOrphans = async (req, res) => {
|
|
||||||
if (req.header('X-Appengine-Cron') === undefined) {
|
|
||||||
return res.status(400).send({ error: 'This task can only be run from a cron job' })
|
|
||||||
}
|
|
||||||
|
|
||||||
const threeMonthsAgo = dayjs().subtract(3, 'month').unix()
|
|
||||||
|
|
||||||
console.log(`Running orphan removal task at ${dayjs().format('h:mma D MMM YYYY')}`)
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Fetch people that are older than 3 months
|
|
||||||
const peopleQuery = req.datastore.createQuery(req.types.person).filter('created', '<', threeMonthsAgo)
|
|
||||||
const oldPeople = (await req.datastore.runQuery(peopleQuery))[0]
|
|
||||||
|
|
||||||
if (oldPeople && oldPeople.length > 0) {
|
|
||||||
console.log(`Found ${oldPeople.length} people older than 3 months, checking for events`)
|
|
||||||
|
|
||||||
// Fetch events linked to the people discovered
|
|
||||||
let peopleWithoutEvents = 0
|
|
||||||
await Promise.all(oldPeople.map(async person => {
|
|
||||||
const event = (await req.datastore.get(req.datastore.key([req.types.event, person.eventId])))[0]
|
|
||||||
|
|
||||||
if (!event) {
|
|
||||||
peopleWithoutEvents++
|
|
||||||
await req.datastore.delete(person[req.datastore.KEY])
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
if (peopleWithoutEvents > 0) {
|
|
||||||
console.log(`Orphan removal successful: ${peopleWithoutEvents} people removed`)
|
|
||||||
res.sendStatus(200)
|
|
||||||
} else {
|
|
||||||
console.log('Found 0 people without events, ending orphan removal')
|
|
||||||
res.sendStatus(404)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('Found 0 people older than 3 months, ending orphan removal')
|
|
||||||
res.sendStatus(404)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
res.sendStatus(404)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default taskRemoveOrphans
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
import bcrypt from 'bcrypt'
|
|
||||||
|
|
||||||
const updatePerson = async (req, res) => {
|
|
||||||
const { eventId, personName } = req.params
|
|
||||||
const { person } = req.body
|
|
||||||
|
|
||||||
try {
|
|
||||||
const query = req.datastore.createQuery(req.types.person)
|
|
||||||
.filter('eventId', eventId)
|
|
||||||
.filter('name', personName)
|
|
||||||
const personResult = (await req.datastore.runQuery(query))[0][0]
|
|
||||||
|
|
||||||
if (personResult) {
|
|
||||||
if (person && person.availability) {
|
|
||||||
if (personResult.password) {
|
|
||||||
const passwordsMatch = person.password && await bcrypt.compare(person.password, personResult.password)
|
|
||||||
if (!passwordsMatch) {
|
|
||||||
return res.status(401).send({ error: 'Incorrect password' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await req.datastore.upsert({
|
|
||||||
...personResult,
|
|
||||||
availability: person.availability,
|
|
||||||
})
|
|
||||||
|
|
||||||
res.status(200).send({ success: 'Updated' })
|
|
||||||
} else {
|
|
||||||
res.status(400).send({ error: 'Availability must be set' })
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
res.status(404).send({ error: 'Person not found' })
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
res.status(400).send('An error occurred')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default updatePerson
|
|
||||||
|
|
@ -1,245 +0,0 @@
|
||||||
swagger: "2.0"
|
|
||||||
info:
|
|
||||||
title: "Crab Fit"
|
|
||||||
description: "Compare and align schedules to find a time that works for everyone"
|
|
||||||
version: "1.0.0"
|
|
||||||
host: "api-dot-crabfit.appspot.com"
|
|
||||||
x-google-endpoints:
|
|
||||||
- name: "api-dot-crabfit.appspot.com"
|
|
||||||
allowCors: true
|
|
||||||
schemes:
|
|
||||||
- "https"
|
|
||||||
produces:
|
|
||||||
- "application/json"
|
|
||||||
definitions:
|
|
||||||
Event:
|
|
||||||
type: "object"
|
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
type: "string"
|
|
||||||
name:
|
|
||||||
type: "string"
|
|
||||||
timezone:
|
|
||||||
type: "string"
|
|
||||||
created:
|
|
||||||
type: "integer"
|
|
||||||
times:
|
|
||||||
type: "array"
|
|
||||||
items:
|
|
||||||
type: "string"
|
|
||||||
Person:
|
|
||||||
type: "object"
|
|
||||||
properties:
|
|
||||||
name:
|
|
||||||
type: "string"
|
|
||||||
availability:
|
|
||||||
type: "array"
|
|
||||||
items:
|
|
||||||
type: "string"
|
|
||||||
created:
|
|
||||||
type: "integer"
|
|
||||||
paths:
|
|
||||||
"/stats":
|
|
||||||
get:
|
|
||||||
summary: "Return stats for crabfit"
|
|
||||||
operationId: "getStats"
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
description: "OK"
|
|
||||||
schema:
|
|
||||||
type: "object"
|
|
||||||
properties:
|
|
||||||
eventCount:
|
|
||||||
type: "integer"
|
|
||||||
personCount:
|
|
||||||
type: "integer"
|
|
||||||
version:
|
|
||||||
type: "string"
|
|
||||||
"/event/{eventId}":
|
|
||||||
get:
|
|
||||||
summary: "Return an event details"
|
|
||||||
operationId: "getEvent"
|
|
||||||
parameters:
|
|
||||||
- in: "path"
|
|
||||||
name: "eventId"
|
|
||||||
required: true
|
|
||||||
type: "string"
|
|
||||||
description: "The ID of the event"
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
description: "OK"
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/Event'
|
|
||||||
404:
|
|
||||||
description: "Not found"
|
|
||||||
"/event":
|
|
||||||
post:
|
|
||||||
summary: "Create a new event"
|
|
||||||
operationId: "postEvent"
|
|
||||||
parameters:
|
|
||||||
- in: "body"
|
|
||||||
name: "event"
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: "object"
|
|
||||||
properties:
|
|
||||||
name:
|
|
||||||
type: "string"
|
|
||||||
timezone:
|
|
||||||
type: "string"
|
|
||||||
times:
|
|
||||||
type: "array"
|
|
||||||
items:
|
|
||||||
type: "string"
|
|
||||||
description: "New event details"
|
|
||||||
responses:
|
|
||||||
201:
|
|
||||||
description: "Created"
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/Event'
|
|
||||||
400:
|
|
||||||
description: "Invalid data"
|
|
||||||
"/event/{eventId}/people":
|
|
||||||
get:
|
|
||||||
summary: "Get availabilities for an event"
|
|
||||||
operationId: "getPeople"
|
|
||||||
parameters:
|
|
||||||
- in: "path"
|
|
||||||
name: "eventId"
|
|
||||||
required: true
|
|
||||||
type: "string"
|
|
||||||
description: "The ID of the event"
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
description: "OK"
|
|
||||||
schema:
|
|
||||||
type: "object"
|
|
||||||
properties:
|
|
||||||
people:
|
|
||||||
type: "array"
|
|
||||||
items:
|
|
||||||
$ref: "#/definitions/Person"
|
|
||||||
404:
|
|
||||||
description: "Not found"
|
|
||||||
post:
|
|
||||||
summary: "Add a new person to the event"
|
|
||||||
operationId: "postPeople"
|
|
||||||
parameters:
|
|
||||||
- in: "path"
|
|
||||||
name: "eventId"
|
|
||||||
required: true
|
|
||||||
type: "string"
|
|
||||||
description: "The ID of the event"
|
|
||||||
- in: "body"
|
|
||||||
name: "person"
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: "object"
|
|
||||||
properties:
|
|
||||||
name:
|
|
||||||
type: "string"
|
|
||||||
password:
|
|
||||||
type: "string"
|
|
||||||
description: "New person details"
|
|
||||||
responses:
|
|
||||||
201:
|
|
||||||
description: "Created"
|
|
||||||
404:
|
|
||||||
description: "Not found"
|
|
||||||
400:
|
|
||||||
description: "Invalid data"
|
|
||||||
"/event/{eventId}/people/{personName}":
|
|
||||||
post:
|
|
||||||
summary: "Login as this person"
|
|
||||||
operationId: "getPerson"
|
|
||||||
parameters:
|
|
||||||
- in: "path"
|
|
||||||
name: "eventId"
|
|
||||||
required: true
|
|
||||||
type: "string"
|
|
||||||
description: "The ID of the event"
|
|
||||||
- in: "path"
|
|
||||||
name: "personName"
|
|
||||||
required: true
|
|
||||||
type: "string"
|
|
||||||
description: "The name of the person"
|
|
||||||
- in: "body"
|
|
||||||
name: "person"
|
|
||||||
required: false
|
|
||||||
schema:
|
|
||||||
type: "object"
|
|
||||||
properties:
|
|
||||||
password:
|
|
||||||
type: "string"
|
|
||||||
description: "Login details"
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
description: "OK"
|
|
||||||
schema:
|
|
||||||
$ref: "#/definitions/Person"
|
|
||||||
401:
|
|
||||||
description: "Incorrect password"
|
|
||||||
404:
|
|
||||||
description: "Not found"
|
|
||||||
patch:
|
|
||||||
summary: "Update this person's availabilities"
|
|
||||||
operationId: "patchPerson"
|
|
||||||
parameters:
|
|
||||||
- in: "path"
|
|
||||||
name: "eventId"
|
|
||||||
required: true
|
|
||||||
type: "string"
|
|
||||||
description: "The ID of the event"
|
|
||||||
- in: "path"
|
|
||||||
name: "personName"
|
|
||||||
required: true
|
|
||||||
type: "string"
|
|
||||||
description: "The name of the person"
|
|
||||||
- in: "body"
|
|
||||||
name: "person"
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: "object"
|
|
||||||
properties:
|
|
||||||
password:
|
|
||||||
type: "string"
|
|
||||||
availability:
|
|
||||||
type: "array"
|
|
||||||
items:
|
|
||||||
type: "string"
|
|
||||||
description: "Updated person details"
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
description: "OK"
|
|
||||||
401:
|
|
||||||
description: "Incorrect password"
|
|
||||||
404:
|
|
||||||
description: "Not found"
|
|
||||||
400:
|
|
||||||
description: "Invalid data"
|
|
||||||
"/tasks/cleanup":
|
|
||||||
get:
|
|
||||||
summary: "Delete events inactive for more than 3 months"
|
|
||||||
operationId: "taskCleanup"
|
|
||||||
tags:
|
|
||||||
- tasks
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
description: "OK"
|
|
||||||
404:
|
|
||||||
description: "Not found"
|
|
||||||
400:
|
|
||||||
description: "Not called from a cron job"
|
|
||||||
"/tasks/removeOrphans":
|
|
||||||
get:
|
|
||||||
summary: "Deletes people if the event they were created under no longer exists"
|
|
||||||
operationId: "taskRemoveOrphans"
|
|
||||||
tags:
|
|
||||||
- tasks
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
description: "OK"
|
|
||||||
404:
|
|
||||||
description: "Not found"
|
|
||||||
400:
|
|
||||||
description: "Not called from a cron job"
|
|
||||||
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |