use std::collections::HashMap;
use sea_orm::DatabaseConnection;
use serde::Serialize;
use serde_json::Value;
use ferro_projections::{schema_contract, SchemaContract, ServiceDef};
use crate::http::Request;
use crate::inertia::context::Inertia;
use crate::permitted_actions::permitted_actions;
use crate::projection_read::{dispatch, DispatchResult};
use crate::Response;
#[derive(Debug, Clone)]
pub struct ProjectionQuery {
pub filters: Value,
pub limit: u64,
pub offset: u64,
}
impl Default for ProjectionQuery {
fn default() -> Self {
Self {
filters: Value::Object(Default::default()),
limit: 25,
offset: 0,
}
}
}
impl ProjectionQuery {
pub fn filters(mut self, f: Value) -> Self {
self.filters = f;
self
}
pub fn limit(mut self, n: u64) -> Self {
self.limit = n;
self
}
pub fn offset(mut self, n: u64) -> Self {
self.offset = n;
self
}
}
#[derive(Debug, Serialize)]
struct ProjectionProps {
schema: SchemaContract,
data: Vec<Value>,
permitted_actions: Vec<String>,
total: u64,
limit: u64,
offset: u64,
}
impl Inertia {
pub async fn from_projection(
req: &Request,
component: &str,
service: &ServiceDef,
query: ProjectionQuery,
db: &DatabaseConnection,
tenant_id: Option<i64>,
evaluated_guards: &HashMap<String, bool>,
) -> Response {
let schema = schema_contract(service);
let actions = permitted_actions(service, evaluated_guards);
let result: DispatchResult = match dispatch(
service,
query.filters,
query.limit,
query.offset,
db,
tenant_id,
)
.await
{
Ok(r) => r,
Err(e) => {
return Inertia::render(
req,
component,
serde_json::json!({ "error": e.to_string() }),
);
}
};
let props = ProjectionProps {
schema,
data: result.rows,
permitted_actions: actions,
total: result.total,
limit: result.limit,
offset: result.offset,
};
Inertia::render(req, component, props)
}
}
#[cfg(test)]
mod tests {
use super::*;
use ferro_projections::{ActionDef, DataType, FieldMeaning, GuardDef, ServiceDef};
use serde_json::json;
#[test]
fn projection_query_default_values() {
let q = ProjectionQuery::default();
assert_eq!(q.limit, 25);
assert_eq!(q.offset, 0);
assert_eq!(q.filters, json!({}));
}
#[test]
fn projection_query_builder_methods() {
let q = ProjectionQuery::default()
.limit(50)
.offset(10)
.filters(json!({"status": "active"}));
assert_eq!(q.limit, 50);
assert_eq!(q.offset, 10);
assert_eq!(q.filters, json!({"status": "active"}));
}
#[test]
fn projection_props_serializes_six_keys() {
use ferro_projections::schema_contract;
let service = ServiceDef::new("order")
.field("id", DataType::Integer, FieldMeaning::Identifier)
.guard(GuardDef::new("is_manager"))
.action(ActionDef::new("approve").precondition("is_manager"))
.action(ActionDef::new("submit"));
let schema = schema_contract(&service);
let props = ProjectionProps {
schema,
data: vec![json!({"id": 1})],
permitted_actions: vec!["submit".to_string()],
total: 1,
limit: 25,
offset: 0,
};
let value = serde_json::to_value(&props).expect("serialize ok");
let obj = value.as_object().expect("is object");
for key in &[
"schema",
"data",
"permitted_actions",
"total",
"limit",
"offset",
] {
assert!(obj.contains_key(*key), "missing key: {key}");
}
assert_eq!(obj.len(), 6, "exactly six keys");
}
#[test]
fn permitted_actions_excludes_denied_guard() {
let service = ServiceDef::new("order")
.guard(GuardDef::new("is_manager"))
.action(ActionDef::new("approve").precondition("is_manager"))
.action(ActionDef::new("submit"));
let guards: HashMap<String, bool> =
[("is_manager".to_string(), false)].into_iter().collect();
let allowed = permitted_actions(&service, &guards);
assert!(!allowed.contains(&"approve".to_string()));
assert!(allowed.contains(&"submit".to_string()));
}
}