#![allow(clippy::unwrap_used)] #![allow(missing_docs)]
mod common;
use std::sync::Arc;
use fraiseql_core::{
db::{DatabaseAdapter, postgres::PostgresAdapter},
runtime::Executor,
schema::CompiledSchema,
};
use serde_json::json;
const SCHEMA: &str = "issue_489";
async fn provision(adapter: &PostgresAdapter) {
adapter
.execute_raw_query(&format!("DROP SCHEMA IF EXISTS {SCHEMA} CASCADE"))
.await
.unwrap();
adapter.execute_raw_query(&format!("CREATE SCHEMA {SCHEMA}")).await.unwrap();
adapter
.execute_raw_query(&format!(
"CREATE TABLE {SCHEMA}.tb_order (\
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), code TEXT)"
))
.await
.unwrap();
adapter
.execute_raw_query(&format!(
"CREATE TABLE {SCHEMA}.tb_line (\
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), \
fk_order UUID REFERENCES {SCHEMA}.tb_order(id), \
unit_price NUMERIC, sku_code TEXT, position INT)"
))
.await
.unwrap();
adapter
.execute_raw_query(&format!(
"CREATE VIEW {SCHEMA}.v_order AS SELECT id, jsonb_build_object(\
'id', id, 'code', code, \
'line_items', COALESCE((SELECT jsonb_agg(jsonb_build_object(\
'id', l.id::text, 'unit_price', l.unit_price, 'sku_code', l.sku_code) \
ORDER BY l.position) FROM {SCHEMA}.tb_line l WHERE l.fk_order = {SCHEMA}.tb_order.id), \
'[]'::jsonb)\
) AS data FROM {SCHEMA}.tb_order"
))
.await
.unwrap();
}
async fn seed(adapter: &PostgresAdapter) -> String {
let rows = adapter
.execute_raw_query(&format!(
"INSERT INTO {SCHEMA}.tb_order (code) VALUES ('ORD-1') RETURNING id::text AS id"
))
.await
.unwrap();
let id = rows
.into_iter()
.next()
.and_then(|r| r.get("id").and_then(|v| v.as_str().map(ToString::to_string)))
.expect("order id");
adapter
.execute_raw_query(&format!(
"INSERT INTO {SCHEMA}.tb_line (fk_order, unit_price, sku_code, position) VALUES \
('{id}'::uuid, 9.99, 'ABC', 1), ('{id}'::uuid, 4.50, 'XYZ', 2)"
))
.await
.unwrap();
id
}
fn schema() -> CompiledSchema {
serde_json::from_value(json!({
"naming_convention": "camelCase",
"types": [
{
"name": "Order",
"sql_source": format!("{SCHEMA}.v_order"),
"fields": [
{ "name": "id", "field_type": "ID" },
{ "name": "code", "field_type": "String" },
{ "name": "lineItems", "field_type": { "List": { "Object": "LineItem" } } }
]
},
{
"name": "LineItem",
"sql_source": format!("{SCHEMA}.v_line"),
"fields": [
{ "name": "id", "field_type": "ID" },
{ "name": "unitPrice", "field_type": "Float" },
{ "name": "skuCode", "field_type": "String" }
]
}
],
"queries": [
{
"name": "order",
"return_type": "Order",
"returns_list": false,
"nullable": true,
"sql_source": format!("{SCHEMA}.v_order"),
"arguments": [ { "name": "id", "arg_type": "ID", "nullable": false } ]
}
]
}))
.expect("schema")
}
#[tokio::test]
async fn nested_list_query_output_is_recased_and_projected() {
let container = common::testcontainer::get_test_container().await;
let adapter = Arc::new(PostgresAdapter::new(&container.connection_string()).await.unwrap());
provision(&adapter).await;
let id = seed(&adapter).await;
let executor = Executor::new(schema(), Arc::clone(&adapter));
let doc = format!("{{ order(id: \"{id}\") {{ id lineItems {{ unitPrice skuCode }} }} }}");
let res = executor.execute(&doc, None).await.unwrap();
let order = res.get("data").and_then(|d| d.get("order")).cloned().unwrap();
assert!(
order["lineItems"].is_array(),
"lineItems must be an array; got:\n{}",
serde_json::to_string_pretty(&order).unwrap()
);
let items = order["lineItems"].as_array().unwrap();
assert_eq!(items.len(), 2, "both line items present");
assert_eq!(items[0]["skuCode"], json!("ABC"), "element recased to camelCase (skuCode)");
assert_eq!(items[1]["skuCode"], json!("XYZ"));
assert!(
items[0].get("unitPrice").is_some(),
"element field `unitPrice` recased + present"
);
for it in items {
assert!(it.get("sku_code").is_none(), "snake_case `sku_code` must not leak");
assert!(it.get("unit_price").is_none(), "snake_case `unit_price` must not leak");
assert!(it.get("id").is_none(), "unselected element key `id` must not leak");
}
}