use crate::roles::RoleDef;
use crate::{GraphDb, Result, RuleDef, ViewDef};
use core_storage::{fs::Fs, GraphError};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Default, Debug)]
pub struct Schema {
#[serde(default)]
pub fulltext: Vec<(String, String)>,
#[serde(default)]
pub rules: Vec<RuleDef>,
#[serde(default)]
pub views: Vec<ViewDef>,
#[serde(default)]
pub roles: Vec<RoleDef>,
}
#[derive(Debug, PartialEq)]
pub struct SchemaDiff {
pub created: Vec<String>,
pub updated: Vec<String>,
pub unchanged: Vec<String>,
}
impl<F: Fs> GraphDb<F> {
pub fn apply_schema(&mut self, schema: &Schema) -> Result<SchemaDiff> {
let live_views = self.views();
let live_rules = self.rules();
{
let mut seen_rules = std::collections::HashSet::new();
for rule_def in &schema.rules {
if !seen_rules.insert(rule_def.name.as_str()) {
return Err(GraphError::RuleInvalid {
detail: format!("duplicate rule name in schema: {}", rule_def.name),
});
}
}
let mut seen_views = std::collections::HashSet::new();
for view_def in &schema.views {
if !seen_views.insert(view_def.name.as_str()) {
return Err(GraphError::RuleInvalid {
detail: format!("duplicate view name in schema: {}", view_def.name),
});
}
}
}
for view_def in &schema.views {
let would_mutate = live_views
.iter()
.find(|v| v.name == view_def.name)
.is_none_or(|live| live != view_def);
if would_mutate {
view_def
.validate()
.map_err(|e| GraphError::RuleInvalid { detail: e })?;
}
}
for rule_def in &schema.rules {
let would_mutate = live_rules
.iter()
.find(|r| r.name == rule_def.name)
.is_none_or(|live| live != rule_def);
if would_mutate {
rule_def
.validate()
.map_err(|e| GraphError::RuleInvalid { detail: e })?;
}
}
{
let mut seen = std::collections::HashSet::new();
for role_def in &schema.roles {
if role_def.name.is_empty() {
return Err(GraphError::RuleInvalid {
detail: "role name must not be empty".into(),
});
}
if !seen.insert(role_def.name.as_str()) {
return Err(GraphError::RuleInvalid {
detail: format!("duplicate role name: {}", role_def.name),
});
}
}
}
let mut created = Vec::new();
let mut updated = Vec::new();
let mut unchanged = Vec::new();
for (label, field) in &schema.fulltext {
let key = format!("fulltext:{label}.{field}");
if self.is_fulltext_enabled(label, field) {
unchanged.push(key);
} else {
self.enable_fulltext(label, field)?;
created.push(key);
}
}
for view_def in &schema.views {
let key = format!("view:{}", view_def.name);
if let Some(live) = live_views.iter().find(|v| v.name == view_def.name) {
if live == view_def {
unchanged.push(key);
} else {
self.delete_view(&view_def.name)?;
self.create_view(view_def.clone())?;
updated.push(key);
}
} else {
self.create_view(view_def.clone())?;
created.push(key);
}
}
for rule_def in &schema.rules {
let key = format!("rule:{}", rule_def.name);
if let Some(live) = live_rules.iter().find(|r| r.name == rule_def.name) {
if live == rule_def {
unchanged.push(key);
} else {
self.delete_rule(&rule_def.name)?;
self.create_rule(rule_def.clone())?;
updated.push(key);
}
} else {
self.create_rule(rule_def.clone())?;
created.push(key);
}
}
{
let live_roles = self.roles();
let mut new_roles: Vec<RoleDef> = live_roles.clone();
let mut roles_changed = false;
for role_def in &schema.roles {
let key = format!("role:{}", role_def.name);
if let Some(live) = live_roles.iter().find(|r| r.name == role_def.name) {
if live == role_def {
unchanged.push(key);
} else {
if let Some(slot) = new_roles.iter_mut().find(|r| r.name == role_def.name) {
*slot = role_def.clone();
}
roles_changed = true;
updated.push(key);
}
} else {
new_roles.push(role_def.clone());
roles_changed = true;
created.push(key);
}
}
if roles_changed {
self.commit_roles(new_roles)?;
}
}
Ok(SchemaDiff {
created,
updated,
unchanged,
})
}
}