use std::collections::{HashMap, HashSet};
use anyhow::{Result, bail};
use tracing::warn;
use crate::schema::intermediate::{IntermediateSchema, IntermediateType};
#[derive(Debug, Default)]
pub struct AnnotatedTypeIndex {
tenant_fields: HashMap<String, HashSet<String>>,
}
impl AnnotatedTypeIndex {
#[must_use]
pub fn build(types: &[IntermediateType]) -> Self {
let mut tenant_fields: HashMap<String, HashSet<String>> = HashMap::new();
for typ in types {
for field in &typ.fields {
let has_tenant_id = field
.directives
.as_ref()
.is_some_and(|dirs| dirs.iter().any(|d| d.name == "tenant_id"));
if has_tenant_id {
tenant_fields.entry(typ.name.clone()).or_default().insert(field.name.clone());
}
}
}
Self { tenant_fields }
}
#[must_use]
pub fn has_annotations(&self) -> bool {
!self.tenant_fields.is_empty()
}
#[must_use]
pub fn fields_for_type(&self, type_name: &str) -> Option<&HashSet<String>> {
self.tenant_fields.get(type_name)
}
}
pub fn validate_tenant_annotations(
schema: &mut IntermediateSchema,
tenant_claim: &str,
) -> Result<()> {
let index = AnnotatedTypeIndex::build(&schema.types);
if !index.has_annotations() {
warn!(
"tenancy mode is 'row' but no types have @tenant_id annotations. \
Add @tenant_id to fields that carry the tenant identifier."
);
return Ok(());
}
for query in &mut schema.queries {
if let Some(fields) = index.fields_for_type(&query.return_type) {
for field_name in fields {
let inject_source = format!("jwt:{tenant_claim}");
if query.inject.is_empty() {
query.inject.insert(field_name.clone(), inject_source);
} else if !query.inject.contains_key(field_name) {
bail!(
"Query '{}' references @tenant_id-annotated type '{}' but \
lacks inject_params for '{}'. Add `inject.{} = \"{}\"` or \
remove the explicit inject to use auto-injection.",
query.name,
query.return_type,
field_name,
field_name,
inject_source,
);
}
}
}
}
for mutation in &mut schema.mutations {
if let Some(fields) = index.fields_for_type(&mutation.return_type) {
for field_name in fields {
let inject_source = format!("jwt:{tenant_claim}");
if mutation.inject.is_empty() {
mutation.inject.insert(field_name.clone(), inject_source);
} else if !mutation.inject.contains_key(field_name) {
bail!(
"Mutation '{}' references @tenant_id-annotated type '{}' but \
lacks inject_params for '{}'. Add `inject.{} = \"{}\"` or \
remove the explicit inject to use auto-injection.",
mutation.name,
mutation.return_type,
field_name,
field_name,
inject_source,
);
}
}
}
}
Ok(())
}