use crate::util::surrealdb::DB; use super::{Member, MembersMigration}; use once_cell::sync::Lazy; use std::collections::HashMap; use surrealdb::sql::statements::{BeginStatement, CommitStatement}; use tokio::sync::Mutex; // Create a store for saving information when migrating to a new members list static MEMBERS_STORE: Lazy> = Lazy::new(|| Mutex::new(MembersStore::new())); struct MembersStore { store: HashMap, count: u16, } impl MembersStore { fn new() -> Self { Self { store: HashMap::new(), count: 0, } } fn insert(&mut self, input: MembersMigration) -> u16 { let count = self.count + 1; self.store.insert(count, input); count } fn get(&self, key: &u16) -> Option<&MembersMigration> { self.store.get(key) } fn remove(&mut self, key: &u16) -> Option { self.store.remove(key) } } // Create a row for the csv file #[derive(Debug, serde::Deserialize)] struct Row { #[serde(rename = "Relatiecode")] id: String, #[serde(rename = "Roepnaam")] first_name: String, #[serde(rename = "Tussenvoegsel(s)")] middle_name: String, #[serde(rename = "Achternaam")] last_name: String, #[serde(rename = "E-mail")] email: String, #[serde(rename = "Verenigingssporten")] hours: String, #[serde(rename = "Diploma dropdown 1")] diploma: Option, } impl Row { fn to_member(&self) -> Member { Member { id: self.id.trim().to_string(), name: super::Name { first: self.first_name.clone(), full: self.generate_full_name(), }, hours: self.generate_hours(), groups: Vec::new(), diploma: self.diploma.clone(), registration_token: None, } } // Get the hour data from the raw string fn generate_hours(&self) -> Vec { let mut hours: Vec = Vec::new(); let group_parts: Vec<&str> = self.hours.split(", ").collect(); for group in group_parts { let hour_parts: Vec<&str> = group.split(" - ").collect(); for part in hour_parts { if &*part != "Groep" { hours.push(part.to_string()); } } } hours } // Generate the full name from 3 parts fn generate_full_name(&self) -> String { let mut parts: Vec<&str> = vec![]; parts.push(&self.first_name); parts.push(&self.middle_name); parts.push(&self.last_name); let filtered_strings: Vec<&str> = parts.into_iter().filter(|s| !s.is_empty()).collect(); filtered_strings.join(" ") } } impl MembersMigration { // Convert the raw csv file to rust objects fn csv_to_rows(input: String) -> Result, Box> { let mut members: Vec = vec![]; let mut rdr = csv::Reader::from_reader(input.as_bytes()); for result in rdr.deserialize() { let row: Row = result?; members.push(row); } Ok(members) } // Covert the rows to formatted members fn rows_to_members(rows: Vec) -> Vec { let mut members: Vec = vec![]; for row in rows { members.push(row.to_member()); } members } // Compare the new members list with the current fn combine_members_lists( new_members_list: Vec, current_members_list: Vec, ) -> MembersMigration { let current_members_map: HashMap = current_members_list .clone() .into_iter() .map(|m| (m.id.clone(), m)) .collect(); let new_members_map: HashMap = new_members_list .clone() .into_iter() .map(|m| (m.id.clone(), m)) .collect(); let mut inserted_members: Vec = vec![]; let mut updated_members: Vec = vec![]; let mut removed_members: Vec = vec![]; for current_member in current_members_list { if let Some(new_member) = new_members_map.get(¤t_member.id) { // Update existing member let new_member_clone = new_member.clone(); updated_members.push(Member { id: current_member.id, name: new_member_clone.name, hours: new_member_clone.hours, groups: current_member.groups, diploma: new_member_clone.diploma, registration_token: current_member.registration_token, }) } else { // Remove member removed_members.push(current_member); } } for new_member in new_members_list { // Insert new member if !current_members_map.contains_key(&new_member.id) { inserted_members.push(new_member); } } Self { inserted: inserted_members, updated: updated_members, removed: removed_members, } } pub async fn migrate_proposal( csv: String, ) -> Result<(u16, MembersMigration), Box> { let rows = Self::csv_to_rows(csv)?; let new_members = Self::rows_to_members(rows); let current_members = Member::fetch_all().await?; let members_migration = Self::combine_members_lists(new_members, current_members); let count = MEMBERS_STORE.lock().await.insert(members_migration.clone()); Ok((count, members_migration)) } pub async fn migrate(id: u16) -> Result<(), Box> { let mut members_store = MEMBERS_STORE.lock().await; let members_migration = match members_store.get(&id) { Some(mm) => mm, None => return Err("Could not get members from store".into()), }; let mut transaction = DB.query(BeginStatement::default()); for member in members_migration.updated.clone() { let id = member.id.clone(); transaction = transaction.query(format!("UPDATE type::thing('member', $id_{id}) SET name.first = $name_first_{id}, name.full = $name_full_{id}, hours = $hours_{id}, groups = $groups_{id}, diploma = $diploma_{id};")) .bind((format!("id_{id}"), member.id)) .bind((format!("name_first_{id}"), member.name.first)) .bind((format!("name_full_{id}"), member.name.full)) .bind((format!("hours_{id}"), member.hours)) .bind((format!("groups_{id}"), member.groups)) .bind((format!("diploma_{id}"), member.diploma)); } for member in members_migration.inserted.clone() { let id = member.id.clone(); transaction = transaction.query(format!("CREATE type::thing('member', $id_{id}) SET name.first = $name_first_{id}, name.full = $name_full_{id}, hours = $hours_{id}, groups = $groups_{id}, diploma = $diploma_{id};")) .bind((format!("id_{id}"), member.id)) .bind((format!("name_first_{id}"), member.name.first)) .bind((format!("name_full_{id}"), member.name.full)) .bind((format!("hours_{id}"), member.hours)) .bind((format!("groups_{id}"), member.groups)) .bind((format!("diploma_{id}"), member.diploma)); } for member in members_migration.removed.clone() { let id = member.id.clone(); transaction = transaction .query(format!("DELETE type::thing('member', $id_{id});")) .bind((format!("id_{id}"), member.id)); } transaction .query(CommitStatement::default()) .await? .check()?; members_store.remove(&id); Ok(()) } } #[cfg(test)] mod tests { use super::*; #[test] fn csv_to_members_test() -> Result<(), String> { let data = "Relatiecode,Volledige naam (1),Roepnaam,Tussenvoegsel(s),Achternaam,E-mail,2e E-mail,Verenigingssporten,Diploma dropdown 1 D000001,\"Last, First\",First,,Last,first.last@example.com,first.last@example.nl,\"Groep - Wedstrijd - Zaterdag, Groep - Z5 - Zaterdag\",LS1 D000002,\"Last2, First2\",First2,,Last2,first1.last@example.nl,,Groep - Z5 - Zaterdag,ZR4".to_string(); let expected = vec![ Member { id: "D000001".to_string(), name: super::super::Name { first: "First".to_string(), full: "First Last".to_string(), }, hours: Vec::from([ "Wedstrijd".to_string(), "Z5".to_string(), "Zaterdag".to_string(), ]), groups: vec![], diploma: Some("LS1".to_string()), registration_token: None, }, Member { id: "D000002".to_string(), name: super::super::Name { first: "First2".to_string(), full: "First2 Last2".to_string(), }, hours: Vec::from(["Z5".to_string(), "Zaterdag".to_string()]), groups: Vec::new(), diploma: Some("ZR4".to_string()), registration_token: None, }, ]; let rows = match MembersMigration::csv_to_rows(data) { Ok(r) => r, Err(err) => return Err(err.to_string()), }; let members = MembersMigration::rows_to_members(rows); assert_eq!(expected, members); Ok(()) } }