Repository: p2panda/aquadoggo (language: rust)

Issue:
Allow `Content-Type` header for cross-domain GraphQL requests



Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2506bb3d0..64b578348 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -30,6 +30,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 - Don't return errors from `SchemaStore` when a schema could not be constructed [#192](https://github.com/p2panda/aquadoggo/pull/192)
 - Filter out deleted documents in `get_documents_by_schema` SQL query [#193](https://github.com/p2panda/aquadoggo/pull/193)
+- Resolve implicit `__typename` field on dynamically generated GraphQL objects [#236](https://github.com/p2panda/aquadoggo/pull/236)
+- Allow `Content-Type` header [#236](https://github.com/p2panda/aquadoggo/pull/236)
 
 ## [0.3.0]
 
diff --git a/aquadoggo/Cargo.toml b/aquadoggo/Cargo.toml
index 7712f70e3..18582da95 100644
--- a/aquadoggo/Cargo.toml
+++ b/aquadoggo/Cargo.toml
@@ -56,6 +56,7 @@ tokio = { version = "^1.19.2", features = [
 tower-http = { version = "^0.3.4", default-features = false, features = [
     "cors",
 ] }
+http = "^0.2.8"
 triggered = "^0.1.2"
 
 [dev-dependencies]
diff --git a/aquadoggo/src/graphql/client/dynamic_query.rs b/aquadoggo/src/graphql/client/dynamic_query.rs
index b971fef71..7a7ee0560 100644
--- a/aquadoggo/src/graphql/client/dynamic_query.rs
+++ b/aquadoggo/src/graphql/client/dynamic_query.rs
@@ -241,22 +241,47 @@ impl DynamicQuery {
         let mut document_fields = IndexMap::new();
 
         for field in selected_fields {
-            // Assemble selected metadata values.
-            if field.name() == dynamic_types::document::META_FIELD {
-                document_fields.insert(
-                    Name::new(field.alias().unwrap_or_else(|| field.name())),
-                    DocumentMeta::resolve(field, None, Some(view.id())),
-                );
-            }
-
-            // Assemble selected document field values.
-            if field.name() == dynamic_types::document::FIELDS_FIELD {
-                let subselection = field.selection_set().collect();
-                document_fields.insert(
-                    Name::new(field.alias().unwrap_or_else(|| field.name())),
-                    self.document_fields_response(view.clone(), ctx, subselection)
-                        .await?,
-                );
+            // Name with which this field appears in the response
+            let response_key = Name::new(field.alias().unwrap_or_else(|| field.name()));
+
+            match field.name() {
+                "__typename" => {
+                    let store = ctx.data_unchecked::<SqlStorage>();
+                    let schema_id = store
+                        .get_schema_by_document_view(view.id())
+                        .await
+                        .map_err(|err| ServerError::new(err.to_string(), None))?
+                        .unwrap()
+                        .to_string();
+                    document_fields.insert(response_key, Value::String(schema_id));
+                }
+                dynamic_types::document::META_FIELD => {
+                    document_fields.insert(
+                        response_key,
+                        DocumentMeta::resolve(field, None, Some(view.id()))?,
+                    );
+                }
+                dynamic_types::document::FIELDS_FIELD => {
+                    let subselection = field.selection_set().collect();
+                    document_fields.insert(
+                        response_key,
+                        self.document_fields_response(view.clone(), ctx, subselection)
+                            .await?,
+                    );
+                }
+                _ => {
+                    let store = ctx.data_unchecked::<SqlStorage>();
+                    let schema_id = store
+                        .get_schema_by_document_view(view.id())
+                        .await
+                        .map_err(|err| ServerError::new(err.to_string(), None))?
+                        .unwrap()
+                        .to_string();
+                    Err(ServerError::new(
+                        format!("Field '{}' does not exist on {}", field.name(), schema_id,),
+                        None,
+                    ))?
+                }
             }
         }
 
@@ -287,6 +312,21 @@ impl DynamicQuery {
         // Construct GraphQL value for every field of the given view that has been selected.
         let mut view_fields = IndexMap::new();
         for selected_field in selected_fields {
+            // Name with which this field appears in the response
+            let response_key = Name::new(
+                selected_field
+                    .alias()
+                    .unwrap_or_else(|| selected_field.name()),
+            );
+
+            // Handle `__typename` field and continue to next selection.
+            if selected_field.name() == "__typename" {
+                let type_name = dynamic_types::document_fields::type_name(&schema);
+                view_fields.insert(response_key, Value::String(type_name));
+                continue;
+            }
+
+            // Handle document fields.
             if !schema.fields().contains_key(selected_field.name()) {
                 return Err(ServerError::new(
                     format!(
@@ -343,14 +383,7 @@ impl DynamicQuery {
                 // Convert all simple fields to scalar values.
                 _ => gql_scalar(document_view_value.value()),
             };
-            view_fields.insert(
-                Name::new(
-                    selected_field
-                        .alias()
-                        .unwrap_or_else(|| selected_field.name()),
-                ),
-                value,
-            );
+            view_fields.insert(response_key, value);
         }
         Ok(Value::Object(view_fields))
     }
@@ -561,4 +594,66 @@ mod test {
             assert_eq!(response.data, expected_data, "{:#?}", response.errors);
         });
     }
+
+    #[rstest]
+    fn type_name(#[from(test_db)] runner: TestDatabaseRunner) {
+        // Test availability of `__typename` on all objects.
+
+        runner.with_db_teardown(&|mut db: TestDatabase| async move {
+            let key_pair = random_key_pair();
+
+            // Add schema to node.
+            let schema = db
+                .add_schema("schema_name", vec![("bool", FieldType::Boolean)], &key_pair)
+                .await;
+
+            // Publish document on node.
+            let view_id = db
+                .add_document(
+                    &schema.id(),
+                    vec![("bool", true.into())].try_into().unwrap(),
+                    &key_pair,
+                )
+                .await;
+
+            // Configure and send test query.
+            let client = graphql_test_client(&db).await;
+            let query = format!(
+                r#"{{
+                single: {type_name}(id: "{view_id}") {{
+                    __typename,
+                    meta {{ __typename }}
+                    fields {{ __typename }}
+                }},
+                collection: all_{type_name} {{
+                    __typename,
+                }},
+            }}"#,
+                type_name = schema.id(),
+                view_id = view_id,
+            );
+
+            let response = client
+                .post("/graphql")
+                .json(&json!({
+                    "query": query,
+                }))
+                .send()
+                .await;
+
+            let response: Response = response.json().await;
+
+            let expected_data = value!({
+                "single": {
+                    "__typename": schema.id(),
+                    "meta": { "__typename": "DocumentMeta" },
+                    "fields": { "__typename": format!("{}Fields", schema.id()), }
+                },
+                "collection": [{
+                    "__typename": schema.id()
+                }]
+            });
+            assert_eq!(response.data, expected_data, "{:#?}", response.errors);
+        });
+    }
 }
diff --git a/aquadoggo/src/graphql/client/dynamic_types/document_fields.rs b/aquadoggo/src/graphql/client/dynamic_types/document_fields.rs
index 7d20c9ece..c3bb785b9 100644
--- a/aquadoggo/src/graphql/client/dynamic_types/document_fields.rs
+++ b/aquadoggo/src/graphql/client/dynamic_types/document_fields.rs
@@ -5,6 +5,11 @@ use p2panda_rs::schema::Schema;
 
 use crate::graphql::client::dynamic_types::utils::{graphql_typename, metafield, metaobject};
 
+/// Returns the GraphQL type name of the document fields object for a given schema.
+pub fn type_name(schema: &Schema) -> String {
+    format!("{}Fields", schema.id())
+}
+
 /// Represents fields for documents of a specific schema in the GraphQL client API.
 pub struct DocumentFields(&'static Schema);
 
@@ -16,7 +21,7 @@ impl DocumentFields {
 
     /// Returns the type name, formatted like `<SchemaId>Fields`.
     pub fn type_name(&self) -> String {
-        format!("{}Fields", self.0.id())
+        type_name(self.0)
     }
 
     /// Generate an object type and register it in a GraphQL schema registry.
diff --git a/aquadoggo/src/graphql/client/dynamic_types/document_meta.rs b/aquadoggo/src/graphql/client/dynamic_types/document_meta.rs
index 8d14e836d..003a60bf9 100644
--- a/aquadoggo/src/graphql/client/dynamic_types/document_meta.rs
+++ b/aquadoggo/src/graphql/client/dynamic_types/document_meta.rs
@@ -1,7 +1,9 @@
 // SPDX-License-Identifier: AGPL-3.0-or-later
 
 use async_graphql::indexmap::IndexMap;
-use async_graphql::{Name, OutputType, ScalarType, SelectionField, Value};
+use async_graphql::{
+    Name, OutputType, ScalarType, SelectionField, ServerError, ServerResult, Value,
+};
 use p2panda_rs::document::{DocumentId, DocumentViewId};
 
 use crate::graphql::client::dynamic_types::utils::{metafield, metaobject};
@@ -65,24 +67,40 @@ impl DocumentMeta {
         root_field: SelectionField,
         document_id: Option<&DocumentId>,
         view_id: Option<&DocumentViewId>,
-    ) -> Value {
-        let mut meta_fields = IndexMap::new();
+    ) -> ServerResult<Value> {
+        let mut meta_fields = IndexMap::<Name, Value>::new();
 
         for meta_field in root_field.selection_set() {
-            if meta_field.name() == DOCUMENT_ID_FIELD && document_id.is_some() {
-                meta_fields.insert(
-                    Name::new(DOCUMENT_ID_FIELD),
-                    DocumentIdScalar::from(document_id.unwrap()).to_value(),
-                );
-            }
+            let response_key = Name::new(meta_field.alias().unwrap_or_else(|| meta_field.name()));
 
-            if meta_field.name() == VIEW_ID_FIELD && view_id.is_some() {
-                meta_fields.insert(
-                    Name::new(VIEW_ID_FIELD),
-                    Value::String(view_id.unwrap().to_string()),
-                );
+            match meta_field.name() {
+                "__typename" => {
+                    meta_fields.insert(
+                        response_key,
+                        Value::String(DocumentMeta::type_name().to_string()),
+                    );
+                }
+                DOCUMENT_ID_FIELD => {
+                    if let Some(document_id) = document_id {
+                        meta_fields
+                            .insert(response_key, DocumentIdScalar::from(document_id).to_value());
+                    }
+                }
+                VIEW_ID_FIELD => {
+                    if let Some(view_id) = view_id {
+                        meta_fields.insert(response_key, Value::String(view_id.to_string()));
+                    }
+                }
+                _ => Err(ServerError::new(
+                    format!(
+                        "Field '{}' does not exist on {}",
+                        meta_field.name(),
+                        Self::type_name(),
+                    ),
+                    None,
+                ))?,
             }
         }
-        Value::Object(meta_fields)
+        Ok(Value::Object(meta_fields))
     }
 }
diff --git a/aquadoggo/src/graphql/client/dynamic_types/mod.rs b/aquadoggo/src/graphql/client/dynamic_types/mod.rs
index c7152f123..541d8d4ed 100644
--- a/aquadoggo/src/graphql/client/dynamic_types/mod.rs
+++ b/aquadoggo/src/graphql/client/dynamic_types/mod.rs
@@ -5,7 +5,7 @@
 //! All dynamic type definitions are inserted from the `OutputType` implementation in the
 //! [`dynamic_query_output`] module.
 pub(crate) mod document;
-mod document_fields;
+pub(crate) mod document_fields;
 pub(crate) mod document_meta;
 pub(crate) mod dynamic_query_output;
 #[cfg(test)]
diff --git a/aquadoggo/src/http/service.rs b/aquadoggo/src/http/service.rs
index 0d3560fc0..285c67c56 100644
--- a/aquadoggo/src/http/service.rs
+++ b/aquadoggo/src/http/service.rs
@@ -7,6 +7,7 @@ use axum::extract::Extension;
 use axum::http::Method;
 use axum::routing::get;
 use axum::Router;
+use http::header::CONTENT_TYPE;
 use log::{debug, warn};
 use tower_http::cors::{Any, CorsLayer};
 
@@ -24,6 +25,7 @@ pub fn build_server(http_context: HttpServiceContext) -> Router {
     // Configure CORS middleware
     let cors = CorsLayer::new()
         .allow_methods(vec![Method::GET, Method::POST, Method::OPTIONS])
+        .allow_headers([CONTENT_TYPE])
         .allow_credentials(false)
         .allow_origin(Any);
 


Test patch: