Auto-indexing service and GraphQL API for AT Protocol Records
quickslice.slices.network/
atproto
gleam
graphql
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}