Auto-indexing service and GraphQL API for AT Protocol Records quickslice.slices.network/
atproto gleam graphql
at main 184 lines 6.1 kB view raw
1/// Value converters for lexicon GraphQL API 2/// 3/// Transform database records and dynamic values to GraphQL value.Value objects 4import database/executor.{type Executor} 5import database/repositories/actors 6import database/repositories/label_definitions 7import database/types 8import gleam/dict 9import gleam/dynamic 10import gleam/dynamic/decode 11import gleam/json 12import gleam/list 13import gleam/string 14import swell/value 15 16/// Convert a database Record to a GraphQL value.Value 17/// 18/// Creates an Object with all the record metadata plus the parsed JSON value 19pub fn record_to_graphql_value( 20 record: types.Record, 21 db: Executor, 22) -> value.Value { 23 // Parse the record JSON and convert to GraphQL value 24 let value_object = case parse_json_to_value(record.json) { 25 Ok(val) -> val 26 Error(_) -> value.Object([]) 27 // Fallback to empty object on parse error 28 } 29 30 // Look up actor handle from actor table 31 let actor_handle = case actors.get(db, record.did) { 32 Ok([actor, ..]) -> value.String(actor.handle) 33 _ -> value.Null 34 } 35 36 // Create the full record object with metadata and value 37 value.Object([ 38 #("uri", value.String(record.uri)), 39 #("cid", value.String(record.cid)), 40 #("did", value.String(record.did)), 41 #("collection", value.String(record.collection)), 42 #("indexedAt", value.String(record.indexed_at)), 43 #("actorHandle", actor_handle), 44 #("value", value_object), 45 ]) 46} 47 48/// Parse a JSON string and convert it to a GraphQL value.Value 49pub fn parse_json_to_value(json_str: String) -> Result(value.Value, String) { 50 // Parse JSON string to dynamic value 51 case json.parse(json_str, decode.dynamic) { 52 Ok(dyn) -> Ok(dynamic_to_value(dyn)) 53 Error(_) -> Error("Failed to parse JSON") 54 } 55} 56 57/// Convert a dynamic value to a GraphQL value.Value 58pub fn dynamic_to_value(dyn: dynamic.Dynamic) -> value.Value { 59 // Try different decoders in order 60 case decode.run(dyn, decode.string) { 61 Ok(s) -> value.String(s) 62 Error(_) -> 63 case decode.run(dyn, decode.int) { 64 Ok(i) -> value.Int(i) 65 Error(_) -> 66 case decode.run(dyn, decode.float) { 67 Ok(f) -> value.Float(f) 68 Error(_) -> 69 case decode.run(dyn, decode.bool) { 70 Ok(b) -> value.Boolean(b) 71 Error(_) -> 72 case decode.run(dyn, decode.list(decode.dynamic)) { 73 Ok(items) -> { 74 let converted_items = list.map(items, dynamic_to_value) 75 value.List(converted_items) 76 } 77 Error(_) -> 78 case 79 decode.run( 80 dyn, 81 decode.dict(decode.string, decode.dynamic), 82 ) 83 { 84 Ok(dict) -> { 85 let fields = 86 dict 87 |> dict.to_list 88 |> list.map(fn(entry) { 89 let #(key, val) = entry 90 #(key, dynamic_to_value(val)) 91 }) 92 value.Object(fields) 93 } 94 Error(_) -> value.Null 95 } 96 } 97 } 98 } 99 } 100 } 101} 102 103/// Convert a dynamic JSON value to graphql value.Value 104pub fn json_dynamic_to_value(dyn: dynamic.Dynamic) -> value.Value { 105 // Try different decoders in order 106 case decode.run(dyn, decode.string) { 107 Ok(s) -> value.String(s) 108 Error(_) -> 109 case decode.run(dyn, decode.int) { 110 Ok(i) -> value.Int(i) 111 Error(_) -> 112 case decode.run(dyn, decode.float) { 113 Ok(f) -> value.Float(f) 114 Error(_) -> 115 case decode.run(dyn, decode.bool) { 116 Ok(b) -> value.Boolean(b) 117 Error(_) -> 118 // Try as a list 119 case decode.run(dyn, decode.list(decode.dynamic)) { 120 Ok(items) -> 121 value.List(list.map(items, json_dynamic_to_value)) 122 Error(_) -> 123 // Try as an object (dict) 124 case 125 decode.run( 126 dyn, 127 decode.dict(decode.string, decode.dynamic), 128 ) 129 { 130 Ok(d) -> 131 value.Object( 132 list.map(dict.to_list(d), fn(pair) { 133 #(pair.0, json_dynamic_to_value(pair.1)) 134 }), 135 ) 136 Error(_) -> value.Null 137 } 138 } 139 } 140 } 141 } 142 } 143} 144 145/// Extract a reference URI from a record's JSON 146/// This handles both simple string fields (at-uri) and strongRef objects 147pub fn extract_reference_uri( 148 json_str: String, 149 field_name: String, 150) -> Result(String, Nil) { 151 // Parse the JSON 152 case parse_json_to_value(json_str) { 153 Ok(value.Object(fields)) -> { 154 // Find the field 155 case list.key_find(fields, field_name) { 156 Ok(value.String(uri)) -> Ok(uri) 157 Ok(value.Object(ref_fields)) -> { 158 // Handle strongRef: { "uri": "...", "cid": "..." } 159 case list.key_find(ref_fields, "uri") { 160 Ok(value.String(uri)) -> Ok(uri) 161 _ -> Error(Nil) 162 } 163 } 164 _ -> Error(Nil) 165 } 166 } 167 _ -> Error(Nil) 168 } 169} 170 171/// Convert a LabelPreference to GraphQL value 172/// Takes a label definition and the user's effective visibility setting 173pub fn label_preference_to_value( 174 def: label_definitions.LabelDefinition, 175 visibility: String, 176) -> value.Value { 177 value.Object([ 178 #("val", value.String(def.val)), 179 #("description", value.String(def.description)), 180 #("severity", value.Enum(string.uppercase(def.severity))), 181 #("defaultVisibility", value.Enum(string.uppercase(def.default_visibility))), 182 #("visibility", value.Enum(string.uppercase(visibility))), 183 ]) 184}