feat(api): admin set flexible fields + field-definition listing
- GET /api/admin/field-definitions (ViewInternal) — lists all registered
field definitions with key, data_type, vocabulary_id, authority_kind,
required, group, and localized labels
- PUT /api/admin/objects/{id}/fields (EditCatalogue) — replaces an
object's flexible-field values with replace semantics; validates every
key against the registry (UnknownField → 422, TypeMismatch → 422,
Unresolved → 422, ObjectNotFound → 404, Db → 500)
- FieldDefinitionView DTO added; both handlers registered in OpenAPI
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,10 @@ use api::{AppState, build_app, migrate_sessions};
|
||||
use axum::body::Body;
|
||||
use axum::http::{Request, StatusCode, header};
|
||||
use db::{catalog, users};
|
||||
use domain::{AuditActor, Email, NewUser, ObjectInput, Role, Visibility};
|
||||
use domain::{
|
||||
AuditActor, Email, FieldType, LocalizedLabel, NewFieldDefinition, NewUser, ObjectInput, Role,
|
||||
Visibility,
|
||||
};
|
||||
use http_body_util::BodyExt;
|
||||
use sqlx::PgPool;
|
||||
use tower::ServiceExt;
|
||||
@@ -329,6 +332,105 @@ async fn create_rejects_public_visibility(pool: PgPool) {
|
||||
assert_eq!(resp.status(), StatusCode::UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
|
||||
#[sqlx::test(migrations = "../db/migrations")]
|
||||
async fn set_fields_and_list_field_definitions(pool: PgPool) {
|
||||
migrate_sessions(&db::Db::from_pool(pool.clone()))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
seed_user(&pool, "ed@example.com", "pw-editor-123", Role::Editor).await;
|
||||
|
||||
let db = db::Db::from_pool(pool.clone());
|
||||
let mut tx = db.pool().begin().await.unwrap();
|
||||
|
||||
db::fields::create_field_definition(
|
||||
&mut tx,
|
||||
&NewFieldDefinition {
|
||||
key: "inscription".into(),
|
||||
field_type: FieldType::Text,
|
||||
required: false,
|
||||
group_key: None,
|
||||
labels: vec![LocalizedLabel {
|
||||
lang: "en".into(),
|
||||
label: "Inscription".into(),
|
||||
}],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let id = catalog::create_object(
|
||||
&mut tx,
|
||||
AuditActor::System,
|
||||
&obj("A-1", "amphora", Visibility::Draft),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tx.commit().await.unwrap();
|
||||
|
||||
let app = build_app(state(pool.clone()));
|
||||
let cookie = login(&app, "ed@example.com", "pw-editor-123").await;
|
||||
|
||||
// field-definitions list
|
||||
let defs = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/api/admin/field-definitions")
|
||||
.header(header::COOKIE, &cookie)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(defs.status(), StatusCode::OK);
|
||||
|
||||
let defs_json: serde_json::Value =
|
||||
serde_json::from_slice(&defs.into_body().collect().await.unwrap().to_bytes()).unwrap();
|
||||
assert!(
|
||||
defs_json
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|d| d["key"] == "inscription" && d["data_type"] == "text")
|
||||
);
|
||||
|
||||
// set the field
|
||||
let set = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("PUT")
|
||||
.uri(format!("/api/admin/objects/{id}/fields"))
|
||||
.header(header::COOKIE, &cookie)
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.body(Body::from(r#"{"inscription":"To the gods"}"#))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(set.status(), StatusCode::NO_CONTENT);
|
||||
|
||||
let stored = catalog::object_by_id(db.pool(), id).await.unwrap().unwrap();
|
||||
assert_eq!(stored.fields["inscription"], "To the gods");
|
||||
|
||||
// unknown field → 422
|
||||
let bad = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("PUT")
|
||||
.uri(format!("/api/admin/objects/{id}/fields"))
|
||||
.header(header::COOKIE, &cookie)
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.body(Body::from(r#"{"nope":"x"}"#))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(bad.status(), StatusCode::UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
|
||||
#[sqlx::test(migrations = "../db/migrations")]
|
||||
async fn create_requires_auth(pool: PgPool) {
|
||||
migrate_sessions(&db::Db::from_pool(pool.clone()))
|
||||
|
||||
Reference in New Issue
Block a user