Dynamic Relationships

Dynamic relationships are similiar to dynamic properties, but returning dynamically calculated relationships to other nodes as opposed to individual properties.

Configuration

The configuration below includes a dynamic resolver called resolve_project_top_contributor for the top_contributor relationship. That resolver name will be used later to associate a Rust function to carry out the dynamic resolution.

static CONFIG: &str = "
version: 1
model: 
 - name: User
   props:
    - name: name
      type: String
 - name: Project
   props: 
    - name: name
      type: String 
   rels:
     - name: top_contributor
       nodes: [User]

Implementation

The next step is to define the custom resolution function in Rust. In this example, the custom relationship resolver creates a hard-coded node and relationship. In a real system, the function might load records and do some calculation or analytic logic to determine who is the top contributor to a project, and then return that user.


fn resolve_project_top_contributor(
    facade: ResolverFacade<AppRequestContext>,
) -> BoxFuture<ExecutionResult> {
    Box::pin(async move {
        // create dynamic dst node
        let mut top_contributor_props = HashMap::<String, Value>::new();
        top_contributor_props.insert(
            "id".to_string(),
            Value::from(Uuid::new_v4().to_hyphenated().to_string()),
        );
        top_contributor_props.insert("name".to_string(), Value::from("user0".to_string()));
        let top_contributor = facade.node("User", top_contributor_props);

        // create dynamic rel
        let rel_id = "1234567890".to_string();
        let top_contributor_rel = facade.create_rel_with_dst_node(
            Value::from(rel_id),
            "topdev",
            HashMap::new(),
            top_contributor,

Add the Resolver to the Engine

The resolver is added to a map associated with the name used in the configuration, above. The map is then passed to the Warpgrapher engine. This allows the engine to find the Rust function implementing the custom resolver when it is needed.

        .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(
        "resolve_project_top_contributor".to_string(),
        Box::new(resolve_project_top_contributor),
    );

Example API Call

The following GraphQL query uses the dynamic resolver defined above.

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

    // create new project
    let query = "
        mutation {
            ProjectCreate(input: {
                name: \"Project1\"
            }) {
                id
                top_contributor {
                    dst {
                        ... on User {
                            id
                            name
                        }
                    }
                }

Note that the Warpgrapher engine does not create a top level relationship query for properties that have custom resolvers. For example, there is no ProjectTopContributor root level relationship query. This is because the standard Warpgarpher resolver generated for a relationship query would not know how to handle the dynamic relationship.

Full Example Source

See below for the full source code to the example above.

use std::collections::HashMap;
use std::convert::TryFrom;
use uuid::Uuid;
use warpgrapher::engine::config::Configuration;
use warpgrapher::engine::context::RequestContext;
use warpgrapher::engine::database::cypher::CypherEndpoint;
use warpgrapher::engine::database::DatabaseEndpoint;
use warpgrapher::engine::objects::Options;
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: User
   props:
    - name: name
      type: String
 - name: Project
   props: 
    - name: name
      type: String 
   rels:
     - name: top_contributor
       nodes: [User]
       resolver: resolve_project_top_contributor
";

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

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

fn resolve_project_top_contributor(
    facade: ResolverFacade<AppRequestContext>,
) -> BoxFuture<ExecutionResult> {
    Box::pin(async move {
        // create dynamic dst node
        let mut top_contributor_props = HashMap::<String, Value>::new();
        top_contributor_props.insert(
            "id".to_string(),
            Value::from(Uuid::new_v4().to_hyphenated().to_string()),
        );
        top_contributor_props.insert("name".to_string(), Value::from("user0".to_string()));
        let top_contributor = facade.node("User", top_contributor_props);

        // create dynamic rel
        let rel_id = "1234567890".to_string();
        let top_contributor_rel = facade.create_rel_with_dst_node(
            Value::from(rel_id),
            "topdev",
            HashMap::new(),
            top_contributor,
            Options::default(),
        )?;

        facade.resolve_rel(&top_contributor_rel).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(
        "resolve_project_top_contributor".to_string(),
        Box::new(resolve_project_top_contributor),
    );

    // 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 = "
        mutation {
            ProjectCreate(input: {
                name: \"Project1\"
            }) {
                id
                top_contributor {
                    dst {
                        ... on User {
                            id
                            name
                        }
                    }
                }
            }
        }
    "
    .to_string();
    let metadata = HashMap::new();
    let result = engine.execute(query, None, metadata).await.unwrap();

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