Skip to content

Commit

Permalink
feat: ✨ modularised the subscriptions server
Browse files Browse the repository at this point in the history
  • Loading branch information
arctic-hen7 committed Jun 22, 2021
1 parent 2a50470 commit a508e81
Show file tree
Hide file tree
Showing 8 changed files with 189 additions and 144 deletions.
9 changes: 6 additions & 3 deletions graphql_server/bin/serverful.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,30 @@
// This binary runs a serverful setup with Actix Web, as opposed to a serverless approach (TODO)
// Even so, this system does NOT support subscriptions so we maintain the separity in development that will be present in production

use actix_web::{App, HttpServer};
use std::env;
use lib::{
schemas::users::{Query, Mutation, Subscription},
graphql_utils::Context,
db::DbPool,
load_env::load_env,

App, HttpServer,
create_graphql_server, OptionsBuilder, AuthCheckBlockState
};

#[actix_web::main]
async fn main() -> std::io::Result<()> {
load_env().expect("Failed env.");
let opts = OptionsBuilder::new()
.ctx(Context {
pool: DbPool::default()
})
.subscriptions_server_hostname("http://subscriptions-server")
.subscriptions_server_port("6000")
.subscriptions_server_endpoint("/graphql")
.jwt_to_connect_to_subscriptions_server("invalidtoken")
.jwt_to_connect_to_subscriptions_server(&env::var("SUBSCRIPTIONS_SERVER_PUBLISH_JWT").unwrap())
.auth_block_state(AuthCheckBlockState::AllowAll)
.jwt_secret("aninsecuresecret")
.jwt_secret(&env::var("JWT_SECRET").unwrap())
.schema(Query {}, Mutation {}, Subscription {})
// Endpoints are set up as `/graphql` and `/graphiql` automatically
.finish().expect("Options building failed!");
Expand Down
2 changes: 1 addition & 1 deletion lib/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use mongodb::{
};

use crate::errors::*;
use crate::load_env;
use crate::load_env::load_env;

// A helper function for implementations of the DbClient trait that gets a handle to a DB client from environment variables
// All errors are given in GraphQL format, seeing as this function will be called in resolver logic and conversion is annoying
Expand Down
75 changes: 26 additions & 49 deletions lib/src/graphql.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,6 @@
// We merge all the component schemas together here, so this code will need to be regularly updated in early development

use crate::schemas::{
users::{
Query as UsersQuery,
Mutation as UsersMutation,
Subscription as UsersSubscription
}
};

// These will eventually be generated by the function
#[derive(MergedObject, Default)]
pub struct QueryRoot(BaseQuery, UsersQuery);

#[derive(MergedObject, Default)]
pub struct MutationRoot(UsersMutation);

#[derive(MergedSubscription, Default)]
pub struct SubscriptionRoot(UsersSubscription);

// GRAPHQL CODE

use std::any::Any;
use std::sync::Mutex;
use async_graphql::{
MergedObject,
MergedSubscription,
Object as GQLObject,
Schema,
EmptySubscription,
Expand All @@ -33,30 +9,24 @@ use async_graphql::{
};

use crate::errors::*;
use crate::db::DbPool;
use crate::pubsub::{PubSub, Publisher};
use crate::graphql_utils::{
Context,
get_auth_data_from_ctx, get_pubsub_from_ctx
};
use crate::graphql_utils::{get_auth_data_from_ctx, get_pubsub_from_ctx};
use crate::if_authed;

// The base query type unrelated to any particular logic
// This needs to be public because it's used directly by the subscriptions server
#[derive(Default)]
pub struct BaseQuery;
// The base query type simply allows us to set up the subscriptions schema (has to have at least one query)
#[derive(Default, Clone)]
pub struct SubscriptionQuery;
#[GQLObject]
impl BaseQuery {
// All APIs should implement this method for best practices so clients know what the hell they're doing
async fn api_version(&self) -> String {
// TODO use an environment variable to get the API version
"v0.1.0".to_string()
impl SubscriptionQuery {
// TODO disable introspection on this endpoint
async fn _query(&self) -> String {
"This is a meaningless endpoint needed only for initialisation.".to_string()
}
}

// This mutation type is utilised by the subscriptions server to allow the publishing of data
// We pass around the PubSub state internally to that GraphQL system (see get_schema_for_subscriptions)
#[derive(Default)]
#[derive(Default, Clone)]
pub struct PublishMutation;
#[GQLObject]
impl PublishMutation {
Expand All @@ -82,11 +52,6 @@ impl PublishMutation {
}
}

// Serverless functions cannnot handle subscriptions, so we separate the schema here
pub type AppSchemaWithoutSubscriptions = Schema<QueryRoot, MutationRoot, EmptySubscription>;
// We need to be able to work out the API version on the subscriptions server, so we still provide the basic queries
pub type AppSchemaForSubscriptions = Schema<BaseQuery, PublishMutation, SubscriptionRoot>;

// Information about the subscriptions server for the rest of the system
#[derive(Clone)]
pub struct SubscriptionsServerInformation {
Expand Down Expand Up @@ -139,11 +104,23 @@ where

Ok(schema)
}
pub fn get_schema_for_subscriptions() -> AppSchemaForSubscriptions {
Schema::build(BaseQuery, PublishMutation, SubscriptionRoot::default())
.data(Context {
pool: DbPool::default()
})
pub fn get_schema_for_subscriptions<C, Q, M, S>(
user_schema: UserSchema<Q, M, S>,
subscription_server_info: SubscriptionsServerInformation,
user_ctx: C
) -> Schema<SubscriptionQuery, PublishMutation, S>
where
C: Any + Send + Sync,
Q: ObjectType + 'static,
M: ObjectType + 'static,
S: SubscriptionType + 'static
{
// The schema for the subscriptions server should only have subscriptions, and a mutation to allow publishing
// Unfortunately, we have to have at least one query, so we implement a meaningless one that isn't introspected
Schema::build(SubscriptionQuery, PublishMutation, user_schema.subscription_root)
// We add some custom user-defined context (e.g. a database connection pool)
.data(user_ctx)
// We add a mutable PubSub instance for managing subscriptions internally
.data(
Mutex::new(PubSub::default())
) // We add a PubSub instance to internally manage state in the serverful subscriptions system
Expand Down
19 changes: 8 additions & 11 deletions lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,21 @@ mod oid;
pub mod db;

pub mod errors;
mod load_env;
pub mod load_env;
pub mod schemas;
mod graphql;
mod pubsub;
mod options;
pub mod graphql_utils;
mod graphql_server;
pub mod auth;
pub mod routes;
mod subscriptions_server;
mod auth;
mod routes;

// Public exports accessible from the root (everything the user will need)
pub use crate::graphql_server::create_graphql_server;
pub use crate::subscriptions_server::create_subscriptions_server;
pub use crate::options::{Options, OptionsBuilder, AuthCheckBlockState};
pub use crate::graphql::{
AppSchemaWithoutSubscriptions,
AppSchemaForSubscriptions,
get_schema_without_subscriptions,
get_schema_for_subscriptions,
};
pub use crate::load_env::load_env;
pub use crate::pubsub::PubSub;

// Users shouldn't have to install Actix Web themselves for basic usage
pub use actix_web::{App, HttpServer};
4 changes: 2 additions & 2 deletions lib/src/pubsub.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ pub struct Publisher {
token: String
}
impl Publisher {
pub fn new(port: String, hostname: String, endpoint: String, token: String) -> Result<Self> {
pub fn new(hostname: String, port: String, endpoint: String, token: String) -> Result<Self> {
let address = format!(
"{hostname}:{port}/{endpoint}",
"{hostname}:{port}{endpoint}", // The endpoint should start with '/'
hostname=hostname,
port=port,
endpoint=endpoint
Expand Down
31 changes: 16 additions & 15 deletions lib/src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,13 @@
// The GraphQL and GraphQL WS routes would require generic functions as arguments to work, and so are left as they are

use async_graphql::{
Schema, ObjectType, SubscriptionType,
http::{playground_source, GraphQLPlaygroundConfig}
Schema, ObjectType, SubscriptionType
};
use actix_web::{web, HttpRequest, HttpResponse, Result as ActixResult};
use async_graphql_actix_web::{Request, Response};
use async_graphql_actix_web::{Request, Response, WSSubscription};

use crate::auth::auth_state::AuthState;

const GRAPHQL_ENDPOINT: &str = "/graphql";

// The endpoint for the development graphical playground
pub async fn graphiql() -> ActixResult<HttpResponse> {
Ok(HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(playground_source(
GraphQLPlaygroundConfig::new(GRAPHQL_ENDPOINT).subscription_endpoint(GRAPHQL_ENDPOINT),
)))
}

// The main GraphQL endpoint for queries and mutations with authentication support
// This handler does not support subscriptions
pub async fn graphql<Q, M, S>(
Expand All @@ -40,8 +28,21 @@ where
let auth_data = extensions.get::<AuthState>();

// Clone the internal AuthState so we can place the variable into the context (lifetimes...)
let auth_data_for_ctx = auth_data.map(|auth_data| auth_data.clone());
let auth_data_for_ctx = auth_data.cloned();
// Add that to the GraphQL request data so we can access it in the resolvers
query = query.data(auth_data_for_ctx);
schema.execute(query).await.into()
}

pub async fn graphql_ws<Q, M, S>(
schema: web::Data<Schema<Q, M, S>>,
http_req: HttpRequest,
payload: web::Payload,
) -> ActixResult<HttpResponse>
where
Q: ObjectType + 'static,
M: ObjectType + 'static,
S: SubscriptionType + 'static
{
WSSubscription::start(Schema::clone(&schema), &http_req, payload)
}
104 changes: 104 additions & 0 deletions lib/src/subscriptions_server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Contains the logic to actually create the GraphQL server that the user will use
// This file does not include any logic for the subscriptions server

use std::any::Any;
use std::sync::Mutex;
use async_graphql::{
ObjectType, SubscriptionType,
http::{playground_source, GraphQLPlaygroundConfig}
};
use actix_web::{
web::{self, ServiceConfig}, guard, HttpResponse,
};

use crate::graphql::{
get_schema_for_subscriptions,
SubscriptionQuery, PublishMutation
};
use crate::auth::middleware::AuthCheck;
use crate::routes::{graphql, graphql_ws};
use crate::options::{Options, AuthCheckBlockState};
use crate::pubsub::PubSub;

pub fn create_subscriptions_server<C, Q, M, S>(opts: Options<C, Q, M, S>) -> impl FnOnce(&mut ServiceConfig) + Clone
where
C: Any + Send + Sync + Clone,
Q: Clone + ObjectType + 'static,
M: Clone + ObjectType + 'static,
S: Clone + SubscriptionType + 'static
{
let subscriptions_server_data = opts.subscriptions_server_data.clone();
// Get the schema (this also creates a publisher to the subscriptions server and inserts context)
// The one for subscriptions can't fail (no publisher)
let schema = get_schema_for_subscriptions(opts.schema, subscriptions_server_data, opts.ctx);
// Get the appropriate authentication middleware set up with the JWT secret
// This is only used if the GraphiQL playground needs authentication in production
let auth_middleware = match opts.authentication_block_state {
AuthCheckBlockState::AllowAll => AuthCheck::new(&opts.jwt_secret).allow_all(),
AuthCheckBlockState::AllowMissing => AuthCheck::new(&opts.jwt_secret).allow_missing(),
AuthCheckBlockState::BlockUnauthenticated => AuthCheck::new(&opts.jwt_secret).block_unauthenticated()
};

let graphql_endpoint = opts.subscriptions_server_data.endpoint; // The subscriptions server can have a different endpoint if needed
let playground_endpoint = opts.playground_endpoint;
let jwt_secret = opts.jwt_secret;

// Actix Web allows us to configure apps with `.configure()`, which is what the user will do
// Now we create the closure that will configure the user's app to support a GraphQL server
move |cfg: &mut ServiceConfig| {
// Add everything except for the playground endpoint (which may not even exist)
cfg
.data(schema.clone()) // Clone the full schema we got before and provide it here
.data(Mutex::new(PubSub::default())) // The subscriptions server also uses an internal PubSub system
// The primary GraphQL endpoint for the publish mutation
.service(web::resource(&graphql_endpoint)
.guard(guard::Post()) // Should accept POST requests
// The subscriptions server mandatorily blocks anything not authenticated
.wrap(AuthCheck::new(&jwt_secret).block_unauthenticated())
// This endpoint supports basically only the publish mutations
.to(graphql::<SubscriptionQuery, PublishMutation, S>) // The handler function it should use
)
// The GraphQL endpoint for subscriptions over WebSockets
.service(web::resource(&graphql_endpoint)
.guard(guard::Get())
.guard(guard::Header("upgrade", "websocket"))
.to(graphql_ws::<SubscriptionQuery, PublishMutation, S>)
);

// Define the closure for the GraphiQL endpoint
// We don't do this in routes because of annoying type annotations
let graphql_endpoint_for_closure = graphql_endpoint; // We need this because moving
let graphiql_closure = move || {
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(playground_source(
GraphQLPlaygroundConfig::new(&graphql_endpoint_for_closure).subscription_endpoint(&graphql_endpoint_for_closure),
))
};

// Set up the endpoint for the GraphQL playground (same endpoint as the queries/mutations system)
match playground_endpoint {
// If we're in development and it's enabled, set it up without authentication
Some(playground_endpoint) if cfg!(debug_assertions) => {
cfg
.service(web::resource(playground_endpoint)
.guard(guard::Get())
.to(graphiql_closure) // The playground needs to know where to send its queries
);
},
// If we're in production and it's enabled, set it up with authentication
// The playground doesn't process the auth headers, so the token just needs to be valid (no further access control yet)
Some(playground_endpoint) => {
cfg
.service(web::resource(playground_endpoint)
.guard(guard::Get())
// TODO by request, the JWT secret and block level can be different here
.wrap(auth_middleware.clone())
.to(graphiql_closure) // The playground needs to know where to send its queries
);
},
None => ()
};
// This closure works entirely with side effects, so we don't need to return anything here
}
}
Loading

0 comments on commit a508e81

Please sign in to comment.