Persons & Names full implementation
This commit is contained in:
@@ -6,11 +6,13 @@ use axum::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::DatabaseError,
|
database::DatabaseError,
|
||||||
|
persons::PersonError,
|
||||||
tags::TagError,
|
tags::TagError,
|
||||||
users::{UserError, auth::AuthError, sessions::SessionError},
|
users::{UserError, auth::AuthError, sessions::SessionError},
|
||||||
};
|
};
|
||||||
|
|
||||||
mod auth;
|
mod auth;
|
||||||
|
mod persons;
|
||||||
mod sessions;
|
mod sessions;
|
||||||
mod tags;
|
mod tags;
|
||||||
mod users;
|
mod users;
|
||||||
@@ -39,6 +41,14 @@ pub fn api_router() -> Router {
|
|||||||
.route("/api/tags/{id}", patch(tags::rename))
|
.route("/api/tags/{id}", patch(tags::rename))
|
||||||
.route("/api/tags/{id}", delete(tags::delete))
|
.route("/api/tags/{id}", delete(tags::delete))
|
||||||
.route("/api/tags/#{name}", get(tags::get_by_name))
|
.route("/api/tags/#{name}", get(tags::get_by_name))
|
||||||
|
//
|
||||||
|
.route("/api/persons", get(persons::get_all))
|
||||||
|
.route("/api/persons", post(persons::create))
|
||||||
|
.route("/api/persons/{id}", get(persons::get_by_id))
|
||||||
|
.route("/api/persons/{id}/names", get(persons::pid_names))
|
||||||
|
.route("/api/persons/{id}/addname", post(persons::add_name))
|
||||||
|
.route("/api/names/{id}", get(persons::n_by_id))
|
||||||
|
.route("/api/names/{id}/setprimary", post(persons::n_setprimary))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CompositeError(Response);
|
pub struct CompositeError(Response);
|
||||||
@@ -59,4 +69,11 @@ macro_rules! composite_from {
|
|||||||
)+
|
)+
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
composite_from!(AuthError, UserError, SessionError, TagError, DatabaseError);
|
composite_from!(
|
||||||
|
AuthError,
|
||||||
|
UserError,
|
||||||
|
SessionError,
|
||||||
|
TagError,
|
||||||
|
PersonError,
|
||||||
|
DatabaseError
|
||||||
|
);
|
||||||
|
|||||||
83
src/api/persons.rs
Normal file
83
src/api/persons.rs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
use axum::{
|
||||||
|
Json,
|
||||||
|
extract::Path,
|
||||||
|
http::{HeaderMap, StatusCode},
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
api::CompositeError,
|
||||||
|
persons::{Name, Person},
|
||||||
|
users::{
|
||||||
|
User,
|
||||||
|
auth::{UserAuthRequired, UserAuthenticate},
|
||||||
|
permissions::Permission,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const CANT_SET_PRIMARYNAME: &str = "You don't have permission to swap primary names.";
|
||||||
|
|
||||||
|
pub async fn get_all(headers: HeaderMap) -> Result<Response, CompositeError> {
|
||||||
|
User::authenticate(&headers)?.required()?;
|
||||||
|
Ok(Json(Person::get_all()?).into_response())
|
||||||
|
}
|
||||||
|
pub async fn get_by_id(
|
||||||
|
Path(id): Path<Uuid>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
) -> Result<Response, CompositeError> {
|
||||||
|
User::authenticate(&headers)?.required()?;
|
||||||
|
Ok(Json(Person::get_by_id(id)?).into_response())
|
||||||
|
}
|
||||||
|
pub async fn pid_names(
|
||||||
|
Path(id): Path<Uuid>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
) -> Result<Response, CompositeError> {
|
||||||
|
User::authenticate(&headers)?.required()?;
|
||||||
|
Ok(Json(Person::get_by_id(id)?.get_all_names()?).into_response())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct PersonNameForm {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Json(form): Json<PersonNameForm>,
|
||||||
|
) -> Result<Response, CompositeError> {
|
||||||
|
let u = User::authenticate(&headers)?.required()?;
|
||||||
|
let p = Person::create(form.name, u.id)?;
|
||||||
|
Ok((StatusCode::CREATED, Json(p)).into_response())
|
||||||
|
}
|
||||||
|
pub async fn add_name(
|
||||||
|
Path(id): Path<Uuid>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Json(form): Json<PersonNameForm>,
|
||||||
|
) -> Result<Response, CompositeError> {
|
||||||
|
let u = User::authenticate(&headers)?.required()?;
|
||||||
|
let p = Person::get_by_id(id)?;
|
||||||
|
let n = p.add_name(form.name, u.id)?;
|
||||||
|
|
||||||
|
Ok((StatusCode::CREATED, Json(n)).into_response())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn n_by_id(Path(id): Path<Uuid>, headers: HeaderMap) -> Result<Response, CompositeError> {
|
||||||
|
User::authenticate(&headers)?.required()?;
|
||||||
|
Ok(Json(Name::get_by_id(id)?).into_response())
|
||||||
|
}
|
||||||
|
pub async fn n_setprimary(
|
||||||
|
Path(id): Path<Uuid>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
) -> Result<Response, CompositeError> {
|
||||||
|
let u = User::authenticate(&headers)?.required()?;
|
||||||
|
if !u.has_permission(Permission::ChangePersonPrimaryName)? {
|
||||||
|
return Ok((StatusCode::FORBIDDEN, CANT_SET_PRIMARYNAME).into_response());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut n = Name::get_by_id(id)?;
|
||||||
|
n.set_primary()?;
|
||||||
|
n.is_primary = true;
|
||||||
|
Ok(Json(n).into_response())
|
||||||
|
}
|
||||||
@@ -1,4 +1,182 @@
|
|||||||
pub mod names;
|
use axum::{
|
||||||
|
http::StatusCode,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use rusqlite::OptionalExtension;
|
||||||
|
use serde::Serialize;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[allow(unused)]
|
use crate::database::{self, DatabaseError};
|
||||||
pub struct Person;
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct Person {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub primary_name: String,
|
||||||
|
pub created_by: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct Name {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub is_primary: bool,
|
||||||
|
pub person_id: Uuid,
|
||||||
|
pub created_by: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum PersonError {
|
||||||
|
#[error("No person found with ID {0}")]
|
||||||
|
NoPersonWithId(Uuid),
|
||||||
|
#[error("No name found with ID {0}")]
|
||||||
|
NoNameWithId(Uuid),
|
||||||
|
#[error("A name with this value already exists for this person")]
|
||||||
|
NameAlreadyExists,
|
||||||
|
#[error("This name is already the primary name")]
|
||||||
|
AlreadyPrimary,
|
||||||
|
#[error("{0}")]
|
||||||
|
DatabaseError(#[from] DatabaseError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Person {
|
||||||
|
pub fn get_all() -> Result<Vec<Person>, PersonError> {
|
||||||
|
Ok(database::conn()?
|
||||||
|
.prepare("SELECT p.id, p.created_by, n.name FROM persons p JOIN names n ON p.id = n.person_id AND n.is_primary = 1")?
|
||||||
|
.query_map((), |r| {
|
||||||
|
Ok(Person {
|
||||||
|
id: r.get(0)?,
|
||||||
|
created_by: r.get(1)?,
|
||||||
|
primary_name: r.get(2)?,
|
||||||
|
})
|
||||||
|
})?
|
||||||
|
.collect::<Result<Vec<Person>, _>>()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_by_id(id: Uuid) -> Result<Person, PersonError> {
|
||||||
|
let res = database::conn()?
|
||||||
|
.prepare("SELECT p.created_by, n.name FROM persons p JOIN names n ON p.id = n.person_id AND n.is_primary = 1 WHERE p.id = ?1")?
|
||||||
|
.query_one((&id,), |r| {
|
||||||
|
Ok(Person {
|
||||||
|
id,
|
||||||
|
created_by: r.get(0)?,
|
||||||
|
primary_name: r.get(1)?,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.optional()?;
|
||||||
|
match res {
|
||||||
|
Some(p) => Ok(p),
|
||||||
|
None => Err(PersonError::NoPersonWithId(id)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_all_names(&self) -> Result<Vec<Name>, PersonError> {
|
||||||
|
Ok(database::conn()?
|
||||||
|
.prepare("SELECT id, is_primary, person_id, created_by, name FROM names WHERE person_id = ?1")?
|
||||||
|
.query_map((&self.id,), |r| {
|
||||||
|
Ok(Name {
|
||||||
|
id: r.get(0)?,
|
||||||
|
is_primary: r.get(1)?,
|
||||||
|
person_id: r.get(2)?,
|
||||||
|
created_by: r.get(3)?,
|
||||||
|
name: r.get(4)?,
|
||||||
|
})
|
||||||
|
})?
|
||||||
|
.collect::<Result<Vec<Name>, _>>()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_name(&self, name: String, created_by: Uuid) -> Result<Name, PersonError> {
|
||||||
|
let id = Uuid::now_v7();
|
||||||
|
database::conn()?
|
||||||
|
.prepare("INSERT INTO names VALUES (?1, ?2, ?3, ?4, ?5)")?
|
||||||
|
.execute((id, 0, self.id, created_by, &name))?;
|
||||||
|
Ok(Name {
|
||||||
|
id,
|
||||||
|
is_primary: false,
|
||||||
|
person_id: self.id,
|
||||||
|
created_by,
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create(primary_name: String, created_by: Uuid) -> Result<Person, PersonError> {
|
||||||
|
let person_id = Uuid::now_v7();
|
||||||
|
let name_id = Uuid::now_v7();
|
||||||
|
|
||||||
|
let conn = database::conn()?;
|
||||||
|
conn.execute("BEGIN TRANSACTION", ())?;
|
||||||
|
|
||||||
|
conn.prepare("INSERT INTO persons(id, created_by) VALUES (?1, ?2)")?
|
||||||
|
.execute((person_id, created_by))?;
|
||||||
|
conn.prepare("INSERT INTO names VALUES (?1, ?2, ?3, ?4, ?5)")?
|
||||||
|
.execute((name_id, 1, person_id, created_by, &primary_name))?;
|
||||||
|
conn.execute("COMMIT", ())?;
|
||||||
|
|
||||||
|
Ok(Person {
|
||||||
|
id: person_id,
|
||||||
|
primary_name,
|
||||||
|
created_by,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Name {
|
||||||
|
pub fn get_by_id(id: Uuid) -> Result<Name, PersonError> {
|
||||||
|
let res = database::conn()?
|
||||||
|
.prepare("SELECT id, is_primary, person_id, created_by, name FROM names WHERE id = ?1")?
|
||||||
|
.query_one((&id,), |r| {
|
||||||
|
Ok(Name {
|
||||||
|
id: r.get(0)?,
|
||||||
|
is_primary: r.get(1)?,
|
||||||
|
person_id: r.get(2)?,
|
||||||
|
created_by: r.get(3)?,
|
||||||
|
name: r.get(4)?,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.optional()?;
|
||||||
|
match res {
|
||||||
|
Some(n) => Ok(n),
|
||||||
|
None => Err(PersonError::NoNameWithId(id)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn set_primary(&mut self) -> Result<(), PersonError> {
|
||||||
|
if self.is_primary {
|
||||||
|
return Err(PersonError::AlreadyPrimary);
|
||||||
|
}
|
||||||
|
|
||||||
|
let conn = database::conn()?;
|
||||||
|
conn.execute("BEGIN TRANSACTION", ())?;
|
||||||
|
|
||||||
|
conn.prepare("UPDATE names SET is_primary = 0 WHERE person_id = ?1 AND is_primary = 1")?
|
||||||
|
.execute((&self.person_id,))?;
|
||||||
|
conn.prepare("UPDATE names SET is_primary = 1 WHERE id = ?1")?
|
||||||
|
.execute((&self.id,))?;
|
||||||
|
|
||||||
|
conn.execute("COMMIT", ())?;
|
||||||
|
self.is_primary = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<rusqlite::Error> for PersonError {
|
||||||
|
fn from(error: rusqlite::Error) -> Self {
|
||||||
|
if let rusqlite::Error::SqliteFailure(e, Some(msg)) = &error
|
||||||
|
&& e.extended_code == rusqlite::ffi::SQLITE_CONSTRAINT_UNIQUE
|
||||||
|
&& msg.contains("name")
|
||||||
|
{
|
||||||
|
return PersonError::NameAlreadyExists;
|
||||||
|
}
|
||||||
|
PersonError::DatabaseError(DatabaseError::from(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for PersonError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
match self {
|
||||||
|
Self::DatabaseError(e) => e.into_response(),
|
||||||
|
Self::NoPersonWithId(_) => (StatusCode::BAD_REQUEST, self.to_string()).into_response(),
|
||||||
|
Self::NoNameWithId(_) => (StatusCode::BAD_REQUEST, self.to_string()).into_response(),
|
||||||
|
Self::NameAlreadyExists => (StatusCode::CONFLICT, self.to_string()).into_response(),
|
||||||
|
Self::AlreadyPrimary => (StatusCode::BAD_REQUEST, self.to_string()).into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
#[allow(unused)]
|
|
||||||
pub struct Name;
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::persons::{Person, names::Name};
|
use crate::persons::{Name, Person};
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub struct QuoteLine {
|
pub struct QuoteLine {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ pub enum Permission {
|
|||||||
CreateTags,
|
CreateTags,
|
||||||
RenameTags,
|
RenameTags,
|
||||||
DeleteTags,
|
DeleteTags,
|
||||||
|
ChangePersonPrimaryName,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl User {
|
impl User {
|
||||||
|
|||||||
Reference in New Issue
Block a user