Custom resolvers
NOTICE: Custom resolvers are temporarily disabled. See this issue. Until certain candid
and authorization
issues are worked out, you'll want to create custom functions in your Rust or Motoko canisters that provide their own authorization and call into your graphql canister
. See here and here for some information and examples.
DISCLAIMER: Custom resolvers have only been minimally tested. Information presented here may not be entirely accurate. If you find issues please get in contact with @lastmjs or open issues on the repository.
Though Sudograph generates many powerful CRUD operations for you, it will not be able to cover every conceivable requirement of your applications. Custom resolvers provide a way for you to create your own functionality that is accessible through the same GraphQL API as Sudograph's generated functionality. There are two main locations a resolver can be written, within the graphql canister
or in a separate canister.
Resolvers within the graphql canister
You can see a full example of Rust custom resolvers here.
To write resolvers within your graphql canister
, start by augmenting your schema, for example in canisters/graphql/src/schema.graphql
:
type Query {
custom_get(id: ID!): Message
}
type Mutation {
custom_set(id: ID!, text: String): Boolean!
}
type Message {
id: ID!
text: String!
}
We've added one custom query and one custom mutation to the schema. Next we need to implement the resolvers in code.
To implement a resolver, we add an asynchronous function to the Rust file that contains our graphql_database
macro invocation. The function should have the same name as the query or mutation in the schema, and should use parameter and return types that match the types in the schema. The return type should be a Result
with the Ok
variant matching the return type in the schema, and you should use sudograph::async_graphql::Error
as the Err
variant. Object types generated from your schema are automatically in scope in Rust, because they are generated by the graphql_database
macro.
Type conversions between GraphQL and Rust can be found here.
Now we'll implement the custom resolvers for the query and mutation in canisters/graphql/src/graphql.rs
:
#![allow(unused)] fn main() { use sudograph::graphql_database; graphql_database!("canisters/graphql/src/schema.graphql"); type PrimaryKey = String; type MessageStore = HashMap<PrimaryKey, Option<Message>>; async fn custom_get(id: ID) -> Result<Option<Message>, sudograph::async_graphql::Error> { let message_store = sudograph::ic_cdk::storage::get::<MessageStore>(); let message_option = message_store.get(&id.to_string()); match message_option { Some(message) => { return Ok(message.clone()); }, None => { return Ok(None); } }; } async fn custom_set(id: ID, text: Option<String>) -> Result<bool, sudograph::async_graphql::Error> { let message_store = sudograph::ic_cdk::storage::get_mut::<MessageStore>(); let message = match text { Some(text_value) => Some(Message { id: id.clone(), text: text_value }), None => None }; message_store.insert(id.to_string(), message); return Ok(true); } }
Resolvers within a different canister
You can also write resolvers that are deployed to other canisters, using any language supported by the Internet Computer. For now you'll most likely be using Rust or Motoko, so examples are included below.
The process is similar to what you've just seen above, but in your GraphQL schema the custom queries and mutations have the addition of a @canister
directive with the canister id of the canister that implements your resolver function.
Rust
In a Rust canister, start by augmenting your schema, for example in canisters/graphql/src/schema.graphql
:
type Query {
custom_get(id: ID!): Message @canister(id: "ryjl3-tyaaa-aaaaa-aaaba-cai")
}
type Mutation {
custom_set(id: ID!, text: String): Boolean! @canister(id: "ryjl3-tyaaa-aaaaa-aaaba-cai")
}
type Message {
id: ID!
text: String!
}
Notice we've added @canister(id: "ryjl3-tyaaa-aaaaa-aaaba-cai")
to the custom query and mutation.
Now we need to implement the Rust canister. Let's imagine we've created another Rust canister in canisters/another-rust-canister
. We might have a file called canisters/another-rust-canister/src/lib.rs
, and it would look like this:
#![allow(unused)] fn main() { use sudograph; // TODO This hasn't been tested, might need some derive macros struct ID(String); impl ID { fn to_string(&self) -> String { return String::from(&self.0); } } // TODO This hasn't been tested, might need some derive macros struct Message { id: String, text: String }; type PrimaryKey = String; type MessageStore = HashMap<PrimaryKey, Option<Message>>; #[sudograph::ic_cdk_macros::query] async fn custom_get(id: ID) -> Option<Message> { let message_store = sudograph::ic_cdk::storage::get::<MessageStore>(); let message_option = message_store.get(&id.to_string()); match message_option { Some(message) => { return message.clone(); }, None => { return None; } }; } #[sudograph::ic_cdk_macros::update] async fn custom_set(id: ID, text: Option<String>) -> bool { let message_store = sudograph::ic_cdk::storage::get_mut::<MessageStore>(); let message = match text { Some(text_value) => Some(Message { id: id.clone(), text: text_value }), None => None }; message_store.insert(id.to_string(), message); return true; } }
Notice that these functions do not return a Result
, they directly return the Rust types that correspond to the GraphQL types. This may change in the future as returning a Result
may end up being more appropriate.
Also notice that we had to implement the ID
and Message
types ourselves. We do not have all of the generated types available because we are not using the graphql_database
macro in this canister. In the future Sudograph may provide a simple way to generate these types for you without generating the entire database, but for now you'll have to implement them yourself or figure out an appropriate way to induce proper serialization and deserialization. For example, Candid might serialize and deserialize ID
to and from strings for us...you'll just have to figure this out on your own for now.
Motoko
You can see a full example of Motoko custom resolvers here.
In a Motoko canister, start by augmenting your schema, for example in canisters/graphql/src/schema.graphql
:
type Query {
customGet(id: ID!): Message @canister(id: "ryjl3-tyaaa-aaaaa-aaaba-cai")
}
type Mutation {
customSet(id: ID!, text: String): Boolean! @canister(id: "ryjl3-tyaaa-aaaaa-aaaba-cai")
}
type Message {
id: ID!
text: String!
}
Notice we've added @canister(id: "ryjl3-tyaaa-aaaaa-aaaba-cai")
to the custom query and mutation.
Now we need to implement the Motoko canister. Let's imagine we've created a Motoko canister in canisters/motoko
. We might have a file called canisters/motoko/main.mo
, and it would look like this:
import Text "mo:base/Text";
import Map "mo:base/HashMap";
import Option "mo:base/Option";
actor Motoko {
let message_store = Map.HashMap<Text, ?Message>(10, Text.equal, Text.hash);
type Message = {
id: Text;
text: Text;
};
public query func customGet(id: Text): async ?Message {
return Option.flatten(message_store.get(id));
};
public func customSet(id: Text, text: ?Text): async Bool {
let message: ?Message = switch (text) {
case null null;
case (?text_value) Option.make({
id;
text = text_value;
});
};
message_store.put(id, message);
return true;
};
}
Implementing the Motoko resolvers is very similar to implementing the Rust resolvers, the biggest difference besides the lanuage itself being the type conversions. We've implemented the Message
type, and we've excluded the ID
type and just used the native Motoko Text
type. Again, you might have to experiment with the serialization and deserialization of values between canisters, a lot of it has to do with Candid.
Other languages
Other languages are somewhat possible to use now (C
, C++
, AssemblyScript
), and many more will come in the future as WebAssembly matures. Writing resolvers in each of these languages will be similar to writing them in Rust or Motoko. Once your schema is setup and correctly pointing to a canister, you simply implement the resolver in the language of choice and ensure that the types align correctly.
Type conversions
GraphQL -> Rust
Object
, ID
, and Date
types must be created in Rust canisters if the graphql_database
macro is not invoked. ID
and Date
types might work as String
in Rust.
Blob
->Vec<u8>
Boolean
->bool
Date
->Date
Float
->f32
ID
->ID
Int
->i32
JSON
->serde_json::Value
String
->String
Creating a custom ID
type:
#![allow(unused)] fn main() { // TODO This hasn't been tested, might need some derive macros struct ID(String); impl ID { fn to_string(&self) -> String { return String::from(&self.0); } } }
Creating a custom Date
type:
#![allow(unused)] fn main() { // TODO This hasn't been tested, might need some derive macros struct Date(String); impl Date { fn to_string(&self) -> String { return String::from(&self.0); } } }
GraphQL -> Motoko
Object types must be manually created in Motoko.
Blob
->Blob
Boolean
->Bool
Date
->Text
Float
->Float
ID
->Text
Int
->Int32
JSON
->Text
(it's unclear if this will work)String
->Text