/* Simple web app using rocket to help maintain inventory data. * Copyright (C) 2024 Johannes Randerath * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see FromSql for FIELDTYPE where DB: Backend, i32: FromSql { fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result { match i32::from_sql(bytes)? { 0 => Ok(FIELDTYPE::TEXT), 1 => Ok(FIELDTYPE::NUMBER), x => Err(format!("Unknown field type {}.", x).into()), } } } impl From for FIELDTYPE { fn from(value: isize) -> Self { match value { 1 => FIELDTYPE::NUMBER, _ => FIELDTYPE::TEXT, } } } /// represents and summarised all relevant data of a table. /// Standard return type if whole tables should be returned #[derive(Clone, Debug)] pub struct Tbl { /// table id of the represented object pub tblid: i32, pub name: String, pub column_names: Vec, pub column_types: Vec, pub rows: Vec, } /// Represents a table's row. /// Internal data structure is abstracted until strings. #[derive(Hash, Clone, Debug)] pub struct TRow { pub row_pos: i32, pub cells: Vec, } impl PartialEq for TRow { fn eq(&self, other: &Self) -> bool { let mut hasher = DefaultHasher::new(); self.hash(&mut hasher) == other.hash(&mut hasher) } } impl Eq for TRow {} /// Identifies a user /// All fields are unique within the db at any given point. pub struct User { /// User's unique id. pub uid: i32, pub uname: String, pub email: String, } #[derive(Clone)] pub struct ShareCard { pub tblid: i32, pub sharee_id: i32, pub sharee_name: String, pub readonly: bool, } /// Connect to database. /// Return value must be passed to most of the crate's functions. pub fn establish_connection() -> MysqlConnection { dotenv().ok(); let db_url = env::var("DATABASE_URL").expect("No database url set."); MysqlConnection::establish(&db_url) .unwrap_or_else(|_| panic!("Couldn't open database.")) } /// Fetch one whole table from the database. Identified by it's ID. /// Needs the id of the requesting user. /// Returns: /// A Tbl struct /// Or: /// None /// if the table was not found or /// if it could not be read or /// if the requesting user is not the table's owner or /// if there has been another error /// pub fn get_table(conn: &mut MysqlConnection, tblid: i32, uid: i32) -> Option { let tbl = fetch_table(conn, tblid); if tbl.is_none() || tbl.clone().unwrap().1 != uid { return None; } Some(tbl.unwrap().0) } fn fetch_table(conn: &mut MysqlConnection, tblid: i32) -> Option<(Tbl, i32)>{ let tbl = jrtables::get_tbl(conn, tblid); if tbl.is_err() { return None; } let tbl = tbl.unwrap(); let clmns = jrcolumns::get_clmns_of(conn, tblid); if clmns.is_err() { return None; } let clmns = clmns.unwrap(); let mut clmn_nms = Vec::new(); for clmn in &clmns { clmn_nms.push(clmn.name.clone()); } let clmn_nms = clmn_nms; let mut clmn_tps = Vec::new(); for clmn in &clmns { clmn_tps.push(clmn.column_type); } let clmn_tps = clmn_tps; let rowids = jrentries::get_entries_of(conn, tblid); if rowids.is_err() { return None; } let rowids = rowids.unwrap(); let mut rows = Vec::new(); for rowid in rowids { let row = jrcells::get_entry_cells(conn, rowid.id); if row.is_err() { return None; } let row = row.unwrap(); let mut data = Vec::new(); for cell in row { data.push(cell.cell_value); } let data = data; rows.push(TRow { row_pos: rowid.row_pos, cells: data }); } Some((Tbl { tblid: tbl.id, name: tbl.name, column_names: clmn_nms, column_types: clmn_tps,rows: rows }, tbl.owner_id)) } pub fn get_table_shared(conn: &mut MysqlConnection, tblid: i32, uid: i32) -> Option { let tbl = fetch_table(conn, tblid); let share = shares::is_shared(conn, tblid, uid); if !share { return None; } let tbl = tbl; if tbl.is_none() { return None; } Some(tbl.unwrap().0) } /// Take a Tbl object, sort it according to parameters and return a sorted Tbl object. /// sort_field is the index of the column to sort by. /// 0 is the position of the row in the table. /// then 1-indexed column index pub fn sort_table(tbl: Tbl, sort_field: usize, sort_dir: u8) -> Tbl { let mut rows = tbl.rows; if sort_field == 0 { rows.sort_unstable_by_key(|a: &TRow| a.row_pos); if sort_dir == 1 { rows.reverse(); } }else { if tbl.column_types[sort_field-1] == FIELDTYPE::NUMBER { rows.sort_by_key(|a: &TRow| a.cells[sort_field - 1].trim().parse::().expect("Number not a number.")); }else { rows.sort_by_key(|a: &TRow| a.cells[sort_field - 1].clone()); } if sort_dir == 1 { rows.reverse(); } } let rows = rows; Tbl { tblid: tbl.tblid, name: tbl.name, column_names: tbl.column_names, column_types: tbl.column_types, rows: rows.clone() } } /// Take a Tbl object, find all rows matching the search queries and return a new Tbl object containing only those. /// search_fields contains a Vec of (1-indexed) column positions to be included in the search /// search_value is a string to search for in the search_fields. /// Returned Tbl is not sorted and should be according to user preferences before being displayed. pub fn search_table(tbl: Tbl, search_fields: Vec, search_value: String) -> Tbl { let rows = tbl.rows; let mut field_sets = HashSet::new(); for field in search_fields { for row in &rows { if row.cells[field as usize].to_lowercase().contains(&search_value.to_lowercase()) { field_sets.insert(row.clone()); } } } Tbl { tblid: tbl.tblid, name: tbl.name, column_names: tbl.column_names, column_types: tbl.column_types, rows: Vec::from_iter(field_sets) } } /// For a Vec a database ids of tables, return a Vec of their names in the same order. pub fn get_tblnames(conn: &mut MysqlConnection, tblids: Vec) -> Option> { let mut tblnames = Vec::new(); for tblid in tblids { let tblname = jrtables::get_tbl(conn, tblid); if tblname.is_err() { return None; } tblnames.push(tblname.unwrap().name); } Some(tblnames) } /// Returns a Vec of the ids of a user's table if the user exists, else None. /// User is identified by its user ID. pub fn get_user_tblids(conn: &mut MysqlConnection, uid: i32) -> Option> { let tblids = jrtables::get_tblids_uid(conn, uid); if tblids.is_err() { return None; } Some(tblids.unwrap()) } /// Change the name of a table. /// Needs the user ID of the requesting user. /// Returns Some(true) if the action seems to have been successful and the requesting user is the /// table's owner. /// None otherwise. pub fn rename_table(conn: &mut MysqlConnection, tblid: i32, new_name: String, uid: i32) -> Option { let owner = jrtables::get_owner_id(conn, tblid); if owner.is_err() || owner.unwrap() != uid { return None; } let rnm = jrtables::rename_jrtable(conn, tblid, &new_name); if rnm.is_err() { return None; } Some(true) } /// Delete a table. /// Needs the user ID of the requesting user. /// Returns Some(true) if the action seems to have been successful and the requesting user is the /// table's owner. /// None otherwise. pub fn delete_table(conn: &mut MysqlConnection, tblid: i32, uid: i32) -> Option { let owner = jrtables::get_owner_id(conn, tblid); if owner.is_err() || owner.unwrap() != uid { return None; } let dlt = jrtables::delete_jrtable(conn, tblid); if dlt.is_err() { return None; } Some(true) } /// Create a new table. /// Needs the user ID of the requesting user. /// Returns the id of the newly created table if the action seems to have been successful and the requesting user is the /// table's owner. /// None otherwise. pub fn create_table(conn: &mut MysqlConnection, name: String, field_names: Vec, field_types: Vec, uid: i32) -> Option { if jrtables::get_tbl_by_name_uid(conn, &name, uid).is_ok() { return None; } if jrtables::create_jrtable(conn, &name, field_names, field_types, uid).is_err() { return None; } let tblid = jrtables::get_tbl_by_name_uid(conn, &name, uid); if tblid.is_err() { return None; } Some(tblid.unwrap()) } /// Returns a User struct of a requested user if the combination of username and email are /// consistent or both do not exist yet, in which case the user is newly created. pub fn register_or_login(conn: &mut MysqlConnection, uname: String, mail: String) -> Option { let mut uid = users::get_uid_email(conn, &mail); if uid.is_err() { if users::get_uid_uname(conn, &uname).is_ok() || users::create_user(conn, &uname, &mail).is_err() { return None; } uid = users::get_uid_email(conn, &mail); if uid.is_err() { return None; } } let uid = uid.unwrap(); let nm = users::get_uname(conn, uid); if nm.is_err() || nm.unwrap() != uname { return None; } Some(User { uid: uid, uname: uname, email: mail }) } /// Take a user id (database representation, not username) and delete it from the database. /// All its tables are cascadingly deleted. /// Return Some(true) if successful, None otherwise pub fn delete_user(conn: &mut MysqlConnection, uid: i32) -> Option { if users::delete_user(conn, uid).is_err() { return None; } Some(true) } /// For a given id of a table and a vec of string values, create a new jrentry (table row) and all its jrcells (table cells) filled with the string values. /// Only works if uid corresponds to the user id of the table's owner. /// Return the id of the newly created row if successful, None otherwise. pub fn add_row(conn: &mut MysqlConnection, tblid: i32, values: Vec, uid: i32) -> Option { let owner = jrtables::get_owner_id(conn, tblid); let nrows = jrtables::get_nrows(conn, tblid); if nrows.is_err() || owner.is_err() || owner.unwrap() != uid { return None; } let nrows = nrows.unwrap() as i32; if jrentries::create_jrentry(conn, tblid).is_err() { return None; } let entryid = jrentries::get_jrentry_id(conn, tblid, nrows+1); if entryid.is_err() { return None; } let entryid = entryid.unwrap(); let cols = jrcolumns::get_clmns_of(conn, tblid); if cols.is_err() { return None; } let cols = cols.unwrap(); for (i,col) in cols.iter().enumerate() { if jrcells::create_jrcell(conn, entryid, col.id, &values[i]).is_err() { return None; } } Some(entryid) } /// Delete a jrentry (table row) identified by the id of its table and the current position wihthin it /// Return Some(true) if successful, None otherwise pub fn delete_row(conn: &mut MysqlConnection, tblid: i32, row_pos: i32, uid: i32) -> Option { let owner = jrtables::get_owner_id(conn, tblid); if owner.is_err() || owner.unwrap() != uid || jrentries::delete_jrentry_relative(conn, tblid, row_pos).is_err() { return None; } Some(true) } /// Change a jrentry's (table row's) current position within its table, given the table's id, the current position and the new position it should be moved to. /// Only works if uid equals to the owner's user id. /// Returns Some(true) if successful, None otherwise. pub fn move_row(conn: &mut MysqlConnection, tblid: i32, rowpos: i32, newpos: i32, uid: i32) -> Option { let owner = jrtables::get_owner_id(conn, tblid); if owner.is_err() || owner.unwrap() != uid || jrentries::move_jrentry(conn, tblid, rowpos, newpos).is_err() { return None; } Some(true) } /// Update a jrentry (table row) with a new set of string values, given those new values and the row identified by its table and its position within the table. /// Only works if uid equals to the table's owner's user id. /// Returns Some(true) if successful, None otherwise. pub fn edit_row(conn: &mut MysqlConnection, tblid: i32, cells: Vec, row_pos: i32, uid: i32) -> Option { let owner = jrtables::get_owner_id(conn, tblid); if owner.is_err() || owner.unwrap() != uid { return None; } for (i, cell) in cells.iter().enumerate() { if jrcells::change_jrcell_value_relative(conn, tblid, row_pos, (i+1) as i32, cell).is_err() { return None; } } Some(true) } /// Edit the content of a single jrcell (table cell), given its table id, the position of its row and column within the table and the new value. /// Only works if uid equals to the table's owner's user id. /// Returns Some(true) if successful, None otherwise. pub fn edit_cell(conn: &mut MysqlConnection, tblid: i32, row_pos: i32, column_pos: i32, new_value: &String, uid: i32) -> Option { let owner = jrtables::get_owner_id(conn, tblid); if owner.is_err() || owner.unwrap() != uid || jrcells::change_jrcell_value_relative(conn, tblid, row_pos, column_pos, new_value).is_err() { return None; } Some(true) } /// Change a jrcolumn's (table column's) current position within its table, given the table's id, the current position and the new position it should be moved to. /// Only works if uid equals to the owner's user id. /// Returns Some(true) if successful, None otherwise. pub fn move_column(conn: &mut MysqlConnection, tblid:i32, column_pos: i32, new_column_pos: i32, uid: i32) -> Option { let owner = jrtables::get_owner_id(conn, tblid); if owner.is_err() || owner.unwrap() != uid || jrcolumns::move_column_relative(conn, tblid, column_pos, new_column_pos).is_err() { return None; } Some(true) } /// Add a new jrcolumn (table column) to an existing table. /// Required are the table's id, the column's name, the column's FIELDTYPE and the id of the logged in user. /// Only works if uid equals to the table's owner's user id. /// Returns the id of the newly created column if successful, None otherwise. pub fn add_column(conn: &mut MysqlConnection, tblid: i32, name: String, clmtype: FIELDTYPE, uid: i32) -> Option { let owner = jrtables::get_owner_id(conn, tblid); if owner.is_err() || owner.unwrap() != uid || jrcolumns::create_jrcolumn(conn, tblid, name, clmtype).is_err() { return None; } let clmid = jrcolumns::get_last_column_id_of(conn, tblid); if clmid.is_err() { return None; } Some(clmid.unwrap()) } /// Delete a column identified by its table's id and its position within the table. /// Only works if uid equals to the table's owner's id. /// Returns Some(true) if successful, None otherwise. pub fn delete_column(conn: &mut MysqlConnection, tblid: i32, column_pos: i32, uid: i32) -> Option { let owner = jrtables::get_owner_id(conn, tblid); if owner.is_err() || owner.unwrap() != uid || jrcolumns::delete_column_relative(conn, tblid, column_pos).is_err() { return None; } Some(true) } /// Update a jrcolumn (table column) with a given new name, and new type. The column is identified by its table's id and its position within it. /// Only works if uid equals to the table's owner's id. /// Returns Some(true) if successful, None otherwise. pub fn edit_column(conn: &mut MysqlConnection, tblid: i32, column_pos: i32, new_name: &String, new_type: FIELDTYPE, uid: i32) -> Option { let owner = jrtables::get_owner_id(conn, tblid); if owner.is_err() || owner.unwrap() != uid || jrcolumns::edit_column_relative(conn, tblid, column_pos, new_name, new_type).is_err() { return None; } Some(true) } pub fn share(conn: &mut MysqlConnection, tblid: i32, sharee_id: &i32, readonly: &bool, uid: i32) -> Option { let owner = jrtables::get_owner_id(conn, tblid); println!(":)"); if owner.is_err() || owner.unwrap() != uid || shares::edit_or_create(conn, tblid, sharee_id, readonly).is_err() { return None; } Some(true) } pub fn unshare(conn: &mut MysqlConnection, tblid: i32, sharee_id: &i32, uid: i32) -> Option { let owner = jrtables::get_owner_id(conn, tblid); if owner.is_err() || owner.unwrap() != uid || shares::delete(conn, tblid, sharee_id).is_err() { return None; } Some(true) } pub fn get_sharees(conn: &mut MysqlConnection, tblid: i32, uid: i32) -> Option> { let owner = jrtables::get_owner_id(conn, tblid); if owner.is_err() || owner.unwrap() != uid { return None; } let result = shares::get_shares_tbl(conn, tblid); if result.is_err() { return None; } let result = result.unwrap(); let mut sharecards = Vec::new(); for share in result { let uname = users::get_uname(conn, uid); if uname.is_err() { continue; } sharecards.push(ShareCard { tblid: tblid, sharee_id: share.sharee_id, sharee_name: uname.unwrap(), readonly: share.readonly }); } return Some(sharecards) } pub fn get_shared_tbls(conn: &mut MysqlConnection, uid: i32) -> Option> { let result = shares::get_shares_usr(conn, uid); if result.is_err() { return None; } let result = result.unwrap(); let uname = users::get_uname(conn, uid); if uname.is_err() { return None; } let uname = uname.unwrap(); let mut sharecards = Vec::new(); for share in result { sharecards.push(ShareCard { tblid: share.tblid, sharee_id: uid, sharee_name: uname.clone(), readonly: share.readonly}); } Some(sharecards) } pub fn query_users(conn: &mut MysqlConnection, query: String, uid: i32) -> Option> { let result = users::query_users(conn, query); if users::get_uname(conn, uid).is_err() || result.is_err() { return None; } let result = result.unwrap(); let mut pubusers: Vec = Vec::new(); for user in result { pubusers.push( User { uid: user.id, uname: user.username, email: user.email } ); } Some(pubusers) }