tauri_plugin_graphql/
lib.rs

1// Copyright 2022 Jonas Kruckenberg
2// SPDX-License-Identifier: MIT
3
4//! This crate contains a Tauri plugin used to expose a [`async_graphql`]
5//! GraphQL endpoint through Tauri's IPC system. This plugin can be used as
6//! safer alternative to Tauri's existing Command API since both the Rust and
7//! JavaScript side of the interface can be generated from a common schema.
8//!
9//! ## Rationale
10//!
11//! Especially in bigger projects that have specialized teams for the Frontend
12//! and Rust core the existing command API falls short of being an optimal
13//! solution. The Frontend is tightly coupled through `invoke()` calls to
14//! backend commands, but there is no type-safety to alert Frontend developers
15//! to changes in command signatures. This results in a very brittle interface
16//! where changes on the Rust side will inadvertently break code in the
17//! Frontend. This problem is similar exiting REST APIs, where the absence of a
18//! formal contract between the server and the frontend makes future changes
19//! very difficult.
20//!
21//! We can employ the same techniques used in traditional web development and
22//! use shared schema that governs which types, methods, etc. are
23//! available. GraphQL is such a schema language.
24//!
25//! ## Examples
26//!
27//! For the following examples, it is assumed you are familiar with [`Tauri
28//! Commands`][`Commands`], [`Events`] and [`GraphQL`].
29//!
30//! ### Queries
31//!
32//! An example app that implements a very simple read-only todo-app using
33//! GraphQL:
34//!
35//! ```rust
36//! use async_graphql::{Schema, EmptySubscription, EmptyMutation, Object, SimpleObject, Result as GraphQLResult};
37//!
38//! #[derive(SimpleObject, Debug, Clone)]
39//! struct ListItem {
40//!     id: i32,
41//!     text: String
42//! }
43//!
44//! impl ListItem {
45//!     pub fn new(text: String) -> Self {
46//!         Self {
47//!             id: rand::random::<i32>(),
48//!             text
49//!         }
50//!     }
51//! }
52//!
53//! struct Query;
54//!
55//! #[Object]
56//! impl Query {
57//!     async fn list(&self) -> GraphQLResult<Vec<ListItem>> {
58//!         let item = vec![
59//!             ListItem::new("foo".to_string()),
60//!             ListItem::new("bar".to_string())
61//!         ];
62//!
63//!         Ok(item)
64//!     }
65//! }
66//!
67//! let schema = Schema::new(
68//!     Query,
69//!     EmptyMutation,
70//!     EmptySubscription,
71//! );
72//!
73//! tauri::Builder::default()
74//!     .plugin(tauri_plugin_graphql::init(schema));
75//! ```
76//!
77//! ### Mutations
78//!
79//! GraphQL mutations provide a way to update or create state in the Core.
80//!
81//! Similarly to queries, mutations have access to a context object and can
82//! manipulate windows, menus or global state.
83//!
84//! ```rust
85//! use async_graphql::{Schema, Object, Context, EmptySubscription, EmptyMutation, SimpleObject, Result as GraphQLObject};
86//! use tauri::{AppHandle, Manager};
87//! use std::sync::Mutex;
88//!
89//! #[derive(Debug, Default)]
90//! struct List(Mutex<Vec<ListItem>>);
91//!
92//! #[derive(SimpleObject, Debug, Clone)]
93//! struct ListItem {
94//!     id: i32,
95//!     text: String
96//! }
97//!
98//! impl ListItem {
99//!     pub fn new(text: String) -> Self {
100//!         Self {
101//!             id: rand::random::<i32>(),
102//!             text
103//!         }
104//!     }
105//! }
106//!
107//! struct Query;
108//!
109//! #[Object]
110//! impl Query {
111//!     async fn list(&self, ctx: &Context<'_>) -> GraphQLObject<Vec<ListItem>> {
112//!       let app = ctx.data::<AppHandle>().unwrap();
113//!
114//!       let list = app.state::<List>();
115//!       let list = list.0.lock().unwrap();
116//!         
117//!       let items = list.iter().cloned().collect::<Vec<_>>();
118//!
119//!       Ok(items)
120//!     }
121//! }
122//!
123//! struct Mutation;
124//!
125//! #[Object]
126//! impl Mutation {
127//!   async fn add_entry(&self, ctx: &Context<'_>, text: String) -> GraphQLObject<ListItem> {
128//!     let app = ctx.data::<AppHandle>().unwrap();
129//!
130//!     let list = app.state::<List>();
131//!     let mut list = list.0.lock().unwrap();
132//!
133//!     let item = ListItem::new(text);
134//!
135//!     list.push(item.clone());
136//!
137//!     Ok(item)
138//!   }
139//! }
140//!
141//! let schema = Schema::new(
142//!     Query,
143//!     Mutation,
144//!     EmptySubscription,
145//! );
146//!
147//! tauri::Builder::default()
148//!     .plugin(tauri_plugin_graphql::init(schema))
149//!     .setup(|app| {
150//!       app.manage(List::default());
151//!
152//!       Ok(())
153//!     });
154//! ```
155//!
156//! ### Subscriptions
157//!
158//! GraphQL subscriptions are a way to push real-time data to the Frontend.
159//! Similarly to queries, a client can request a set of fields, but instead of
160//! immediately returning a single answer, a new result is sent to the Frontend
161//! every time the Core sends one.
162//!
163//! Subscription resolvers should be async and must return a [`Stream`].
164//!
165//! ```rust
166//! use async_graphql::{
167//!   futures_util::{self, stream::Stream},
168//!   Schema, Object, Subscription, EmptySubscription,
169//!   EmptyMutation, SimpleObject, Result as GraphQLResult
170//! };
171//!
172//! struct Query;
173//!
174//! #[Object]
175//! impl Query {
176//!   async fn hello_world(&self) -> GraphQLResult<&str> {
177//!     Ok("Hello World!")
178//!   }
179//! }
180//!
181//! struct Subscription;
182//!
183//! #[Subscription]
184//! impl Subscription {
185//!   async fn hello_world(&self) -> impl Stream<Item = &str> {
186//!     futures_util::stream::iter(vec!["Hello", "World!"])
187//!   }
188//! }
189//!
190//! let schema = Schema::new(
191//!   Query,
192//!   EmptyMutation,
193//!   Subscription,
194//! );
195//!
196//! tauri::Builder::default()
197//!   .plugin(tauri_plugin_graphql::init(schema));
198//! ```
199//!
200//! ## Stability
201//!
202//! To work around limitations with the current command system, this plugin
203//! directly implements an invoke handler instead of reyling on the
204//! [`tauri::generate_handler`] macro.
205//! Since the invoke handler implementation is not considered stable and might
206//! change between releases **this plugin has no backwards compatibility
207//! guarantees**.
208//!
209//! [`Stream`]: https://docs.rs/futures-util/latest/futures_util/stream/trait.Stream.html
210//! [`Commands`]: https://tauri.studio/docs/guides/command
211//! [`Events`]: https://tauri.studio/docs/guides/events
212//! [`GraphQL`]: https://graphql.org
213
214pub use async_graphql;
215use async_graphql::{
216  futures_util::StreamExt, BatchRequest, ObjectType, Request, Schema, SubscriptionType,
217};
218use serde::Deserialize;
219#[cfg(feature = "graphiql")]
220use std::net::SocketAddr;
221use tauri::{
222  plugin::{self, TauriPlugin},
223  Invoke, InvokeError, Manager, Runtime,
224};
225
226fn invoke_handler<R, Query, Mutation, Subscription>(
227  schema: Schema<Query, Mutation, Subscription>,
228) -> impl Fn(Invoke<R>)
229where
230  R: Runtime,
231  Query: ObjectType + 'static,
232  Mutation: ObjectType + 'static,
233  Subscription: SubscriptionType + 'static,
234{
235  move |invoke| {
236    let window = invoke.message.window();
237
238    let schema = schema.clone();
239
240    match invoke.message.command() {
241      "graphql" => invoke.resolver.respond_async(async move {
242        let req: BatchRequest = serde_json::from_value(invoke.message.payload().clone())
243          .map_err(InvokeError::from_serde_json)?;
244
245        let resp = schema
246          .execute_batch(req.data(window.app_handle()).data(window))
247          .await;
248
249        let str = serde_json::to_string(&resp).map_err(InvokeError::from_serde_json)?;
250
251        Ok((str, resp.is_ok()))
252      }),
253      "subscriptions" => invoke.resolver.respond_async(async move {
254        let req: SubscriptionRequest = serde_json::from_value(invoke.message.payload().clone())
255          .map_err(InvokeError::from_serde_json)?;
256
257        let subscription_window = window.clone();
258        let mut stream = schema.execute_stream(req.inner.data(window.app_handle()).data(window));
259
260        let event_id = &format!("graphql://{}", req.id);
261
262        while let Some(result) = stream.next().await {
263          let str = serde_json::to_string(&result).map_err(InvokeError::from_serde_json)?;
264
265          subscription_window.emit(event_id, str)?;
266        }
267        subscription_window.emit(event_id, Option::<()>::None)?;
268
269        Ok(())
270      }),
271      cmd => invoke.resolver.reject(format!(
272        "Invalid endpoint \"{}\". Valid endpoints are: \"graphql\", \"subscriptions\".",
273        cmd
274      )),
275    }
276  }
277}
278
279/// Initializes the GraphQL plugin.
280///
281/// This plugin exposes a async-graphql endpoint via Tauri's IPC system,
282/// allowing the frontend to invoke backend functionality through GraphQL.
283/// **This does not open a web server.**
284///
285/// The `schema` argument must be a valid [`async_graphql::Schema`].
286///
287/// ## Example
288///
289/// ```rust
290/// use async_graphql::{Schema, Object, EmptyMutation, EmptySubscription, SimpleObject, Result as GraphQLResult};
291///
292/// #[derive(SimpleObject)]
293/// struct User {
294///     id: i32,
295///     name: String
296/// }
297///
298/// struct Query;
299///
300/// // Implement resolvers for all possible queries.
301/// #[Object]
302/// impl Query {
303///     async fn me(&self) -> GraphQLResult<User> {
304///         Ok(User {
305///             id: 1,
306///             name: "Luke Skywalker".to_string(),
307///         })
308///     }
309/// }
310///
311/// let schema = Schema::new(
312///     Query,
313///     EmptyMutation,
314///     EmptySubscription,
315/// );
316///
317/// tauri::Builder::default()
318///     .plugin(tauri_plugin_graphql::init(schema));
319/// ```
320pub fn init<R, Query, Mutation, Subscription>(
321  schema: Schema<Query, Mutation, Subscription>,
322) -> TauriPlugin<R>
323where
324  R: Runtime,
325  Query: ObjectType + 'static,
326  Mutation: ObjectType + 'static,
327  Subscription: SubscriptionType + 'static,
328{
329  plugin::Builder::new("graphql")
330    .invoke_handler(invoke_handler(schema))
331    .build()
332}
333
334/// Initializes the GraphQL plugin and GraphiQL IDE.
335///
336/// This plugin exposes a async-graphql endpoint via Tauri's IPC system,
337/// allowing the frontend to invoke backend functionality through GraphQL.
338/// While the regular `init` function does not open a web server, the GraphiQL
339/// client is exposed over http.
340///
341/// The `schema` argument must be a valid [`async_graphql::Schema`].
342///
343/// ## Example
344///
345/// ```rust
346/// use async_graphql::{Schema, Object, EmptyMutation, EmptySubscription, SimpleObject, Result as GraphQLResult};
347///
348/// #[derive(SimpleObject)]
349/// struct User {
350///     id: i32,
351///     name: String
352/// }
353///
354/// struct Query;
355///
356/// // Implement resolvers for all possible queries.
357/// #[Object]
358/// impl Query {
359///     async fn me(&self) -> GraphQLResult<User> {
360///         Ok(User {
361///             id: 1,
362///             name: "Luke Skywalker".to_string(),
363///         })
364///     }
365/// }
366///
367/// let schema = Schema::new(
368///     Query,
369///     EmptyMutation,
370///     EmptySubscription,
371/// );
372///
373/// tauri::Builder::default()
374///     .plugin(tauri_plugin_graphql::init_with_graphiql(schema, ([127,0,0,1], 8080)));
375/// ```
376#[cfg(feature = "graphiql")]
377pub fn init_with_graphiql<R, Query, Mutation, Subscription>(
378  schema: Schema<Query, Mutation, Subscription>,
379  graphiql_addr: impl Into<SocketAddr>,
380) -> TauriPlugin<R>
381where
382  R: Runtime,
383  Query: ObjectType + 'static,
384  Mutation: ObjectType + 'static,
385  Subscription: SubscriptionType + 'static,
386{
387  use async_graphql::http::GraphiQLSource;
388  use async_graphql_warp::{GraphQLBadRequest, GraphQLResponse};
389  use http::StatusCode;
390  use std::convert::Infallible;
391  use warp::{http::Response as HttpResponse, Filter, Rejection};
392
393  let graphiql_addr: SocketAddr = graphiql_addr.into();
394
395  plugin::Builder::new("graphql")
396    .invoke_handler(invoke_handler(schema.clone()))
397    .setup(move |_| {
398      let graphql_post = async_graphql_warp::graphql(schema).and_then(
399        |(schema, request): (
400          Schema<Query, Mutation, Subscription>,
401          async_graphql::Request,
402        )| async move {
403          Ok::<_, Infallible>(GraphQLResponse::from(schema.execute(request).await))
404        },
405      );
406
407      let graphiql = warp::path::end().and(warp::get()).map(move || {
408        HttpResponse::builder()
409          .header("content-type", "text/html")
410          .body(
411            GraphiQLSource::build()
412              .endpoint(&graphiql_addr.to_string())
413              .finish(),
414          )
415      });
416
417      let routes = graphiql
418        .or(graphql_post)
419        .recover(|err: Rejection| async move {
420          if let Some(GraphQLBadRequest(err)) = err.find() {
421            return Ok::<_, Infallible>(warp::reply::with_status(
422              err.to_string(),
423              StatusCode::BAD_REQUEST,
424            ));
425          }
426
427          Ok(warp::reply::with_status(
428            "INTERNAL_SERVER_ERROR".to_string(),
429            StatusCode::INTERNAL_SERVER_ERROR,
430          ))
431        });
432
433      println!("GraphiQL IDE: {}", graphiql_addr);
434
435      tauri::async_runtime::spawn(warp::serve(routes).run(graphiql_addr));
436
437      Ok(())
438    })
439    .build()
440}
441
442#[derive(Debug, Deserialize)]
443struct SubscriptionRequest {
444  #[serde(flatten)]
445  inner: Request,
446  id: u32,
447}