Add tags for schedules

This commit is contained in:
Tobias Reisinger 2022-04-03 01:35:51 +02:00
parent f3f3d36eed
commit 75f8afd624
10 changed files with 375 additions and 184 deletions

View file

@ -1,126 +1,164 @@
CREATE TABLE controllers CREATE TABLE controllers
( (
id INTEGER id
PRIMARY KEY INTEGER
AUTOINCREMENT PRIMARY KEY
NOT NULL, AUTOINCREMENT
uid VARCHAR(36) NOT NULL,
NOT NULL uid
UNIQUE, VARCHAR(36)
name VARCHAR(128), NOT NULL
ip VARCHAR(16), UNIQUE,
port INTEGER, name
relay_count INTEGER, VARCHAR(128)
active BOOLEAN NOT NULL,
NOT NULL ip
VARCHAR(16),
port
INTEGER,
relay_count
INTEGER,
active
BOOLEAN
NOT NULL
); );
CREATE TABLE relays CREATE TABLE relays
( (
id INTEGER id
PRIMARY KEY INTEGER
AUTOINCREMENT PRIMARY KEY
NOT NULL, AUTOINCREMENT
name VARCHAR(128), NOT NULL,
number INTEGER name
NOT NULL, VARCHAR(128)
controller_id INTEGER NOT NULL,
NOT NULL number
REFERENCES controllers (id) INTEGER
ON DELETE CASCADE NOT NULL,
controller_id
INTEGER
NOT NULL
REFERENCES controllers (id)
ON DELETE CASCADE
); );
CREATE TABLE schedules CREATE TABLE schedules
( (
id INTEGER id
PRIMARY KEY INTEGER
AUTOINCREMENT PRIMARY KEY
NOT NULL, AUTOINCREMENT
uid BLOB NOT NULL,
NOT NULL uid
UNIQUE, BLOB
name VARCHAR(128) NOT NULL
NOT NULL, UNIQUE,
periods BLOB name
NOT NULL VARCHAR(128)
NOT NULL,
periods
BLOB
NOT NULL
); );
INSERT INTO schedules (uid, name, periods) VALUES (x'00', 'off', x''); INSERT INTO schedules (uid, name, periods) VALUES (x'00', 'off', x'');
INSERT INTO schedules (uid, name, periods) VALUES (x'01', 'on', x'00000000'); INSERT INTO schedules (uid, name, periods) VALUES (x'01', 'on', x'00000000');
CREATE TABLE tags CREATE TABLE tags
( (
id INTEGER id
PRIMARY KEY INTEGER
AUTOINCREMENT PRIMARY KEY
NOT NULL, AUTOINCREMENT
tag VARCHAR(128) NOT NULL,
NOT NULL tag
UNIQUE VARCHAR(128)
NOT NULL
UNIQUE
); );
CREATE TABLE junction_tag CREATE TABLE junction_tag
( (
id INTEGER id
PRIMARY KEY INTEGER
AUTOINCREMENT PRIMARY KEY
NOT NULL, AUTOINCREMENT
tag_id INTEGER NOT NULL,
NOT NULL tag_id
REFERENCES tags (id) INTEGER
ON DELETE CASCADE, NOT NULL
relay_id INTEGER REFERENCES tags (id)
REFERENCES relays (id) ON DELETE CASCADE,
ON DELETE CASCADE, relay_id
schedule_id INTEGER INTEGER
REFERENCES schedules (id) REFERENCES relays (id)
ON DELETE CASCADE ON DELETE CASCADE,
schedule_id
INTEGER
REFERENCES schedules (id)
ON DELETE CASCADE
); );
CREATE TABLE junction_relay_schedule CREATE TABLE junction_relay_schedule
( (
id INTEGER id
PRIMARY KEY INTEGER
AUTOINCREMENT PRIMARY KEY
NOT NULL, AUTOINCREMENT
weekday SMALLINT NOT NULL,
NOT NULL, weekday
relay_id INTEGER SMALLINT
REFERENCES relays (id) NOT NULL,
ON DELETE CASCADE, relay_id
schedule_id INTEGER INTEGER
DEFAULT 1 REFERENCES relays (id)
REFERENCES schedules (id) ON DELETE CASCADE,
ON DELETE SET DEFAULT schedule_id
INTEGER
DEFAULT 1
REFERENCES schedules (id)
ON DELETE SET DEFAULT
); );
CREATE TABLE macros CREATE TABLE macros
( (
id INTEGER id
PRIMARY KEY INTEGER
AUTOINCREMENT PRIMARY KEY
NOT NULL, AUTOINCREMENT
uid VARCHAR(36) NOT NULL,
NOT NULL uid
UNIQUE, VARCHAR(36)
name VARCHAR(128) NOT NULL
UNIQUE,
name
VARCHAR(128)
NOT NULL
); );
CREATE TABLE macro_actions CREATE TABLE macro_actions
( (
id INTEGER id
PRIMARY KEY INTEGER
AUTOINCREMENT PRIMARY KEY
NOT NULL, AUTOINCREMENT
macro_id INTEGER NOT NULL,
NOT NULL macro_id
REFERENCES macros (id) INTEGER
ON DELETE CASCADE, NOT NULL
relay_id INTEGER REFERENCES macros (id)
REFERENCES relays (id) ON DELETE CASCADE,
ON DELETE CASCADE, relay_id
schedule_id INTEGER INTEGER
REFERENCES schedules (id) NOT NULL
ON DELETE CASCADE, REFERENCES relays (id)
weekday SMALLINT ON DELETE CASCADE,
NOT NULL schedule_id
INTEGER
NOT NULL
REFERENCES schedules (id)
ON DELETE CASCADE,
weekday
SMALLINT
NOT NULL
); );

