changing data model according to new data
This commit is contained in:
parent
464af45107
commit
9de3c4a482
4 changed files with 271 additions and 72 deletions
23
api/migrations/20250101002000_update_persons_schema.sql
Normal file
23
api/migrations/20250101002000_update_persons_schema.sql
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
BEGIN;
|
||||
|
||||
DROP TABLE IF EXISTS persons;
|
||||
|
||||
CREATE TABLE persons (
|
||||
id SERIAL PRIMARY KEY,
|
||||
first_name TEXT NOT NULL,
|
||||
last_name TEXT NOT NULL,
|
||||
grade INTEGER NOT NULL,
|
||||
parent_name TEXT NOT NULL,
|
||||
parent_phone_number TEXT NOT NULL,
|
||||
checked_in BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
inside BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
visitor BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
sleeping_spot BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_persons_first_name_trgm ON persons USING GIN (first_name gin_trgm_ops);
|
||||
CREATE INDEX idx_persons_last_name_trgm ON persons USING GIN (last_name gin_trgm_ops);
|
||||
CREATE INDEX idx_persons_parent_name_trgm ON persons USING GIN (parent_name gin_trgm_ops);
|
||||
CREATE INDEX idx_persons_parent_phone_number ON persons (parent_phone_number);
|
||||
|
||||
COMMIT;
|
||||
192
api/src/main.rs
192
api/src/main.rs
|
|
@ -187,13 +187,25 @@ async fn search_persons(
|
|||
let persons = if let Some(id) = id_value {
|
||||
sqlx::query_as::<_, Person>(
|
||||
r#"
|
||||
SELECT id, name, age, phone_number, checked_in, inside
|
||||
SELECT
|
||||
id,
|
||||
first_name,
|
||||
last_name,
|
||||
grade,
|
||||
parent_name,
|
||||
parent_phone_number,
|
||||
checked_in,
|
||||
inside,
|
||||
visitor,
|
||||
sleeping_spot
|
||||
FROM persons
|
||||
WHERE name ILIKE $1
|
||||
OR phone_number ILIKE $1
|
||||
WHERE first_name ILIKE $1
|
||||
OR last_name ILIKE $1
|
||||
OR parent_name ILIKE $1
|
||||
OR parent_phone_number ILIKE $1
|
||||
OR (first_name || ' ' || last_name) ILIKE $1
|
||||
OR id = $2
|
||||
ORDER BY name
|
||||
LIMIT 100
|
||||
ORDER BY last_name, first_name
|
||||
"#,
|
||||
)
|
||||
.bind(&like_pattern)
|
||||
|
|
@ -203,12 +215,24 @@ async fn search_persons(
|
|||
} else {
|
||||
sqlx::query_as::<_, Person>(
|
||||
r#"
|
||||
SELECT id, name, age, phone_number, checked_in, inside
|
||||
SELECT
|
||||
id,
|
||||
first_name,
|
||||
last_name,
|
||||
grade,
|
||||
parent_name,
|
||||
parent_phone_number,
|
||||
checked_in,
|
||||
inside,
|
||||
visitor,
|
||||
sleeping_spot
|
||||
FROM persons
|
||||
WHERE name ILIKE $1
|
||||
OR phone_number ILIKE $1
|
||||
ORDER BY name
|
||||
LIMIT 100
|
||||
WHERE first_name ILIKE $1
|
||||
OR last_name ILIKE $1
|
||||
OR parent_name ILIKE $1
|
||||
OR parent_phone_number ILIKE $1
|
||||
OR (first_name || ' ' || last_name) ILIKE $1
|
||||
ORDER BY last_name, first_name
|
||||
"#,
|
||||
)
|
||||
.bind(&like_pattern)
|
||||
|
|
@ -252,7 +276,7 @@ async fn list_checked_in(
|
|||
let id_value = search_term.as_ref().and_then(|s| s.parse::<i32>().ok());
|
||||
|
||||
let mut qb = QueryBuilder::<sqlx::Postgres>::new(
|
||||
"SELECT id, name, age, phone_number, checked_in, inside FROM persons",
|
||||
"SELECT id, first_name, last_name, grade, parent_name, parent_phone_number, checked_in, inside, visitor, sleeping_spot FROM persons",
|
||||
);
|
||||
|
||||
let mut first_condition = true;
|
||||
|
|
@ -278,8 +302,12 @@ async fn list_checked_in(
|
|||
if let Some(like) = like_pattern.as_ref() {
|
||||
append_condition(&mut qb);
|
||||
qb.push("(");
|
||||
qb.push("name ILIKE ").push_bind(like);
|
||||
qb.push(" OR phone_number ILIKE ").push_bind(like);
|
||||
qb.push("first_name ILIKE ").push_bind(like);
|
||||
qb.push(" OR last_name ILIKE ").push_bind(like);
|
||||
qb.push(" OR parent_name ILIKE ").push_bind(like);
|
||||
qb.push(" OR parent_phone_number ILIKE ").push_bind(like);
|
||||
qb.push(" OR (first_name || ' ' || last_name) ILIKE ")
|
||||
.push_bind(like);
|
||||
if let Some(id) = id_value {
|
||||
qb.push(" OR id = ").push_bind(id);
|
||||
}
|
||||
|
|
@ -289,12 +317,9 @@ async fn list_checked_in(
|
|||
if let Some(id) = id_value {
|
||||
qb.push(" ORDER BY CASE WHEN id = ")
|
||||
.push_bind(id)
|
||||
.push(" THEN 0 ELSE 1 END, id, name");
|
||||
.push(" THEN 0 ELSE 1 END, id, last_name, first_name");
|
||||
} else {
|
||||
qb.push(" ORDER BY id, name");
|
||||
}
|
||||
if like_pattern.is_some() {
|
||||
qb.push(" LIMIT 1000");
|
||||
qb.push(" ORDER BY id, last_name, first_name");
|
||||
}
|
||||
|
||||
let persons = qb.build_query_as::<Person>().fetch_all(&state.db).await?;
|
||||
|
|
@ -345,53 +370,116 @@ async fn create_person(
|
|||
state: &State<AppState>,
|
||||
payload: Json<NewPersonRequest>,
|
||||
) -> Result<Json<PersonActionResponse>, ApiError> {
|
||||
let name = payload.name.trim();
|
||||
if name.is_empty() {
|
||||
return Err(ApiError::bad_request("Namn får inte vara tomt."));
|
||||
let first_name = payload.first_name.trim();
|
||||
if first_name.is_empty() {
|
||||
return Err(ApiError::bad_request("Förnamn får inte vara tomt."));
|
||||
}
|
||||
|
||||
if payload.age < 0 {
|
||||
return Err(ApiError::bad_request("Ålder måste vara noll eller högre."));
|
||||
let last_name = payload.last_name.trim();
|
||||
if last_name.is_empty() {
|
||||
return Err(ApiError::bad_request("Efternamn får inte vara tomt."));
|
||||
}
|
||||
|
||||
let phone_number = payload.phone_number.trim();
|
||||
if phone_number.is_empty() {
|
||||
return Err(ApiError::bad_request("Telefonnummer krävs."));
|
||||
if payload.grade < 0 {
|
||||
return Err(ApiError::bad_request("Klass måste vara noll eller högre."));
|
||||
}
|
||||
|
||||
let age = payload.age;
|
||||
let parent_name = payload.parent_name.trim();
|
||||
if parent_name.is_empty() {
|
||||
return Err(ApiError::bad_request("Kontaktperson krävs."));
|
||||
}
|
||||
|
||||
let parent_phone_number = payload.parent_phone_number.trim();
|
||||
if parent_phone_number.is_empty() {
|
||||
return Err(ApiError::bad_request(
|
||||
"Kontaktpersonens telefonnummer krävs.",
|
||||
));
|
||||
}
|
||||
|
||||
let grade = payload.grade;
|
||||
let checked_in = payload.checked_in.unwrap_or(false);
|
||||
let inside = payload.inside.unwrap_or(false);
|
||||
let visitor = payload.visitor.unwrap_or(false);
|
||||
let sleeping_spot = payload.sleeping_spot.unwrap_or(false);
|
||||
|
||||
let person = match payload.id {
|
||||
Some(id) => sqlx::query_as::<_, Person>(
|
||||
r#"
|
||||
INSERT INTO persons (id, name, age, phone_number, checked_in, inside)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id, name, age, phone_number, checked_in, inside
|
||||
INSERT INTO persons (
|
||||
id,
|
||||
first_name,
|
||||
last_name,
|
||||
grade,
|
||||
parent_name,
|
||||
parent_phone_number,
|
||||
checked_in,
|
||||
inside,
|
||||
visitor,
|
||||
sleeping_spot
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
RETURNING
|
||||
id,
|
||||
first_name,
|
||||
last_name,
|
||||
grade,
|
||||
parent_name,
|
||||
parent_phone_number,
|
||||
checked_in,
|
||||
inside,
|
||||
visitor,
|
||||
sleeping_spot
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.bind(name)
|
||||
.bind(age)
|
||||
.bind(phone_number)
|
||||
.bind(first_name)
|
||||
.bind(last_name)
|
||||
.bind(grade)
|
||||
.bind(parent_name)
|
||||
.bind(parent_phone_number)
|
||||
.bind(checked_in)
|
||||
.bind(inside)
|
||||
.bind(visitor)
|
||||
.bind(sleeping_spot)
|
||||
.fetch_one(&state.db)
|
||||
.await
|
||||
.map_err(|err| map_db_error(err, "Kunde inte skapa person"))?,
|
||||
None => sqlx::query_as::<_, Person>(
|
||||
r#"
|
||||
INSERT INTO persons (name, age, phone_number, checked_in, inside)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id, name, age, phone_number, checked_in, inside
|
||||
INSERT INTO persons (
|
||||
first_name,
|
||||
last_name,
|
||||
grade,
|
||||
parent_name,
|
||||
parent_phone_number,
|
||||
checked_in,
|
||||
inside,
|
||||
visitor,
|
||||
sleeping_spot
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING
|
||||
id,
|
||||
first_name,
|
||||
last_name,
|
||||
grade,
|
||||
parent_name,
|
||||
parent_phone_number,
|
||||
checked_in,
|
||||
inside,
|
||||
visitor,
|
||||
sleeping_spot
|
||||
"#,
|
||||
)
|
||||
.bind(name)
|
||||
.bind(age)
|
||||
.bind(phone_number)
|
||||
.bind(first_name)
|
||||
.bind(last_name)
|
||||
.bind(grade)
|
||||
.bind(parent_name)
|
||||
.bind(parent_phone_number)
|
||||
.bind(checked_in)
|
||||
.bind(inside)
|
||||
.bind(visitor)
|
||||
.bind(sleeping_spot)
|
||||
.fetch_one(&state.db)
|
||||
.await
|
||||
.map_err(|err| map_db_error(err, "Kunde inte skapa person"))?,
|
||||
|
|
@ -415,7 +503,17 @@ async fn update_checked_in(
|
|||
SET checked_in = $2,
|
||||
inside = $2
|
||||
WHERE id = $1
|
||||
RETURNING id, name, age, phone_number, checked_in, inside
|
||||
RETURNING
|
||||
id,
|
||||
first_name,
|
||||
last_name,
|
||||
grade,
|
||||
parent_name,
|
||||
parent_phone_number,
|
||||
checked_in,
|
||||
inside,
|
||||
visitor,
|
||||
sleeping_spot
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
|
|
@ -446,7 +544,17 @@ async fn update_inside(
|
|||
SET inside = $2
|
||||
WHERE id = $1
|
||||
AND checked_in = TRUE
|
||||
RETURNING id, name, age, phone_number, checked_in, inside
|
||||
RETURNING
|
||||
id,
|
||||
first_name,
|
||||
last_name,
|
||||
grade,
|
||||
parent_name,
|
||||
parent_phone_number,
|
||||
checked_in,
|
||||
inside,
|
||||
visitor,
|
||||
sleeping_spot
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
|
|
@ -484,7 +592,7 @@ fn map_db_error(err: sqlx::Error, context: &str) -> ApiError {
|
|||
if let Some(code) = db_err.code() {
|
||||
if code == "23505" {
|
||||
return ApiError::bad_request(
|
||||
"Krock i databasen – kontrollera id eller telefonnummer.",
|
||||
"Krock i databasen – kontrollera id eller kontaktuppgifter.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,11 +12,15 @@ pub struct User {
|
|||
#[derive(Debug, FromRow)]
|
||||
pub struct Person {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub age: i32,
|
||||
pub phone_number: String,
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub grade: i32,
|
||||
pub parent_name: String,
|
||||
pub parent_phone_number: String,
|
||||
pub checked_in: bool,
|
||||
pub inside: bool,
|
||||
pub visitor: bool,
|
||||
pub sleeping_spot: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
@ -36,25 +40,30 @@ pub struct LoginResponse {
|
|||
#[serde(crate = "rocket::serde")]
|
||||
pub struct PersonResponse {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub age: i32,
|
||||
pub phone_number: String,
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub grade: i32,
|
||||
pub parent_name: String,
|
||||
pub parent_phone_number: String,
|
||||
pub checked_in: bool,
|
||||
pub inside: bool,
|
||||
pub under_ten: bool,
|
||||
pub visitor: bool,
|
||||
pub sleeping_spot: bool,
|
||||
}
|
||||
|
||||
impl From<Person> for PersonResponse {
|
||||
fn from(person: Person) -> Self {
|
||||
let under_ten = person.age < 10;
|
||||
PersonResponse {
|
||||
id: person.id,
|
||||
name: person.name,
|
||||
age: person.age,
|
||||
phone_number: person.phone_number,
|
||||
first_name: person.first_name,
|
||||
last_name: person.last_name,
|
||||
grade: person.grade,
|
||||
parent_name: person.parent_name,
|
||||
parent_phone_number: person.parent_phone_number,
|
||||
checked_in: person.checked_in,
|
||||
inside: person.inside,
|
||||
under_ten,
|
||||
visitor: person.visitor,
|
||||
sleeping_spot: person.sleeping_spot,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -74,13 +83,19 @@ pub struct PersonActionResponse {
|
|||
#[derive(Debug, Deserialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct NewPersonRequest {
|
||||
pub name: String,
|
||||
pub age: i32,
|
||||
pub phone_number: String,
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub grade: i32,
|
||||
pub parent_name: String,
|
||||
pub parent_phone_number: String,
|
||||
#[serde(default)]
|
||||
pub id: Option<i32>,
|
||||
#[serde(default)]
|
||||
pub checked_in: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub inside: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub visitor: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub sleeping_spot: Option<bool>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,13 +45,30 @@ async fn ensure_person_seed(pool: &PgPool) -> anyhow::Result<()> {
|
|||
|
||||
for person in persons {
|
||||
sqlx::query(
|
||||
"INSERT INTO persons (name, age, phone_number, checked_in, inside) VALUES ($1, $2, $3, $4, $5)",
|
||||
r#"
|
||||
INSERT INTO persons (
|
||||
first_name,
|
||||
last_name,
|
||||
grade,
|
||||
parent_name,
|
||||
parent_phone_number,
|
||||
checked_in,
|
||||
inside,
|
||||
visitor,
|
||||
sleeping_spot
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
"#,
|
||||
)
|
||||
.bind(&person.name)
|
||||
.bind(person.age)
|
||||
.bind(&person.phone_number)
|
||||
.bind(&person.first_name)
|
||||
.bind(&person.last_name)
|
||||
.bind(person.grade)
|
||||
.bind(&person.parent_name)
|
||||
.bind(&person.parent_phone_number)
|
||||
.bind(person.checked_in)
|
||||
.bind(person.inside)
|
||||
.bind(person.visitor)
|
||||
.bind(person.sleeping_spot)
|
||||
.execute(pool)
|
||||
.await
|
||||
.context("Misslyckades att lägga till seed-person")?;
|
||||
|
|
@ -61,20 +78,24 @@ async fn ensure_person_seed(pool: &PgPool) -> anyhow::Result<()> {
|
|||
}
|
||||
|
||||
struct PersonSeed {
|
||||
name: String,
|
||||
age: i32,
|
||||
phone_number: String,
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
grade: i32,
|
||||
parent_name: String,
|
||||
parent_phone_number: String,
|
||||
checked_in: bool,
|
||||
inside: bool,
|
||||
visitor: bool,
|
||||
sleeping_spot: bool,
|
||||
}
|
||||
|
||||
fn generate_people() -> Vec<PersonSeed> {
|
||||
let first_names = vec![
|
||||
let first_names = [
|
||||
"Alex", "Bianca", "Cecilia", "David", "Elias", "Fatima", "Gabriel", "Hanna", "Isak",
|
||||
"Johanna", "Karin", "Liam", "Maja", "Nils", "Olivia",
|
||||
];
|
||||
|
||||
let last_names = vec![
|
||||
let last_names = [
|
||||
"Andersson",
|
||||
"Berg",
|
||||
"Carlsson",
|
||||
|
|
@ -87,20 +108,52 @@ fn generate_people() -> Vec<PersonSeed> {
|
|||
"Johansson",
|
||||
];
|
||||
|
||||
let guardian_first_names = [
|
||||
"Anna",
|
||||
"Bertil",
|
||||
"Charlotte",
|
||||
"Daniel",
|
||||
"Emma",
|
||||
"Fredrik",
|
||||
"Greta",
|
||||
"Henrik",
|
||||
"Ingrid",
|
||||
"Jakob",
|
||||
"Klara",
|
||||
"Lars",
|
||||
"Maria",
|
||||
"Niklas",
|
||||
"Petra",
|
||||
];
|
||||
|
||||
let guardian_last_names = [
|
||||
"Lind", "Sandberg", "Forsberg", "Nyström", "Sjöberg", "Viklund", "Ågren", "Öberg",
|
||||
"Boström", "Engman",
|
||||
];
|
||||
|
||||
let mut people = Vec::with_capacity(first_names.len() * last_names.len());
|
||||
|
||||
let mut idx: usize = 0;
|
||||
for first in &first_names {
|
||||
for last in &last_names {
|
||||
let name = format!("{} {}", first, last);
|
||||
let age = 5 + ((idx * 11) % 60) as i32;
|
||||
let phone_number = format!("070{:03}{:04}", idx % 1_000, (idx * 37) % 10_000);
|
||||
let grade = 1 + ((idx * 3) % 9) as i32; // Grades between 1 and 9
|
||||
let parent_first = guardian_first_names[idx % guardian_first_names.len()];
|
||||
let parent_last = guardian_last_names[idx % guardian_last_names.len()];
|
||||
let parent_name = format!("{} {}", parent_first, parent_last);
|
||||
let parent_phone_number = format!("070{:03}{:04}", idx % 1_000, (idx * 37) % 10_000);
|
||||
let visitor = idx % 7 == 0;
|
||||
let sleeping_spot = !visitor && grade >= 3 && idx % 5 == 0;
|
||||
|
||||
people.push(PersonSeed {
|
||||
name,
|
||||
age,
|
||||
phone_number,
|
||||
first_name: (*first).to_string(),
|
||||
last_name: (*last).to_string(),
|
||||
grade,
|
||||
parent_name,
|
||||
parent_phone_number,
|
||||
checked_in: false,
|
||||
inside: false,
|
||||
visitor,
|
||||
sleeping_spot,
|
||||
});
|
||||
idx += 1;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue