Highly ambitious ATProtocol AppView service and sdks
138
fork

Configure Feed

Select the types of activity you want to include in your feed.

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 })

+308 -5
+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 }