#![allow(clippy::unwrap_used, clippy::panic, clippy::print_stderr)] use std::collections::HashMap;
use fraiseql_core::{
db::{WhereClause, WhereOperator},
federation::{
database_resolver::DatabaseEntityResolver, selection_parser::FieldSelection,
types::EntityRepresentation,
},
};
use serde_json::{Value, json};
use super::common;
fn map(pairs: &[(&str, Value)]) -> HashMap<String, Value> {
pairs.iter().map(|(k, v)| ((*k).to_string(), v.clone())).collect()
}
fn rep(typename: &str, keys: &[(&str, Value)]) -> EntityRepresentation {
let key_fields = map(keys);
EntityRepresentation {
typename: typename.to_string(),
all_fields: key_fields.clone(),
key_fields,
}
}
#[tokio::test]
async fn test_resolve_entity_from_postgres_table() {
let rows = vec![map(&[
("id", json!("user123")),
("name", json!("John Doe")),
("email", json!("john@example.com")),
])];
let Some((_pg, adapter)) =
common::pg_entity_fixture("user", &["id text", "name text", "email text"], &rows).await
else {
eprintln!("SKIP test_resolve_entity_from_postgres_table: no postgres (set DATABASE_URL)");
return;
};
let metadata = common::metadata_single_key("User", "id");
let representation = rep("User", &[("id", json!("user123"))]);
let selection = FieldSelection::new(vec![
"__typename".to_string(),
"id".to_string(),
"name".to_string(),
"email".to_string(),
]);
let resolver = DatabaseEntityResolver::new(adapter, metadata);
let entities = resolver
.resolve_entities_from_db("User", &[representation], &selection)
.await
.unwrap_or_else(|e| panic!("resolve_entities_from_db (postgres table) failed: {e}"));
assert_eq!(entities.len(), 1);
let entity = entities[0].as_ref().expect("entity must resolve");
assert_eq!(entity["__typename"], "User");
assert_eq!(entity["id"], "user123");
assert_eq!(entity["name"], "John Doe");
}
#[tokio::test]
async fn test_resolve_entities_batch_from_postgres() {
let rows = vec![
map(&[("id", json!("user1")), ("name", json!("Alice"))]),
map(&[("id", json!("user2")), ("name", json!("Bob"))]),
];
let Some((_pg, adapter)) =
common::pg_entity_fixture("user", &["id text", "name text"], &rows).await
else {
eprintln!("SKIP test_resolve_entities_batch_from_postgres: no postgres (set DATABASE_URL)");
return;
};
let metadata = common::metadata_single_key("User", "id");
let reps = vec![
rep("User", &[("id", json!("user1"))]),
rep("User", &[("id", json!("user2"))]),
];
let selection = FieldSelection::new(vec![
"__typename".to_string(),
"id".to_string(),
"name".to_string(),
]);
let resolver = DatabaseEntityResolver::new(adapter, metadata);
let entities = resolver
.resolve_entities_from_db("User", &reps, &selection)
.await
.unwrap_or_else(|e| panic!("resolve_entities_from_db (batch) failed: {e}"));
assert_eq!(entities.len(), 2);
assert_eq!(entities[0].as_ref().expect("entity 0")["name"], "Alice");
assert_eq!(entities[1].as_ref().expect("entity 1")["name"], "Bob");
}
#[tokio::test]
async fn test_resolve_entity_composite_key_from_postgres() {
let rows = vec![map(&[
("tenant_id", json!("t1")),
("user_id", json!("u1")),
("name", json!("John")),
])];
let Some((_pg, adapter)) =
common::pg_entity_fixture("user", &["tenant_id text", "user_id text", "name text"], &rows)
.await
else {
eprintln!("SKIP test_resolve_entity_composite_key_from_postgres: no postgres");
return;
};
let metadata = common::metadata_composite_key("User", &["tenant_id", "user_id"]);
let representation = rep("User", &[("tenant_id", json!("t1")), ("user_id", json!("u1"))]);
let selection = FieldSelection::new(vec![
"__typename".to_string(),
"tenant_id".to_string(),
"user_id".to_string(),
"name".to_string(),
]);
let resolver = DatabaseEntityResolver::new(adapter, metadata);
let entities = resolver
.resolve_entities_from_db("User", &[representation], &selection)
.await
.unwrap_or_else(|e| panic!("resolve_entities_from_db (composite key) failed: {e}"));
assert_eq!(entities.len(), 1);
assert_eq!(entities[0].as_ref().expect("entity must resolve")["name"], "John");
}
#[tokio::test]
async fn test_resolve_entity_with_null_values_from_postgres() {
let rows = vec![map(&[
("id", json!("user123")),
("name", json!("John")),
("email", Value::Null),
])];
let Some((_pg, adapter)) =
common::pg_entity_fixture("user", &["id text", "name text", "email text"], &rows).await
else {
eprintln!("SKIP test_resolve_entity_with_null_values_from_postgres: no postgres");
return;
};
let metadata = common::metadata_single_key("User", "id");
let representation = rep("User", &[("id", json!("user123"))]);
let selection = FieldSelection::new(vec![
"__typename".to_string(),
"id".to_string(),
"name".to_string(),
"email".to_string(),
]);
let resolver = DatabaseEntityResolver::new(adapter, metadata);
let entities = resolver
.resolve_entities_from_db("User", &[representation], &selection)
.await
.unwrap_or_else(|e| panic!("resolve_entities_from_db (null values) failed: {e}"));
assert_eq!(entities.len(), 1);
let entity = entities[0].as_ref().expect("entity must resolve");
assert_eq!(entity["name"], "John");
assert_eq!(entity["email"], Value::Null);
}
#[tokio::test]
async fn test_resolve_entity_large_result_set_from_postgres() {
let mut rows = Vec::new();
let mut reps = Vec::new();
for i in 0..100 {
let id = format!("user{i}");
rows.push(map(&[("id", json!(id)), ("name", json!(format!("User {i}")))]));
reps.push(rep("User", &[("id", json!(id))]));
}
let Some((_pg, adapter)) =
common::pg_entity_fixture("user", &["id text", "name text"], &rows).await
else {
eprintln!("SKIP test_resolve_entity_large_result_set_from_postgres: no postgres");
return;
};
let metadata = common::metadata_single_key("User", "id");
let selection = FieldSelection::new(vec![
"__typename".to_string(),
"id".to_string(),
"name".to_string(),
]);
let resolver = DatabaseEntityResolver::new(adapter, metadata);
let entities = resolver
.resolve_entities_from_db("User", &reps, &selection)
.await
.unwrap_or_else(|e| panic!("resolve_entities_from_db (large result set) failed: {e}"));
assert_eq!(entities.len(), 100);
for entity in &entities {
assert!(entity.is_some());
}
}
#[tokio::test]
async fn test_resolve_entities_enforced_filters_cross_tenant() {
let rows = vec![
map(&[
("id", json!("u-a")),
("name", json!("Alice")),
("tenant_id", json!("tenant-a")),
]),
map(&[
("id", json!("u-b")),
("name", json!("Bob")),
("tenant_id", json!("tenant-b")),
]),
];
let Some((_pg, adapter)) =
common::pg_entity_fixture("user", &["id text", "name text", "tenant_id text"], &rows).await
else {
eprintln!("SKIP test_resolve_entities_enforced_filters_cross_tenant: no postgres");
return;
};
let metadata = common::metadata_single_key("User", "id");
let selection = FieldSelection::new(vec![
"__typename".to_string(),
"id".to_string(),
"name".to_string(),
]);
let reps = vec![
rep("User", &[("id", json!("u-a"))]),
rep("User", &[("id", json!("u-b"))]),
];
let tenant_filter = WhereClause::NativeField {
column: "tenant_id".to_string(),
pg_cast: String::new(),
operator: WhereOperator::Eq,
value: json!("tenant-a"),
};
let resolver = DatabaseEntityResolver::new(adapter, metadata);
let entities = resolver
.resolve_entities_from_db_enforced(
"User",
&reps,
&selection,
None,
Some(&tenant_filter),
&[],
)
.await
.unwrap_or_else(|e| panic!("resolve_entities_from_db_enforced failed: {e}"));
assert_eq!(entities.len(), 2, "order preserved: one slot per representation");
let alice = entities[0].as_ref().expect("tenant A's own user must resolve");
assert_eq!(alice["id"], "u-a");
assert_eq!(alice["name"], "Alice");
assert!(
entities[1].is_none(),
"tenant B's user must be filtered out by the per-row tenant predicate, got: {:?}",
entities[1]
);
}