Files
biggus-dickus/crates/db/src/fields.rs
T
logaritmisk 66ad67ca77 feat(db): add field-definition registry repository
Implements create_field_definition, field_definition_by_key, and
list_field_definitions in db::fields, with TDD integration tests
covering text, term, and authority field type round-trips.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 10:21:39 +02:00

124 lines
4.0 KiB
Rust

//! Registry of flexible field definitions.
use domain::{
AuthorityKind, FieldDefinition, FieldDefinitionId, FieldType, LocalizedLabel,
NewFieldDefinition, VocabularyId,
};
use sqlx::Row;
/// Labels aggregated per row as JSON, to read a definition and its labels in one query.
const LABELS_JSON: &str = "COALESCE(json_agg(json_build_object('lang', fdl.lang, 'label', fdl.label) \
ORDER BY fdl.lang) FILTER (WHERE fdl.field_definition_id IS NOT NULL), '[]'::json)";
const SELECT_COLUMNS: &str =
"fd.id, fd.key, fd.data_type, fd.vocabulary_id, fd.authority_kind, fd.required, fd.group_key";
/// Create a field definition and its labels. Multiple statements — pass a
/// transaction connection (`&mut *tx`) for atomicity.
pub async fn create_field_definition(
conn: &mut sqlx::PgConnection,
new: &NewFieldDefinition,
) -> Result<FieldDefinitionId, sqlx::Error> {
let id = FieldDefinitionId::new();
let (data_type, vocabulary_id, authority_kind) = new.field_type.to_parts();
sqlx::query(
"INSERT INTO field_definition \
(id, key, data_type, vocabulary_id, authority_kind, required, group_key) \
VALUES ($1, $2, $3, $4, $5, $6, $7)",
)
.bind(id.to_uuid())
.bind(&new.key)
.bind(data_type)
.bind(vocabulary_id.map(|v| v.to_uuid()))
.bind(authority_kind.map(|k| k.as_str()))
.bind(new.required)
.bind(new.group_key.as_deref())
.execute(&mut *conn)
.await?;
for label in &new.labels {
sqlx::query(
"INSERT INTO field_definition_label (field_definition_id, lang, label) \
VALUES ($1, $2, $3)",
)
.bind(id.to_uuid())
.bind(&label.lang)
.bind(&label.label)
.execute(&mut *conn)
.await?;
}
Ok(id)
}
/// Look up a field definition by its key (with labels).
pub async fn field_definition_by_key<'e, E>(
executor: E,
key: &str,
) -> Result<Option<FieldDefinition>, sqlx::Error>
where
E: sqlx::PgExecutor<'e>,
{
let sql = format!(
"SELECT {SELECT_COLUMNS}, {LABELS_JSON} AS labels \
FROM field_definition fd \
LEFT JOIN field_definition_label fdl ON fdl.field_definition_id = fd.id \
WHERE fd.key = $1 GROUP BY fd.id"
);
let row = sqlx::query(&sql).bind(key).fetch_optional(executor).await?;
row.map(map_field_definition).transpose()
}
/// List all field definitions (with labels), ordered by key.
pub async fn list_field_definitions<'e, E>(executor: E) -> Result<Vec<FieldDefinition>, sqlx::Error>
where
E: sqlx::PgExecutor<'e>,
{
let sql = format!(
"SELECT {SELECT_COLUMNS}, {LABELS_JSON} AS labels \
FROM field_definition fd \
LEFT JOIN field_definition_label fdl ON fdl.field_definition_id = fd.id \
GROUP BY fd.id ORDER BY fd.key"
);
let rows = sqlx::query(&sql).fetch_all(executor).await?;
rows.into_iter().map(map_field_definition).collect()
}
fn map_field_definition(row: sqlx::postgres::PgRow) -> Result<FieldDefinition, sqlx::Error> {
let data_type: String = row.try_get("data_type")?;
let vocabulary_id: Option<uuid::Uuid> = row.try_get("vocabulary_id")?;
let authority_kind: Option<String> = row.try_get("authority_kind")?;
let authority_kind = authority_kind
.map(|k| {
AuthorityKind::from_db(&k)
.ok_or_else(|| sqlx::Error::Decode(format!("unknown authority kind: {k}").into()))
})
.transpose()?;
let field_type = FieldType::from_parts(
&data_type,
vocabulary_id.map(VocabularyId::from_uuid),
authority_kind,
)
.ok_or_else(|| {
sqlx::Error::Decode(format!("inconsistent field type stored: {data_type}").into())
})?;
let labels: sqlx::types::Json<Vec<LocalizedLabel>> = row.try_get("labels")?;
Ok(FieldDefinition {
id: FieldDefinitionId::from_uuid(row.try_get("id")?),
key: row.try_get("key")?,
field_type,
required: row.try_get("required")?,
group_key: row.try_get("group_key")?,
labels: labels.0,
})
}