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);
}