use chrono::Utc;
use vti_common::error::AppError;
use vti_common::store::KeyspaceHandle;
use super::{SchemaEntry, SchemaKind, delete_schema, get_schema, schema_exists, store_schema};
pub const DEFAULT_ISSUES_TYPES: &[(&str, Option<&str>)] = &[
("MembershipCredential", Some("MembershipCredential")),
("AuthorityCredential", Some("AuthorityCredential")),
("StatementCredential", Some("StatementCredential")),
("InvitationCredential", Some("InvitationCredential")),
];
const RETIRED_ISSUES_TYPES: &[&str] = &["IdentityVerificationCredential"];
const SEED_AUTHOR: &str = "did:vtc:system";
pub async fn seed_default_issues(schemas_ks: &KeyspaceHandle) -> Result<(), AppError> {
for type_uri in RETIRED_ISSUES_TYPES {
if let Some(entry) = get_schema(schemas_ks, type_uri).await?
&& entry.created_by_did == SEED_AUTHOR
&& entry.kind == SchemaKind::Issues
{
delete_schema(schemas_ks, type_uri).await?;
}
}
let now = Utc::now();
for (type_uri, dtg_type) in DEFAULT_ISSUES_TYPES {
if schema_exists(schemas_ks, type_uri).await? {
continue;
}
store_schema(
schemas_ks,
&SchemaEntry {
type_uri: (*type_uri).to_string(),
dtg_type: dtg_type.map(str::to_string),
credential_schema: None,
kind: SchemaKind::Issues,
description: Some("Built-in catalog type (seeded default)".to_string()),
created_at: now,
created_by_did: SEED_AUTHOR.to_string(),
},
)
.await?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::super::{SchemaEntry, SchemaKind, get_schema, is_issues_registered, store_schema};
use super::*;
use vti_common::config::StoreConfig;
use vti_common::store::Store;
async fn ks() -> (tempfile::TempDir, Store, KeyspaceHandle) {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(&StoreConfig {
data_dir: dir.path().to_path_buf(),
})
.unwrap();
let ks = store.keyspace("schemas").unwrap();
(dir, store, ks)
}
#[tokio::test]
async fn seeds_the_catalog_issues_types() {
let (_d, _s, ks) = ks().await;
seed_default_issues(&ks).await.unwrap();
for (type_uri, _) in DEFAULT_ISSUES_TYPES {
assert!(
is_issues_registered(&ks, type_uri).await.unwrap(),
"{type_uri} must be seeded as an Issues type"
);
}
}
#[tokio::test]
async fn a_retired_seeded_default_is_withdrawn_but_an_operator_row_is_kept() {
let (_d, _s, ks) = ks().await;
let row = |by: &str| SchemaEntry {
type_uri: RETIRED_ISSUES_TYPES[0].into(),
dtg_type: None,
credential_schema: None,
kind: SchemaKind::Issues,
description: None,
created_at: Utc::now(),
created_by_did: by.into(),
};
store_schema(&ks, &row(SEED_AUTHOR)).await.unwrap();
seed_default_issues(&ks).await.unwrap();
assert!(
get_schema(&ks, RETIRED_ISSUES_TYPES[0])
.await
.unwrap()
.is_none()
);
store_schema(&ks, &row("did:key:zAdmin")).await.unwrap();
seed_default_issues(&ks).await.unwrap();
assert!(
get_schema(&ks, RETIRED_ISSUES_TYPES[0])
.await
.unwrap()
.is_some()
);
}
#[tokio::test]
async fn is_idempotent_and_preserves_operator_edits() {
let (_d, _s, ks) = ks().await;
let custom = SchemaEntry {
type_uri: "MembershipCredential".into(),
dtg_type: Some("MembershipCredential".into()),
credential_schema: Some(serde_json::json!({ "type": "object" })),
kind: SchemaKind::Issues,
description: Some("operator-tuned".into()),
created_at: Utc::now(),
created_by_did: "did:key:zAdmin".into(),
};
store_schema(&ks, &custom).await.unwrap();
seed_default_issues(&ks).await.unwrap();
seed_default_issues(&ks).await.unwrap();
let got = get_schema(&ks, "MembershipCredential")
.await
.unwrap()
.unwrap();
assert_eq!(got, custom, "seeding must not overwrite an existing entry");
assert!(
is_issues_registered(&ks, "InvitationCredential")
.await
.unwrap()
);
}
}