1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
mod query;
pub use query::Query;

mod mutation;
use async_graphql::{EmptySubscription, Schema, SchemaBuilder};
pub use mutation::Mutation;

use crate::{gql::mutation::ResponseCode, principal::Principal, usecase};

pub mod object;
pub mod scalar;

pub type SyndSchema = Schema<Query, Mutation, EmptySubscription>;

pub mod handler {
    use async_graphql::http::GraphiQLSource;
    use async_graphql_axum::{GraphQLRequest, GraphQLResponse};
    use axum::{response::IntoResponse, Extension};
    use synd_o11y::audit_span;
    use tracing::Instrument;

    use crate::principal::Principal;

    use super::SyndSchema;

    pub async fn graphiql() -> impl IntoResponse {
        axum::response::Html(GraphiQLSource::build().endpoint("/graphql").finish())
    }

    pub async fn graphql(
        Extension(schema): Extension<SyndSchema>,
        Extension(principal): Extension<Principal>,
        req: GraphQLRequest,
    ) -> GraphQLResponse {
        // Inject authentication
        let req = req.into_inner().data(principal);
        schema.execute(req).instrument(audit_span!()).await.into()
    }
}

#[must_use]
pub fn schema_builder() -> SchemaBuilder<Query, Mutation, EmptySubscription> {
    let schema = Schema::build(Query, Mutation, EmptySubscription);

    if cfg!(not(feature = "introspection")) {
        schema
            .disable_introspection()
            .limit_depth(10)
            .limit_complexity(60)
    } else {
        schema.limit_depth(20).limit_complexity(300)
    }
    // disabled
    // schema.extension(Tracing)
}

impl<'a> usecase::Context for &async_graphql::Context<'a> {
    fn principal(&self) -> Principal {
        self.data_unchecked::<Principal>().clone()
    }
}

impl<E> async_graphql::ErrorExtensions for usecase::Error<E>
where
    E: std::fmt::Display + Send + Sync + 'static,
{
    fn extend(&self) -> async_graphql::Error {
        async_graphql::Error::new(format!("{self}")).extend_with(|_, ext| match self {
            usecase::Error::Usecase(_) => unreachable!(),
            usecase::Error::Unauthorized(_) => ext.set("code", ResponseCode::Unauthorized),
            usecase::Error::Repository(_) => ext.set("code", ResponseCode::InternalError),
        })
    }
}

impl async_graphql::ErrorExtensions for usecase::FetchEntriesError {
    fn extend(&self) -> async_graphql::Error {
        async_graphql::Error::new(format!("{self}"))
            .extend_with(|_, ext| ext.set("code", ResponseCode::InternalError))
    }
}

impl async_graphql::ErrorExtensions for usecase::FetchSubscribedFeedsError {
    fn extend(&self) -> async_graphql::Error {
        async_graphql::Error::new(format!("{self}"))
            .extend_with(|_, ext| ext.set("code", ResponseCode::InternalError))
    }
}

macro_rules! run_usecase {
    ($usecase:ty, $cx:expr, $input:expr,$err_handle:expr) => {{
        let runtime = $cx.data_unchecked::<crate::usecase::Runtime>();
        let err_handle = $err_handle;

        match runtime.run::<$usecase, _, _>($cx, $input).await {
            Ok(output) => Ok(output.into()),
            Err($crate::usecase::Error::Usecase(uc_err)) => err_handle(uc_err),
            Err(err) => Err(async_graphql::ErrorExtensions::extend(&err)),
        }
    }};
}

pub(super) use run_usecase;