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 indexes: 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),
});
}
if let Some(pred) = &role_def.visible_where {
pred.validate().map_err(|e| GraphError::RuleInvalid {
detail: format!("role '{}': {e}", role_def.name),
})?;
if role_def.labels.is_empty() {
return Err(GraphError::RuleInvalid {
detail: format!(
"role '{}': visible_where narrows the labels leg and the role \
declares no labels",
role_def.name
),
});
}
}
if let Some(list) = &role_def.namespaces {
if list.is_empty() {
return Err(GraphError::RuleInvalid {
detail: format!(
"role '{}': namespaces: [] would make the role see nothing; \
omit keys and labels instead",
role_def.name
),
});
}
for name in list {
if !core_storage::valid_namespace(name) {
return Err(GraphError::RuleInvalid {
detail: format!(
"role '{}': {name:?} is not a valid namespace name — 1 to \
{} characters of [A-Za-z0-9_.-]",
role_def.name,
core_storage::NS_MAX_LEN
),
});
}
}
for key in &role_def.keys {
if let Some(key_ns) = self.namespace_of(key) {
if !role_def.sees_namespace(&key_ns) {
return Err(GraphError::RuleInvalid {
detail: format!(
"role '{}': key '{key}' is in namespace '{key_ns}', \
which is outside the role's namespaces [{}]",
role_def.name,
list.join(", ")
),
});
}
}
}
}
if let Some(write) = &role_def.write {
let read_labels: std::collections::HashSet<&str> =
role_def.labels.iter().map(String::as_str).collect();
for (field, labels) in [
("create_labels", &write.create_labels),
("update_labels", &write.update_labels),
("delete_labels", &write.delete_labels),
] {
for label in labels {
if !read_labels.contains(label.as_str()) {
return Err(GraphError::RuleInvalid {
detail: format!(
"role '{}': write scope {field} contains label '{}' \
that is not in the role's read labels (subset rule)",
role_def.name, label
),
});
}
}
}
}
}
}
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 (label, field) in &schema.indexes {
let key = format!("index:{label}.{field}");
if self.is_index_enabled(label, field) {
unchanged.push(key);
} else {
self.enable_index(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,
})
}
}