From 351df4c817a3ea875932acd55c7fb79a2f5983c5 Mon Sep 17 00:00:00 2001 From: ElysaSrc <101974839+ElysaSrc@users.noreply.github.com> Date: Sat, 14 Sep 2024 10:55:52 +0200 Subject: [PATCH] backend, frontend: revamp utoipa/ts types --- ...f570fe793ad077357017afd4aa456569e5162.json | 28 +++ ...8b13b28df1369b0f1971c8f4c65b9a065cb13.json | 28 --- backend/Cargo.toml | 2 +- backend/src/api/admin/entities.rs | 6 +- backend/src/api/admin/statistics.rs | 2 +- backend/src/api/map.rs | 2 +- backend/src/doc.rs | 6 +- backend/src/models/access_token.rs | 136 ++++++----- backend/src/models/entity.rs | 10 +- backend/src/models/entity_cache.rs | 10 +- backend/src/models/family.rs | 6 +- backend/src/models/statistics.rs | 27 ++- frontend/components/admin/Navbar.vue | 15 +- .../components/admin/families/EditForm.vue | 8 +- frontend/components/form/DynamicField.vue | 14 +- frontend/lib.d.ts | 57 ++--- frontend/lib/admin-client.ts | 21 +- frontend/lib/admin-state.ts | 33 ++- frontend/lib/viewer-state.ts | 10 +- frontend/openapi.json | 214 ++++++++++++++---- frontend/pages/admin/access-tokens/[id].vue | 2 +- 21 files changed, 402 insertions(+), 235 deletions(-) create mode 100644 backend/.sqlx/query-1f3ff6a401eab7441ce33dda930f570fe793ad077357017afd4aa456569e5162.json delete mode 100644 backend/.sqlx/query-26285b50be723917b21f3fc61168b13b28df1369b0f1971c8f4c65b9a065cb13.json diff --git a/backend/.sqlx/query-1f3ff6a401eab7441ce33dda930f570fe793ad077357017afd4aa456569e5162.json b/backend/.sqlx/query-1f3ff6a401eab7441ce33dda930f570fe793ad077357017afd4aa456569e5162.json new file mode 100644 index 0000000..07f751c --- /dev/null +++ b/backend/.sqlx/query-1f3ff6a401eab7441ce33dda930f570fe793ad077357017afd4aa456569e5162.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n COALESCE((\n WITH origins AS (\n SELECT DISTINCT(COALESCE(referrer, 'unknown')) AS referrer, COUNT(*) AS total\n FROM access_tokens_visits\n WHERE token_id = $1\n GROUP BY referrer\n )\n SELECT json_object_agg(referrer, total) FROM origins\n ), '{}') as \"origins: JsonValue\",\n (\n WITH date_series AS (\n SELECT generate_series(\n NOW()::date - INTERVAL '30 days',\n NOW()::date,\n INTERVAL '1 day'\n )::date AS visit_date\n ),\n aggregated_visits AS (\n SELECT\n ds.visit_date,\n COALESCE(COUNT(atv.visited_at), 0) AS visit_count\n FROM\n date_series ds\n LEFT JOIN\n access_tokens_visits atv\n ON\n ds.visit_date = DATE(atv.visited_at) \n AND atv.token_id = $1\n GROUP BY\n ds.visit_date\n ORDER BY\n ds.visit_date\n )\n SELECT COALESCE(json_object_agg(\n TO_CHAR(visit_date, 'YYYY-MM-DD'),\n visit_count\n ), '{}') AS visits\n FROM aggregated_visits\n ) as \"visits_30_days: JsonValue\"\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "origins: JsonValue", + "type_info": "Json" + }, + { + "ordinal": 1, + "name": "visits_30_days: JsonValue", + "type_info": "Json" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + null, + null + ] + }, + "hash": "1f3ff6a401eab7441ce33dda930f570fe793ad077357017afd4aa456569e5162" +} diff --git a/backend/.sqlx/query-26285b50be723917b21f3fc61168b13b28df1369b0f1971c8f4c65b9a065cb13.json b/backend/.sqlx/query-26285b50be723917b21f3fc61168b13b28df1369b0f1971c8f4c65b9a065cb13.json deleted file mode 100644 index 31ef0c0..0000000 --- a/backend/.sqlx/query-26285b50be723917b21f3fc61168b13b28df1369b0f1971c8f4c65b9a065cb13.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\n COALESCE((\n WITH origins AS (\n SELECT DISTINCT(COALESCE(referrer, 'unknown')) AS referrer, COUNT(*) AS total\n FROM access_tokens_visits\n WHERE token_id = $1\n GROUP BY referrer\n )\n SELECT json_object_agg(referrer, total) FROM origins\n ), '{}') as \"origins!\",\n (\n WITH date_series AS (\n SELECT generate_series(\n NOW()::date - INTERVAL '30 days',\n NOW()::date,\n INTERVAL '1 day'\n )::date AS visit_date\n ),\n aggregated_visits AS (\n SELECT\n ds.visit_date,\n COALESCE(COUNT(atv.visited_at), 0) AS visit_count\n FROM\n date_series ds\n LEFT JOIN\n access_tokens_visits atv\n ON\n ds.visit_date = DATE(atv.visited_at) \n AND atv.token_id = $1\n GROUP BY\n ds.visit_date\n ORDER BY\n ds.visit_date\n )\n SELECT COALESCE(json_object_agg(\n TO_CHAR(visit_date, 'YYYY-MM-DD'),\n visit_count\n ), '{}') AS visits\n FROM aggregated_visits\n ) as \"visits_30_days!\";\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "origins!", - "type_info": "Json" - }, - { - "ordinal": 1, - "name": "visits_30_days!", - "type_info": "Json" - } - ], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [ - null, - null - ] - }, - "hash": "26285b50be723917b21f3fc61168b13b28df1369b0f1971c8f4c65b9a065cb13" -} diff --git a/backend/Cargo.toml b/backend/Cargo.toml index c1cc5de..db2dca8 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -30,7 +30,7 @@ jsonwebtoken = "9" tracing = "0.1" tower-http = { version = "0.5", features = ["fs", "trace", "cors"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] } -utoipa = { version = "4.2.0", features = ["axum_extras", "uuid", "chrono"] } +utoipa = { version = "4", features = ["axum_extras", "uuid", "chrono"] } clap = { version = "4", features = ["derive"] } clap_derive = { version = "4" } number_range = "0.3.2" diff --git a/backend/src/api/admin/entities.rs b/backend/src/api/admin/entities.rs index 25ff9ec..a08f9a4 100644 --- a/backend/src/api/admin/entities.rs +++ b/backend/src/api/admin/entities.rs @@ -41,8 +41,10 @@ pub struct AdminEntityWithRelations { pub display_name: String, pub category_id: Uuid, pub family_id: Uuid, - #[schema(value_type = Object)] + + #[schema(value_type = Vec)] pub locations: sqlx::types::Json>, + pub data: Value, pub tags: Vec, pub hidden: bool, @@ -64,7 +66,7 @@ pub struct AdminEntityWithRelations { ("page_size" = i64, Query, description = "Number of items per page (default: 20)") ), responses( - (status = 200, description = "Search results for entities", body = PaginatedVec), + (status = 200, description = "Search results for entities", body = AdminCachedEntitiesWithPagination), (status = 401, description = "Invalid permissions", body = ErrorResponse), ) )] diff --git a/backend/src/api/admin/statistics.rs b/backend/src/api/admin/statistics.rs index e38b179..3dc2320 100644 --- a/backend/src/api/admin/statistics.rs +++ b/backend/src/api/admin/statistics.rs @@ -21,7 +21,7 @@ pub async fn admin_home_stats( get, path = "/api/admin/stats/count-comments-entities", responses( - (status = 200, description = "Dicts of entities and comments counts by family and category id"), + (status = 200, description = "Dicts of entities and comments counts by family and category id", body = (HashMap,HashMap),), (status = 401, description = "Invalid permissions", body = ErrorResponse), ) )] diff --git a/backend/src/api/map.rs b/backend/src/api/map.rs index cd71968..669124d 100644 --- a/backend/src/api/map.rs +++ b/backend/src/api/map.rs @@ -242,7 +242,7 @@ impl Display for SearchRequest { path = "/api/map/search", request_body = SearchRequest, responses( - (status = 200, description = "List of entities", body = PaginatedVec), + (status = 200, description = "List of entities", body = ViewerCachedEntitiesWithPagination), (status = 401, description = "Invalid token", body = ErrorResponse), ) )] diff --git a/backend/src/doc.rs b/backend/src/doc.rs index 4ba8b32..e1e633d 100644 --- a/backend/src/doc.rs +++ b/backend/src/doc.rs @@ -28,8 +28,8 @@ use crate::{ }, entity_cache::{ AdminCachedEntitiesWithPagination, AdminCachedEntity, Cluster, EntitiesAndClusters, - LocationRepresentation, PaginatedVec, ParentRepresentation, - ViewerCachedEntitiesWithPagination, ViewerCachedEntity, ViewerSearchedCachedEntity, + LocationRepresentation, ParentRepresentation, ViewerCachedEntitiesWithPagination, + ViewerCachedEntity, ViewerSearchedCachedEntity, }, family::{Family, Field, FieldType, Form, NewOrUpdateFamily}, options::{ @@ -161,8 +161,6 @@ use utoipa::OpenApi; PublicEntity, PublicListedEntity, PublicNewEntity, - PaginatedVec, - PaginatedVec, ViewerCachedEntity, ViewerSearchedCachedEntity, LocationRepresentation, diff --git a/backend/src/models/access_token.rs b/backend/src/models/access_token.rs index d16fb15..46c3436 100644 --- a/backend/src/models/access_token.rs +++ b/backend/src/models/access_token.rs @@ -1,7 +1,12 @@ +use std::collections::{BTreeMap, HashMap}; + use crate::{api::AppError, helpers::postgis_polygons::MultiPolygon}; use serde::{Deserialize, Serialize}; -use serde_json::{to_value, Value}; -use sqlx::{types::Json, PgConnection}; +use serde_json::{json, to_value}; +use sqlx::{ + types::{Json, JsonValue}, + PgConnection, +}; use utoipa::ToSchema; use uuid::Uuid; @@ -55,7 +60,7 @@ pub struct PermissionPolicy { pub struct NewOrUpdateAccessToken { pub title: String, pub token: String, - #[schema(value_type = Object)] + #[schema(value_type = Permissions)] pub permissions: Json, pub active: bool, } @@ -65,7 +70,7 @@ pub struct AccessToken { pub id: Uuid, pub title: String, pub token: String, - #[schema(value_type = Object)] + #[schema(value_type = Permissions)] pub permissions: Json, pub last_week_visits: i64, pub active: bool, @@ -73,11 +78,8 @@ pub struct AccessToken { #[derive(Deserialize, Serialize, ToSchema, Debug)] pub struct AccessTokenStats { - #[schema(value_type = Object)] - pub origins: Json, - - #[schema(value_type = Object)] - pub visits_30_days: Json, + pub origins: HashMap, + pub visits_30_days: BTreeMap, } impl AccessToken { @@ -249,54 +251,72 @@ impl AccessToken { access_token_id: Uuid, conn: &mut PgConnection, ) -> Result { - sqlx::query_as!( - AccessTokenStats, - r#" - SELECT - COALESCE(( - WITH origins AS ( - SELECT DISTINCT(COALESCE(referrer, 'unknown')) AS referrer, COUNT(*) AS total - FROM access_tokens_visits - WHERE token_id = $1 - GROUP BY referrer - ) - SELECT json_object_agg(referrer, total) FROM origins - ), '{}') as "origins!", - ( - WITH date_series AS ( - SELECT generate_series( - NOW()::date - INTERVAL '30 days', - NOW()::date, - INTERVAL '1 day' - )::date AS visit_date - ), - aggregated_visits AS ( - SELECT - ds.visit_date, - COALESCE(COUNT(atv.visited_at), 0) AS visit_count - FROM - date_series ds - LEFT JOIN - access_tokens_visits atv - ON - ds.visit_date = DATE(atv.visited_at) - AND atv.token_id = $1 - GROUP BY - ds.visit_date - ORDER BY - ds.visit_date - ) - SELECT COALESCE(json_object_agg( - TO_CHAR(visit_date, 'YYYY-MM-DD'), - visit_count - ), '{}') AS visits - FROM aggregated_visits - ) as "visits_30_days!"; - "#, - access_token_id - ) - .fetch_one(conn) - .await - .map_err(AppError::Database) + let result = sqlx::query!( + r#" + SELECT + COALESCE(( + WITH origins AS ( + SELECT DISTINCT(COALESCE(referrer, 'unknown')) AS referrer, COUNT(*) AS total + FROM access_tokens_visits + WHERE token_id = $1 + GROUP BY referrer + ) + SELECT json_object_agg(referrer, total) FROM origins + ), '{}') as "origins: JsonValue", + ( + WITH date_series AS ( + SELECT generate_series( + NOW()::date - INTERVAL '30 days', + NOW()::date, + INTERVAL '1 day' + )::date AS visit_date + ), + aggregated_visits AS ( + SELECT + ds.visit_date, + COALESCE(COUNT(atv.visited_at), 0) AS visit_count + FROM + date_series ds + LEFT JOIN + access_tokens_visits atv + ON + ds.visit_date = DATE(atv.visited_at) + AND atv.token_id = $1 + GROUP BY + ds.visit_date + ORDER BY + ds.visit_date + ) + SELECT COALESCE(json_object_agg( + TO_CHAR(visit_date, 'YYYY-MM-DD'), + visit_count + ), '{}') AS visits + FROM aggregated_visits + ) as "visits_30_days: JsonValue" + "#, + access_token_id + ) + .fetch_one(conn) + .await + .map_err(AppError::Database)?; + + // Provide a default empty object if origins is None + let origins_json: JsonValue = result.origins.unwrap_or_else(|| json!({})); + let visits_30_days_json: JsonValue = result.visits_30_days.unwrap_or_else(|| json!({})); + + // Convert the JsonValue to HashMap + let origins: HashMap = + serde_json::from_value(origins_json).map_err(|err| { + AppError::Internal(format!("Failed to deserialize origins: {}", err).into()) + })?; + let visits_30_days: BTreeMap = serde_json::from_value(visits_30_days_json) + .map_err(|err| { + AppError::Internal(format!("Failed to deserialize visits_30_days: {}", err).into()) + })?; + + Ok(AccessTokenStats { + origins, + visits_30_days, + }) } } diff --git a/backend/src/models/entity.rs b/backend/src/models/entity.rs index 90cf869..18f5145 100644 --- a/backend/src/models/entity.rs +++ b/backend/src/models/entity.rs @@ -40,13 +40,13 @@ pub struct PublicEntity { pub display_name: String, pub category_id: Uuid, pub family_id: Uuid, - #[schema(value_type = Object)] + #[schema(value_type = Vec)] pub locations: Json>, pub data: Value, pub tags: Vec, - #[schema(value_type = Object)] + #[schema(value_type = Form)] pub entity_form: Json
, - #[schema(value_type = Object)] + #[schema(value_type = Form)] pub comment_form: Json, pub created_at: chrono::NaiveDateTime, pub updated_at: chrono::NaiveDateTime, @@ -196,7 +196,7 @@ impl PublicEntity { pub struct AdminNewOrUpdateEntity { pub display_name: String, pub category_id: Uuid, - #[schema(value_type = Object)] + #[schema(value_type = Vec)] pub locations: Json>, pub data: Value, pub tags: Vec, @@ -224,7 +224,7 @@ pub struct AdminEntity { pub display_name: String, pub category_id: Uuid, pub family_id: Uuid, - #[schema(value_type = Object)] + #[schema(value_type = Vec)] pub locations: Json>, pub data: Value, pub tags: Vec, diff --git a/backend/src/models/entity_cache.rs b/backend/src/models/entity_cache.rs index 1c0402f..1b57d21 100644 --- a/backend/src/models/entity_cache.rs +++ b/backend/src/models/entity_cache.rs @@ -146,9 +146,12 @@ impl From> for AdminCachedEntitiesWithPagination } } -#[derive(Deserialize, Serialize, ToSchema, Debug)] +#[derive(Serialize, ToSchema, Debug)] +#[aliases( + ViewerCachedEntitiesWithPagination = PaginatedVec, + AdminCachedEntitiesWithPagination = PaginatedVec +)] pub struct PaginatedVec { - #[schema(value_type = Object)] pub entities: Vec, pub total_results: i64, @@ -156,9 +159,6 @@ pub struct PaginatedVec { pub response_current_page: i64, } -pub type ViewerCachedEntitiesWithPagination = PaginatedVec; -pub type AdminCachedEntitiesWithPagination = PaginatedVec; - #[derive(Deserialize, Serialize, ToSchema, Debug)] pub struct CachedClusteredEntity { pub id: Uuid, diff --git a/backend/src/models/family.rs b/backend/src/models/family.rs index a5d68b7..ffd8ca4 100644 --- a/backend/src/models/family.rs +++ b/backend/src/models/family.rs @@ -48,7 +48,7 @@ pub struct Field { /// only for the frontend. For instance, if the field is an enum /// use it to store possible values. If it is a SingleLineText, specify /// if it's an email, a phone number, etc... - #[schema(value_type = Object)] + #[schema(value_type = Value, additional_properties)] pub field_type_metadata: Option, /// Sets if the field is indexed (used in full text search, or constraints search) @@ -80,9 +80,9 @@ pub struct Family { pub id: Uuid, pub title: String, pub icon_hash: Option, - #[schema(value_type = Object)] + #[schema(value_type = Form)] pub entity_form: Json, - #[schema(value_type = Object)] + #[schema(value_type = Form)] pub comment_form: Json, pub sort_order: i32, pub version: i32, diff --git a/backend/src/models/statistics.rs b/backend/src/models/statistics.rs index 2bce73e..0deb5aa 100644 --- a/backend/src/models/statistics.rs +++ b/backend/src/models/statistics.rs @@ -2,9 +2,8 @@ use crate::api::AppError; use serde::Serialize; -use serde_json::Value; -use sqlx::{types::Json, PgConnection}; -use std::collections::HashMap; +use sqlx::PgConnection; +use std::collections::{BTreeMap, HashMap}; use utoipa::ToSchema; pub type CountResult = ( @@ -93,13 +92,11 @@ pub struct HomePageStats { pub total_visits_30_days: i64, pub total_visits_7_days: i64, - #[schema(value_type = Object)] - pub visits_30_days: Json, + pub visits_30_days: BTreeMap, } pub async fn home_page_stats(conn: &mut PgConnection) -> Result { - let stats = sqlx::query_as!( - HomePageStats, + let result = sqlx::query!( r#" SELECT (SELECT COUNT(*) FROM entities WHERE moderated) as "total_entities!", @@ -146,5 +143,19 @@ pub async fn home_page_stats(conn: &mut PgConnection) -> Result + let visits_30_days: BTreeMap = serde_json::from_value(result.visits_30_days) + .map_err(|err| { + AppError::Internal(format!("Failed to deserialize visits_30_days: {}", err).into()) + })?; + + Ok(HomePageStats { + total_entities: result.total_entities, + total_comments: result.total_comments, + pending_entities: result.pending_entities, + pending_comments: result.pending_comments, + total_visits_30_days: result.total_visits_30_days, + total_visits_7_days: result.total_visits_7_days, + visits_30_days, + }) } diff --git a/frontend/components/admin/Navbar.vue b/frontend/components/admin/Navbar.vue index facf0eb..fcdf8ed 100644 --- a/frontend/components/admin/Navbar.vue +++ b/frontend/components/admin/Navbar.vue @@ -128,15 +128,18 @@ Une mise à jour est disponible.
Rendez-vous sur GitHub pour voir et mettre à jour vers la dernière version. -

+

Version actuelle:
{{ state.versionInformation?.version ?? 'Inconnu' }}
-

-

+

+
Git hash:
{{ state.versionInformation?.git_hash ?? 'Inconnu' }}
-

-

+

+
Dernière version:
{{ state.versionInformation?.github_latest_version }}
-

+
diff --git a/frontend/components/admin/families/EditForm.vue b/frontend/components/admin/families/EditForm.vue index 95c6467..db91fea 100644 --- a/frontend/components/admin/families/EditForm.vue +++ b/frontend/components/admin/families/EditForm.vue @@ -521,7 +521,7 @@