use std::collections::{HashMap, HashSet};
use async_graphql::{
dynamic::{
Enum, Field, FieldFuture, FieldValue, InputObject, InputValue, Object,
ResolverContext, Scalar, Schema as DynamicSchema,
SchemaBuilder as DynamicSchemaBuilder, SchemaError, TypeRef,
},
Request,
};
use async_graphql_parser::types::{BaseType, Type};
use async_graphql_value::Name;
use fuel_indexer_database::{queries, IndexerConnectionPool};
use fuel_indexer_schema::db::tables::IndexerSchema;
use lazy_static::lazy_static;
use serde_json::Value;
use crate::graphql::{GraphqlError, GraphqlQueryBuilder, GraphqlResult};
lazy_static! {
static ref SCALAR_TYPES: HashSet<&'static str> = HashSet::from([
"Address",
"AssetId",
"Boolean",
"Bytes",
"Bytes32",
"Bytes4",
"Bytes64",
"Bytes8",
"ContractId",
"I128",
"I16",
"I32",
"I64",
"I8",
"ID",
"Identity",
"Json",
"U128",
"U16",
"U32",
"U64",
"U8",
"UID",
]);
static ref NUMERIC_SCALAR_TYPES: HashSet<&'static str> = HashSet::from([
"I128",
"I16",
"I32",
"I64",
"U128",
"U16",
"U32",
"U64",
]);
static ref STRING_SCALAR_TYPES: HashSet<&'static str> = HashSet::from([
"Address",
"AssetId",
"Bytes",
"Bytes32",
"Bytes4",
"Bytes64",
"Bytes64",
"Bytes8",
"ContractId",
"ID",
"Identity",
"Json",
"UID",
]);
static ref SORTABLE_SCALAR_TYPES: HashSet<&'static str> = HashSet::from([
"Address",
"AssetId",
"ContractId",
"I128",
"I16",
"I32",
"I64",
"ID",
"Identity",
"U128",
"U16",
"U32",
"U64",
"UID",
]);
static ref IGNORED_ENTITY_TYPES: HashSet<&'static str> =
HashSet::from(["IndexMetadataEntity"]);
static ref IGNORED_ENTITY_FIELD_TYPES: HashSet<&'static str> =
HashSet::from(["object"]);
}
pub async fn execute_query(
dynamic_request: Request,
dynamic_schema: DynamicSchema,
user_query: String,
pool: IndexerConnectionPool,
schema: IndexerSchema,
) -> GraphqlResult<Value> {
match dynamic_request.operation_name.as_deref() {
Some("IntrospectionQuery") | Some("introspectionquery") => {
let introspection_results = dynamic_schema.execute(dynamic_request).await;
let data = introspection_results.data.into_json()?;
Ok(data)
}
Some(_) | None => {
let query =
GraphqlQueryBuilder::new(&schema, user_query.as_str())?.build()?;
let queries = query.as_sql(&schema, pool.database_type())?.join(";\n");
let mut conn = match pool.acquire().await {
Ok(c) => c,
Err(e) => return Err(GraphqlError::QueryError(e.to_string())),
};
match queries::run_query(&mut conn, queries).await {
Ok(r) => Ok(r),
Err(e) => Err(GraphqlError::QueryError(e.to_string())),
}
}
}
}
pub fn build_dynamic_schema(schema: &IndexerSchema) -> GraphqlResult<DynamicSchema> {
let mut schema_builder: DynamicSchemaBuilder = SCALAR_TYPES.iter().fold(
DynamicSchema::build("QueryRoot", None, None).introspection_only(),
|sb, scalar| {
if *scalar == "Boolean" || *scalar == "ID" {
sb
} else {
sb.register(Scalar::new(*scalar))
}
},
);
let mut input_objects = Vec::new();
let mut filter_object_list = Vec::new();
let mut filter_tracker = HashMap::new();
let mut sort_object_list = Vec::new();
let mut sorter_tracker = HashMap::new();
let mut query_root = Object::new("QueryRoot");
let sort_enum = Enum::new("SortOrder").item("asc").item("desc");
for (entity_type, field_map) in schema.parsed().object_field_mappings() {
if IGNORED_ENTITY_TYPES.contains(&entity_type.as_str()) {
continue;
}
let mut filter_input_vals = Vec::new();
let mut sort_input_vals = Vec::new();
let mut object_field_enum = Enum::new(format!("{entity_type}Fields"));
for (field_name, field_type) in field_map.clone() {
if IGNORED_ENTITY_FIELD_TYPES.contains(&field_name.as_str()) {
continue;
}
let (field_filter_input_val, mut field_input_objects, sort_input_val) =
create_input_values_and_objects_for_field(
field_name.clone(),
field_type,
entity_type.clone(),
&sort_enum,
)?;
filter_input_vals.push(field_filter_input_val);
input_objects.append(&mut field_input_objects);
if let Some(input_val) = sort_input_val {
sort_input_vals.push(input_val);
}
object_field_enum = object_field_enum.item(field_name);
}
if !filter_input_vals.is_empty() {
let filter_object = filter_input_vals
.into_iter()
.fold(
InputObject::new(format!("{entity_type}Filter")),
|input_obj, input_val| input_obj.field(input_val),
)
.field(InputValue::new(
"has",
TypeRef::named_nn_list(object_field_enum.type_name()),
));
filter_object_list.push(filter_object);
filter_tracker.insert(entity_type.to_string(), filter_object_list.len() - 1);
}
if !sort_input_vals.is_empty() {
let sort_object = sort_input_vals.into_iter().fold(
InputObject::new(format!("{entity_type}Sort")),
|input_obj, input_val| input_obj.field(input_val),
);
sort_object_list.push(sort_object);
sorter_tracker.insert(entity_type.to_string(), sort_object_list.len() - 1);
}
let mut fields = Vec::new();
for (field_name, field_type) in field_map {
if IGNORED_ENTITY_FIELD_TYPES.contains(&field_name.as_str()) {
continue;
}
if let Some(field_def) = Type::new(field_type) {
let base_field_type = &field_def.base;
let nullable = field_def.nullable;
let field_type = match base_field_type {
BaseType::Named(type_name) => {
if nullable {
if schema.parsed().is_virtual_typedef(field_type) {
TypeRef::named(TypeRef::STRING)
} else {
TypeRef::named(type_name.to_string())
}
} else if schema.parsed().is_virtual_typedef(field_type) {
TypeRef::named_nn(TypeRef::STRING)
} else {
TypeRef::named_nn(type_name.to_string())
}
}
BaseType::List(list_type) => {
let inner_base_type = list_type.base.to_string();
let nullable_inner = list_type.nullable;
if nullable && nullable_inner {
TypeRef::named_list(inner_base_type)
} else if nullable && !nullable_inner {
TypeRef::named_nn_list(inner_base_type)
} else if !nullable && nullable_inner {
TypeRef::named_list_nn(inner_base_type)
} else {
TypeRef::named_nn_list_nn(inner_base_type)
}
}
};
let field = create_field_with_assoc_args(
field_name.to_string(),
field_type,
base_field_type,
&filter_tracker,
&filter_object_list,
&sorter_tracker,
&sort_object_list,
);
fields.push(field);
}
}
let obj = fields
.into_iter()
.fold(Object::new(entity_type.clone()), |obj, f| obj.field(f));
let field = create_field_with_assoc_args(
entity_type.to_string().to_lowercase(),
TypeRef::named(obj.type_name()),
&BaseType::Named(Name::new(obj.type_name())),
&filter_tracker,
&filter_object_list,
&sorter_tracker,
&sort_object_list,
);
if !SCALAR_TYPES.contains(&obj.type_name()) {
query_root = query_root.field(field);
}
schema_builder = schema_builder.register(obj).register(object_field_enum);
}
for filter_obj in filter_object_list {
schema_builder = schema_builder.register(filter_obj);
}
for sort_obj in sort_object_list {
schema_builder = schema_builder.register(sort_obj);
}
for io in input_objects {
schema_builder = schema_builder.register(io);
}
schema_builder = schema_builder.register(sort_enum);
schema_builder = schema_builder.register(query_root);
Ok(schema_builder.finish()?)
}
fn create_input_values_and_objects_for_field(
field_name: String,
field_type: String,
entity_type: String,
sort_enum: &Enum,
) -> GraphqlResult<(InputValue, Vec<InputObject>, Option<InputValue>)> {
let field_type =
Type::new(&field_type).ok_or(GraphqlError::DynamicSchemaBuildError(
SchemaError::from("Could not create type defintion from field type string"),
))?;
match field_type.base {
BaseType::Named(field_type) => {
let (field_filter_input_val, field_input_objects) =
create_filter_val_and_objects_for_field(
&field_name,
field_type.as_str(),
entity_type.as_str(),
);
if SORTABLE_SCALAR_TYPES.contains(field_type.as_str()) {
let sort_input_val =
InputValue::new(field_name, TypeRef::named(sort_enum.type_name()));
return Ok((
field_filter_input_val,
field_input_objects,
Some(sort_input_val),
));
}
Ok((field_filter_input_val, field_input_objects, None))
}
BaseType::List(_) => unimplemented!("List types are not currently supported"),
}
}
fn create_field_with_assoc_args(
field_name: String,
field_type_ref: TypeRef,
base_field_type: &BaseType,
filter_tracker: &HashMap<String, usize>,
filter_object_list: &[InputObject],
sorter_tracker: &HashMap<String, usize>,
sort_object_list: &[InputObject],
) -> Field {
let mut field =
Field::new(field_name, field_type_ref, move |_ctx: ResolverContext| {
return FieldFuture::new(async move { Ok(Some(FieldValue::value(1))) });
});
match base_field_type {
BaseType::Named(field_type) => {
if !SCALAR_TYPES.contains(field_type.as_str()) {
if let Some(idx) = filter_tracker.get(&field_type.to_string()) {
let object_filter_arg = InputValue::new(
"filter",
TypeRef::named(filter_object_list[*idx].type_name()),
);
field = field.argument(object_filter_arg)
}
if let Some(idx) = sorter_tracker.get(&field_type.to_string()) {
let object_sort_arg = InputValue::new(
"order",
TypeRef::named(sort_object_list[*idx].type_name()),
);
field = field.argument(object_sort_arg);
}
let offset_arg = InputValue::new("offset", TypeRef::named(TypeRef::INT));
let limit_arg = InputValue::new("first", TypeRef::named(TypeRef::INT));
let id_selection_arg =
InputValue::new("id", TypeRef::named(TypeRef::STRING));
field = field
.argument(offset_arg)
.argument(limit_arg)
.argument(id_selection_arg);
}
}
BaseType::List(_) => unimplemented!("List types are not currently supported"),
}
field
}
fn create_filter_val_and_objects_for_field<'a>(
field_name: &'a str,
field_type: &'a str,
obj_name: &'a str,
) -> (InputValue, Vec<InputObject>) {
let mut input_objs: Vec<InputObject> = Vec::new();
let filter_arg_type = if NUMERIC_SCALAR_TYPES.contains(field_type) {
TypeRef::INT
} else {
TypeRef::STRING
};
let complex_comparison_obj =
InputObject::new(format!("{obj_name}_{field_name}_ComplexComparisonObject"))
.field(InputValue::new("min", TypeRef::named_nn(filter_arg_type)))
.field(InputValue::new("max", TypeRef::named_nn(filter_arg_type)));
let complete_comparison_obj =
InputObject::new(format!("{obj_name}{field_name}FilterObject"))
.field(InputValue::new(
"between",
TypeRef::named(complex_comparison_obj.type_name()),
))
.field(InputValue::new("equals", TypeRef::named(filter_arg_type)))
.field(InputValue::new("gt", TypeRef::named(filter_arg_type)))
.field(InputValue::new("gte", TypeRef::named(filter_arg_type)))
.field(InputValue::new("lt", TypeRef::named(filter_arg_type)))
.field(InputValue::new("lte", TypeRef::named(filter_arg_type)))
.field(InputValue::new(
"in",
TypeRef::named_nn_list(filter_arg_type),
));
let input_val_for_field = InputValue::new(
field_name,
TypeRef::named(complete_comparison_obj.type_name()),
);
input_objs.append(&mut vec![complex_comparison_obj, complete_comparison_obj]);
(input_val_for_field, input_objs)
}