Expand description

This crate contains a Tauri plugin used to expose a juniper GraphQL endpoint through Tauri’s IPC system. This plugin can be used as safer alternative to Tauri’s existing Command API since both the Rust and JavaScript side of the interface can be generated from a common schema.

Rationale

Especially in bigger projects that have specialized teams for the Frontend and Rust core the existing command API falls short of being an optimal solution. The Frontend is tightly coupled through invoke() calls to backend commands, but there is no type-safety to alert Frontend developers to changes in command signatures. This results in a very brittle interface where changes on the Rust side will inadvertently break code in the Frontend. This problem is similar exiting REST APIs, where the absence of a formal contract between the server and the frontend makes future changes very difficult.

We can employ the same techniques used in traditional web development and use shared schema that governs which types, methods, etc. are available. GraphQL is such a schema language.

Examples

For the following examples, it is assumed you are familiar with Tauri Commands, Events and GraphQL.

Queries

An example app that implements a very simple read-only todo-app using GraphQL:

use juniper::{graphql_object, EmptySubscription, EmptyMutation, FieldResult, GraphQLObject, RootNode};
use tauri_plugin_graphql::Context as GraphQLContext;

#[derive(GraphQLObject, Debug, Clone)]
struct ListItem {
    id: i32,
    text: String
}

impl ListItem {
    pub fn new(text: String) -> Self {
        Self {
            id: rand::random::<i32>(),
            text
        }
    }
}

struct Query;

#[graphql_object(context = GraphQLContext)]
impl Query {
    fn list() -> FieldResult<Vec<ListItem>> {
        let item = vec![
            ListItem::new("foo".to_string()),
            ListItem::new("bar".to_string())
        ];

        Ok(item)
    }
}

// Consumers of this schema can only read data,
// so we must specifcy `EmptyMutation` and `EmptySubscription`
type Schema = RootNode<
  'static,
  Query,
  EmptyMutation<GraphQLContext>,
  EmptySubscription<GraphQLContext>
>;

let schema = Schema::new(
    Query,
    EmptyMutation::<GraphQLContext>::new(),
    EmptySubscription::<GraphQLContext>::new(),
);

tauri::Builder::default()
    .plugin(tauri_plugin_graphql::init(schema));

Mutations

GraphQL mutations provide a way to update or create state in the Core.

Similarly to queries, mutations have access to a context object and can manipulate windows, menus or global state.

use juniper::{graphql_object, EmptySubscription, EmptyMutation, FieldResult, GraphQLObject, RootNode};
use tauri_plugin_graphql::Context as GraphQLContext;
use tauri::Manager;
use std::sync::Mutex;

#[derive(Debug, Default)]
struct List(Mutex<Vec<ListItem>>);

#[derive(GraphQLObject, Debug, Clone)]
struct ListItem {
    id: i32,
    text: String
}

impl ListItem {
    pub fn new(text: String) -> Self {
        Self {
            id: rand::random::<i32>(),
            text
        }
    }
}

struct Query;

#[graphql_object(context = GraphQLContext)]
impl Query {
    fn list(ctx: &GraphQLContext) -> FieldResult<Vec<ListItem>> {
      let list = ctx.app().state::<List>();
      let list = list.0.lock().unwrap();
         
      let items = list.iter().cloned().collect::<Vec<_>>();

      Ok(items)
    }
}

struct Mutation;

#[graphql_object(context = GraphQLContext)]
impl Mutation {
  fn add_entry(ctx: &GraphQLContext, text: String) -> FieldResult<ListItem> {
    let list = ctx.app().state::<List>();
    let mut list = list.0.lock().unwrap();

    let item = ListItem::new(text);

    list.push(item.clone());

    Ok(item)
  }
}

// Consumers of this schema can read and write data,
// so set only Subscription as being empty
type Schema = RootNode<
  'static,
  Query,
  Mutation,
  EmptySubscription<GraphQLContext>
>;

let schema = Schema::new(
    Query,
    Mutation,
    EmptySubscription::<GraphQLContext>::new(),
);

tauri::Builder::default()
    .plugin(tauri_plugin_graphql::init(schema))
    .setup(|app| {
      app.manage(List::default());

      Ok(())
    });

Subscriptions

Support for GraphQL Subscriptions requires the subscriptions feature flag

GraphQL subscriptions are a way to push real-time data to the Frontend. Similarly to queries, a client can request a set of fields, but instead of immediately returning a single answer, a new result is sent to the Frontend every time the Core sends one.

Subscription resolvers should be async and must return a Stream.

use juniper::{
 graphql_object, graphql_subscription, EmptyMutation, FieldResult, GraphQLObject,
 RootNode, FieldError,
 futures::Stream
};
use tauri_plugin_graphql::Context as GraphQLContext;
use std::pin::Pin;

struct Query;

#[graphql_object(context = GraphQLContext)]
impl Query {
  fn hello_world() -> FieldResult<String> {
    Ok("Hello World!".to_string())
  }
}

struct Subscription;

type StringStream = Pin<Box<dyn Stream<Item = Result<String, FieldError>> + Send>>;

#[graphql_subscription(context = GraphQLContext)]
impl Subscription {
  async fn hello_world() -> StringStream {
    let stream = juniper::futures::stream::iter(vec![
      Ok("Hello".to_string()),
      Ok("World!".to_string())
    ]);

    Box::pin(stream)
  }
}

// This schema allows queries and subscriptions,
// so the mutations must be set to empty
type Schema = RootNode<
  'static,
  Query,
  EmptyMutation<GraphQLContext>,
  Subscription,
>;

let schema = Schema::new(
  Query,
  EmptyMutation::<GraphQLContext>::new(),
  Subscription,
);

tauri::Builder::default()
  .plugin(tauri_plugin_graphql::init(schema));

Stability

To work around limitations with the current command system, this plugin directly implements an invoke handler instead of reyling on the [tauri::generate_handler] macro. Since the invoke handler implementation is not considered stable and might change between releases this plugin has no backwards compatibility guarantees.

Re-exports

pub use juniper;

Structs

The context that is available to GraphQL resolvers.

Functions

Initializes the GraphQL plugin