Introduction

DianaCrate PageAPI DocumentationContributing

Welcome to the Diana book, the central location for all Diana documentation! This is designed to be read in order, but each page should also be self-containing for later reference. If you find an issue with this documentation or you'd like to contribute in any way to the project, please open an issue and let us know!

If you're looking for a more general overview of the project, please see the README.

Getting Started

Diana is a high-level wrapper around async_graphql, and is designed to be as easy as possible to get started with! This page is a basic tutorial of how to get started with a full and working setup.

Installation

Assuming you have Rust already installed (if not, here's a guide on how to do so), you can add Diana as a dependency to your project easily by adding the following to your project's Cargo.toml under [dependencies]:

diana = "0.2.9"
async-graphql = "2.8.2"

We also install async_graphql directly to prevent errors with asynchronous usage. Now run cargo build to download all dependencies. Diana is large and complex, so this will take quite a while!

If you're new to GraphQL, we highly recommend reading more about it before diving further into Diana. You can see more about it on the official GraphQL website.

Project Structure

We recommend a specific project structure for new projects using Diana, but it's entirely optional! It is however designed to minimize code duplication and maximize efficiency by allowing you to run both the queries/mutations system and the subscriptions server simultaneously.

Critically, we recommend having three binary crates for your two servers and the serverless function, as well as a library crate for your schemas and configurations. These should all be Cargo workspaces.

lib/
	src/
		lib.rs
	Cargo.toml
server/
	src/
		main.rs
	Cargo.toml
serverless/
	src/
		main.rs
	Cargo.toml
subscriptions/
	src/
		main.rs
	Cargo.toml
Cargo.lock
Cargo.toml

Set this up for now if possible, and we'll add to it later across the book (it will be assumed that you're using this or something similar).

You should also have the following in your root Cargo.toml to set up workspaces (which you can read more about here):

[workspace]

members = [
	"lib",
    "server",
    "serverless",
	"subscriptions"
]

Then you can make all the binary crates (everything except lib) dependent on your shared logic by adding this to the Cargo.toml files in server, serverless, and subscriptions under [dependencies]:

lib = { path = "../lib" }

You can then reference lib in those crates as if it were just another external module!

Your first schema

If you're familiar with GraphQL, then the first thing you'll need to do to set up Diana is to write a basic schema. Diana depends entirely on async_graphql for this, so their documentation may also help you (particularly in more advanced cases), though this book should be enough for the simple stuff.

Your first schema can be really simple, we'll just make a simple query that reports the API version when queried (we won't add any mutations or subscriptions for now). Try adding this somewhere in your shared logic:


#![allow(unused)]
fn main() {
use diana::{
	async_graphql::{
		Object as GQLObject
	}
}

#[derive(Default, Clone)]
pub struct Query {}
#[GQLObject]
impl Query {
    async fn api_version(&self) -> &str {
        "0.1.0"
    }
}
}

This is probably the simplest schema you'll ever create! Crucially though, you MUST derive the Default and Clone traits on it. The former is required by async_graphql, and the latter for Diana.

Hopefully you can see that our Query object is simply defining one query, api_version, which just returns 0.1.0, the version of our API! Conveniently, async_graphql automatically parses this into the more conventional apiVersion when we call this, so you can conform to Rust and GraphQL conventions at the same time!

Your first options

Every part of Diana is configured using the Options struct, which can be created with Options::builder(). For now, we'll set up a simple configuration without any subscriptions support. Add this to your shared logic:


#![allow(unused)]
fn main() {
use diana::{Options, AuthBlockState};
use diana::async_graphql::{EmptyMutation, EmptySubscription};
use crate::Query; // Or wherever you put your `Query` object from the previous section

#[derive(Clone)]
pub struct Context(String);

pub fn get_opts() -> Options<Context, Query, EmptyMutation, EmptySubscription> {
    Options::builder()
        .ctx(Context("test".to_string()))
        .auth_block_state(AuthBlockLevel::AllowAll)
        .jwt_secret("this is a secret")
        .schema(Query {}, Mutation {}, Subscription {})
        .finish()
        .expect("Failed to build options!")
}
}

Notice that we define a Context struct here. This will get passed around to every GraphQL resolver and you'll always be able to access it. As long as it's Cloneable, you can put anything in here safely. A common use-case of this in reality would be as a database connection pool. Here, we just define it with a random string inside.

