Highly ambitious ATProtocol AppView service and sdks

add support for typed nested objects and lexicon refs in graphql schema

- Add GraphQLType::LexiconRef variant to distinguish lexicon type refs from record refs
- Implement type registry to track and deduplicate generated nested object types
- Add resolve_lexicon_ref_type() to resolve and generate types for lexicon refs like community.lexicon.location.hthree
- Add generate_nested_object_type() to recursively create GraphQL types for inline objects
- Update field resolvers to wrap nested object data in NestedObjectContainer
- Support both inline object definitions and lexicon type references
- Enable nested field queries in GraphQL (e.g., homeTown { name value })

Changed files
+308 -5
api
+302 -4
api/src/graphql/schema_builder.rs
··· 45 45 at_uri_fields: Vec<String>, // Fields with format "at-uri" for reverse joins 46 46 } 47 47 48 + /// Type registry for tracking generated nested object types 49 + type TypeRegistry = HashMap<String, Object>; 50 + 51 + /// Container for nested object field values 52 + #[derive(Clone)] 53 + struct NestedObjectContainer { 54 + data: serde_json::Value, 55 + } 56 + 57 + /// Generates a unique type name for a nested object field 58 + fn generate_nested_type_name(parent_type: &str, field_name: &str) -> String { 59 + let mut chars = field_name.chars(); 60 + let capitalized_field = match chars.next() { 61 + None => String::new(), 62 + Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(), 63 + }; 64 + format!("{}{}", parent_type, capitalized_field) 65 + } 66 + 67 + /// Resolves a lexicon ref and generates a GraphQL type for it 68 + /// Returns the generated type name 69 + fn resolve_lexicon_ref_type( 70 + ref_nsid: &str, 71 + all_lexicons: &[serde_json::Value], 72 + type_registry: &mut TypeRegistry, 73 + database: &Database, 74 + ) -> String { 75 + // Generate type name from NSID 76 + let type_name = nsid_to_type_name(ref_nsid); 77 + 78 + // Check if already generated 79 + if type_registry.contains_key(&type_name) { 80 + return type_name; 81 + } 82 + 83 + // Find the lexicon definition 84 + let lexicon = all_lexicons.iter().find(|lex| { 85 + lex.get("id").and_then(|id| id.as_str()) == Some(ref_nsid) 86 + }); 87 + 88 + if let Some(lex) = lexicon { 89 + // Extract the main definition 90 + if let Some(defs) = lex.get("defs") { 91 + let fields = extract_collection_fields(defs); 92 + 93 + if !fields.is_empty() { 94 + // Generate the type using existing nested object generator 95 + generate_nested_object_type(&type_name, &fields, type_registry, database); 96 + return type_name; 97 + } 98 + } 99 + } 100 + 101 + // Fallback: couldn't resolve the ref, will use JSON 102 + tracing::warn!("Could not resolve lexicon ref: {}", ref_nsid); 103 + type_name 104 + } 105 + 106 + /// Recursively generates GraphQL object types for nested objects 107 + /// Returns the type name of the generated object type 108 + fn generate_nested_object_type( 109 + type_name: &str, 110 + fields: &[GraphQLField], 111 + type_registry: &mut TypeRegistry, 112 + database: &Database, 113 + ) -> String { 114 + // Check if type already exists in registry 115 + if type_registry.contains_key(type_name) { 116 + return type_name.to_string(); 117 + } 118 + 119 + let mut object = Object::new(type_name); 120 + 121 + // Add fields to the object 122 + for field in fields { 123 + let field_name = field.name.clone(); 124 + let field_name_for_field = field_name.clone(); // Clone for Field::new 125 + let field_type = field.field_type.clone(); 126 + 127 + // Determine the TypeRef for this field 128 + let type_ref = match &field.field_type { 129 + GraphQLType::Object(nested_fields) => { 130 + // Generate nested object type recursively 131 + let nested_type_name = generate_nested_type_name(type_name, &field_name); 132 + let actual_type_name = generate_nested_object_type( 133 + &nested_type_name, 134 + nested_fields, 135 + type_registry, 136 + database, 137 + ); 138 + 139 + if field.is_required { 140 + TypeRef::named_nn(actual_type_name) 141 + } else { 142 + TypeRef::named(actual_type_name) 143 + } 144 + } 145 + GraphQLType::Array(inner) => { 146 + if let GraphQLType::Object(nested_fields) = inner.as_ref() { 147 + // Generate nested object type for array items 148 + let nested_type_name = generate_nested_type_name(type_name, &field_name); 149 + let actual_type_name = generate_nested_object_type( 150 + &nested_type_name, 151 + nested_fields, 152 + type_registry, 153 + database, 154 + ); 155 + 156 + if field.is_required { 157 + TypeRef::named_nn_list(actual_type_name) 158 + } else { 159 + TypeRef::named_list(actual_type_name) 160 + } 161 + } else { 162 + // Use standard type ref for arrays of primitives 163 + graphql_type_to_typeref(&field.field_type, field.is_required) 164 + } 165 + } 166 + _ => { 167 + // Use standard type ref for other types 168 + graphql_type_to_typeref(&field.field_type, field.is_required) 169 + } 170 + }; 171 + 172 + // Add field with resolver 173 + object = object.field(Field::new(&field_name_for_field, type_ref, move |ctx| { 174 + let field_name = field_name.clone(); 175 + let field_type = field_type.clone(); 176 + 177 + FieldFuture::new(async move { 178 + // Get parent container 179 + let container = ctx.parent_value.try_downcast_ref::<NestedObjectContainer>()?; 180 + let value = container.data.get(&field_name); 181 + 182 + if let Some(val) = value { 183 + if val.is_null() { 184 + return Ok(None); 185 + } 186 + 187 + // For nested objects, wrap in container 188 + if matches!(field_type, GraphQLType::Object(_)) { 189 + let nested_container = NestedObjectContainer { 190 + data: val.clone(), 191 + }; 192 + return Ok(Some(FieldValue::owned_any(nested_container))); 193 + } 194 + 195 + // For arrays of objects, wrap each item 196 + if let GraphQLType::Array(inner) = &field_type { 197 + if matches!(inner.as_ref(), GraphQLType::Object(_)) { 198 + if let Some(arr) = val.as_array() { 199 + let containers: Vec<FieldValue> = arr 200 + .iter() 201 + .map(|item| { 202 + let nested_container = NestedObjectContainer { 203 + data: item.clone(), 204 + }; 205 + FieldValue::owned_any(nested_container) 206 + }) 207 + .collect(); 208 + return Ok(Some(FieldValue::list(containers))); 209 + } 210 + return Ok(Some(FieldValue::list(Vec::<FieldValue>::new()))); 211 + } 212 + } 213 + 214 + // For other types, return the GraphQL value 215 + let graphql_val = json_to_graphql_value(val); 216 + Ok(Some(FieldValue::value(graphql_val))) 217 + } else { 218 + Ok(None) 219 + } 220 + }) 221 + })); 222 + } 223 + 224 + // Store the generated type in registry 225 + type_registry.insert(type_name.to_string(), object); 226 + type_name.to_string() 227 + } 228 + 48 229 /// Builds a dynamic GraphQL schema from lexicons for a given slice 49 230 pub async fn build_graphql_schema(database: Database, slice_uri: String, auth_base_url: String) -> Result<Schema, String> { 50 231 // Fetch all lexicons for this slice ··· 110 291 } 111 292 } 112 293 294 + // Initialize type registry for nested object types 295 + let mut type_registry: TypeRegistry = HashMap::new(); 296 + 113 297 // Second pass: create types and queries 114 298 for lexicon in &lexicons { 115 299 // get_lexicons_by_slice returns {lexicon: 1, id: "nsid", defs: {...}} ··· 136 320 slice_uri.clone(), 137 321 &all_collections, 138 322 auth_base_url.clone(), 323 + &mut type_registry, 324 + &lexicons, 139 325 ); 140 326 141 327 // Create edge and connection types for this collection (Relay standard) ··· 1051 1237 schema_builder = schema_builder.register(mutation_input); 1052 1238 } 1053 1239 1240 + // Register all nested object types from the type registry 1241 + for (_, nested_type) in type_registry { 1242 + schema_builder = schema_builder.register(nested_type); 1243 + } 1244 + 1054 1245 schema_builder 1055 1246 .finish() 1056 1247 .map_err(|e| format!("Schema build error: {:?}", e)) ··· 1079 1270 slice_uri: String, 1080 1271 all_collections: &[CollectionMeta], 1081 1272 auth_base_url: String, 1273 + type_registry: &mut TypeRegistry, 1274 + all_lexicons: &[serde_json::Value], 1082 1275 ) -> Object { 1083 1276 let mut object = Object::new(type_name); 1084 1277 ··· 1222 1415 let field_type = field.field_type.clone(); 1223 1416 let db_clone = database.clone(); 1224 1417 1225 - let type_ref = graphql_type_to_typeref(&field.field_type, field.is_required); 1418 + // Determine type ref - handle nested objects and lexicon refs specially 1419 + let type_ref = match &field.field_type { 1420 + GraphQLType::LexiconRef(ref_nsid) => { 1421 + // Resolve lexicon ref and generate type for it 1422 + let resolved_type_name = resolve_lexicon_ref_type( 1423 + ref_nsid, 1424 + all_lexicons, 1425 + type_registry, 1426 + &database, 1427 + ); 1428 + 1429 + if field.is_required { 1430 + TypeRef::named_nn(resolved_type_name) 1431 + } else { 1432 + TypeRef::named(resolved_type_name) 1433 + } 1434 + } 1435 + GraphQLType::Object(nested_fields) => { 1436 + // Generate nested object type 1437 + let nested_type_name = generate_nested_type_name(type_name, &field_name); 1438 + let actual_type_name = generate_nested_object_type( 1439 + &nested_type_name, 1440 + nested_fields, 1441 + type_registry, 1442 + &database, 1443 + ); 1444 + 1445 + if field.is_required { 1446 + TypeRef::named_nn(actual_type_name) 1447 + } else { 1448 + TypeRef::named(actual_type_name) 1449 + } 1450 + } 1451 + GraphQLType::Array(inner) => { 1452 + match inner.as_ref() { 1453 + GraphQLType::LexiconRef(ref_nsid) => { 1454 + // Resolve lexicon ref for array items 1455 + let resolved_type_name = resolve_lexicon_ref_type( 1456 + ref_nsid, 1457 + all_lexicons, 1458 + type_registry, 1459 + &database, 1460 + ); 1461 + 1462 + if field.is_required { 1463 + TypeRef::named_nn_list(resolved_type_name) 1464 + } else { 1465 + TypeRef::named_list(resolved_type_name) 1466 + } 1467 + } 1468 + GraphQLType::Object(nested_fields) => { 1469 + // Generate nested object type for array items 1470 + let nested_type_name = generate_nested_type_name(type_name, &field_name); 1471 + let actual_type_name = generate_nested_object_type( 1472 + &nested_type_name, 1473 + nested_fields, 1474 + type_registry, 1475 + &database, 1476 + ); 1477 + 1478 + if field.is_required { 1479 + TypeRef::named_nn_list(actual_type_name) 1480 + } else { 1481 + TypeRef::named_list(actual_type_name) 1482 + } 1483 + } 1484 + _ => graphql_type_to_typeref(&field.field_type, field.is_required), 1485 + } 1486 + } 1487 + _ => graphql_type_to_typeref(&field.field_type, field.is_required), 1488 + }; 1226 1489 1227 1490 object = object.field(Field::new(&field_name_for_field, type_ref, move |ctx| { 1228 1491 let field_name = field_name.clone(); ··· 1346 1609 } 1347 1610 } 1348 1611 1349 - // For non-ref fields, return the raw JSON value 1612 + // Check if this is a lexicon ref field 1613 + if matches!(field_type, GraphQLType::LexiconRef(_)) { 1614 + let nested_container = NestedObjectContainer { 1615 + data: val.clone(), 1616 + }; 1617 + return Ok(Some(FieldValue::owned_any(nested_container))); 1618 + } 1619 + 1620 + // Check if this is a nested object field 1621 + if matches!(field_type, GraphQLType::Object(_)) { 1622 + let nested_container = NestedObjectContainer { 1623 + data: val.clone(), 1624 + }; 1625 + return Ok(Some(FieldValue::owned_any(nested_container))); 1626 + } 1627 + 1628 + // Check if this is an array of nested objects or lexicon refs 1629 + if let GraphQLType::Array(inner) = &field_type { 1630 + if matches!(inner.as_ref(), GraphQLType::LexiconRef(_)) || matches!(inner.as_ref(), GraphQLType::Object(_)) { 1631 + if let Some(arr) = val.as_array() { 1632 + let containers: Vec<FieldValue> = arr 1633 + .iter() 1634 + .map(|item| { 1635 + let nested_container = NestedObjectContainer { 1636 + data: item.clone(), 1637 + }; 1638 + FieldValue::owned_any(nested_container) 1639 + }) 1640 + .collect(); 1641 + return Ok(Some(FieldValue::list(containers))); 1642 + } 1643 + return Ok(Some(FieldValue::list(Vec::<FieldValue>::new()))); 1644 + } 1645 + } 1646 + 1647 + // For non-ref, non-object fields, return the raw JSON value 1350 1648 let graphql_val = json_to_graphql_value(val); 1351 1649 Ok(Some(FieldValue::value(graphql_val))) 1352 1650 } else { ··· 1914 2212 // Always nullable since blob data might be missing or malformed 1915 2213 TypeRef::named("Blob") 1916 2214 } 1917 - GraphQLType::Json | GraphQLType::Ref | GraphQLType::Object(_) | GraphQLType::Union => { 1918 - // JSON scalar type - linked records and complex objects return as JSON 2215 + GraphQLType::Json | GraphQLType::Ref | GraphQLType::LexiconRef(_) | GraphQLType::Object(_) | GraphQLType::Union => { 2216 + // JSON scalar type - linked records, lexicon refs, and complex objects return as JSON (fallback) 1919 2217 if is_required { 1920 2218 TypeRef::named_nn("JSON") 1921 2219 } else {
+6 -1
api/src/graphql/types.rs
··· 20 20 Float, 21 21 /// Reference to another record (for strongRef) 22 22 Ref, 23 + /// Reference to a lexicon type definition (e.g., community.lexicon.location.hthree) 24 + LexiconRef(String), 23 25 /// Array of a type 24 26 Array(Box<GraphQLType>), 25 27 /// Object with nested fields ··· 45 47 "unknown" => GraphQLType::Json, 46 48 "null" => GraphQLType::Json, 47 49 "ref" => { 48 - // Check if this is a strongRef (link to another record) 50 + // Check if this is a strongRef (link to another record) or a lexicon type ref 49 51 let ref_name = lexicon_def 50 52 .get("ref") 51 53 .and_then(|r| r.as_str()) ··· 53 55 54 56 if ref_name == "com.atproto.repo.strongRef" { 55 57 GraphQLType::Ref 58 + } else if !ref_name.is_empty() { 59 + // This is a reference to a lexicon type definition 60 + GraphQLType::LexiconRef(ref_name.to_string()) 56 61 } else { 57 62 GraphQLType::Json 58 63 }