use chrono::Utc;
use serde::Deserialize;
use serde_json::Value as JsonValue;
use tracing::info;
use vti_common::audit::{AuditEvent, SchemaChangeData};
use vti_common::error::AppError;
use crate::schemas::{
AcceptsCriterion, SchemaEntry, SchemaKind, TYPE_URI_MAX_BYTES, delete_accepts, delete_schema,
get_accepts, get_schema, store_accepts, store_schema,
};
use crate::server::AppState;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
#[derive(utoipa::ToSchema)]
pub struct RegisterSchemaBody {
pub type_uri: String,
#[serde(default)]
pub dtg_type: Option<String>,
#[serde(default)]
pub credential_schema: Option<JsonValue>,
pub kind: SchemaKind,
#[serde(default)]
pub description: Option<String>,
}
pub(crate) async fn register_inner(
state: &AppState,
actor: &str,
body: RegisterSchemaBody,
) -> Result<SchemaEntry, AppError> {
let uri = body.type_uri.trim();
if uri.is_empty() {
return Err(AppError::Validation("type_uri cannot be empty".into()));
}
if uri.len() > TYPE_URI_MAX_BYTES {
return Err(AppError::Validation(format!(
"type_uri exceeds {TYPE_URI_MAX_BYTES} bytes"
)));
}
if let Some(schema) = &body.credential_schema {
jsonschema::validator_for(schema)
.map_err(|e| AppError::Validation(format!("invalid credentialSchema: {e}")))?;
}
let entry = SchemaEntry {
type_uri: uri.to_string(),
dtg_type: body.dtg_type,
credential_schema: body.credential_schema,
kind: body.kind,
description: body.description,
created_at: Utc::now(),
created_by_did: actor.to_string(),
};
store_schema(&state.schemas_ks, &entry).await?;
if let Some(writer) = state.audit_writer.as_ref() {
writer
.write(
actor,
None,
AuditEvent::SchemaRegistered(SchemaChangeData {
id: uri.to_string(),
kind: "schema".into(),
admission: None,
}),
)
.await?;
}
info!(type_uri = %uri, kind = ?entry.kind, by = %actor, "schema registered");
Ok(entry)
}
pub(crate) async fn delete_inner(
state: &AppState,
actor: &str,
type_uri: &str,
) -> Result<String, AppError> {
if get_schema(&state.schemas_ks, type_uri).await?.is_none() {
return Err(AppError::NotFound(format!("schema `{type_uri}` not found")));
}
delete_schema(&state.schemas_ks, type_uri).await?;
if let Some(writer) = state.audit_writer.as_ref() {
writer
.write(
actor,
None,
AuditEvent::SchemaDeleted(SchemaChangeData {
id: type_uri.to_string(),
kind: "schema".into(),
admission: None,
}),
)
.await?;
}
info!(type_uri = %type_uri, by = %actor, "schema deleted");
Ok(type_uri.to_string())
}
pub(crate) async fn register_accepts_inner(
state: &AppState,
actor: &str,
mut criterion: AcceptsCriterion,
) -> Result<AcceptsCriterion, AppError> {
let id = criterion.id.trim().to_string();
if id.is_empty() {
return Err(AppError::Validation(
"accepts criterion id cannot be empty".into(),
));
}
if let Some(vetting) = &criterion.vetting
&& !crate::endorsement_types::storage::type_exists(
&state.endorsement_types_ks,
&vetting.statement_type,
)
.await?
{
return Err(AppError::Validation(format!(
"vetting.statementType `{}` is not a registered endorsement type — register it \
with vtc/endorsement-types/register first",
vetting.statement_type
)));
}
criterion.id = id.clone();
criterion.created_at = Utc::now();
criterion.created_by_did = actor.to_string();
if criterion.hidden_vetting.is_none()
&& let Some(existing) = crate::schemas::accepts::get_accepts(&state.schemas_ks, &id).await?
&& let Some(hidden) = existing.hidden_vetting
{
if criterion.vetting.is_some() {
criterion.hidden_vetting = Some(hidden);
} else {
info!(
id = %id,
by = %actor,
"accepts criterion no longer asks for vetting; its hidden-vetting parameters are dropped with it"
);
}
}
let criterion = store_accepts(&state.schemas_ks, &criterion).await?;
if let Some(writer) = state.audit_writer.as_ref() {
writer
.write(
actor,
None,
AuditEvent::SchemaRegistered(SchemaChangeData {
id: id.clone(),
kind: "accepts".into(),
admission: Some(criterion.admission.as_str().into()),
}),
)
.await?;
}
info!(
id = %id,
admission = criterion.admission.as_str(),
by = %actor,
"accepts criterion registered"
);
Ok(criterion)
}
pub(crate) async fn delete_accepts_inner(
state: &AppState,
actor: &str,
id: &str,
) -> Result<String, AppError> {
if get_accepts(&state.schemas_ks, id).await?.is_none() {
return Err(AppError::NotFound(format!(
"accepts criterion `{id}` not found"
)));
}
delete_accepts(&state.schemas_ks, id).await?;
if let Some(writer) = state.audit_writer.as_ref() {
writer
.write(
actor,
None,
AuditEvent::SchemaDeleted(SchemaChangeData {
id: id.to_string(),
kind: "accepts".into(),
admission: None,
}),
)
.await?;
}
info!(id = %id, by = %actor, "accepts criterion deleted");
Ok(id.to_string())
}