Next, we define a function get_opts() that initializes our Options. We set the context, define our schema, and do two other things that need some explaining. The first is .auth_block_state(), which sets the required authentication level to access our GraphQL endpoint. Diana has authentication built-in, so this is fundamental. Here, we allow anything, authenticated or not, for educational purposes. In a production app, set this to block everything! You can read more about authentication here. The second thing that needs explaining is .jwt_secret(). Diana's authentication systems is based on JWTs, which are basically tokens that clients send to servers to prove their identity (the server signed them earlier, and so can verify them). JWTs need a secret to be based on, and we define a very silly one here. In a production app, you should read this from an environment variable and it should be randomly generated (more on that here).

Your first server

Let's try plugging this into a basic Diana server! Diana is based around integrations for different platforms, and it currently supports only Actix Web for serverful systems, so that's what we'll use! You should add this to your Cargo.toml in the server crate under [dependencies]:

diana-actix-web = "0.2.9"

Now add the following to your main.rs in the server crate:

use diana_actix_web::{
    actix_web::{App, HttpServer},
    create_graphql_server,
};
use diana::async_graphql::{EmptyMutation, EmptySubscription};
use lib::{get_opts, Query}

#[diana_actix_web::actix_web::main]
async fn main() -> std::io::Result<()> {
    let configurer = create_graphql_server(get_opts()).expect("Failed to set up configurer!");

    HttpServer::new(move || App::new().configure(configurer.clone()))
        .bind("0.0.0.0:9000")?
        .run()
        .await
}

Firstly, we're pulling in the dependencies we need, including the schema and the function to get our Options. Then, we define an asynchronous main function marked as the entrypoint for actix_web, in which we set up our entire GraphQL server using create_graphql-server(), parsing in our Options. After that, we start up a new Actix Web server, using .configure() to configure the entire thing. Pretty convenient, huh?

If you also have some REST endpoints or the like, you can easily add them to this server as well, .configure() is inbuilt into Actix Web to enable this kind of modularization.

Firing it up

The last step of all this is to actually run your server! Go into the server crate and run cargo run to see it in action! If all has gone well, you should see be able to see the GraphiQL playground (GUI for GraphQL development) in your browser at http://localhost:9000/graphiql! Try typing in the following and then run it!

query {
	apiVersion
}

You should see 0.1.0 faithfully printed on the right-hand side of the screen.

Congratulations! You've just set up your first GraphQL server with Diana! The rest of this book will help you to understand how to extend this setup to include mutations, subscriptions, and authentication, as well as helping you to deploy it all serverlessly!

Writing Schemas

The most critical part of GraphQL is schemas, and they're easily the most complex part of Diana. This page will explain how to write schemas that work with Diana, and it will explain how to get subscriptions to work properly, however it will not explain how to write basic schemas from scratch. For that, please refer to async_graphql's documentation.

Subscriptions

A basic Diana subscription would look something like this:


#![allow(unused)]
fn main() {
use diana::async_graphql::{Subscription as GQLSubscription, Context as GQLCtx};
use diana::errors::GQLResult;
use diana::stream;

#[derive(Default, Clone)]
pub struct Subscription;
#[GQLSubscription]
impl Subscription {
    async fn new_blahs(
        &self,
        raw_ctx: &GQLCtx<'_>,
    ) -> impl Stream<Item = GQLResult<String>> {
        let stream_result = get_stream_for_channel_from_ctx("channel_name", raw_ctx);

        stream! {
            let stream = stream_result?;
            for await message in stream {
                yield Ok(message);
            }
        }
    }
}
}

All this does is sets up a subscription that will return the strings on a particular channel. And this shows perfectly how subscriptions in Diana work -- channels. You publish something on a channel from the queries/mutations system and then receive it as above. You can then use the re-exported stream! macro to return a stream for it.

Note that if you're trying to send a struct across channels you'll need to serialize/deserialize it into/out of a string for transport. However, as subscriptions can return errors in their streams, this shouldn't be a problem!

The most common thing to trigger a subscription is some kind of mutation on the queries/mutations system, and so Diana provides a simple programmatic way of publishing data on a particular channel:


#![allow(unused)]
fn main() {
use diana::async_graphql::{Subscription as GQLSubscription, Context as GQLCtx};
use diana::errors::GQLResult;
use diana::stream;
use diana::Publisher;

#[derive(Default, Clone)]
pub struct Mutation {}
#[GQLObject]
impl Mutation {
    async fn update_blah(
        &self,
        raw_ctx: &async_graphql::Context<'_>,
    ) -> GQLResult<bool> {
        let publisher = raw_ctx.data::<Publisher>()?;
        publisher.publish("channel_name", "important message").await?;
        Ok(true)
    }
}
}

