From 6df2fc8ceea24e4c61973109e0a5e9906fda0274 Mon Sep 17 00:00:00 2001 From: MicaiahReid Date: Tue, 28 Oct 2025 15:05:37 -0400 Subject: [PATCH 1/2] fix: allow None, object, and array types in graphql --- Cargo.lock | 26 ++-- Cargo.toml | 6 +- crates/gql/src/types/collections.rs | 201 ++++++++++++++++++++------- crates/gql/src/types/mod.rs | 207 ++++++++++++++-------------- crates/gql/src/types/sql.rs | 133 +++++++++++++++--- 5 files changed, 383 insertions(+), 190 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a48009d9..a02e32ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9410,7 +9410,7 @@ dependencies = [ "solana-fee-calculator 2.2.1", "solana-hash 2.3.0", "solana-pubkey 2.4.0", - "solana-sha256-hasher 2.2.1", + "solana-sha256-hasher 2.3.0", ] [[package]] @@ -9633,7 +9633,7 @@ dependencies = [ "solana-secp256k1-recover 2.2.1", "solana-serde-varint 2.2.2", "solana-serialize-utils 2.2.1", - "solana-sha256-hasher 2.2.1", + "solana-sha256-hasher 2.3.0", "solana-short-vec 2.2.1", "solana-slot-hashes 2.2.1", "solana-slot-history 2.2.1", @@ -9808,7 +9808,7 @@ dependencies = [ "solana-decode-error", "solana-define-syscall 2.3.0", "solana-sanitize 2.2.1", - "solana-sha256-hasher 2.2.1", + "solana-sha256-hasher 2.3.0", "wasm-bindgen", ] @@ -10535,9 +10535,9 @@ dependencies = [ [[package]] name = "solana-sha256-hasher" -version = "2.2.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0037386961c0d633421f53560ad7c80675c0447cba4d1bb66d60974dd486c7ea" +checksum = "5aa3feb32c28765f6aa1ce8f3feac30936f16c5c3f7eb73d63a5b8f6f8ecdc44" dependencies = [ "sha2 0.10.9", "solana-define-syscall 2.3.0", @@ -13018,9 +13018,9 @@ dependencies = [ [[package]] name = "txtx-addon-kit" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac9b4152a798352d8704b9fba3fa8e6a6b48d344bd189918aeff04ba574efe7" +checksum = "a51ded8b26a18920ac57f268e5e8fd769de45f97cb8eed04e59da506ab3e90f4" dependencies = [ "crossbeam-channel", "dirs 5.0.1", @@ -13051,9 +13051,9 @@ dependencies = [ [[package]] name = "txtx-addon-network-svm" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "add854320b3aa54e1cd3320bd607ba279796dda9a45990917046d7f56ac5cf1b" +checksum = "f2f683a2c9f5cd9e4ff30cacf8fdf52a83bed90232c17ece10f11cf200956134" dependencies = [ "async-recursion", "bincode", @@ -13095,9 +13095,9 @@ dependencies = [ [[package]] name = "txtx-addon-network-svm-types" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8c8776e6a1429951aa2b538b81b87aaaf6716c1b137499039b7d2d7b860f3e" +checksum = "991c5b1f0484b8fdd21b0c1db68638c5ee3fc0c4f6318a5dcbed7ca66a0cd659" dependencies = [ "anchor-lang-idl", "borsh 1.5.7", @@ -13152,9 +13152,9 @@ dependencies = [ [[package]] name = "txtx-core" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aab3151d0d31587bf224861cf4142bb15442a234d9b104f0ff6f2db49baa3d6" +checksum = "4bfd49818a8a948af2031d7b672bd9bb0df73d58f1c115f7153131eef97d559e" dependencies = [ "base64 0.22.1", "better-debug", diff --git a/Cargo.toml b/Cargo.toml index 55352c94..cced3167 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -159,9 +159,9 @@ surfpool-studio-ui = { path = "crates/studio", default-features = false } surfpool-subgraph = { path = "crates/subgraph", default-features = false } surfpool-types = { path = "crates/types", default-features = false } -txtx-addon-kit = "0.4.10" -txtx-addon-network-svm = { version = "0.3.14" } -txtx-addon-network-svm-types = { version = "0.3.13" } +txtx-addon-kit = "0.4.11" +txtx-addon-network-svm = { version = "0.3.15" } +txtx-addon-network-svm-types = { version = "0.3.14" } txtx-cloud = { version = "0.1.13", features = [ "clap", "toml", diff --git a/crates/gql/src/types/collections.rs b/crates/gql/src/types/collections.rs index 83381032..67fdd898 100644 --- a/crates/gql/src/types/collections.rs +++ b/crates/gql/src/types/collections.rs @@ -78,58 +78,13 @@ impl GraphQLType for CollectionMetadata { for field in metadata.fields.iter() { let registration = { let description = field.data.description.as_deref().unwrap_or(""); - match &field.data.expected_type { - Type::String => registry - .field::<&String>(&field.data.display_name, &()) - .description(description), - Type::Integer => registry - .field::<&BigInt>(&field.data.display_name, &()) - .description(description), - Type::Bool => registry - .field::<&String>(&field.data.display_name, &()) - .description(description), - Type::Float => registry - .field::<&String>(&field.data.display_name, &()) - .description(description), - Type::Null => registry - .field::<&String>(&field.data.display_name, &()) - .description(description), - Type::Array(_array) => registry - .field::<&String>(&field.data.display_name, &()) - .description(description), - Type::Buffer => registry - .field::<&String>(&field.data.display_name, &()) - .description(description), - Type::Addon(addon_id) => match addon_id.as_str() { - SVM_PUBKEY => registry - .field::<&PublicKey>(&field.data.display_name, &()) - .description(description), - SVM_SIGNATURE => registry - .field::<&Signature>(&field.data.display_name, &()) - .description(description), - SVM_U8 | SVM_U16 | SVM_I8 | SVM_I16 | SVM_I32 => registry - .field::<&i32>(&field.data.display_name, &()) - .description(description), - SVM_U32 | SVM_U64 | SVM_I64 | SVM_I128 => registry - .field::<&BigInt>(&field.data.display_name, &()) - .description(description), - SVM_U128 | SVM_U256 | SVM_I256 => registry - .field::<&String>(&field.data.display_name, &()) - .description(description), - SVM_F32 | SVM_F64 => registry - .field::<&f64>(&field.data.display_name, &()) - .description(description), - _ => registry - .field::<&String>(&field.data.display_name, &()) - .description(description), - }, - Type::Object(_object) => registry - .field::<&String>(&field.data.display_name, &()) - .description(description), - Type::Map(_map) => registry - .field::<&String>(&field.data.display_name, &()) - .description(description), - } + register_txtx_type( + &field.data.expected_type, + &field.data.display_name, + description, + registry, + false, + ) }; fields.push(registration); } @@ -142,6 +97,148 @@ impl GraphQLType for CollectionMetadata { } } +fn register_txtx_type<'r>( + txtx_type: &Type, + display_name: &str, + description: &str, + registry: &mut Registry<'r, DefaultScalarValue>, + is_opt: bool, +) -> Field<'r, DefaultScalarValue> { + match &txtx_type { + Type::String => match is_opt { + true => registry + .field::<&Option>(display_name, &()) + .description(description), + false => registry + .field::<&String>(display_name, &()) + .description(description), + }, + Type::Integer => match is_opt { + true => registry + .field::<&Option>(display_name, &()) + .description(description), + false => registry + .field::<&BigInt>(display_name, &()) + .description(description), + }, + Type::Bool => match is_opt { + true => registry + .field::<&Option>(display_name, &()) + .description(description), + false => registry + .field::<&bool>(display_name, &()) + .description(description), + }, + Type::Float => match is_opt { + true => registry + .field::<&Option>(display_name, &()) + .description(description), + false => registry + .field::<&f64>(display_name, &()) + .description(description), + }, + Type::Null(inner) => { + if let Some(inner) = inner { + register_txtx_type(inner, display_name, description, registry, true) + } else { + registry + .field::<&Option>(display_name, &()) + .description(description) + } + } + Type::Array(_array) => match is_opt { + true => registry + .field::<&Option>(display_name, &()) + .description(description), + false => registry + .field::<&String>(display_name, &()) + .description(description), + }, + Type::Buffer => match is_opt { + true => registry + .field::<&Option>(display_name, &()) + .description(description), + false => registry + .field::<&String>(display_name, &()) + .description(description), + }, + Type::Addon(addon_id) => match addon_id.as_str() { + SVM_PUBKEY => match is_opt { + true => registry + .field::<&Option>(display_name, &()) + .description(description), + false => registry + .field::<&PublicKey>(display_name, &()) + .description(description), + }, + SVM_SIGNATURE => match is_opt { + true => registry + .field::<&Option>(display_name, &()) + .description(description), + false => registry + .field::<&Signature>(display_name, &()) + .description(description), + }, + SVM_U8 | SVM_U16 | SVM_I8 | SVM_I16 | SVM_I32 => match is_opt { + true => registry + .field::<&Option>(display_name, &()) + .description(description), + false => registry + .field::<&i32>(display_name, &()) + .description(description), + }, + SVM_U32 | SVM_U64 | SVM_I64 | SVM_I128 => match is_opt { + true => registry + .field::<&Option>(display_name, &()) + .description(description), + false => registry + .field::<&BigInt>(display_name, &()) + .description(description), + }, + SVM_U128 | SVM_U256 | SVM_I256 => match is_opt { + true => registry + .field::<&Option>(display_name, &()) + .description(description), + false => registry + .field::<&String>(display_name, &()) + .description(description), + }, + SVM_F32 | SVM_F64 => match is_opt { + true => registry + .field::<&Option>(display_name, &()) + .description(description), + false => registry + .field::<&f64>(display_name, &()) + .description(description), + }, + _ => match is_opt { + true => registry + .field::<&Option>(display_name, &()) + .description(description), + false => registry + .field::<&String>(display_name, &()) + .description(description), + }, + }, + Type::Object(_object) => match is_opt { + true => registry + .field::<&Option>(display_name, &()) + .description(description), + false => registry + .field::<&String>(display_name, &()) + .description(description), + }, + Type::Map(_map) => match is_opt { + true => registry + .field::<&Option>(display_name, &()) + .description(description), + false => registry + .field::<&String>(display_name, &()) + .description(description), + }, + } +} + impl GraphQLValue for CollectionMetadata { type Context = DataloaderContext; type TypeInfo = CollectionMetadata; diff --git a/crates/gql/src/types/mod.rs b/crates/gql/src/types/mod.rs index c61d24b6..f4844be6 100644 --- a/crates/gql/src/types/mod.rs +++ b/crates/gql/src/types/mod.rs @@ -122,114 +122,117 @@ fn convert_txtx_values_to_juniper_values( let mut dst = HashMap::new(); for (k, old) in src.into_iter() { - let new = match &old { - TxtxValue::Bool(b) => Value::scalar(*b), - TxtxValue::String(s) => Value::scalar(s.to_owned()), - TxtxValue::Integer(n) => BigInt(*n).to_output(), - TxtxValue::Float(f) => Value::scalar(*f), - TxtxValue::Buffer(_bytes) => unimplemented!(), - TxtxValue::Addon(AddonData { bytes, id }) => match id.as_str() { - SVM_SIGNATURE => { - let bytes: [u8; 64] = bytes[0..64] - .try_into() - .expect("could not convert value to signature"); - let signature = solana_signature::Signature::from(bytes); - Signature(signature).to_output() - } - SVM_PUBKEY => { - let bytes: [u8; 32] = bytes[0..32] - .try_into() - .expect("could not convert value to pubkey"); - let pubkey = solana_pubkey::Pubkey::new_from_array(bytes); - PublicKey(pubkey).to_output() - } - SVM_U8 => { - let num = - SvmValue::to_number::(&old).expect("could not convert value to u8"); - Value::scalar(num as i32) - } - SVM_U16 => { - let num = - SvmValue::to_number::(&old).expect("could not convert value to u16"); - Value::scalar(num as i32) - } - SVM_U32 => { - let num = - SvmValue::to_number::(&old).expect("could not convert value to u32"); - BigInt(num as i128).to_output() - } - SVM_U64 => { - let num = - SvmValue::to_number::(&old).expect("could not convert value to u64"); - BigInt(num as i128).to_output() - } - SVM_U128 => { - let num = - SvmValue::to_number::(&old).expect("could not convert value to u128"); - let bytes = num.to_le_bytes(); - Value::scalar(hex::encode(bytes)) - } - SVM_U256 => { - let num = - SvmValue::to_number::(&old).expect("could not convert value to u256"); - let bytes = num.0; - Value::scalar(hex::encode(bytes)) - } - SVM_I8 => { - let num = - SvmValue::to_number::(&old).expect("could not convert value to i8"); - Value::scalar(num as i32) - } - SVM_I16 => { - let num = - SvmValue::to_number::(&old).expect("could not convert value to i16"); - Value::scalar(num as i32) - } - SVM_I32 => { - let num = - SvmValue::to_number::(&old).expect("could not convert value to i32"); - Value::scalar(num) - } - SVM_I64 => { - let num = - SvmValue::to_number::(&old).expect("could not convert value to i64"); - BigInt(num as i128).to_output() - } - SVM_I128 => { - let num = - SvmValue::to_number::(&old).expect("could not convert value to i128"); - BigInt(num).to_output() - } - SVM_I256 => { - let num = - SvmValue::to_number::(&old).expect("could not convert value to i256"); - let bytes = num.0; - Value::scalar(hex::encode(bytes)) - } - SVM_F32 => { - let num = - SvmValue::to_number::(&old).expect("could not convert value to f32"); - Value::scalar(num as f64) - } - SVM_F64 => { - let num = - SvmValue::to_number::(&old).expect("could not convert value to f64"); - Value::scalar(num) - } - _ => { - Value::scalar(String::from_utf8(bytes.to_vec()).expect("addon data not utf-8")) - } - }, - TxtxValue::Null => unimplemented!(), - TxtxValue::Array(_arr) => unimplemented!(), - TxtxValue::Object(_obj) => unimplemented!(), - }; + let new = txtx_value_to_juniper_value(&old); dst.insert(k, new); } dst } +fn txtx_value_to_juniper_value(value: &TxtxValue) -> Value { + match value { + TxtxValue::Bool(b) => Value::scalar(*b), + TxtxValue::String(s) => Value::scalar(s.to_owned()), + TxtxValue::Integer(n) => BigInt(*n).to_output(), + TxtxValue::Float(f) => Value::scalar(*f), + TxtxValue::Addon(AddonData { bytes, id }) => match id.as_str() { + SVM_SIGNATURE => { + let bytes: [u8; 64] = bytes[0..64] + .try_into() + .expect("could not convert value to signature"); + let signature = solana_signature::Signature::from(bytes); + Signature(signature).to_output() + } + SVM_PUBKEY => { + let bytes: [u8; 32] = bytes[0..32] + .try_into() + .expect("could not convert value to pubkey"); + let pubkey = solana_pubkey::Pubkey::new_from_array(bytes); + PublicKey(pubkey).to_output() + } + SVM_U8 => { + let num = SvmValue::to_number::(&value).expect("could not convert value to u8"); + Value::scalar(num as i32) + } + SVM_U16 => { + let num = + SvmValue::to_number::(&value).expect("could not convert value to u16"); + Value::scalar(num as i32) + } + SVM_U32 => { + let num = + SvmValue::to_number::(&value).expect("could not convert value to u32"); + BigInt(num as i128).to_output() + } + SVM_U64 => { + let num = + SvmValue::to_number::(&value).expect("could not convert value to u64"); + BigInt(num as i128).to_output() + } + SVM_U128 => { + let num = + SvmValue::to_number::(&value).expect("could not convert value to u128"); + let bytes = num.to_le_bytes(); + Value::scalar(hex::encode(bytes)) + } + SVM_U256 => { + let num = + SvmValue::to_number::(&value).expect("could not convert value to u256"); + let bytes = num.0; + Value::scalar(hex::encode(bytes)) + } + SVM_I8 => { + let num = SvmValue::to_number::(&value).expect("could not convert value to i8"); + Value::scalar(num as i32) + } + SVM_I16 => { + let num = + SvmValue::to_number::(&value).expect("could not convert value to i16"); + Value::scalar(num as i32) + } + SVM_I32 => { + let num = + SvmValue::to_number::(&value).expect("could not convert value to i32"); + Value::scalar(num) + } + SVM_I64 => { + let num = + SvmValue::to_number::(&value).expect("could not convert value to i64"); + BigInt(num as i128).to_output() + } + SVM_I128 => { + let num = + SvmValue::to_number::(&value).expect("could not convert value to i128"); + BigInt(num).to_output() + } + SVM_I256 => { + let num = + SvmValue::to_number::(&value).expect("could not convert value to i256"); + let bytes = num.0; + Value::scalar(hex::encode(bytes)) + } + SVM_F32 => { + let num = + SvmValue::to_number::(&value).expect("could not convert value to f32"); + Value::scalar(num as f64) + } + SVM_F64 => { + let num = + SvmValue::to_number::(&value).expect("could not convert value to f64"); + Value::scalar(num) + } + _ => Value::scalar(String::from_utf8(bytes.to_vec()).expect("addon data not utf-8")), + }, + TxtxValue::Array(arr) => Value::List(arr.iter().map(txtx_value_to_juniper_value).collect()), + TxtxValue::Object(obj) => Value::Object(juniper::Object::from_iter( + obj.iter() + .map(|(k, v)| (k.clone(), txtx_value_to_juniper_value(v))), + )), + TxtxValue::Null => Value::null(), + TxtxValue::Buffer(bytes) => Value::scalar(hex::encode(bytes)), + } +} + impl CollectionEntryData { pub fn from_entries_bytes( subgraph_uuid: &Uuid, diff --git a/crates/gql/src/types/sql.rs b/crates/gql/src/types/sql.rs index 30996213..b68da975 100644 --- a/crates/gql/src/types/sql.rs +++ b/crates/gql/src/types/sql.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use base64::{Engine, prelude::BASE64_STANDARD}; use diesel::prelude::*; use juniper::{DefaultScalarValue, Executor, FieldError, Value, graphql_value}; +use surfpool_db::diesel::sql_types::Nullable; use surfpool_db::{ diesel::{ self, ExpressionMethods, QueryDsl, RunQueryDsl, @@ -18,7 +19,7 @@ use surfpool_db::{ table, }, }; -use txtx_addon_kit::types::types::Type; +use txtx_addon_kit::{indexmap::IndexMap, types::types::Type}; use txtx_addon_network_svm_types::subgraph::SubgraphRequest; use uuid::Uuid; @@ -78,7 +79,13 @@ pub fn fetch_dynamic_entries_from_postres( pg_conn: &mut diesel::pg::PgConnection, metadata: &CollectionMetadata, executor: Option<&Executor>, -) -> Result<(Vec, Vec>>), FieldError> { +) -> Result< + ( + Vec, + Vec>>>, + ), + FieldError, +> { let mut select = DynamicSelectClause::new(); let dynamic_table = table(metadata.table_name.as_str()); let (filters_specs, fetched_fields) = extract_graphql_features(executor); @@ -91,7 +98,7 @@ pub fn fetch_dynamic_entries_from_postres( for (field, predicate, value) in filters_specs { match value { DefaultScalarValue::String(s) => { - let col = dynamic_table.column::(field); + let col = dynamic_table.column::, _>(field); query = match predicate { "equals" => query.filter(col.eq(s)), "not" => query.filter(col.ne(s)), @@ -108,7 +115,7 @@ pub fn fetch_dynamic_entries_from_postres( }; } DefaultScalarValue::Int(i) => { - let col = dynamic_table.column::(field); + let col = dynamic_table.column::, _>(field); query = match predicate { "equals" => query.filter(col.eq(*i)), "not" => query.filter(col.ne(*i)), @@ -125,7 +132,7 @@ pub fn fetch_dynamic_entries_from_postres( }; } DefaultScalarValue::Boolean(b) => { - let col = dynamic_table.column::(field); + let col = dynamic_table.column::, _>(field); query = match predicate { "equals" => query.filter(col.eq(*b)), "not" => query.filter(col.ne(*b)), @@ -147,7 +154,7 @@ pub fn fetch_dynamic_entries_from_postres( } let fetched_data = query - .load::>>(&mut *pg_conn) + .load::>>>(&mut *pg_conn) .map_err(|err| { FieldError::new( format!("Internal error: unable to fetch data"), @@ -164,7 +171,13 @@ pub fn fetch_dynamic_entries_from_sqlite( sqlite_conn: &mut diesel::sqlite::SqliteConnection, metadata: &CollectionMetadata, executor: Option<&Executor>, -) -> Result<(Vec, Vec>>), FieldError> { +) -> Result< + ( + Vec, + Vec>>>, + ), + FieldError, +> { // Isolate filters let mut select = DynamicSelectClause::new(); @@ -180,7 +193,7 @@ pub fn fetch_dynamic_entries_from_sqlite( for (field, predicate, value) in filters_specs { match value { DefaultScalarValue::String(s) => { - let col = dynamic_table.column::(field); + let col = dynamic_table.column::, _>(field); query = match predicate { "equals" => query.filter(col.eq(s)), "not" => query.filter(col.ne(s)), @@ -197,7 +210,7 @@ pub fn fetch_dynamic_entries_from_sqlite( }; } DefaultScalarValue::Int(i) => { - let col = dynamic_table.column::(field); + let col = dynamic_table.column::, _>(field); query = match predicate { "equals" => query.filter(col.eq(*i)), "not" => query.filter(col.ne(*i)), @@ -214,7 +227,7 @@ pub fn fetch_dynamic_entries_from_sqlite( }; } DefaultScalarValue::Boolean(b) => { - let col = dynamic_table.column::(field); + let col = dynamic_table.column::, _>(field); query = match predicate { "equals" => query.filter(col.eq(*b)), "not" => query.filter(col.ne(*b)), @@ -236,7 +249,7 @@ pub fn fetch_dynamic_entries_from_sqlite( } let fetched_data = query - .load::>>(&mut *sqlite_conn) + .load::>>>(&mut *sqlite_conn) .map_err(|err| { FieldError::new( "Internal error: unable to fetch data".to_string(), @@ -267,11 +280,53 @@ impl Dataloader for Pool> { } }?; - let mut results = Vec::new(); + let mut results: Vec = Vec::new(); for row in fetched_data { - let mut values = HashMap::new(); + let mut values: HashMap = HashMap::new(); for (i, field) in fetch_fields.iter().enumerate() { - values.insert(field.clone(), row[i].value.0.clone()); // FIXME + let value = row[i].value.as_ref(); + + let value = match value { + Some(dynamic_value) => match &dynamic_value.0 { + Value::Null => Value::Null, + Value::Scalar(s) => match s { + DefaultScalarValue::String(s) => { + fn string_to_value(s: &String) -> Value { + // Try to parse as IndexMap + let obj: Option> = + serde_json::from_str(s).ok(); + if let Some(o) = obj { + return Value::object(juniper::Object::from_iter( + o.into_iter().map(|(k, v)| (k, string_to_value(&v))), + )); + } + + let list: Option> = serde_json::from_str(s).ok(); + if let Some(l) = list { + return Value::list( + l.into_iter().map(|v| string_to_value(&v)).collect(), + ); + } + + let v: DefaultScalarValue = serde_json::from_str(s) + .unwrap_or_else(|_| DefaultScalarValue::String(s.clone())); + Value::Scalar(v) + } + string_to_value(s) + } + rest => Value::scalar(rest.clone()), + }, + Value::List(_) => { + unreachable!("Data fetched from db should not be list") + } + Value::Object(_) => { + unreachable!("Data fetched from db should not be object") + } + }, + None => Value::Null, + }; + + values.insert(field.clone(), value); // FIXME } results.push(CollectionEntry(CollectionEntryData { id: Uuid::new_v4(), @@ -367,13 +422,51 @@ impl Dataloader for Pool> { for field in &metadata.fields { let col = field.data.display_name.as_str(); if let Some(val) = entry.values.get(col) { - let val_str = match val { - juniper::Value::Scalar(DefaultScalarValue::String(value)) => { - format!("'{}'", value) + fn value_to_string(val: &Value, in_obj: bool) -> Result { + match val { + Value::Null => Ok("NULL".to_string()), + Value::Scalar(s) => match s { + DefaultScalarValue::String(s) => Ok(if in_obj { + s.to_string() + } else { + format!("'{}'", s) + }), + rest => Ok(rest.to_string()), + }, + Value::Object(obj) => { + let map = obj + .iter() + .map(|(k, v)| { + value_to_string(v, true).map(|vs| (k.clone(), vs)) + }) + .collect::, _>>()?; + let json_str = serde_json::to_string(&map).map_err(|e| { + format!("Failed to serialize object to JSON: {e}") + })?; + Ok(if in_obj { + json_str + } else { + format!("'{}'", json_str) + }) + } + Value::List(list) => { + let list = list + .iter() + .map(|v| value_to_string(v, true)) + .collect::, _>>()?; + let json_str = serde_json::to_string(&list).map_err(|e| { + format!("Failed to serialize list to JSON: {e}") + })?; + Ok(if in_obj { + json_str + } else { + format!("'{}'", json_str) + }) + } } - juniper::Value::Scalar(value) => value.to_string(), - _ => unimplemented!(), - }; + } + + let val_str = format!("{}", value_to_string(val, false)?); values.push(val_str); columns.push(format!("\"{}\"", col)); } From 41763e8dfe82a5ebab0a55f263d8b0d504080821 Mon Sep 17 00:00:00 2001 From: MicaiahReid Date: Tue, 28 Oct 2025 20:19:19 -0400 Subject: [PATCH 2/2] fms --- crates/gql/src/types/sql.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/gql/src/types/sql.rs b/crates/gql/src/types/sql.rs index b68da975..50b8dc19 100644 --- a/crates/gql/src/types/sql.rs +++ b/crates/gql/src/types/sql.rs @@ -3,7 +3,6 @@ use std::collections::HashMap; use base64::{Engine, prelude::BASE64_STANDARD}; use diesel::prelude::*; use juniper::{DefaultScalarValue, Executor, FieldError, Value, graphql_value}; -use surfpool_db::diesel::sql_types::Nullable; use surfpool_db::{ diesel::{ self, ExpressionMethods, QueryDsl, RunQueryDsl, @@ -11,7 +10,7 @@ use surfpool_db::{ r2d2::{ConnectionManager, Pool}, result::DatabaseErrorKind, sql_query, - sql_types::{Bool, Integer, Text, Untyped}, + sql_types::{Bool, Integer, Nullable, Text, Untyped}, }, diesel_dynamic_schema::{ DynamicSelectClause,