use std::collections::{BTreeMap, HashMap};
use std::sync::Arc;
use async_graphql::dynamic::indexmap::IndexMap;
use async_graphql::dynamic::{
Enum, Field, FieldFuture, FieldValue, InputObject, InputValue, Interface, InterfaceField,
Object, Scalar, Schema, Type, TypeRef, Union,
};
use async_graphql::{Name, Value as GraphqlValue};
use rust_decimal::Decimal;
use rust_decimal::prelude::FromPrimitive;
use serde_json::Number;
use surrealdb_types::ToSql;
use super::auth::add_auth_mutations;
use super::error::{GraphqlError, resolver_error};
#[cfg(debug_assertions)]
use super::ext::ValidatorExt;
use crate::catalog::providers::{AuthorisationProvider, DatabaseProvider, TableProvider};
use crate::catalog::{
DatabaseId, FieldDefinition, GraphQLConfig, GraphQLFunctionsConfig, GraphQLIntrospectionConfig,
GraphQLTablesConfig, NamespaceId,
};
use crate::dbs::Session;
use crate::expr::kind::{GeometryKind, KindLiteral};
use crate::expr::{Expr, Kind, Literal};
use crate::graphql::error::{internal_error, schema_error, type_error};
use crate::graphql::functions::process_fns;
use crate::graphql::mutations::process_mutations;
use crate::graphql::relations::collect_relations;
use crate::graphql::subscriptions::process_subscriptions;
use crate::graphql::tables::{process_tbs, register_filter_helper_types};
use crate::kvs::{Datastore, LockType, Transaction, TransactionType};
use crate::val::{
Array as SurArray, Geometry as SurGeometry, Number as SurNumber, Object as SurObject,
RecordId as SurRecordId, RecordIdKey as SurRecordIdKey, Set as SurSet, TableName,
Value as SurValue,
};
pub(crate) struct SchemaContext<'a> {
pub tx: &'a Transaction,
pub ns: NamespaceId,
pub db: DatabaseId,
pub datastore: &'a Arc<Datastore>,
}
pub async fn generate_schema(
datastore: &Arc<Datastore>,
session: &Session,
graphql_config: GraphQLConfig,
) -> Result<Schema, GraphqlError> {
let kvs = datastore;
let tx = kvs.transaction(TransactionType::Read, LockType::Optimistic).await?;
let ns = session.ns.as_ref().ok_or(GraphqlError::UnspecifiedNamespace)?;
let db = session.db.as_ref().ok_or(GraphqlError::UnspecifiedDatabase)?;
let db_def = match tx.get_db_by_name(ns, db, None).await? {
Some(db) => db,
None => return Err(GraphqlError::NotConfigured),
};
let tbs = match graphql_config.tables {
GraphQLTablesConfig::None => None,
_ => {
let tbs = tx.all_tb(db_def.namespace_id, db_def.database_id, None).await?;
match graphql_config.tables {
GraphQLTablesConfig::None => None,
GraphQLTablesConfig::Auto => Some(tbs),
GraphQLTablesConfig::Include(inc) => {
Some(tbs.iter().filter(|t| inc.contains(&t.name)).cloned().collect())
}
GraphQLTablesConfig::Exclude(exc) => {
Some(tbs.iter().filter(|t| !exc.contains(&t.name)).cloned().collect())
}
}
}
};
let fns = match graphql_config.functions {
GraphQLFunctionsConfig::None => None,
_ => {
let fns = tx.all_db_functions(db_def.namespace_id, db_def.database_id, None).await?;
match graphql_config.functions {
GraphQLFunctionsConfig::None => None,
GraphQLFunctionsConfig::Auto => Some(fns),
GraphQLFunctionsConfig::Include(inc) => Some(
fns.iter()
.filter(|f| inc.iter().any(|n| n.as_str() == f.name.as_str()))
.cloned()
.collect(),
),
GraphQLFunctionsConfig::Exclude(exc) => Some(
fns.iter()
.filter(|f| !exc.iter().any(|n| n.as_str() == f.name.as_str()))
.cloned()
.collect(),
),
}
}
};
let mut query = Object::new("Query");
let mut types: Vec<Type> = Vec::new();
trace!(ns, db, ?tbs, ?fns, "generating schema");
let has_tables = tbs.as_ref().is_some_and(|t| !t.is_empty());
let has_fns = fns.as_ref().is_some_and(|f| !f.is_empty());
if !has_tables && !has_fns {
return Err(schema_error(
"no items found in database: GraphQL requires at least one table or function",
));
}
let relations = match &tbs {
Some(tbs) => collect_relations(tbs),
None => Vec::new(),
};
let schema_ctx = SchemaContext {
tx: &tx,
ns: db_def.namespace_id,
db: db_def.database_id,
datastore,
};
let mut mutation_obj: Option<Object> = None;
let mut subscription_obj = None;
match tbs {
Some(ref tbs) if !tbs.is_empty() => {
let mut table_fields: HashMap<TableName, Arc<[FieldDefinition]>> = HashMap::new();
query = process_tbs(
Arc::clone(tbs),
query,
&mut types,
&schema_ctx,
&relations,
&mut table_fields,
)
.await?;
mutation_obj = Some(process_mutations(Arc::clone(tbs), &mut types, &schema_ctx).await?);
subscription_obj = process_subscriptions(&tbs[..], &table_fields);
}
_ => {}
}
{
let accesses = tx.all_db_accesses(db_def.namespace_id, db_def.database_id, None).await?;
if !accesses.is_empty() {
let mut auth_mutation = mutation_obj.take().unwrap_or_else(|| Object::new("Mutation"));
auth_mutation = add_auth_mutations(auth_mutation, &accesses, ns, db, datastore);
mutation_obj = Some(auth_mutation);
}
}
if let Some(fns) = fns {
query = process_fns(fns, query, &mut types, datastore).await?;
}
register_geometry_types(&mut types);
register_filter_helper_types(&mut types);
trace!("current Query object for schema: {:?}", query);
let has_mutation = mutation_obj.is_some();
let mutation_name = if has_mutation {
Some("Mutation")
} else {
None
};
let subscription_name = if subscription_obj.is_some() {
Some("Subscription")
} else {
None
};
let mut schema = Schema::build("Query", mutation_name, subscription_name).register(query);
if let Some(depth) = graphql_config.depth_limit {
schema = schema.limit_depth(depth as usize);
}
if let Some(complexity) = graphql_config.complexity_limit {
schema = schema.limit_complexity(complexity as usize);
}
if matches!(graphql_config.introspection, GraphQLIntrospectionConfig::None) {
schema = schema.disable_introspection();
}
if let Some(mutation) = mutation_obj {
schema = schema.register(mutation);
}
if let Some(subscription) = subscription_obj {
schema = schema.register(subscription);
}
for ty in types {
trace!("adding type: {ty:?}");
schema = schema.register(ty);
}
macro_rules! scalar_debug_validated {
($schema:ident, $name:expr_2021, $kind:expr_2021) => {
scalar_debug_validated!(
$schema,
$name,
$kind,
::std::option::Option::<&str>::None,
::std::option::Option::<&str>::None
)
};
($schema:ident, $name:expr_2021, $kind:expr_2021, $desc:literal) => {
scalar_debug_validated!($schema, $name, $kind, std::option::Option::Some($desc), None)
};
($schema:ident, $name:expr_2021, $kind:expr_2021, $desc:literal, $url:literal) => {
scalar_debug_validated!(
$schema,
$name,
$kind,
std::option::Option::Some($desc),
Some($url)
)
};
($schema:ident, $name:expr_2021, $kind:expr_2021, $desc:expr_2021, $url:expr_2021) => {{
let new_type = Type::Scalar({
let mut tmp = Scalar::new($name);
if let Some(desc) = $desc {
tmp = tmp.description(desc);
}
if let Some(url) = $url {
tmp = tmp.specified_by_url(url);
}
#[cfg(debug_assertions)]
tmp.add_validator(|v| graphql_to_sql_kind(v, $kind).is_ok());
tmp
});
$schema = $schema.register(new_type);
}};
}
scalar_debug_validated!(
schema,
"uuid",
Kind::Uuid,
"String encoded UUID",
"https://datatracker.ietf.org/doc/html/rfc4122"
);
scalar_debug_validated!(schema, "decimal", Kind::Decimal);
scalar_debug_validated!(schema, "number", Kind::Number);
scalar_debug_validated!(schema, "null", Kind::Null);
scalar_debug_validated!(schema, "datetime", Kind::Datetime);
scalar_debug_validated!(schema, "duration", Kind::Duration);
scalar_debug_validated!(schema, "bytes", Kind::Bytes);
scalar_debug_validated!(schema, "object", Kind::Object);
scalar_debug_validated!(schema, "any", Kind::Any);
schema = schema.register(Type::Scalar(
Scalar::new("JSON")
.description("Arbitrary JSON value (object, array, string, number, or boolean)"),
));
let id_interface =
Interface::new("record").field(InterfaceField::new("id", TypeRef::named_nn(TypeRef::ID)));
schema = schema.register(id_interface);
let relation_interface = Interface::new("relation")
.field(InterfaceField::new("id", TypeRef::named_nn(TypeRef::ID)))
.field(InterfaceField::new("in", TypeRef::named_nn("record")))
.field(InterfaceField::new("out", TypeRef::named_nn("record")))
.implement("record");
schema = schema.register(relation_interface);
schema
.finish()
.map_err(|e| schema_error(format!("there was an error generating schema: {e:?}")))
}
pub(crate) fn sql_value_to_graphql_value(v: SurValue) -> Result<GraphqlValue, GraphqlError> {
let out = match v {
SurValue::None => GraphqlValue::Null,
SurValue::Null => GraphqlValue::Null,
SurValue::Bool(b) => GraphqlValue::Boolean(b),
SurValue::Number(n) => match n {
SurNumber::Int(i) => GraphqlValue::Number(i.into()),
SurNumber::Float(f) => GraphqlValue::Number(
Number::from_f64(f)
.ok_or(resolver_error("unimplemented: graceful NaN and Inf handling"))?,
),
num @ SurNumber::Decimal(_) => GraphqlValue::String(num.to_string()),
},
SurValue::String(s) => GraphqlValue::String(s.into_string()),
SurValue::Duration(d) => GraphqlValue::String(d.to_string()),
SurValue::Datetime(d) => GraphqlValue::String(d.to_rfc3339()),
SurValue::Uuid(uuid) => GraphqlValue::String(uuid.to_string()),
SurValue::Array(a) => {
let items: Result<Vec<GraphqlValue>, GraphqlError> =
a.into_iter().map(sql_value_to_graphql_value).collect();
GraphqlValue::List(items?)
}
SurValue::Object(o) => {
let entries: Result<IndexMap<Name, GraphqlValue>, GraphqlError> =
o.0.into_iter()
.map(|(k, v)| sql_value_to_graphql_value(v).map(|gv| (Name::new(k), gv)))
.collect();
GraphqlValue::Object(entries?)
}
SurValue::Geometry(ref g) => return geometry_to_graphql_object(g),
SurValue::Bytes(b) => {
use base64::Engine;
GraphqlValue::String(base64::engine::general_purpose::STANDARD.encode(b.into_inner()))
}
SurValue::RecordId(t) => GraphqlValue::String(record_id_to_raw(&t)),
v => GraphqlValue::String(v.to_raw_string()),
};
Ok(out)
}
pub(crate) fn sql_value_to_graphql_value_with_kind(
v: SurValue,
kind: Option<&Kind>,
enum_scope: Option<&str>,
) -> Result<GraphqlValue, GraphqlError> {
if let SurValue::String(s) = &v
&& let Some(Kind::Either(ks)) = kind
{
for k in ks {
if let Kind::Literal(KindLiteral::String(lit)) = k
&& lit.as_str() == s.as_str()
{
return Ok(GraphqlValue::Enum(Name::new(literal_enum_item_name(enum_scope, lit))));
}
}
}
sql_value_to_graphql_value(v)
}
fn record_id_to_raw(t: &SurRecordId) -> String {
let key_str = match &t.key {
SurRecordIdKey::Number(n) => n.to_string(),
SurRecordIdKey::String(s) => s.to_string(),
SurRecordIdKey::Uuid(u) => u.to_string(),
_ => t.key.to_sql(),
};
format!("{}:{}", t.table, key_str)
}
fn sanitize_graphql_identifier_component(raw: &str) -> String {
let mut out = String::with_capacity(raw.len() + 1);
let mut last_was_underscore = false;
for c in raw.chars() {
if c.is_ascii_alphanumeric() {
out.push(c.to_ascii_uppercase());
last_was_underscore = false;
} else if !last_was_underscore && !out.is_empty() {
out.push('_');
last_was_underscore = true;
}
}
while out.ends_with('_') {
out.pop();
}
if out.is_empty() || !out.as_bytes()[0].is_ascii_alphabetic() {
out.insert(0, 'V');
}
out
}
fn literal_enum_item_name(scope: Option<&str>, literal: &str) -> String {
let literal_component = sanitize_graphql_identifier_component(literal);
match scope {
Some(s) if !s.is_empty() => {
let scope_component = sanitize_graphql_identifier_component(s);
format!("{scope_component}_{literal_component}")
}
_ => format!("VALUE_{literal_component}"),
}
}
fn literal_enum_type_name(scope: Option<&str>, literals: &[String]) -> String {
let parts: Vec<String> =
literals.iter().map(|s| sanitize_graphql_identifier_component(s)).collect();
let joined = parts.join("_OR_");
match scope {
Some(s) if !s.is_empty() => {
let scope_component = sanitize_graphql_identifier_component(s);
format!("{scope_component}_ENUM_{joined}")
}
_ => format!("Literal_{joined}"),
}
}
fn enum_token_to_literal(ks: &[Kind], token: &str, enum_scope: Option<&str>) -> Option<String> {
for kind in ks {
if let Kind::Literal(KindLiteral::String(lit)) = kind {
let expected = literal_enum_item_name(enum_scope, lit);
if expected == token {
return Some(lit.as_str().to_owned());
}
}
}
None
}
fn literal_to_base_kind(literal: &KindLiteral) -> Kind {
match literal {
KindLiteral::String(_) => Kind::String,
KindLiteral::Integer(_) => Kind::Int,
KindLiteral::Float(_) => Kind::Float,
KindLiteral::Decimal(_) => Kind::Decimal,
KindLiteral::Duration(_) => Kind::Duration,
KindLiteral::Bool(_) => Kind::Bool,
KindLiteral::Object(_) => Kind::Object,
KindLiteral::Array(kinds) => {
let len = Some(kinds.len() as u64);
if let Some(first) = kinds.first()
&& kinds.iter().all(|k| k == first)
{
Kind::Array(Box::new(first.clone()), len)
} else {
Kind::Array(Box::new(Kind::Any), len)
}
}
}
}
pub fn kind_to_type(
kind: Kind,
types: &mut Vec<Type>,
is_input: bool,
) -> Result<TypeRef, GraphqlError> {
kind_to_type_with_enum_prefix(kind, types, is_input, None)
}
const MAX_ARRAY_DEPTH: usize = 3;
pub fn kind_to_type_with_enum_prefix(
kind: Kind,
types: &mut Vec<Type>,
is_input: bool,
enum_scope: Option<&str>,
) -> Result<TypeRef, GraphqlError> {
kind_to_type_inner(kind, types, is_input, enum_scope, 0)
}
fn kind_to_type_inner(
kind: Kind,
types: &mut Vec<Type>,
is_input: bool,
enum_scope: Option<&str>,
array_depth: usize,
) -> Result<TypeRef, GraphqlError> {
let optional = kind.can_be_none();
let out_ty = match kind {
Kind::Any => TypeRef::named("any"),
Kind::None => TypeRef::named("none"),
Kind::Null => TypeRef::named("null"),
Kind::Bool => TypeRef::named(TypeRef::BOOLEAN),
Kind::Bytes => TypeRef::named("bytes"),
Kind::Datetime => TypeRef::named("datetime"),
Kind::Decimal => TypeRef::named("decimal"),
Kind::Duration => TypeRef::named("duration"),
Kind::Float => TypeRef::named(TypeRef::FLOAT),
Kind::Int => TypeRef::named(TypeRef::INT),
Kind::Number => TypeRef::named("number"),
Kind::Object => TypeRef::named("object"),
Kind::Regex => return Err(schema_error("Kind::Regex is not yet supported")),
Kind::String => TypeRef::named(TypeRef::STRING),
Kind::Uuid => TypeRef::named("uuid"),
Kind::Table(ref _t) => TypeRef::named(kind.to_sql()),
Kind::Record(mut tables) => match tables.len() {
0 => TypeRef::named("record"),
1 => TypeRef::named(tables.pop().expect("single table in record kind").into_string()),
_ => {
let ty_name = tables.join("_or_");
let mut tmp_union = Union::new(ty_name.clone())
.description(format!("A record which is one of: {}", tables.join(", ")));
for n in tables {
tmp_union = tmp_union.possible_type(n.into_string());
}
types.push(Type::Union(tmp_union));
TypeRef::named(ty_name)
}
},
Kind::Geometry(ref geo_kinds) => {
if is_input {
match geo_kinds.len() {
0 => TypeRef::named("GeometryInput"),
1 => TypeRef::named(geometry_kind_to_graphql_input_type_name(&geo_kinds[0])),
_ => {
TypeRef::named("GeometryInput")
}
}
} else {
match geo_kinds.len() {
0 => TypeRef::named("Geometry"),
1 => TypeRef::named(geometry_kind_to_graphql_type_name(&geo_kinds[0])),
_ => {
let names: Vec<&str> =
geo_kinds.iter().map(geometry_kind_to_graphql_type_name).collect();
let ty_name = names.join("_or_");
let mut partial_union = Union::new(ty_name.clone()).description(format!(
"A geometry which is one of: {}",
names.join(", ")
));
for name in &names {
partial_union = partial_union.possible_type(*name);
}
types.push(Type::Union(partial_union));
TypeRef::named(ty_name)
}
}
}
}
Kind::Either(ks) => {
let ks: Vec<Kind> =
ks.into_iter().filter(|k| !matches!(k, Kind::None | Kind::Null)).collect();
if ks.is_empty() {
return Ok(TypeRef::named("null"));
}
if ks.len() == 1 {
let inner = ks.into_iter().next().expect("checked len == 1");
let inner_ty = kind_to_type_inner(inner, types, is_input, enum_scope, array_depth)?;
return Ok(match optional {
true => unwrap_type(inner_ty),
false => inner_ty,
});
}
let (ls, others): (Vec<Kind>, Vec<Kind>) =
ks.into_iter().partition(|k| matches!(k, Kind::Literal(KindLiteral::String(_))));
let enum_ty = if !ls.is_empty() {
let vals: Vec<String> = ls
.into_iter()
.map(|l| {
let Kind::Literal(KindLiteral::String(out)) = l else {
unreachable!(
"just checked that this is a Kind::Literal(Literal::String(_))"
);
};
out.into_string()
})
.collect();
let enum_name = literal_enum_type_name(enum_scope, &vals);
let enum_items: Vec<String> =
vals.iter().map(|v| literal_enum_item_name(enum_scope, v)).collect();
let mut tmp = Enum::new(enum_name);
tmp = tmp.items(enum_items);
let enum_ty = tmp.type_name().to_string();
types.push(Type::Enum(tmp));
if others.is_empty() {
return Ok(TypeRef::named(enum_ty));
}
Some(enum_ty)
} else {
None
};
let pos_names: Result<Vec<TypeRef>, GraphqlError> = others
.into_iter()
.map(|k| kind_to_type_inner(k, types, is_input, enum_scope, array_depth))
.collect();
let pos_names: Vec<String> = pos_names?.into_iter().map(|tr| tr.to_string()).collect();
let ty_name = pos_names.join("_or_");
let mut tmp_union = Union::new(ty_name.clone());
for n in pos_names {
tmp_union = tmp_union.possible_type(n);
}
if let Some(ty) = enum_ty {
tmp_union = tmp_union.possible_type(ty);
}
types.push(Type::Union(tmp_union));
TypeRef::named(ty_name)
}
Kind::Set(_, _) => return Err(schema_error("Kind::Set is not yet supported")),
Kind::Array(k, _) => {
if array_depth >= MAX_ARRAY_DEPTH {
TypeRef::named("JSON")
} else {
TypeRef::List(Box::new(kind_to_type_inner(
*k,
types,
is_input,
enum_scope,
array_depth + 1,
)?))
}
}
Kind::Function(_, _) => return Err(schema_error("Kind::Function is not yet supported")),
Kind::Range => return Err(schema_error("Kind::Range is not yet supported")),
Kind::Literal(literal) => {
return kind_to_type_inner(
literal_to_base_kind(&literal),
types,
is_input,
enum_scope,
array_depth,
);
}
Kind::File(_) => return Err(schema_error("Kind::File is not yet supported")),
};
let out = match optional {
true => out_ty,
false => TypeRef::NonNull(Box::new(out_ty)),
};
Ok(out)
}
pub fn unwrap_type(ty: TypeRef) -> TypeRef {
match ty {
TypeRef::NonNull(t) => unwrap_type(*t),
_ => ty,
}
}
pub fn filter_type_name(ty: &TypeRef) -> String {
match ty {
TypeRef::Named(n) => n.to_string(),
TypeRef::NonNull(inner) => filter_type_name(inner),
TypeRef::List(inner) => format!("list_{}", filter_type_name(inner)),
}
}
macro_rules! either_try_kind {
($ks:ident, $val:expr_2021, Kind::Array) => {
for arr_kind in $ks.iter().filter(|k| matches!(k, Kind::Array(_, _))).cloned() {
either_try_kind!($ks, $val, arr_kind);
}
};
($ks:ident, $val:expr_2021, Array) => {
for arr_kind in $ks.iter().filter(|k| matches!(k, Kind::Array(_, _))).cloned() {
either_try_kind!($ks, $val, arr_kind);
}
};
($ks:ident, $val:expr_2021, Record) => {
for rec_kind in $ks.iter().filter(|k| matches!(k, Kind::Record(_))).cloned() {
either_try_kind!($ks, $val, rec_kind);
}
};
($ks:ident, $val:expr_2021, AllNumbers) => {
either_try_kind!($ks, $val, Kind::Int);
either_try_kind!($ks, $val, Kind::Float);
either_try_kind!($ks, $val, Kind::Decimal);
either_try_kind!($ks, $val, Kind::Number);
};
($ks:ident, $val:expr_2021, $kind:expr_2021) => {
if $ks.contains(&$kind) {
if let Ok(out) = graphql_to_sql_kind($val, $kind) {
return Ok(out);
}
}
};
}
macro_rules! either_try_kinds {
($ks:ident, $val:expr_2021, $($kind:tt),+) => {
$(either_try_kind!($ks, $val, $kind));+
};
}
macro_rules! any_try_kind {
($val:expr_2021, $kind:expr_2021) => {
if let Ok(out) = graphql_to_sql_kind($val, $kind) {
return Ok(out);
}
};
}
macro_rules! any_try_kinds {
($val:expr_2021, $($kind:tt),+) => {
$(any_try_kind!($val, $kind));+
};
}
fn convert_static_record_id_key(
key: crate::expr::RecordIdKeyLit,
) -> Result<SurRecordIdKey, GraphqlError> {
use crate::expr::RecordIdKeyLit;
match key {
RecordIdKeyLit::Number(n) => Ok(SurRecordIdKey::Number(n)),
RecordIdKeyLit::String(s) => Ok(SurRecordIdKey::String(s)),
RecordIdKeyLit::Uuid(u) => Ok(SurRecordIdKey::Uuid(u)),
RecordIdKeyLit::Array(exprs) => {
let vals: Result<Vec<SurValue>, GraphqlError> =
exprs.into_iter().map(convert_static_expr).collect();
Ok(SurRecordIdKey::Array(SurArray(vals?)))
}
RecordIdKeyLit::Object(entries) => {
let mut map = BTreeMap::new();
for entry in entries {
map.insert(entry.key, convert_static_expr(entry.value)?);
}
Ok(SurRecordIdKey::Object(SurObject::from(map)))
}
RecordIdKeyLit::Generate(_) => Err(resolver_error(
"Generated RecordId keys (rand(), ulid(), uuid()) are not supported in GraphQL inputs",
)),
RecordIdKeyLit::Range(_) => {
Err(resolver_error("RecordId key ranges are not supported in GraphQL"))
}
}
}
fn convert_static_expr(expr: Expr) -> Result<SurValue, GraphqlError> {
match expr {
Expr::Literal(lit) => convert_static_literal(lit),
Expr::Table(t) => Ok(SurValue::Table(t)),
_ => Err(resolver_error("Only literal values are supported in GraphQL inputs")),
}
}
fn convert_static_literal(lit: Literal) -> Result<SurValue, GraphqlError> {
match lit {
Literal::None => Ok(SurValue::None),
Literal::Null => Ok(SurValue::Null),
Literal::Bool(b) => Ok(SurValue::Bool(b)),
Literal::Float(f) => Ok(SurValue::Number(SurNumber::Float(f))),
Literal::Integer(i) => Ok(SurValue::Number(SurNumber::Int(i))),
Literal::Decimal(d) => Ok(SurValue::Number(SurNumber::Decimal(d))),
Literal::String(s) => Ok(SurValue::String(s)),
Literal::Bytes(b) => Ok(SurValue::Bytes(b)),
Literal::Regex(r) => Ok(SurValue::Regex(r)),
Literal::RecordId(record_id_lit) => {
let key = convert_static_record_id_key(record_id_lit.key)?;
Ok(SurValue::RecordId(SurRecordId {
table: record_id_lit.table,
key,
}))
}
Literal::Array(exprs) => {
let vals: Result<Vec<SurValue>, GraphqlError> =
exprs.into_iter().map(convert_static_expr).collect();
Ok(SurValue::Array(SurArray(vals?)))
}
Literal::Set(exprs) => {
let vals: Result<Vec<SurValue>, GraphqlError> =
exprs.into_iter().map(convert_static_expr).collect();
Ok(SurValue::Set(SurSet::from(vals?)))
}
Literal::Object(entries) => {
let mut map = BTreeMap::new();
for entry in entries {
map.insert(entry.key, convert_static_expr(entry.value)?);
}
Ok(SurValue::Object(SurObject::from(map)))
}
Literal::Duration(d) => Ok(SurValue::Duration(d)),
Literal::Datetime(dt) => Ok(SurValue::Datetime(dt)),
Literal::Uuid(u) => Ok(SurValue::Uuid(u)),
Literal::Geometry(g) => Ok(SurValue::Geometry(g)),
Literal::File(f) => Ok(SurValue::File(f)),
Literal::UnboundedRange => {
Err(resolver_error("Unbounded ranges are not supported in GraphQL"))
}
}
}
pub(crate) fn graphql_to_sql_kind(
val: &GraphqlValue,
kind: Kind,
) -> Result<SurValue, GraphqlError> {
graphql_to_sql_kind_with_scope(val, kind, None)
}
pub(crate) fn graphql_to_sql_kind_with_scope(
val: &GraphqlValue,
kind: Kind,
enum_scope: Option<&str>,
) -> Result<SurValue, GraphqlError> {
use crate::syn;
match kind {
Kind::Any => match val {
GraphqlValue::String(s) => {
use Kind::*;
any_try_kinds!(val, Datetime, Duration, Uuid);
Ok(SurValue::String(s.as_str().into()))
}
GraphqlValue::Null => Ok(SurValue::Null),
obj @ GraphqlValue::Object(_) => graphql_to_sql_kind(obj, Kind::Object),
num @ GraphqlValue::Number(_) => graphql_to_sql_kind(num, Kind::Number),
GraphqlValue::Boolean(b) => Ok(SurValue::Bool(*b)),
bin @ GraphqlValue::Binary(_) => graphql_to_sql_kind(bin, Kind::Bytes),
GraphqlValue::Enum(s) => Ok(SurValue::String(s.as_str().into())),
arr @ GraphqlValue::List(_) => {
graphql_to_sql_kind(arr, Kind::Array(Box::new(Kind::Any), None))
}
},
Kind::None => match val {
GraphqlValue::Null => Ok(SurValue::None),
_ => Err(type_error(kind, val)),
},
Kind::Null => match val {
GraphqlValue::Null => Ok(SurValue::Null),
_ => Err(type_error(kind, val)),
},
Kind::Bool => match val {
GraphqlValue::Boolean(b) => Ok(SurValue::Bool(*b)),
_ => Err(type_error(kind, val)),
},
Kind::Bytes => match val {
GraphqlValue::Binary(b) => Ok(SurValue::Bytes(bytes::Bytes::copy_from_slice(b).into())),
GraphqlValue::String(s) => {
use base64::Engine;
let decoded = base64::engine::general_purpose::STANDARD
.decode(s.as_bytes())
.map_err(|_| type_error(kind, val))?;
Ok(SurValue::Bytes(bytes::Bytes::from(decoded).into()))
}
_ => Err(type_error(kind, val)),
},
Kind::Datetime => match val {
GraphqlValue::String(s) => match syn::datetime(s) {
Ok(dt) => Ok(SurValue::Datetime(dt.into())),
Err(_) => Err(type_error(kind, val)),
},
_ => Err(type_error(kind, val)),
},
Kind::Decimal => match val {
GraphqlValue::Number(n) => {
if let Some(int) = n.as_i64() {
Ok(SurValue::Number(SurNumber::Decimal(int.into())))
} else if let Some(d) = n.as_f64().and_then(Decimal::from_f64) {
Ok(SurValue::Number(SurNumber::Decimal(d)))
} else if let Some(uint) = n.as_u64() {
Ok(SurValue::Number(SurNumber::Decimal(uint.into())))
} else {
Err(type_error(kind, val))
}
}
GraphqlValue::String(s) => {
if let Ok(d) = s.parse::<Decimal>() {
return Ok(SurValue::Number(SurNumber::Decimal(d)));
}
let decimal_expr: Expr = syn::expr(s)?.into();
match decimal_expr {
Expr::Literal(Literal::Decimal(d)) => {
Ok(SurValue::Number(SurNumber::Decimal(d)))
}
_ => Err(type_error(kind, val)),
}
}
_ => Err(type_error(kind, val)),
},
Kind::Duration => match val {
GraphqlValue::String(s) => match syn::duration(s) {
Ok(d) => Ok(SurValue::Duration(d.into())),
Err(_) => Err(type_error(kind, val)),
},
_ => Err(type_error(kind, val)),
},
Kind::Float => match val {
GraphqlValue::Number(n) => {
if let Some(i) = n.as_i64() {
Ok(SurValue::Number(SurNumber::Float(i as f64)))
} else if let Some(f) = n.as_f64() {
Ok(SurValue::Number(SurNumber::Float(f)))
} else if let Some(uint) = n.as_u64() {
Ok(SurValue::Number(SurNumber::Float(uint as f64)))
} else {
unreachable!("serde_json::Number must be either i64, u64 or f64")
}
}
GraphqlValue::String(s) => {
let float_expr: Expr = syn::expr(s)?.into();
match float_expr {
Expr::Literal(Literal::Float(f)) => Ok(SurValue::Number(SurNumber::Float(f))),
_ => Err(type_error(kind, val)),
}
}
_ => Err(type_error(kind, val)),
},
Kind::Int => match val {
GraphqlValue::Number(n) => {
if let Some(i) = n.as_i64() {
Ok(SurValue::Number(SurNumber::Int(i)))
} else {
Err(type_error(kind, val))
}
}
GraphqlValue::String(s) => {
let int_expr: Expr = syn::expr(s)?.into();
match int_expr {
Expr::Literal(Literal::Integer(i)) => Ok(SurValue::Number(SurNumber::Int(i))),
_ => Err(type_error(kind, val)),
}
}
_ => Err(type_error(kind, val)),
},
Kind::Number => match val {
GraphqlValue::Number(n) => {
if let Some(i) = n.as_i64() {
Ok(SurValue::Number(SurNumber::Int(i)))
} else if let Some(f) = n.as_f64() {
Ok(SurValue::Number(SurNumber::Float(f)))
} else if let Some(uint) = n.as_u64() {
Ok(SurValue::Number(SurNumber::Decimal(uint.into())))
} else {
unreachable!("serde_json::Number must be either i64, u64 or f64")
}
}
GraphqlValue::String(s) => {
let number_expr: Expr = syn::expr(s)?.into();
match number_expr {
Expr::Literal(Literal::Integer(i)) => Ok(SurValue::Number(SurNumber::Int(i))),
Expr::Literal(Literal::Float(f)) => Ok(SurValue::Number(SurNumber::Float(f))),
Expr::Literal(Literal::Decimal(d)) => {
Ok(SurValue::Number(SurNumber::Decimal(d)))
}
_ => Err(type_error(kind, val)),
}
}
_ => Err(type_error(kind, val)),
},
Kind::Object => match val {
GraphqlValue::Object(o) => {
let out: Result<BTreeMap<String, SurValue>, GraphqlError> = o
.iter()
.map(|(k, v)| {
graphql_to_sql_kind(v, Kind::Any).map(|sqlv| (k.to_string(), sqlv))
})
.collect();
Ok(SurValue::Object(out?.into()))
}
GraphqlValue::String(s) => {
let expr = syn::expr_legacy_strand(s.as_str())?;
let expr: Expr = expr.into();
match expr {
Expr::Literal(Literal::Object(o)) => {
let mut map = BTreeMap::new();
for entry in o {
map.insert(entry.key, convert_static_expr(entry.value)?);
}
Ok(SurValue::Object(SurObject::from(map)))
}
_ => Err(type_error(kind, val)),
}
}
_ => Err(type_error(kind, val)),
},
Kind::String => match val {
GraphqlValue::String(s) => Ok(SurValue::String(s.as_str().into())),
GraphqlValue::Enum(s) => Ok(SurValue::String(s.as_str().into())),
_ => Err(type_error(kind, val)),
},
Kind::Uuid => match val {
GraphqlValue::String(s) => match s.parse::<uuid::Uuid>() {
Ok(u) => Ok(SurValue::Uuid(u.into())),
Err(_) => Err(type_error(kind, val)),
},
_ => Err(type_error(kind, val)),
},
Kind::Table(ref ts) => match val {
GraphqlValue::String(s) => match ts.contains(&s.as_str().into()) {
true => Ok(SurValue::Table(TableName::new(s.clone()))),
false => Err(type_error(kind, val)),
},
_ => Err(type_error(kind, val)),
},
Kind::Record(ref ts) => match val {
GraphqlValue::String(s) => {
let record_id_expr = syn::expr(s)?;
let record_id_expr: Expr = record_id_expr.into();
match record_id_expr {
Expr::Literal(Literal::RecordId(t)) => {
let table_allowed = ts.is_empty() || ts.contains(&t.table);
if table_allowed {
let key = convert_static_record_id_key(t.key)?;
Ok(SurValue::RecordId(SurRecordId {
table: t.table,
key,
}))
} else {
Err(type_error(kind, val))
}
}
_ => Err(type_error(kind, val)),
}
}
_ => Err(type_error(kind, val)),
},
Kind::Geometry(ref geo_kinds) => match val {
GraphqlValue::Object(obj) => {
let geometry = graphql_geometry_from_object(obj, geo_kinds)?;
Ok(SurValue::Geometry(geometry))
}
_ => Err(type_error(kind, val)),
},
Kind::Either(ks) => {
let ks = if ks.iter().any(|k| matches!(k, Kind::Either(_))) {
let flat = Kind::either(ks);
let Kind::Either(ks) = flat else {
return graphql_to_sql_kind_with_scope(val, flat, enum_scope);
};
ks
} else {
ks
};
use Kind::*;
match val {
GraphqlValue::Null => {
if ks.contains(&Kind::None) {
Ok(SurValue::None)
} else if ks.contains(&Kind::Null) {
Ok(SurValue::Null)
} else {
Err(type_error(Kind::Either(ks), val))
}
}
num @ GraphqlValue::Number(_) => {
either_try_kind!(ks, num, AllNumbers);
Err(type_error(Kind::Either(ks), val))
}
string @ GraphqlValue::String(_) => {
either_try_kinds!(
ks, string, Datetime, Duration, AllNumbers, Uuid, Record, Array, Any,
String
);
either_try_kind!(ks, string, Kind::Object);
Err(type_error(Kind::Either(ks), val))
}
bool @ GraphqlValue::Boolean(_) => {
either_try_kind!(ks, bool, Kind::Bool);
Err(type_error(Kind::Either(ks), val))
}
GraphqlValue::Binary(_) => {
Err(resolver_error("binary input for Either is not yet supported"))
}
GraphqlValue::Enum(n) => {
if let Some(literal) = enum_token_to_literal(&ks, n.as_str(), enum_scope) {
return Ok(SurValue::String(literal.into()));
}
either_try_kind!(ks, &GraphqlValue::String(n.to_string()), Kind::String);
Err(type_error(Kind::Either(ks), val))
}
list @ GraphqlValue::List(_) => {
either_try_kind!(ks, list, Kind::Array);
Err(type_error(Kind::Either(ks), val))
}
obj @ GraphqlValue::Object(_) => {
for geo_kind in ks.iter().filter(|k| matches!(k, Kind::Geometry(_))).cloned() {
if let Ok(out) = graphql_to_sql_kind(obj, geo_kind) {
return Ok(out);
}
}
either_try_kind!(ks, obj, Kind::Object);
Err(type_error(Kind::Either(ks), val))
}
}
}
Kind::Set(_k, _n) => Err(resolver_error("Sets are not yet supported")),
Kind::Array(ref k, n) => match val {
GraphqlValue::List(l) => {
let list_iter = l.iter().map(|v| graphql_to_sql_kind(v, *k.to_owned()));
let list: Result<Vec<SurValue>, GraphqlError> = list_iter.collect();
match (list, n) {
(Ok(l), Some(n)) => {
if l.len() as u64 == n {
Ok(l.into())
} else {
Err(type_error(kind, val))
}
}
(Ok(l), None) => Ok(l.into()),
(Err(e), _) => Err(e),
}
}
_ => Err(type_error(kind, val)),
},
Kind::Function(_, _) => Err(resolver_error("Functions are not yet supported")),
Kind::Range => Err(resolver_error("Ranges are not yet supported")),
Kind::Literal(literal) => {
let base_kind = literal_to_base_kind(&literal);
let converted = graphql_to_sql_kind_with_scope(val, base_kind, enum_scope)?;
if literal.validate_value(&converted) {
Ok(converted)
} else {
Err(type_error(Kind::Literal(literal), val))
}
}
Kind::Regex => Err(resolver_error("Regexes are not yet supported")),
Kind::File(_) => Err(resolver_error("Files are not yet supported")),
}
}
pub(crate) fn geometry_kind_to_graphql_type_name(kind: &GeometryKind) -> &'static str {
match kind {
GeometryKind::Point => "GeometryPoint",
GeometryKind::Line => "GeometryLineString",
GeometryKind::Polygon => "GeometryPolygon",
GeometryKind::MultiPoint => "GeometryMultiPoint",
GeometryKind::MultiLine => "GeometryMultiLineString",
GeometryKind::MultiPolygon => "GeometryMultiPolygon",
GeometryKind::Collection => "GeometryCollection",
}
}
fn geometry_kind_to_graphql_input_type_name(kind: &GeometryKind) -> &'static str {
match kind {
GeometryKind::Point => "GeometryPointInput",
GeometryKind::Line => "GeometryLineStringInput",
GeometryKind::Polygon => "GeometryPolygonInput",
GeometryKind::MultiPoint => "GeometryMultiPointInput",
GeometryKind::MultiLine => "GeometryMultiLineStringInput",
GeometryKind::MultiPolygon => "GeometryMultiPolygonInput",
GeometryKind::Collection => "GeometryCollectionInput",
}
}
pub(crate) fn geometry_graphql_type_name(g: &SurGeometry) -> &'static str {
match g {
SurGeometry::Point(_) => "GeometryPoint",
SurGeometry::Line(_) => "GeometryLineString",
SurGeometry::Polygon(_) => "GeometryPolygon",
SurGeometry::MultiPoint(_) => "GeometryMultiPoint",
SurGeometry::MultiLine(_) => "GeometryMultiLineString",
SurGeometry::MultiPolygon(_) => "GeometryMultiPolygon",
SurGeometry::Collection(_) => "GeometryCollection",
}
}
fn make_geometry_object_type(obj_name: &str, geojson_type: &'static str) -> Object {
let coords_ty = TypeRef::named_nn("JSON");
Object::new(obj_name)
.field(Field::new("type", TypeRef::named_nn("GeometryType"), {
move |_ctx| {
FieldFuture::new(async move {
Ok(Some(FieldValue::value(GraphqlValue::Enum(Name::new(geojson_type)))))
})
}
}))
.field(Field::new("coordinates", coords_ty, |ctx| {
FieldFuture::new(async move {
let g = ctx.parent_value.try_downcast_ref::<SurGeometry>()?;
let coords = g.as_coordinates();
let graphql_coords = sql_value_to_graphql_value(coords)?;
Ok(Some(FieldValue::value(graphql_coords)))
})
}))
}
fn make_geometry_collection_type() -> Object {
Object::new("GeometryCollection")
.field(Field::new("type", TypeRef::named_nn("GeometryType"), |_ctx| {
FieldFuture::new(async move {
Ok(Some(FieldValue::value(GraphqlValue::Enum(Name::new("GeometryCollection")))))
})
}))
.field(Field::new("geometries", TypeRef::named_nn_list_nn("Geometry"), |ctx| {
FieldFuture::new(async move {
let g = ctx.parent_value.try_downcast_ref::<SurGeometry>()?;
match g {
SurGeometry::Collection(geometries) => {
let items: Vec<FieldValue> = geometries
.iter()
.map(|g| {
let type_name = geometry_graphql_type_name(g);
FieldValue::owned_any(g.clone()).with_type(type_name)
})
.collect();
Ok(Some(FieldValue::list(items)))
}
_ => Err(internal_error("Expected GeometryCollection value").into()),
}
})
}))
}
pub(crate) fn register_geometry_types(types: &mut Vec<Type>) {
types.push(Type::Enum(
Enum::new("GeometryType")
.description("GeoJSON geometry type discriminator")
.item("Point")
.item("LineString")
.item("Polygon")
.item("MultiPoint")
.item("MultiLineString")
.item("MultiPolygon")
.item("GeometryCollection"),
));
types.push(Type::Object(make_geometry_object_type("GeometryPoint", "Point")));
types.push(Type::Object(make_geometry_object_type("GeometryLineString", "LineString")));
types.push(Type::Object(make_geometry_object_type("GeometryPolygon", "Polygon")));
types.push(Type::Object(make_geometry_object_type("GeometryMultiPoint", "MultiPoint")));
types.push(Type::Object(make_geometry_object_type(
"GeometryMultiLineString",
"MultiLineString",
)));
types.push(Type::Object(make_geometry_object_type("GeometryMultiPolygon", "MultiPolygon")));
types.push(Type::Object(make_geometry_collection_type()));
types.push(Type::Union(
Union::new("Geometry")
.description("A GeoJSON geometry – one of Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, or GeometryCollection")
.possible_type("GeometryPoint")
.possible_type("GeometryLineString")
.possible_type("GeometryPolygon")
.possible_type("GeometryMultiPoint")
.possible_type("GeometryMultiLineString")
.possible_type("GeometryMultiPolygon")
.possible_type("GeometryCollection"),
));
let json_nn = || TypeRef::named_nn("JSON");
types.push(Type::InputObject(
InputObject::new("GeometryPointInput")
.description("GeoJSON Point input – coordinates is [lng, lat]")
.field(InputValue::new("coordinates", json_nn())),
));
types.push(Type::InputObject(
InputObject::new("GeometryLineStringInput")
.description("GeoJSON LineString input")
.field(InputValue::new("coordinates", json_nn())),
));
types.push(Type::InputObject(
InputObject::new("GeometryPolygonInput")
.description("GeoJSON Polygon input")
.field(InputValue::new("coordinates", json_nn())),
));
types.push(Type::InputObject(
InputObject::new("GeometryMultiPointInput")
.description("GeoJSON MultiPoint input")
.field(InputValue::new("coordinates", json_nn())),
));
types.push(Type::InputObject(
InputObject::new("GeometryMultiLineStringInput")
.description("GeoJSON MultiLineString input")
.field(InputValue::new("coordinates", json_nn())),
));
types.push(Type::InputObject(
InputObject::new("GeometryMultiPolygonInput")
.description("GeoJSON MultiPolygon input")
.field(InputValue::new("coordinates", json_nn())),
));
types.push(Type::InputObject(
InputObject::new("GeometryCollectionInput")
.description("GeoJSON GeometryCollection input")
.field(InputValue::new("geometries", TypeRef::named_nn_list_nn("GeometryInput"))),
));
types.push(Type::InputObject(
InputObject::new("GeometryInput")
.description(
"Generic GeoJSON geometry input. Use `type` to select the variant, \
`coordinates` for coordinate-based types, `geometries` for GeometryCollection.",
)
.field(InputValue::new("type", TypeRef::named_nn("GeometryType")))
.field(InputValue::new("coordinates", TypeRef::named("JSON")))
.field(InputValue::new("geometries", TypeRef::named_list("GeometryInput"))),
));
}
fn graphql_coords_to_sur_value(val: &GraphqlValue) -> Result<SurValue, GraphqlError> {
match val {
GraphqlValue::Number(n) => {
let f = n
.as_f64()
.ok_or_else(|| resolver_error("Invalid coordinate: expected finite number"))?;
Ok(SurValue::Number(SurNumber::Float(f)))
}
GraphqlValue::List(items) => {
let vals: Result<Vec<SurValue>, GraphqlError> =
items.iter().map(graphql_coords_to_sur_value).collect();
Ok(vals?.into())
}
_ => Err(resolver_error("Expected number or array in geometry coordinates")),
}
}
fn graphql_geometry_from_object(
obj: &IndexMap<Name, GraphqlValue>,
expected_kind: &[GeometryKind],
) -> Result<SurGeometry, GraphqlError> {
let geo_type: &str = if let Some(ty) = obj.get("type") {
match ty {
GraphqlValue::Enum(s) => s.as_str(),
GraphqlValue::String(s) => s.as_str(),
_ => return Err(resolver_error("Geometry 'type' must be a GeometryType enum value")),
}
} else if expected_kind.len() == 1 {
match &expected_kind[0] {
GeometryKind::Point => "Point",
GeometryKind::Line => "LineString",
GeometryKind::Polygon => "Polygon",
GeometryKind::MultiPoint => "MultiPoint",
GeometryKind::MultiLine => "MultiLineString",
GeometryKind::MultiPolygon => "MultiPolygon",
GeometryKind::Collection => "GeometryCollection",
}
} else {
return Err(resolver_error(
"Geometry input must include a 'type' field when multiple geometry types are allowed",
));
};
if !expected_kind.is_empty() {
let kind = match geo_type {
"Point" => GeometryKind::Point,
"LineString" => GeometryKind::Line,
"Polygon" => GeometryKind::Polygon,
"MultiPoint" => GeometryKind::MultiPoint,
"MultiLineString" => GeometryKind::MultiLine,
"MultiPolygon" => GeometryKind::MultiPolygon,
"GeometryCollection" => GeometryKind::Collection,
other => return Err(resolver_error(format!("Unknown geometry type: {other}"))),
};
if !expected_kind.contains(&kind) {
return Err(resolver_error(format!(
"Geometry type '{geo_type}' is not allowed here; expected one of: {}",
expected_kind
.iter()
.map(|k| geometry_kind_to_graphql_type_name(k))
.collect::<Vec<_>>()
.join(", ")
)));
}
}
match geo_type {
"Point" => {
let coords = obj
.get("coordinates")
.ok_or_else(|| resolver_error("Point requires 'coordinates' field"))?;
let sur_coords = graphql_coords_to_sur_value(coords)?;
SurGeometry::array_to_point(&sur_coords)
.map(SurGeometry::Point)
.ok_or_else(|| resolver_error("Invalid Point coordinates: expected [lng, lat]"))
}
"LineString" => {
let coords = obj
.get("coordinates")
.ok_or_else(|| resolver_error("LineString requires 'coordinates' field"))?;
let sur_coords = graphql_coords_to_sur_value(coords)?;
SurGeometry::array_to_line(&sur_coords).map(SurGeometry::Line).ok_or_else(|| {
resolver_error("Invalid LineString coordinates: expected [[lng, lat], ...]")
})
}
"Polygon" => {
let coords = obj
.get("coordinates")
.ok_or_else(|| resolver_error("Polygon requires 'coordinates' field"))?;
let sur_coords = graphql_coords_to_sur_value(coords)?;
SurGeometry::array_to_polygon(&sur_coords).map(SurGeometry::Polygon).ok_or_else(|| {
resolver_error("Invalid Polygon coordinates: expected [[[lng, lat], ...], ...]")
})
}
"MultiPoint" => {
let coords = obj
.get("coordinates")
.ok_or_else(|| resolver_error("MultiPoint requires 'coordinates' field"))?;
let sur_coords = graphql_coords_to_sur_value(coords)?;
SurGeometry::array_to_multipoint(&sur_coords).map(SurGeometry::MultiPoint).ok_or_else(
|| resolver_error("Invalid MultiPoint coordinates: expected [[lng, lat], ...]"),
)
}
"MultiLineString" => {
let coords = obj
.get("coordinates")
.ok_or_else(|| resolver_error("MultiLineString requires 'coordinates' field"))?;
let sur_coords = graphql_coords_to_sur_value(coords)?;
SurGeometry::array_to_multiline(&sur_coords).map(SurGeometry::MultiLine).ok_or_else(
|| {
resolver_error(
"Invalid MultiLineString coordinates: expected [[[lng, lat], ...], ...]",
)
},
)
}
"MultiPolygon" => {
let coords = obj
.get("coordinates")
.ok_or_else(|| resolver_error("MultiPolygon requires 'coordinates' field"))?;
let sur_coords = graphql_coords_to_sur_value(coords)?;
SurGeometry::array_to_multipolygon(&sur_coords)
.map(SurGeometry::MultiPolygon)
.ok_or_else(|| resolver_error("Invalid MultiPolygon coordinates"))
}
"GeometryCollection" => {
let graphql_geometries = obj
.get("geometries")
.ok_or_else(|| resolver_error("GeometryCollection requires 'geometries' field"))?;
let list = match graphql_geometries {
GraphqlValue::List(l) => l,
_ => {
return Err(resolver_error("GeometryCollection 'geometries' must be an array"));
}
};
let mut geometries = Vec::with_capacity(list.len());
for item in list {
match item {
GraphqlValue::Object(sub_obj) => {
geometries.push(graphql_geometry_from_object(sub_obj, &[])?);
}
_ => {
return Err(resolver_error(
"Each item in 'geometries' must be a geometry object",
));
}
}
}
Ok(SurGeometry::Collection(geometries))
}
other => Err(resolver_error(format!("Unknown geometry type: {other}"))),
}
}
pub(crate) fn geometry_to_graphql_object(g: &SurGeometry) -> Result<GraphqlValue, GraphqlError> {
let mut map = IndexMap::new();
map.insert(Name::new("type"), GraphqlValue::Enum(Name::new(g.as_type())));
match g {
SurGeometry::Collection(geometries) => {
let graphql_geometries: Result<Vec<GraphqlValue>, GraphqlError> =
geometries.iter().map(geometry_to_graphql_object).collect();
map.insert(Name::new("geometries"), GraphqlValue::List(graphql_geometries?));
}
_ => {
let coords = g.as_coordinates();
let graphql_coords = sql_value_to_graphql_value(coords)?;
map.insert(Name::new("coordinates"), graphql_coords);
}
}
Ok(GraphqlValue::Object(map))
}
#[cfg(test)]
mod tests {
use async_graphql::Number;
use super::*;
#[test]
fn either_flattens_nested_variants() {
let kind = Kind::Either(vec![Kind::Either(vec![Kind::Int, Kind::String]), Kind::Bool]);
let int_val = GraphqlValue::Number(Number::from(7));
assert_eq!(graphql_to_sql_kind(&int_val, kind.clone()).unwrap(), SurValue::from(7i64));
let str_val = GraphqlValue::String("hi".to_owned());
assert_eq!(graphql_to_sql_kind(&str_val, kind.clone()).unwrap(), SurValue::from("hi"));
let bool_val = GraphqlValue::Boolean(true);
assert_eq!(graphql_to_sql_kind(&bool_val, kind).unwrap(), SurValue::from(true));
}
#[test]
fn either_collapses_single_variant_after_dedup() {
let kind = Kind::Either(vec![Kind::Int, Kind::Either(vec![Kind::Int, Kind::Int])]);
let val = GraphqlValue::Number(Number::from(42));
assert_eq!(graphql_to_sql_kind(&val, kind).unwrap(), SurValue::from(42i64));
}
#[test]
fn decimal_string_round_trips() {
let decimal = SurValue::Number(SurNumber::Decimal("19.99".parse().unwrap()));
let serialized = sql_value_to_graphql_value(decimal.clone()).unwrap();
assert_eq!(serialized, GraphqlValue::String("19.99".to_owned()));
assert_eq!(graphql_to_sql_kind(&serialized, Kind::Decimal).unwrap(), decimal);
let literal = GraphqlValue::String("19.99dec".to_owned());
assert_eq!(graphql_to_sql_kind(&literal, Kind::Decimal).unwrap(), decimal);
}
}