Files
wrbapp-next/server/src/routes/member/migrate.rs
2025-02-14 15:59:11 +01:00

276 lines
7.4 KiB
Rust

use std::collections::HashMap;
use axum::{extract::State, http::HeaderMap, Json};
use sqlx::PgPool;
use crate::{
auth::get_user_from_header,
database::model::Member as DbMember,
model::{
member::{Groups, Name, Roles},
Member,
},
util::convert_vec,
AppState,
};
pub async fn migrate_request(
State(state): State<AppState>,
headers: HeaderMap,
body: String,
) -> Result<Json<MigrationResponse>, crate::Error> {
let user = get_user_from_header(&state.pool, &headers).await?;
user.authorize(&state.pool, Some(Roles::ADMIN), None)
.await?;
tracing::info!("Migration is requested");
// Convert the input CSV to a vector of members
let members_new: Vec<Member> = Row::from_csv_many(&body)?
.into_iter()
.map(|m| m.into())
.collect();
let members_old = convert_vec(DbMember::get_all(&state.pool).await?);
let members_diff = generate_diff(members_new, members_old);
let count = state
.migration_store
.lock()
.await
.insert(members_diff.clone());
Ok(Json(MigrationResponse::from((count, members_diff))))
}
pub async fn migrate_confirm(
State(state): State<AppState>,
headers: HeaderMap,
body: String,
) -> Result<(), crate::Error> {
let user = get_user_from_header(&state.pool, &headers).await?;
user.authorize(&state.pool, Some(Roles::ADMIN), None)
.await?;
tracing::info!("Migration is confirmed");
let count = match body.trim().parse::<u32>() {
Ok(c) => c,
Err(_) => {
return Err(crate::Error::BadRequest {
expected: String::from("u32"),
})
}
};
let mut store = state.migration_store.lock().await;
let members_diff = match store.remove(&count) {
Some(m) => m,
None => return Err(crate::Error::NotFound),
};
let inserted_len = members_diff.insert.len();
let update_len = members_diff.update.len();
let remove_len = members_diff.remove.len();
migrate_transaction(&state.pool, members_diff).await?;
tracing::info!(
"Migration is successfully executed. Inserted: {}, updated: {}, removed: {}",
inserted_len,
update_len,
remove_len
);
Ok(())
}
async fn migrate_transaction(pool: &PgPool, members_diff: MembersDiff) -> Result<(), sqlx::Error> {
let mut transaction = pool.begin().await?;
DbMember::insert_many(&mut transaction, convert_vec(members_diff.insert)).await?;
DbMember::update_many(&mut transaction, convert_vec(members_diff.update)).await?;
let members_remove_ids: Vec<String> = members_diff.remove.into_iter().map(|m| m.id).collect();
DbMember::remove_many(&mut transaction, &members_remove_ids).await?;
transaction.commit().await?;
Ok(())
}
// Create a row for the csv file
#[derive(Debug, serde::Deserialize, Clone)]
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")]
groups: String,
#[serde(rename = "Diploma dropdown 1")]
diploma: Option<String>,
}
#[derive(Debug, Clone)]
pub struct MembersDiff {
insert: Vec<Member>,
update: Vec<Member>,
remove: Vec<Member>,
}
#[derive(serde::Serialize)]
pub struct MigrationResponse {
count: u32,
insert: Vec<(String, Name)>,
update: Vec<(String, Name)>,
remove: Vec<(String, Name)>,
}
#[derive(Default)]
pub struct MigrationStore {
pub store: HashMap<u32, MembersDiff>,
pub count: u32,
}
impl Row {
fn from_csv_many(input: &str) -> Result<Vec<Self>, csv::Error> {
let mut rdr = csv::ReaderBuilder::new()
.delimiter(b';')
.from_reader(input.as_bytes());
let members: Result<Vec<Row>, csv::Error> = rdr.deserialize().collect();
members
}
fn groups_parsed(&self) -> Groups {
let mut groups: Vec<String> = Vec::new();
let group_parts: Vec<&str> = self.groups.split(", ").collect();
for group in group_parts {
let hour_parts: Vec<&str> = group.split(" - ").collect();
if let Some(group) = hour_parts.get(1) {
groups.push(group.to_uppercase())
}
}
let groups_string = groups.join("|");
bitflags::parser::from_str(&groups_string).unwrap_or(Groups::empty())
}
}
impl From<Row> for Name {
fn from(val: Row) -> Self {
Name {
first: val.first_name,
full: "Temporarely full name".to_string(),
}
}
}
impl From<Row> for Member {
fn from(val: Row) -> Self {
let name: Name = val.clone().into();
Member {
id: val.id.clone(),
name,
registration_token: None,
diploma: val.diploma.clone(),
groups: val.groups_parsed(),
roles: Roles::MEMBER,
}
}
}
impl From<(u32, MembersDiff)> for MigrationResponse {
fn from(value: (u32, MembersDiff)) -> Self {
let members_insert: Vec<(String, Name)> =
value.1.insert.into_iter().map(|m| (m.id, m.name)).collect();
let members_update: Vec<(String, Name)> =
value.1.update.into_iter().map(|m| (m.id, m.name)).collect();
let members_remove: Vec<(String, Name)> =
value.1.remove.into_iter().map(|m| (m.id, m.name)).collect();
Self {
count: value.0,
insert: members_insert,
update: members_update,
remove: members_remove,
}
}
}
impl MigrationStore {
fn insert(&mut self, members_diff: MembersDiff) -> u32 {
let count = self.count + 1;
self.store.insert(count, members_diff);
self.count = count;
count
}
fn get(&self, id: &u32) -> Option<&MembersDiff> {
self.store.get(id)
}
fn remove(&mut self, id: &u32) -> Option<MembersDiff> {
self.store.remove(id)
}
}
fn generate_diff(members_new: Vec<Member>, members_old: Vec<Member>) -> MembersDiff {
let members_old_map: HashMap<String, Member> = members_old
.iter()
.map(|m| (m.id.clone(), m.clone()))
.collect();
let members_new_map: HashMap<String, Member> = members_new
.iter()
.map(|m| (m.id.clone(), m.clone()))
.collect();
let mut members_insert: Vec<Member> = Vec::new();
let mut members_update: Vec<Member> = Vec::new();
let mut members_remove: Vec<Member> = Vec::new();
for old_member in members_old {
if let Some(new_member) = members_new_map.get(&old_member.id) {
members_update.push(Member {
id: old_member.id,
name: new_member.name.clone(),
registration_token: old_member.registration_token,
diploma: new_member.diploma.clone(),
groups: new_member.groups,
roles: old_member.roles,
})
} else {
members_remove.push(old_member);
}
}
for new_member in members_new {
if !members_old_map.contains_key(&new_member.id) {
members_insert.push(new_member);
}
}
MembersDiff {
insert: members_insert,
update: members_update,
remove: members_remove,
}
}