In the above example, we get a Publisher out of the GraphQL context (it's automatically injected), and we use it to easily send a message to the subscriptions server on the channel_name channel. Our subscription from the previous example would pick this up and stream it to the client.

Linking other services to subscriptions

Of course, it's entirely possible that services well beyond GraphQL may need to trigger a subscription message, and so you can easily push a message from anywhere where you can execute a basic HTTP request. Diana's subscriptions server has an inbuilt mutation publish, which takes a channel to publish on and a string message to publish. This can be called over a simple HTTP request from anywhere. However, this endpoint requires authentication, and you must have a valid JWT signed with the secret you've provided to be able to access it.

Configuration

Diana is configured using the Options struct. This page will go through in detail what can be specified using that system. Here's an example options initialization that we'll work through:


#![allow(unused)]
fn main() {
Options::builder()
        .ctx(Context("test".to_string()))
        .subscriptions_server_hostname("http://localhost")
        .subscriptions_server_port("9002")
        .subscriptions_server_endpoint("/graphql")
        .jwt_to_connect_to_subscriptions_server(
            &env::var("SUBSCRIPTIONS_SERVER_PUBLISH_JWT").unwrap(),
        )
        .auth_block_state(AuthBlockLevel::AllowAll)
        .jwt_secret(&env::var("JWT_SECRET").unwrap())
        .schema(Query {}, Mutation {}, Subscription {})
        .graphql_endpoint("/graphql")
		.playground_endpoint("/graphiql")
        .finish()
        .expect("Failed to build options!")
}

Context

You must provide a context struct to Diana by using the .ctx() function. This struct will be parsed to all resolvers, and can be trivially accessed by using this in your resolvers:


#![allow(unused)]
fn main() {
let ctx = raw_ctx.data<Context>()?;
}

A common use of the context struct is for a database pool.

Subscriptions server configuration

You need to provide the details of the subscriptions server in your configuration so the queries/mutation system knows where it is on the internet. This is defined using these four functions:

  • .subscriptions_server_hostname() -- the hostname of the subscriptions server (e.g. http://localhost
  • .subscriptions_server_port() -- the port the subscriptions server is running on
  • .subscriptions_server_endpoint() -- the GraphQL endpoint to connect to on the subscriptions server (e.g. /graphql)
  • .jwt_to_connect_to_subscriptions_server() -- a JWT to use to authenticate against the subscriptions server, which must be signed with the secret define by .jwt_secret(); this JWT must have a payload which defines role: "graphql_server" (see Authentication)

If you aren't using subscriptions at all in your setup, you don't have to use any of these functions.

Authentication

Two properties define authentication data for Diana: .jwt_secret() and .auth_block_state(). The former defines the string secret to use to sign all JWTs (internally used for the communication channel between the two systems of Diana, you can use it too for authenticating clients). The latter defines the level of authentication required to connect to the GraphQL endpoint. This can be one of the following:

  • AuthBlockLevel::AllowAll -- allows everything, only ever use this in development unless you have an excellent reason
  • AuthBlockLevel::BlockUnauthenticated -- blocks anything without a valid JWT
  • AuthBlockLevel::AllowMissing -- blocks invalid tokens, but allows requests without tokens; this is designed for development use to show authentication while also allowing GraphiQL introspection (the hints and error messages like an IDE); do NOT use this in production!

Endpoints

The two functions .graphql_endpoint() and .playground_endpoint define the locations of your GraphQL endpoint and the endpoint for the GraphiQL playground, though you probably won't use them unless you're using something novel, they are set to /graphql and /graphiql respectively by default.

Schema

The last function is .schema(), which defines the actual schema for your app. You'll need to provide your Query, Mutation and Subscription types here. If you're not using subscriptions, you can use diana::async_graphql::EmptySubscription instead. There's also an EmptyMutation type if you need it. At least one query is mandatory. You should initialize each of these structs for this function with this notation:

Query {}

Building it

Once you've run all those functions, you can build the Options by using .finish(), which will return a Result<Options, diana::errors::Error>. Because you can't do anything at all without the options defined properly, it's typical to run .expect() after this to panic! quickly if the configuration couldn't be built.

Authentication

Authentication is built into Diana out of the box using JWTs. It's designed to be as intuitive as possible, but there are a few things you should know when working with it.

🚧 Authentication is not yet supported over subscriptions, this will be added soon! 🚧

Authentication Block Level

In your configuration, you define a required level of authentication for your GraphQL endpoints using .auth_block_state(). The different levels are explained on the configuration page, so all that will be added now is that they apply to all GraphQL endpoints, from both the queries/mutations and the subscriptions systems. BlockUnauthenticated is vastly preferred and recommended in production.

JWTs

Diana has full support for JWTs out of the box, and uses them internally to allow connections between its two systems. That means that you will need to create a JWT to enable this communication, which can be done using diana::create_jwt! Diana provides a few function for managing JWTs: create_jwt, validate_and_decode_jwt, get_jwt_secret, and decode_time_str. Those are all pretty self-explanatory except perhaps the last one, which turns strings like 1w into one week from the present datetime in seconds after January 1st 1970 (Unix epoch), allowing you to more conveniently define JWT expiries. This is based on Vercel's ms module for JavaScript, though only implements a subset of its features.

The documentation for those functions is best seen directly in raw form here. The most important thing to know is that the JWT for connecting to the subscriptions server MUST define the role property in its payload to be graphql_server. Otherwise authentication will fail for BlockUnauthenticated and AllowMissing.

GraphiQL

GraphiQL is currently only supported in development (it will be disabled by force in production), and so there is as yet no need for authenticating for access to it. If and when it is usable in production, this will come with an authentication system for it.

In development, you may need to provide a JWT that you've generated in order to test authentication. You can do this by opening the Headers panel at the bottom of the screen and typing the following:

{
	"Authorization": "Bearer YOUR_TOKEN_HERE"
}

Please note that authentication is not yet supported for subscriptions, and so this will have no effect on them (equivalent to a permanent AllowAll).

Going Serverless

Diana's most unique feature is its ability to bridge the serverless and serverful gap all in one system, and it's about time we covered how to use the serverless system! As for serverful systems, Diana uses integrations to support different serverless platforms. Currently, the only integration is for AWS Lambda and its derivatives, like Netlify and Vercel, so that's what we'll use here!

Crucially, no part of your schemas or options should have to change to go serverless, it should be simply a different way of using them.

Coding it

First off, install diana-aws-lambda by adding the following to your Cargo.toml for the serverless crate under [dependencies] (notice that versions of integrations and the core library are kept in sync deliberately):

diana-aws-lambda = "0.2.9"
use diana_aws_lambda::{
    netlify_lambda_http::{
        lambda, lambda::Context as LambdaCtx, IntoResponse as IntoLambdaResponse,
        Request as LambdaRequest,
    },
    run_aws_req, AwsError,
};
use lib::get_opts;

#[lambda(http)]
#[tokio::main]
async fn main(req: LambdaRequest, _: LambdaCtx) -> Result<impl IntoLambdaResponse, AwsError> {
    let res = run_aws_req(req, get_opts()).await?;
    Ok(res)
}

This example also expects you to have tokio installed, you'll need a version above v1.0.0 for the runtime to work.

The serverless system is quite a bit simpler than the serverful system actually, because it just runs a query/mutation directly, without any need to run a server for a longer period. This handler is now entirely complete.

One thing to remember that could easily stump you for a while is environment variables. if you're reading from an environment variable file in your configuration setup, don't do that when you're in the serverless environment! And don't forget to add your environment variables to the serverless provider so they're available to your code!

Deploying it

This page will only cover deploying this to Netlify, since that's arguably the most convenient service to set up for Rust serverless functions quickly right now. The rest of this section will assume you have a Netlify account and that you've installed the Netlify CLI. The process is however relatively similar for other services.

Firstly, you'll need to set up a few basic things for Netlify deployment to work. Create a file named rust-toolchain in the serverless crate at its root (next to Cargo.toml). Then put the following in that file:

[toolchain]
channel = "stable"
components = ["rustfmt", "clippy"]
targets = ["x86_64-unknown-linux-musl"]

This tells Netlify how to prepare the environment for Rust. Next, you'll need some static files to deploy as a website (you may already have a frontend to use, otherwise just a basic inde.html is fine). Put these in a new directory in the serverless crate called public. Also create another new empty directory called functions next to it.

Now we'll create a basic Netlify configuration. Create a netlify.toml file in the root of the serverless crate and put the following in it:

[build]
publish = "public"
functions = "functions"

This tells Netlify where your static files and functions are. But we haven't actually got any compiled functions yet, so we'll set those up now! Your final function will be the compiled executable of your code in src/main.rs.

Now we'll create a build script to prepare your function automatically. Create a new file in the serverless crate called build.sh and fill it with the following:

#!/bin/bash

cargo build --release --target x86_64-unknown-linux-musl
cp ./target/x86_64-unknown-linux-musl/release/serverless functions

This will compile your binary for production and copy it to the functions directory, where Netlify can access it. Note that we're compiling for the x86_64-unknown-linux-musl target triple, which is the environment on Netlify's servers. To be able to compile for that target (a variant of Linux), you'll need to add it with rustup target add x86_64-unknown-linux-musl, which will download what you need.

There's one more thing we have to do before we can deploy though, and that's minimizing the size of the binary. Rust by default creates very large binaries, optimizing for speed instead. Diana is large and complex, which exacerbates this problem. Netlify does not like large binaries. At all. Which means we need to slim our release binary down significantly. However, because Netlify support for Rust is in beta, certain very powerful optimizations (like running strip to halve the size) will result in Netlify being unable to even detect your binary. Add the following to your root Cargo.toml (the one for all your crates):

[profile.release]
opt-level = "z"
codegen-units = 1
panic = "abort"

This changes the compiler to optimize for size rather than speed, removes extra unnecessary optimization code, and removes the entire panic handling matrix. What this means is that your binary becomes smaller, which is great! However, if your program happens to panic! in production, it will just abort, so if you have any custom panic handling logic, you'll need to play around with this a bit. Netlify will generally accept binaries under 15MB. Now there are more optimizations we could apply here to make the binary tiny, but then Netlify can't even detect it, so this is the best we can do (if you have something else that works better, please open an issue).

Finally, you can run sh build.sh to build your function! Now we just need to send it to Netlify!

  1. Log in to Netlify from the terminal with netlify login.
  2. Create a new site for this project for manual deployment with netlify init --manual (run this in serverless).
  3. Deploy to production with netlify deploy --prod!

You should now be able to query your live production GraphQL serverless function with any GraphQL or HTTP client! If you're having problems, Netlify's docunmentation may help, and don't forget to look at your site's logs!

Getting Started with Diana Core

Diana is built for use with integrations, but if you want to support a platform without an integration, you'll need to work with Diana core. This shouldn't be too daunting, as it's designed to work as well as possible with queries and mutations in particular. Subscriptions are not yet well supported in Diana Core, and we strongly advise using the diana-actix-web integration for your subscriptions server.

Diana core is just the diana package, which you should already have installed from Getting Started.

This guide is designed to be as generic as possible, and it may be useful to have some perspective on how to actually build an integration, for which you should look to the Actix Web integration. That folder also contains examples of using async_graphql and its integrations more directly to support subscriptions (which is how you would probably do it if you were building your own integration).

Finally, if you build a fully-fledged integration for a serverful or serverless platform, please submit a pull request to get it into the Diana codebase! We'd really appreciate your contribution! You can see our contributing guidelines here

Handling Queries and Mutations

The main struct you'll be dealing with here is DianaHandler, and the API documentation for Diana is your friend here.

You can create a new DianaHandler by running DianaHandler::new() and providing it the Options you're using for your setup. That will automatically create schemas internally for queries/mutation and subscriptions. The two are mutually exclusive.

Running a request

There are two functions you can use for running queries and mutations: .run_stateless_for_subscriptions() and .run_stateless_without_subscriptions(). The first uses the schema for the subscriptions system, which would be used basically only for running the internally used publish mutation. The latter is used for running the user's queries. If you're building for an unsupported platform, you'll need to support both if you want to support subscriptions.

Both functions take the same arguments because they do the same thing, just with different schemas. First, they both take a string request body, which is NOT the query the user wrote! Rather, that should be the stringified JSON body that contains fields for the query, variables, etc. If you make that mistake, you'll get some very strange errors about schema validity no matter what you do!

The second argument is an Option of a string authentication header, which should be the raw value extracted from the HTTP Authorization header (which is where JWTs will be given). Do NOT try to pre-parse this in any way, even resolving it to a string, that will all be handled internally.

The third and final argument is an optional authentication verdict, which can be given to force the handling process to not run any authentication checks on the given token, but rather to use a predetermined verdict. This allows the use of authentication middleware to arrive at a verdict before all the HTTP data has been streamed in (more efficient). You can learn more about this here. If you're not using middleware (not recommended unless you really can't), you should provide None here.

Authentication

If you're not using any middleware, you can entirely ignore this page and get on with building your custom system, but if you want to authenticate users more efficiently, this is for you.

DianaHandler has the function .is_authed() that you can call in middleware, parsing in a raw authentication header just as you would if you were handling queries and mutations without middleware. That will return an AuthVerdict, which tells you if the client is allowed, blocked, or if an error occurred. Typically, you would continue the request on Allow, return a 403 on Block, and return a 500 on Error (though this could be caused by a bad request, it occurs in the context of the server). In future, a distinction may be made between server and client caused errors, which would allow reasonable returning of a 400 in some cases, but that's not yet implemented.

After you have an AuthVerdict, you can send that to your final handler in some way (Actix Web uses request extensions) and then extract it there to provide to run_stateless_without_subscriptions() or .run_stateless_for_subscriptions. If you do that, you don't need to provide the raw authentication header, as it won't be used, but you still can.