Skip to main content

meta_language/
graphql_adapter.rs

1//! Registry-driven lowering from validated GraphQL operations to query plans.
2
3mod parser;
4mod registry;
5
6use std::collections::BTreeMap;
7
8use crate::configuration::ParseConfiguration;
9use crate::link_network::LinkNetwork;
10use crate::query_plan::{
11    attach_plan_links, LoweredQueryPlan, QueryAggregate, QueryAggregateFunction,
12    QueryComparisonOperator, QueryFilter, QueryOperation, QueryOrder, QueryPlan,
13    QuerySortDirection, QuerySourceEvidence, QueryValue,
14};
15use crate::source::{ByteRange, Point, SourceSpan};
16
17use parser::{ByteSpan, Field, ValueKind, ValueNode};
18pub use registry::{
19    GraphQlAdapterError, GraphQlArgumentRole, GraphQlOperationType, GraphQlRootMapping,
20    GraphQlSchemaRegistry,
21};
22
23/// Validates and lowers exactly one GraphQL operation/root field.
24pub fn lower_graphql(
25    source: &str,
26    registry: &GraphQlSchemaRegistry,
27) -> Result<LoweredQueryPlan, GraphQlAdapterError> {
28    let mut network = LinkNetwork::parse(source, "GraphQL", ParseConfiguration::default());
29    if !network.verify_full_match(None).is_clean() {
30        return Err(GraphQlAdapterError::new(
31            "GraphQL CST validation failed; semantic lowering was not attempted",
32        ));
33    }
34    let document = parser::parse(source)?;
35    let [root] = document.root_fields.as_slice() else {
36        return Err(GraphQlAdapterError::new(
37            "exactly one GraphQL root field is required",
38        ));
39    };
40    if root.alias.is_some() {
41        return Err(GraphQlAdapterError::new(
42            "GraphQL root aliases are not represented by the query plan",
43        ));
44    }
45    let mapping = registry.root(document.operation, &root.name)?;
46    let mut plan = QueryPlan::new(mapping.operation, &mapping.resource);
47    plan.source_evidence
48        .push(evidence(source, "root", root.span));
49    lower_arguments(source, root, mapping, &mut plan)?;
50    lower_selection(source, root, mapping, &mut plan)?;
51    validate_plan(&plan)?;
52    let root_link = attach_plan_links(&mut network, &plan, "GraphQL");
53    Ok(LoweredQueryPlan::new(plan, network, root_link))
54}
55
56fn lower_arguments(
57    source: &str,
58    root: &Field,
59    mapping: &GraphQlRootMapping,
60    plan: &mut QueryPlan,
61) -> Result<(), GraphQlAdapterError> {
62    for argument in &root.arguments {
63        let role = mapping.arguments.get(&argument.name).ok_or_else(|| {
64            GraphQlAdapterError::new(format!("unmapped GraphQL argument {:?}", argument.name))
65        })?;
66        match role {
67            GraphQlArgumentRole::Filter => {
68                plan.filter = Some(lower_filter(&argument.value, mapping)?);
69            }
70            GraphQlArgumentRole::Order => {
71                plan.order = lower_order(&argument.value, mapping)?;
72            }
73            GraphQlArgumentRole::Limit => {
74                plan.limit = Some(non_negative_integer(&argument.value, "limit")?);
75            }
76            GraphQlArgumentRole::Offset => {
77                plan.offset = Some(non_negative_integer(&argument.value, "offset")?);
78            }
79            GraphQlArgumentRole::Group => {
80                plan.group_by = lower_group(&argument.value, mapping)?;
81            }
82            GraphQlArgumentRole::MutationInput => {
83                plan.mutation = lower_mutation(&argument.value, mapping)?;
84            }
85        }
86        plan.source_evidence.push(evidence(
87            source,
88            format!("argument:{}", role.as_str()),
89            argument.value.span,
90        ));
91    }
92    Ok(())
93}
94
95fn lower_selection(
96    source: &str,
97    root: &Field,
98    mapping: &GraphQlRootMapping,
99    plan: &mut QueryPlan,
100) -> Result<(), GraphQlAdapterError> {
101    if root.selection.is_empty() {
102        return Err(GraphQlAdapterError::new(
103            "a mapped GraphQL root must have a projection selection",
104        ));
105    }
106    for field in &root.selection {
107        if !field.selection.is_empty() {
108            return Err(GraphQlAdapterError::new(format!(
109                "nested projection field {:?} has no explicit mapping",
110                field.name
111            )));
112        }
113        if let Some(function) = mapping.aggregates.get(&field.name) {
114            let aggregate_field = aggregate_field(field, mapping)?;
115            if *function != QueryAggregateFunction::Count && aggregate_field.is_none() {
116                return Err(GraphQlAdapterError::new(format!(
117                    "aggregate {:?} requires a mapped field argument",
118                    field.name
119                )));
120            }
121            plan.aggregates.push(QueryAggregate {
122                function: *function,
123                field: aggregate_field,
124                alias: field.alias.clone(),
125            });
126            plan.source_evidence.push(evidence(
127                source,
128                format!("aggregate:{}", function.as_str()),
129                field.span,
130            ));
131            continue;
132        }
133        if !field.arguments.is_empty() {
134            return Err(GraphQlAdapterError::new(format!(
135                "non-aggregate projection {:?} cannot have arguments",
136                field.name
137            )));
138        }
139        if field.alias.is_some() {
140            return Err(GraphQlAdapterError::new(format!(
141                "projection alias on {:?} is not represented by the query plan",
142                field.name
143            )));
144        }
145        let canonical = mapping.mapped_field(&field.name)?;
146        if !plan.projection.contains(&canonical) {
147            plan.projection.push(canonical.clone());
148        }
149        plan.source_evidence.push(evidence(
150            source,
151            format!("projection:{canonical}"),
152            field.span,
153        ));
154    }
155    Ok(())
156}
157
158fn aggregate_field(
159    field: &Field,
160    mapping: &GraphQlRootMapping,
161) -> Result<Option<String>, GraphQlAdapterError> {
162    match field.arguments.as_slice() {
163        [] => Ok(None),
164        [argument] if argument.name == "field" => {
165            Ok(Some(mapping.mapped_symbol(symbol(&argument.value)?)?))
166        }
167        _ => Err(GraphQlAdapterError::new(format!(
168            "aggregate {:?} accepts only an optional field argument",
169            field.name
170        ))),
171    }
172}
173
174fn lower_filter(
175    value: &ValueNode,
176    mapping: &GraphQlRootMapping,
177) -> Result<QueryFilter, GraphQlAdapterError> {
178    let ValueKind::Object(fields) = &value.value else {
179        return Err(GraphQlAdapterError::new("filter arguments must be objects"));
180    };
181    let mut clauses = Vec::new();
182    for (name, value) in fields {
183        match name.as_str() {
184            "and" => clauses.push(QueryFilter::And(filter_list(value, mapping)?)),
185            "or" => clauses.push(QueryFilter::Or(filter_list(value, mapping)?)),
186            "not" => clauses.push(QueryFilter::Not(Box::new(lower_filter(value, mapping)?))),
187            _ => clauses.extend(field_filter(name, value, mapping)?),
188        }
189    }
190    collapse_and(clauses, "empty filter objects are unsupported")
191}
192
193fn filter_list(
194    value: &ValueNode,
195    mapping: &GraphQlRootMapping,
196) -> Result<Vec<QueryFilter>, GraphQlAdapterError> {
197    let ValueKind::List(values) = &value.value else {
198        return Err(GraphQlAdapterError::new(
199            "GraphQL and/or filter values must be lists",
200        ));
201    };
202    if values.is_empty() {
203        return Err(GraphQlAdapterError::new(
204            "GraphQL and/or filter lists must not be empty",
205        ));
206    }
207    values
208        .iter()
209        .map(|value| lower_filter(value, mapping))
210        .collect()
211}
212
213fn field_filter(
214    source_field: &str,
215    value: &ValueNode,
216    mapping: &GraphQlRootMapping,
217) -> Result<Vec<QueryFilter>, GraphQlAdapterError> {
218    let field = mapping.mapped_field(source_field)?;
219    let ValueKind::Object(operators) = &value.value else {
220        return Ok(vec![QueryFilter::Compare {
221            field,
222            operator: QueryComparisonOperator::Equal,
223            value: query_value(value)?,
224        }]);
225    };
226    if operators.is_empty() {
227        return Err(GraphQlAdapterError::new(
228            "empty comparison objects are unsupported",
229        ));
230    }
231    operators
232        .iter()
233        .map(|(name, value)| {
234            let operator = QueryComparisonOperator::from_graphql_key(name).ok_or_else(|| {
235                GraphQlAdapterError::new(format!("unsupported filter operator {name:?}"))
236            })?;
237            if operator == QueryComparisonOperator::IsNull
238                && !matches!(value.value, ValueKind::Boolean(_))
239            {
240                return Err(GraphQlAdapterError::new("isNull requires a boolean value"));
241            }
242            if matches!(
243                operator,
244                QueryComparisonOperator::In | QueryComparisonOperator::NotIn
245            ) && !matches!(value.value, ValueKind::List(_))
246            {
247                return Err(GraphQlAdapterError::new("in/notIn requires a list value"));
248            }
249            Ok(QueryFilter::Compare {
250                field: field.clone(),
251                operator,
252                value: query_value(value)?,
253            })
254        })
255        .collect()
256}
257
258fn collapse_and(
259    mut clauses: Vec<QueryFilter>,
260    empty_message: &str,
261) -> Result<QueryFilter, GraphQlAdapterError> {
262    match clauses.len() {
263        0 => Err(GraphQlAdapterError::new(empty_message)),
264        1 => Ok(clauses.remove(0)),
265        _ => Ok(QueryFilter::And(clauses)),
266    }
267}
268
269fn lower_order(
270    value: &ValueNode,
271    mapping: &GraphQlRootMapping,
272) -> Result<Vec<QueryOrder>, GraphQlAdapterError> {
273    let entries = match &value.value {
274        ValueKind::List(entries) => entries.as_slice(),
275        ValueKind::Object(_) => std::slice::from_ref(value),
276        _ => {
277            return Err(GraphQlAdapterError::new(
278                "order arguments must be objects or lists of objects",
279            ));
280        }
281    };
282    if entries.is_empty() {
283        return Err(GraphQlAdapterError::new("order lists must not be empty"));
284    }
285    entries
286        .iter()
287        .map(|entry| lower_order_entry(entry, mapping))
288        .collect()
289}
290
291fn lower_order_entry(
292    value: &ValueNode,
293    mapping: &GraphQlRootMapping,
294) -> Result<QueryOrder, GraphQlAdapterError> {
295    let ValueKind::Object(fields) = &value.value else {
296        return Err(GraphQlAdapterError::new(
297            "each order entry must be an object",
298        ));
299    };
300    let field_value = object_field(fields, "field")?;
301    let direction_value = object_field(fields, "direction")?;
302    if fields.len() != 2 {
303        return Err(GraphQlAdapterError::new(
304            "order entries support only field and direction",
305        ));
306    }
307    Ok(QueryOrder {
308        field: mapping.mapped_symbol(symbol(field_value)?)?,
309        direction: direction(direction_value)?,
310    })
311}
312
313fn lower_group(
314    value: &ValueNode,
315    mapping: &GraphQlRootMapping,
316) -> Result<Vec<String>, GraphQlAdapterError> {
317    let values = match &value.value {
318        ValueKind::List(values) => values.as_slice(),
319        ValueKind::Enum(_) | ValueKind::String(_) => std::slice::from_ref(value),
320        _ => return Err(GraphQlAdapterError::new("group arguments must name fields")),
321    };
322    if values.is_empty() {
323        return Err(GraphQlAdapterError::new("group lists must not be empty"));
324    }
325    values
326        .iter()
327        .map(|value| mapping.mapped_symbol(symbol(value)?))
328        .collect()
329}
330
331fn lower_mutation(
332    value: &ValueNode,
333    mapping: &GraphQlRootMapping,
334) -> Result<BTreeMap<String, QueryValue>, GraphQlAdapterError> {
335    let ValueKind::Object(fields) = &value.value else {
336        return Err(GraphQlAdapterError::new(
337            "mutation input arguments must be objects",
338        ));
339    };
340    if fields.is_empty() {
341        return Err(GraphQlAdapterError::new(
342            "mutation inputs must not be empty",
343        ));
344    }
345    fields
346        .iter()
347        .map(|(field, value)| Ok((mapping.mapped_field(field)?, query_value(value)?)))
348        .collect()
349}
350
351fn query_value(value: &ValueNode) -> Result<QueryValue, GraphQlAdapterError> {
352    match &value.value {
353        ValueKind::Null => Ok(QueryValue::Null),
354        ValueKind::Boolean(value) => Ok(QueryValue::Boolean(*value)),
355        ValueKind::Integer(value) => safe_integer(*value).map(QueryValue::Integer),
356        ValueKind::Float(value) => Ok(QueryValue::Float(*value)),
357        ValueKind::String(value) | ValueKind::Enum(value) => Ok(QueryValue::String(value.clone())),
358        ValueKind::List(values) => values
359            .iter()
360            .map(query_value)
361            .collect::<Result<Vec<_>, _>>()
362            .map(QueryValue::List),
363        ValueKind::Object(_) => Err(GraphQlAdapterError::new(
364            "nested GraphQL input objects require explicit field mappings",
365        )),
366    }
367}
368
369fn non_negative_integer(value: &ValueNode, role: &str) -> Result<u64, GraphQlAdapterError> {
370    let ValueKind::Integer(value) = value.value else {
371        return Err(GraphQlAdapterError::new(format!(
372            "GraphQL {role} must be a non-negative integer"
373        )));
374    };
375    safe_integer(value).and_then(|value| {
376        u64::try_from(value).map_err(|_| {
377            GraphQlAdapterError::new(format!("GraphQL {role} must be a non-negative integer"))
378        })
379    })
380}
381
382fn safe_integer(value: i64) -> Result<i64, GraphQlAdapterError> {
383    const MAX_SAFE_INTEGER: i64 = 9_007_199_254_740_991;
384    if (-MAX_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(&value) {
385        Ok(value)
386    } else {
387        Err(GraphQlAdapterError::new(
388            "GraphQL integer is outside the cross-language safe range",
389        ))
390    }
391}
392
393fn symbol(value: &ValueNode) -> Result<&str, GraphQlAdapterError> {
394    match &value.value {
395        ValueKind::Enum(value) | ValueKind::String(value) => Ok(value),
396        _ => Err(GraphQlAdapterError::new(
397            "canonical field references must be GraphQL enums or strings",
398        )),
399    }
400}
401
402fn direction(value: &ValueNode) -> Result<QuerySortDirection, GraphQlAdapterError> {
403    match symbol(value)?.to_ascii_lowercase().as_str() {
404        "asc" => Ok(QuerySortDirection::Ascending),
405        "desc" => Ok(QuerySortDirection::Descending),
406        other => Err(GraphQlAdapterError::new(format!(
407            "unsupported sort direction {other:?}"
408        ))),
409    }
410}
411
412fn object_field<'a>(
413    fields: &'a [(String, ValueNode)],
414    name: &str,
415) -> Result<&'a ValueNode, GraphQlAdapterError> {
416    fields
417        .iter()
418        .find_map(|(field, value)| (field == name).then_some(value))
419        .ok_or_else(|| GraphQlAdapterError::new(format!("missing object field {name:?}")))
420}
421
422fn validate_plan(plan: &QueryPlan) -> Result<(), GraphQlAdapterError> {
423    if plan.projection.is_empty() && plan.aggregates.is_empty() {
424        return Err(GraphQlAdapterError::new(
425            "a query plan requires a projection or aggregate",
426        ));
427    }
428    match plan.operation {
429        QueryOperation::Insert | QueryOperation::Update if plan.mutation.is_empty() => Err(
430            GraphQlAdapterError::new("insert/update mutations require a mapped mutation input"),
431        ),
432        QueryOperation::Select | QueryOperation::Delete if !plan.mutation.is_empty() => {
433            Err(GraphQlAdapterError::new(
434                "select/delete operations cannot contain mutation assignments",
435            ))
436        }
437        _ => Ok(()),
438    }
439}
440
441fn evidence(source: &str, role: impl Into<String>, span: ByteSpan) -> QuerySourceEvidence {
442    QuerySourceEvidence::new(role, source_span(source, span))
443}
444
445fn source_span(source: &str, span: ByteSpan) -> SourceSpan {
446    SourceSpan::new(
447        ByteRange::new(span.start, span.end),
448        point_at(source, span.start),
449        point_at(source, span.end),
450    )
451}
452
453fn point_at(source: &str, offset: usize) -> Point {
454    let prefix = &source[..offset];
455    let row = prefix.bytes().filter(|byte| *byte == b'\n').count();
456    let column = prefix
457        .rsplit_once('\n')
458        .map_or(prefix.len(), |(_, line)| line.len());
459    Point::new(row, column)
460}