View file

@ -1,18 +1,15 @@
use std::env; use std::env;
use diesel::dsl::sql;
use diesel::prelude::*; use diesel::prelude::*;
use diesel_migrations::embed_migrations; use diesel_migrations::embed_migrations;
use dotenv::dotenv; use dotenv::dotenv;
use crate::types::EmgauwaUid;
use errors::DatabaseError;
use models::*;
use schema::schedules::dsl::*;
pub mod errors; pub mod errors;
pub mod models; pub mod models;
pub mod schema; pub mod schema;
pub mod schedule;
pub mod tag;
mod model_utils; mod model_utils;
embed_migrations!("migrations"); embed_migrations!("migrations");
@ -28,63 +25,4 @@ fn get_connection() -> SqliteConnection {
pub fn run_migrations() { pub fn run_migrations() {
let connection = get_connection(); let connection = get_connection();
embedded_migrations::run(&connection).expect("Failed to run migrations."); embedded_migrations::run(&connection).expect("Failed to run migrations.");
} }
pub fn get_schedules() -> Vec<Schedule> {
let connection = get_connection();
schedules
.load::<Schedule>(&connection)
.expect("Error loading schedules")
}
pub fn get_schedule_by_uid(filter_uid: EmgauwaUid) -> Result<Schedule, DatabaseError> {
let connection = get_connection();
let result = schedules
.filter(uid.eq(filter_uid))
.first::<Schedule>(&connection)
.or(Err(DatabaseError::NotFound))?;
Ok(result)
}
pub fn delete_schedule_by_uid(filter_uid: EmgauwaUid) -> Result<(), DatabaseError> {
let filter_uid = match filter_uid {
EmgauwaUid::Off => Err(DatabaseError::Protected),
EmgauwaUid::On => Err(DatabaseError::Protected),
EmgauwaUid::Any(_) => Ok(filter_uid)
}?;
let connection = get_connection();
match diesel::delete(schedules.filter(uid.eq(filter_uid))).execute(&connection) {
Ok(rows) => {
if rows != 0 {
Ok(())
} else {
Err(DatabaseError::DeleteError)
}
}
Err(_) => Err(DatabaseError::DeleteError),
}
}
pub fn create_schedule(new_name: &str, new_periods: &Periods) -> Result<Schedule, DatabaseError> {
let connection = get_connection();
let new_schedule = NewSchedule {
uid: &EmgauwaUid::default(),
name: new_name,
periods: new_periods,
};
diesel::insert_into(schedules)
.values(&new_schedule)
.execute(&connection)
.or(Err(DatabaseError::InsertError))?;
let result = schedules
.find(sql("last_insert_rowid()"))
.get_result::<Schedule>(&connection)
.or(Err(DatabaseError::InsertGetError))?;
Ok(result)
}

View file

@ -3,12 +3,13 @@ use actix_web::http::StatusCode;
use serde::ser::SerializeStruct; use serde::ser::SerializeStruct;
use serde::{Serialize, Serializer}; use serde::{Serialize, Serializer};
#[derive(Debug)]
pub enum DatabaseError { pub enum DatabaseError {
DeleteError, DeleteError,
InsertError, InsertError(diesel::result::Error),
InsertGetError, InsertGetError,
NotFound, NotFound,
Protected Protected,
} }
impl DatabaseError { impl DatabaseError {
@ -37,7 +38,7 @@ impl Serialize for DatabaseError {
impl From<&DatabaseError> for String { impl From<&DatabaseError> for String {
fn from(err: &DatabaseError) -> Self { fn from(err: &DatabaseError) -> Self {
match err { match err {
DatabaseError::InsertError => String::from("error on inserting into database"), DatabaseError::InsertError(_) => String::from("error on inserting into database"),
DatabaseError::InsertGetError => { DatabaseError::InsertGetError => {
String::from("error on retrieving new entry from database (your entry was saved)") String::from("error on retrieving new entry from database (your entry was saved)")
} }

View file

@ -2,10 +2,17 @@ use diesel::sql_types::Binary;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::db::model_utils::Period; use crate::db::model_utils::Period;
use super::schema::schedules; use super::schema::*;
use crate::types::EmgauwaUid; use crate::types::EmgauwaUid;
#[derive(Serialize, Queryable)] #[derive(Debug, Serialize, Identifiable, Queryable)]
pub struct Relay {
#[serde(skip)]
pub id: i32,
// TODO
}
#[derive(Debug, Serialize, Identifiable, Queryable)]
pub struct Schedule { pub struct Schedule {
#[serde(skip)] #[serde(skip)]
pub id: i32, pub id: i32,
@ -27,7 +34,7 @@ pub struct NewSchedule<'a> {
#[sql_type = "Binary"] #[sql_type = "Binary"]
pub struct Periods(pub(crate) Vec<Period>); pub struct Periods(pub(crate) Vec<Period>);
#[derive(Serialize, Queryable)] #[derive(Debug, Serialize, Identifiable, Queryable, Clone)]
pub struct Tag { pub struct Tag {
pub id: i32, pub id: i32,
pub tag: String, pub tag: String,
@ -39,9 +46,22 @@ pub struct NewTag<'a> {
pub tag: &'a str, pub tag: &'a str,
} }
#[derive(Insertable)] #[derive(Queryable, Associations, Identifiable)]
#[table_name = "junction_tag_schedule"] #[belongs_to(Relay)]
pub struct NewJunctionTagSchedule<'a> { #[belongs_to(Schedule)]
#[belongs_to(Tag)]
#[table_name = "junction_tag"]
pub struct JunctionTag {
pub id: i32,
pub tag_id: i32, pub tag_id: i32,
pub schedule_id: i32, pub relay_id: Option<i32>,
pub schedule_id: Option<i32>,
}
#[derive(Insertable)]
#[table_name = "junction_tag"]
pub struct NewJunctionTag {
pub tag_id: i32,
pub relay_id: Option<i32>,
pub schedule_id: Option<i32>,
} }

109
src/db/schedule.rs Normal file
View file

@ -0,0 +1,109 @@
use diesel::dsl::sql;
use diesel::prelude::*;
use crate::types::EmgauwaUid;
use crate::db::errors::DatabaseError;
use crate::db::{get_connection, schema};
use crate::db::models::*;
use crate::db::schema::tags::dsl::tags;
use crate::db::schema::junction_tag::dsl::junction_tag;
use crate::db::schema::schedules::dsl::schedules;
use crate::db::tag::{create_junction_tag, create_tag};
pub fn get_schedule_tags(schedule: &Schedule) -> Vec<String> {
let connection = get_connection();
JunctionTag::belonging_to(schedule)
.inner_join(schema::tags::dsl::tags)
.select(schema::tags::tag)
.load::<String>(&connection)
.expect("Error loading tags")
}
pub fn get_schedules() -> Vec<Schedule> {
let connection = get_connection();
schedules
.load::<Schedule>(&connection)
.expect("Error loading schedules")
}
pub fn get_schedule_by_uid(filter_uid: EmgauwaUid) -> Result<Schedule, DatabaseError> {
let connection = get_connection();
let result = schedules
.filter(schema::schedules::uid.eq(filter_uid))
.first::<Schedule>(&connection)
.or(Err(DatabaseError::NotFound))?;
Ok(result)
}
pub fn delete_schedule_by_uid(filter_uid: EmgauwaUid) -> Result<(), DatabaseError> {
let filter_uid = match filter_uid {
EmgauwaUid::Off => Err(DatabaseError::Protected),
EmgauwaUid::On => Err(DatabaseError::Protected),
EmgauwaUid::Any(_) => Ok(filter_uid)
}?;
let connection = get_connection();
match diesel::delete(schedules.filter(schema::schedules::uid.eq(filter_uid))).execute(&connection) {
Ok(rows) => {
if rows != 0 {
Ok(())
} else {
Err(DatabaseError::DeleteError)
}
}
Err(_) => Err(DatabaseError::DeleteError),
}
}
pub fn create_schedule(new_name: &str, new_periods: &Periods) -> Result<Schedule, DatabaseError> {
let connection = get_connection();
let new_schedule = NewSchedule {
uid: &EmgauwaUid::default(),
name: new_name,
periods: new_periods,
};
diesel::insert_into(schedules)
.values(&new_schedule)
.execute(&connection)
.map_err(DatabaseError::InsertError)?;
let result = schedules
.find(sql("last_insert_rowid()"))
.get_result::<Schedule>(&connection)
.or(Err(DatabaseError::InsertGetError))?;
Ok(result)
}
pub fn set_schedule_tags(schedule: &Schedule, new_tags: &[String]) -> Result<(), DatabaseError> {
let connection = get_connection();
diesel::delete(junction_tag.filter(schema::junction_tag::schedule_id.eq(schedule.id)))
.execute(&connection)
.or(Err(DatabaseError::DeleteError))?;
let mut database_tags: Vec<Tag> = tags.filter(schema::tags::tag.eq_any(new_tags))
.load::<Tag>(&connection)
.expect("Error loading tags");
let mut database_tags_iter = database_tags.clone().into_iter().map(|tag_db| tag_db.tag);
// create missing tags
for new_tag in new_tags {
if !database_tags_iter.any(|t| t.eq(new_tag)) {
database_tags.push(
create_tag(new_tag).expect("Error inserting tag")
);
}
}
for database_tag in database_tags {
create_junction_tag(database_tag, None, Some(schedule))
.expect("Error saving junction between tag and schedule");
}
Ok(())
}

View file

@ -2,7 +2,7 @@ table! {
controllers (id) { controllers (id) {
id -> Integer, id -> Integer,
uid -> Text, uid -> Text,
name -> Nullable<Text>, name -> Text,
ip -> Nullable<Text>, ip -> Nullable<Text>,
port -> Nullable<Integer>, port -> Nullable<Integer>,
relay_count -> Nullable<Integer>, relay_count -> Nullable<Integer>,
@ -32,8 +32,8 @@ table! {
macro_actions (id) { macro_actions (id) {
id -> Integer, id -> Integer,
macro_id -> Integer, macro_id -> Integer,
relay_id -> Nullable<Integer>, relay_id -> Integer,
schedule_id -> Nullable<Integer>, schedule_id -> Integer,
weekday -> SmallInt, weekday -> SmallInt,
} }
} }
@ -42,14 +42,14 @@ table! {
macros (id) { macros (id) {
id -> Integer, id -> Integer,
uid -> Text, uid -> Text,
name -> Nullable<Text>, name -> Text,
} }
} }
table! { table! {
relays (id) { relays (id) {
id -> Integer, id -> Integer,
name -> Nullable<Text>, name -> Text,
number -> Integer, number -> Integer,
controller_id -> Integer, controller_id -> Integer,
} }

52
src/db/tag.rs Normal file
View file

@ -0,0 +1,52 @@
use diesel::dsl::sql;
use diesel::prelude::*;
use crate::db::errors::DatabaseError;
use crate::db::{get_connection};
use crate::db::models::*;
use crate::db::schema::tags::dsl::tags;
use crate::db::schema::junction_tag::dsl::junction_tag;
pub fn create_tag(new_tag: &str) -> Result<Tag, DatabaseError> {
let connection = get_connection();
let new_tag = NewTag {
tag: new_tag,
};
diesel::insert_into(tags)
.values(&new_tag)
.execute(&connection)
.map_err(DatabaseError::InsertError)?;
let result = tags
.find(sql("last_insert_rowid()"))
.get_result::<Tag>(&connection)
.or(Err(DatabaseError::InsertGetError))?;
Ok(result)
}
pub fn create_junction_tag(target_tag: Tag, target_relay: Option<&Relay>, target_schedule: Option<&Schedule>) -> Result<JunctionTag, DatabaseError> {
let connection = get_connection();
let new_junction_tag = NewJunctionTag {
relay_id: target_relay.map(|r| r.id),
schedule_id: target_schedule.map(|s| s.id),
tag_id: target_tag.id
};
diesel::insert_into(junction_tag)
.values(&new_junction_tag)
.execute(&connection)
.map_err(DatabaseError::InsertError)?;
let result = junction_tag
.find(sql("last_insert_rowid()"))
.get_result::<JunctionTag>(&connection)
.or(Err(DatabaseError::InsertGetError))?;
Ok(result)
}

View file

@ -2,20 +2,23 @@ use std::convert::TryFrom;
use actix_web::{HttpResponse, Responder, web, get, delete}; use actix_web::{HttpResponse, Responder, web, get, delete};
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use crate::db;
use crate::db::models::Periods; use crate::db::models::Periods;
use crate::db::schedule::*;
use crate::handlers::errors::HandlerError; use crate::handlers::errors::HandlerError;
use crate::return_models::ReturnSchedule;
use crate::types::EmgauwaUid; use crate::types::EmgauwaUid;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct RequestSchedule { pub struct RequestSchedule {
name: String, name: String,
periods: Periods, periods: Periods,
tags: Vec<String>,
} }
pub async fn index() -> impl Responder { pub async fn index() -> impl Responder {
let schedules = db::get_schedules(); let schedules = get_schedules();
HttpResponse::Ok().json(schedules) let return_schedules: Vec<ReturnSchedule> = schedules.into_iter().map(ReturnSchedule::from).collect();
HttpResponse::Ok().json(return_schedules)
} }
#[get("/api/v1/schedules/{schedule_id}")] #[get("/api/v1/schedules/{schedule_id}")]
@ -25,9 +28,9 @@ pub async fn show(web::Path((schedule_uid,)): web::Path<(String,)>) -> impl Resp
match emgauwa_uid { match emgauwa_uid {
Ok(uid) => { Ok(uid) => {
let schedule = db::get_schedule_by_uid(uid); let schedule = get_schedule_by_uid(uid);
match schedule { match schedule {
Ok(ok) => HttpResponse::Ok().json(ok), Ok(ok) => HttpResponse::Ok().json(ReturnSchedule::from(ok)),
Err(err) => HttpResponse::from(err), Err(err) => HttpResponse::from(err),
} }
}, },
@ -36,12 +39,19 @@ pub async fn show(web::Path((schedule_uid,)): web::Path<(String,)>) -> impl Resp
} }
pub async fn add(post: web::Json<RequestSchedule>) -> impl Responder { pub async fn add(post: web::Json<RequestSchedule>) -> impl Responder {
let new_schedule = db::create_schedule(&post.name, &post.periods); let new_schedule = create_schedule(&post.name, &post.periods);
match new_schedule { if new_schedule.is_err() {
Ok(ok) => HttpResponse::Created().json(ok), return HttpResponse::from(new_schedule.unwrap_err())
Err(err) => HttpResponse::from(err),
} }
let new_schedule = new_schedule.unwrap();
let result = set_schedule_tags(&new_schedule, post.tags.as_slice());
if result.is_err() {
return HttpResponse::from(result.unwrap_err());
}
HttpResponse::Created().json(ReturnSchedule::from(new_schedule))
} }
#[delete("/api/v1/schedules/{schedule_id}")] #[delete("/api/v1/schedules/{schedule_id}")]
@ -53,7 +63,7 @@ pub async fn delete(web::Path((schedule_uid,)): web::Path<(String,)>) -> impl Re
EmgauwaUid::Off => HttpResponse::from(HandlerError::ProtectedSchedule), EmgauwaUid::Off => HttpResponse::from(HandlerError::ProtectedSchedule),
EmgauwaUid::On => HttpResponse::from(HandlerError::ProtectedSchedule), EmgauwaUid::On => HttpResponse::from(HandlerError::ProtectedSchedule),
EmgauwaUid::Any(_) => { EmgauwaUid::Any(_) => {
match db::delete_schedule_by_uid(uid) { match delete_schedule_by_uid(uid) {
Ok(_) => HttpResponse::Ok().json("schedule got deleted"), Ok(_) => HttpResponse::Ok().json("schedule got deleted"),
Err(err) => HttpResponse::from(err) Err(err) => HttpResponse::from(err)
} }

View file

@ -3,6 +3,7 @@ extern crate diesel;
#[macro_use] #[macro_use]
extern crate diesel_migrations; extern crate diesel_migrations;
extern crate dotenv; extern crate dotenv;
extern crate core;
use actix_web::{middleware, web, App, HttpServer}; use actix_web::{middleware, web, App, HttpServer};
use actix_web::middleware::normalize::TrailingSlash; use actix_web::middleware::normalize::TrailingSlash;
@ -10,6 +11,7 @@ use env_logger::{Builder, Env};
mod db; mod db;
mod handlers; mod handlers;
mod return_models;
mod types; mod types;
#[actix_web::main] #[actix_web::main]

21
src/return_models.rs Normal file
View file

@ -0,0 +1,21 @@
use serde::{Serialize};
use crate::db::models::Schedule;
use crate::db::schedule::get_schedule_tags;
#[derive(Debug, Serialize)]
pub struct ReturnSchedule {
#[serde(flatten)]
pub schedule: Schedule,
pub tags: Vec<String>,
}
impl From<Schedule> for ReturnSchedule {
fn from(schedule: Schedule) -> Self {
let tags: Vec<String> = get_schedule_tags(&schedule);
ReturnSchedule {
schedule,
tags,
}
}
}