Highly ambitious ATProtocol AppView service and sdks

fix graphql schema builder to handle local and external lexicon refs

- Handle local refs starting with # (e.g., #image)
- Handle external refs with specific def (e.g., app.bsky.embed.defs#aspectRatio)
- Handle external refs to main def (e.g., community.lexicon.location.hthree)
- Add extract_fields_from_properties() to extract fields from lexicon def properties
- Add capitalize_first() helper function
- Pass current lexicon NSID through resolve chain to support local ref resolution
- Generate proper type names: AppBskyEmbedImagesImage, AppBskyEmbedDefsAspectRatio, etc.
- Fixes schema build error for slices using AT Protocol standard lexicons with local refs

Changed files
+77 -10
api
src
+77 -10
api/src/graphql/schema_builder.rs
··· 68 68 /// Returns the generated type name 69 69 fn resolve_lexicon_ref_type( 70 70 ref_nsid: &str, 71 + current_lexicon_nsid: &str, 71 72 all_lexicons: &[serde_json::Value], 72 73 type_registry: &mut TypeRegistry, 73 74 database: &Database, 74 75 ) -> String { 75 - // Generate type name from NSID 76 - let type_name = nsid_to_type_name(ref_nsid); 76 + // Handle different ref formats: 77 + // 1. Local ref: #image 78 + // 2. External ref with specific def: app.bsky.embed.defs#aspectRatio 79 + // 3. External ref to main: community.lexicon.location.hthree 80 + let (target_nsid, def_name) = if ref_nsid.starts_with('#') { 81 + // Local ref - use current lexicon NSID and the def name without # 82 + (current_lexicon_nsid, &ref_nsid[1..]) 83 + } else if let Some(hash_pos) = ref_nsid.find('#') { 84 + // External ref with specific def - split on # 85 + (&ref_nsid[..hash_pos], &ref_nsid[hash_pos + 1..]) 86 + } else { 87 + // External ref to main def 88 + (ref_nsid, "main") 89 + }; 90 + 91 + // Generate type name from NSID and def name 92 + let type_name = if def_name == "main" { 93 + // For refs to main: CommunityLexiconLocationHthree 94 + nsid_to_type_name(target_nsid) 95 + } else { 96 + // For refs to specific def: AppBskyEmbedDefsAspectRatio 97 + format!("{}{}", nsid_to_type_name(target_nsid), capitalize_first(def_name)) 98 + }; 77 99 78 100 // Check if already generated 79 101 if type_registry.contains_key(&type_name) { ··· 82 104 83 105 // Find the lexicon definition 84 106 let lexicon = all_lexicons.iter().find(|lex| { 85 - lex.get("id").and_then(|id| id.as_str()) == Some(ref_nsid) 107 + lex.get("id").and_then(|id| id.as_str()) == Some(target_nsid) 86 108 }); 87 109 88 110 if let Some(lex) = lexicon { 89 - // Extract the main definition 111 + // Extract the definition (either "main" or specific def like "image") 90 112 if let Some(defs) = lex.get("defs") { 91 - let fields = extract_collection_fields(defs); 113 + if let Some(def) = defs.get(def_name) { 114 + // Extract fields from this specific definition 115 + if let Some(properties) = def.get("properties") { 116 + let fields = extract_fields_from_properties(properties); 92 117 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; 118 + if !fields.is_empty() { 119 + // Generate the type using existing nested object generator 120 + generate_nested_object_type(&type_name, &fields, type_registry, database); 121 + return type_name; 122 + } 123 + } 97 124 } 98 125 } 99 126 } 100 127 101 128 // Fallback: couldn't resolve the ref, will use JSON 102 - tracing::warn!("Could not resolve lexicon ref: {}", ref_nsid); 129 + tracing::warn!("Could not resolve lexicon ref: {} (target: {}, def: {})", ref_nsid, target_nsid, def_name); 103 130 type_name 131 + } 132 + 133 + /// Capitalizes the first character of a string 134 + fn capitalize_first(s: &str) -> String { 135 + let mut chars = s.chars(); 136 + match chars.next() { 137 + None => String::new(), 138 + Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(), 139 + } 140 + } 141 + 142 + /// Extracts fields from a lexicon properties object 143 + fn extract_fields_from_properties(properties: &serde_json::Value) -> Vec<GraphQLField> { 144 + let mut fields = Vec::new(); 145 + 146 + if let Some(props) = properties.as_object() { 147 + for (field_name, field_def) in props { 148 + let field_type_str = field_def.get("type").and_then(|t| t.as_str()).unwrap_or("unknown"); 149 + let field_type = crate::graphql::types::map_lexicon_type_to_graphql(field_type_str, field_def); 150 + 151 + // Check if field is required 152 + let is_required = false; // We'd need the parent's "required" array to know this 153 + 154 + // Extract format if present 155 + let format = field_def.get("format").and_then(|f| f.as_str()).map(|s| s.to_string()); 156 + 157 + fields.push(GraphQLField { 158 + name: field_name.clone(), 159 + field_type, 160 + is_required, 161 + format, 162 + }); 163 + } 164 + } 165 + 166 + fields 104 167 } 105 168 106 169 /// Recursively generates GraphQL object types for nested objects ··· 322 385 auth_base_url.clone(), 323 386 &mut type_registry, 324 387 &lexicons, 388 + nsid, 325 389 ); 326 390 327 391 // Create edge and connection types for this collection (Relay standard) ··· 1272 1336 auth_base_url: String, 1273 1337 type_registry: &mut TypeRegistry, 1274 1338 all_lexicons: &[serde_json::Value], 1339 + lexicon_nsid: &str, 1275 1340 ) -> Object { 1276 1341 let mut object = Object::new(type_name); 1277 1342 ··· 1421 1486 // Resolve lexicon ref and generate type for it 1422 1487 let resolved_type_name = resolve_lexicon_ref_type( 1423 1488 ref_nsid, 1489 + lexicon_nsid, 1424 1490 all_lexicons, 1425 1491 type_registry, 1426 1492 &database, ··· 1454 1520 // Resolve lexicon ref for array items 1455 1521 let resolved_type_name = resolve_lexicon_ref_type( 1456 1522 ref_nsid, 1523 + lexicon_nsid, 1457 1524 all_lexicons, 1458 1525 type_registry, 1459 1526 &database,