Defined Endpoints

In addition to the CRUD endpoints auto-generated for each type, Warpgrapher provides the ability to define additional custom endpoints.

Configuration

The schema for an endpoint entry in the Warpgrapher configuration is as follows.

endpoints:
  - name: String
    class: String         # /Mutation | Query/
    input:                # null if there is no input parameter
      type: String
      list: Boolean
      required: Boolean
    output:               # null if there is no input parameter
      type: String
      list: Boolean       # defaults to false
      required: Boolean   # defaults to false

The name of the endpoint will be used later as the key to a hash of endpoint resolution fuctions. It uniquely identified this endpoint. The class attribute tells Warpgrapher whether this endpoint belongs under the root query or root mutation object. The convention is that any operation with side effects, modifying the persistent data store, should be a mutation. Read-only operations are queries. The input attribute allows specification of an input to the endpoint function. The input type may be a scalar GraphQL type -- Boolean, Float, ID, Int, or String -- or it may be a type defined elsewhere in the model section of the Warpgrapher configuration. The list determines whether the input is actually a list of that type rather than a singular instance. If the required attribute is true, the input is required. If false, the input is optional. The output attribute describes the value returned by the custom endpoint. It has fields similar to input, in that it includes type, lsit, and required attributes.

The following configuration defines a custom endpoints, TopIssue.

version: 1
model: 
 - name: Issue
   props: 
    - name: name
      type: String 
    - name: points
      type: Int 
endpoints:
  - name: TopIssue
    class: Query
    input: null
    output:
      type: Issue

Implementation

To implement the custom endpoint, a resolver function is defined, as follows. In this example, the function just puts together a static response and resolves it. A real system would like do some comparison of nodes and relationships to determine the top issue, and dynamically return that record.

#![allow(unused)]
fn main() {
// endpoint returning a list of `Issue` nodes
fn resolve_top_issue(facade: ResolverFacade<AppRequestContext>) -> BoxFuture<ExecutionResult> {
    Box::pin(async move {
        let top_issue = facade.node(
            "Issue",
            hashmap! {
                "name".to_string() => Value::from("Learn more rust".to_string()),
                "points".to_string() => Value::from(Into::<i64>::into(5))
            },
        );

        facade.resolve_node(&top_issue).await
    })
}
}

Add Resolvers to the Warpgrapher Engine

To add the custom endpoint resolver to the engine, it must be associated with the name the endpoint was given in the configuration above. The example code below creates a HashMap to map from the custom endpoint name and the implementing function. That map is then passed to the Engine when it is created.

#![allow(unused)]
fn main() {
    // define resolvers
    let mut resolvers = Resolvers::<AppRequestContext>::new();
    resolvers.insert("TopIssue".to_string(), Box::new(resolve_top_issue));

    // create warpgrapher engine
    let engine: Engine<AppRequestContext> = Engine::new(config, db)
        .with_resolvers(resolvers)
        .build()
        .expect("Failed to build engine");
}

Example of Calling the Endpoint

The code below calls the endine with a query that exercises the custom endpoint.

    let query = "
        query {
            TopIssue {
                name
                points
            }
        }
    "
    .to_string();
    let metadata = HashMap::new();
    let result = engine.execute(query, None, metadata).await.unwrap();

Full Example Source

See below for the full code for the example above.

use maplit::hashmap;
use std::collections::HashMap;
use std::convert::TryFrom;
use warpgrapher::engine::config::Configuration;
use warpgrapher::engine::context::RequestContext;
use warpgrapher::engine::database::cypher::CypherEndpoint;
use warpgrapher::engine::database::DatabaseEndpoint;
use warpgrapher::engine::resolvers::{ExecutionResult, ResolverFacade, Resolvers};
use warpgrapher::engine::value::Value;
use warpgrapher::juniper::BoxFuture;
use warpgrapher::Engine;

static CONFIG: &str = "
version: 1
model: 
 - name: Issue
   props: 
    - name: name
      type: String 
    - name: points
      type: Int 
endpoints:
  - name: TopIssue
    class: Query
    input: null
    output:
      type: Issue
";

#[derive(Clone, Debug)]
struct AppRequestContext {}

impl RequestContext for AppRequestContext {
    type DBEndpointType = CypherEndpoint;
    fn new() -> AppRequestContext {
        AppRequestContext {}
    }
}

// endpoint returning a list of `Issue` nodes
fn resolve_top_issue(facade: ResolverFacade<AppRequestContext>) -> BoxFuture<ExecutionResult> {
    Box::pin(async move {
        let top_issue = facade.node(
            "Issue",
            hashmap! {
                "name".to_string() => Value::from("Learn more rust".to_string()),
                "points".to_string() => Value::from(Into::<i64>::into(5))
            },
        );

        facade.resolve_node(&top_issue).await
    })
}

#[tokio::main]
async fn main() {
    // parse warpgrapher config
    let config = Configuration::try_from(CONFIG.to_string()).expect("Failed to parse CONFIG");

    // define database endpoint
    let db = CypherEndpoint::from_env()
        .expect("Failed to parse cypher endpoint from environment")
        .pool()
        .await
        .expect("Failed to create cypher database pool");

    // define resolvers
    let mut resolvers = Resolvers::<AppRequestContext>::new();
    resolvers.insert("TopIssue".to_string(), Box::new(resolve_top_issue));

    // create warpgrapher engine
    let engine: Engine<AppRequestContext> = Engine::new(config, db)
        .with_resolvers(resolvers)
        .build()
        .expect("Failed to build engine");

    // create new project
    let query = "
        query {
            TopIssue {
                name
                points
            }
        }
    "
    .to_string();
    let metadata = HashMap::new();
    let result = engine.execute(query, None, metadata).await.unwrap();

    // verify result
    println!("result: {:#?}", result);
}