d81b069b8f
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
215 lines
5.4 KiB
Rust
215 lines
5.4 KiB
Rust
//! Admin vocabulary + term management. Reads require `ViewInternal`; writes `EditCatalogue`.
|
|
|
|
use auth::{Authorized, EditCatalogue, ViewInternal};
|
|
use axum::{
|
|
Json, Router,
|
|
extract::{Path, State},
|
|
http::StatusCode,
|
|
routing::get,
|
|
};
|
|
use domain::{LocalizedLabel, NewTerm, VocabularyId};
|
|
use serde::{Deserialize, Serialize};
|
|
use utoipa::ToSchema;
|
|
|
|
use crate::{AppState, admin_objects::LabelView};
|
|
|
|
#[derive(Serialize, ToSchema)]
|
|
pub(crate) struct VocabularyView {
|
|
pub id: String,
|
|
pub key: String,
|
|
}
|
|
|
|
#[derive(Deserialize, ToSchema)]
|
|
pub(crate) struct NewVocabularyRequest {
|
|
pub key: String,
|
|
}
|
|
|
|
#[derive(Deserialize, ToSchema)]
|
|
pub(crate) struct LabelInput {
|
|
pub lang: String,
|
|
pub label: String,
|
|
}
|
|
|
|
#[derive(Deserialize, ToSchema)]
|
|
pub(crate) struct NewTermRequest {
|
|
pub external_uri: Option<String>,
|
|
pub labels: Vec<LabelInput>,
|
|
}
|
|
|
|
#[derive(Serialize, ToSchema)]
|
|
pub(crate) struct TermView {
|
|
pub id: String,
|
|
pub external_uri: Option<String>,
|
|
pub labels: Vec<LabelView>,
|
|
}
|
|
|
|
#[derive(Serialize, ToSchema)]
|
|
pub(crate) struct CreatedId {
|
|
pub id: String,
|
|
}
|
|
|
|
#[utoipa::path(
|
|
get, path = "/api/admin/vocabularies",
|
|
responses(
|
|
(status = 200, body = [VocabularyView]),
|
|
(status = 401),
|
|
(status = 403)
|
|
)
|
|
)]
|
|
pub(crate) async fn list_vocabularies(
|
|
_auth: Authorized<ViewInternal>,
|
|
State(state): State<AppState>,
|
|
) -> Result<Json<Vec<VocabularyView>>, StatusCode> {
|
|
let vocabs = db::vocab::list_vocabularies(state.db.pool())
|
|
.await
|
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
|
|
|
Ok(Json(
|
|
vocabs
|
|
.into_iter()
|
|
.map(|vocab| VocabularyView {
|
|
id: vocab.id.to_string(),
|
|
key: vocab.key,
|
|
})
|
|
.collect(),
|
|
))
|
|
}
|
|
|
|
#[utoipa::path(
|
|
post, path = "/api/admin/vocabularies",
|
|
request_body = NewVocabularyRequest,
|
|
responses(
|
|
(status = 201, body = VocabularyView),
|
|
(status = 401),
|
|
(status = 403)
|
|
)
|
|
)]
|
|
pub(crate) async fn create_vocabulary(
|
|
_auth: Authorized<EditCatalogue>,
|
|
State(state): State<AppState>,
|
|
Json(req): Json<NewVocabularyRequest>,
|
|
) -> Result<(StatusCode, Json<VocabularyView>), StatusCode> {
|
|
let vocab = db::vocab::create_vocabulary(state.db.pool(), &req.key)
|
|
.await
|
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
|
|
|
Ok((
|
|
StatusCode::CREATED,
|
|
Json(VocabularyView {
|
|
id: vocab.id.to_string(),
|
|
key: vocab.key,
|
|
}),
|
|
))
|
|
}
|
|
|
|
#[utoipa::path(
|
|
get, path = "/api/admin/vocabularies/{id}/terms",
|
|
params(("id" = String, Path, description = "Vocabulary id (UUID)")),
|
|
responses(
|
|
(status = 200, body = [TermView]),
|
|
(status = 401),
|
|
(status = 403),
|
|
(status = 404)
|
|
)
|
|
)]
|
|
pub(crate) async fn list_terms(
|
|
_auth: Authorized<ViewInternal>,
|
|
State(state): State<AppState>,
|
|
Path(id): Path<String>,
|
|
) -> Result<Json<Vec<TermView>>, StatusCode> {
|
|
let vocab_id = id
|
|
.parse::<VocabularyId>()
|
|
.map_err(|_| StatusCode::NOT_FOUND)?;
|
|
|
|
let terms = db::vocab::list_terms(state.db.pool(), vocab_id)
|
|
.await
|
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
|
|
|
Ok(Json(
|
|
terms
|
|
.into_iter()
|
|
.map(|term| TermView {
|
|
id: term.id.to_string(),
|
|
external_uri: term.external_uri,
|
|
labels: term
|
|
.labels
|
|
.into_iter()
|
|
.map(|label| LabelView {
|
|
lang: label.lang,
|
|
label: label.label,
|
|
})
|
|
.collect(),
|
|
})
|
|
.collect(),
|
|
))
|
|
}
|
|
|
|
#[utoipa::path(
|
|
post, path = "/api/admin/vocabularies/{id}/terms",
|
|
request_body = NewTermRequest,
|
|
params(("id" = String, Path, description = "Vocabulary id (UUID)")),
|
|
responses(
|
|
(status = 201, body = CreatedId),
|
|
(status = 401),
|
|
(status = 403),
|
|
(status = 404)
|
|
)
|
|
)]
|
|
pub(crate) async fn add_term(
|
|
_auth: Authorized<EditCatalogue>,
|
|
State(state): State<AppState>,
|
|
Path(id): Path<String>,
|
|
Json(req): Json<NewTermRequest>,
|
|
) -> Result<(StatusCode, Json<CreatedId>), StatusCode> {
|
|
let vocabulary_id = id
|
|
.parse::<VocabularyId>()
|
|
.map_err(|_| StatusCode::NOT_FOUND)?;
|
|
|
|
let new = NewTerm {
|
|
vocabulary_id,
|
|
external_uri: req.external_uri,
|
|
labels: req
|
|
.labels
|
|
.into_iter()
|
|
.map(|label| LocalizedLabel {
|
|
lang: label.lang,
|
|
label: label.label,
|
|
})
|
|
.collect(),
|
|
};
|
|
|
|
let mut tx = state
|
|
.db
|
|
.pool()
|
|
.begin()
|
|
.await
|
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
|
|
|
let term_id = db::vocab::add_term(&mut tx, &new)
|
|
.await
|
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
|
|
|
tx.commit()
|
|
.await
|
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
|
|
|
Ok((
|
|
StatusCode::CREATED,
|
|
Json(CreatedId {
|
|
id: term_id.to_string(),
|
|
}),
|
|
))
|
|
}
|
|
|
|
pub(crate) fn routes() -> Router<AppState> {
|
|
Router::new()
|
|
.route(
|
|
"/api/admin/vocabularies",
|
|
get(list_vocabularies).post(create_vocabulary),
|
|
)
|
|
.route(
|
|
"/api/admin/vocabularies/{id}/terms",
|
|
get(list_terms).post(add_term),
|
|
)
|
|
}
|