User-defined functions
User-defined functions (UDF) are Fauna Query Language
Lambda functions, and they can used as custom resolvers for the
GraphQL API by using the
@resolver directive on fields
in the Query and Mutation types. This directive has no effect if placed
elsewhere in a schema.
Arguments
The UDF must accept an array of arguments, the same number and order as the associated field in the GraphQL schema. While an FQL function can accept a single argument as a scalar value, the GraphQL API always passes arguments, even a single argument, as an array. There is no association between the arguments' names in the GraphQL schema and the arguments' names in the UDF definition. When a UDF field is queried, the GraphQL API simply calls the underlying UDF with an array of the given arguments.
Return values
The UDF return value(s) must be a GraphQL-compatible type. If an object is returned, an equivalent type must exist in the GraphQL schema to allow users to select which fields to return. Embedded types can be used to map return types that are not associated with any existing type.
UDFs can have a specified role, which can grant the UDF privileges that differ from the identity executing the GraphQL query. This is useful to allow a UDF to access or modify documents that the calling identity cannot access. The important point is that GraphQL return values must be expressed as known fields in the schema — if the calling identity cannot access the types returned, the query fails. For this situation, your GraphQL query should use an Embedded type so that the calling identity can access the return values from the UDF.
Pagination
UDFs with pagination support must return a database page. In addition to the function arguments, the API calls the UDF with three additional arguments appended the query’s arguments:
-
size: the requested page size -
after: a marker for the page of results following the current page, if available (nullif not). -
before: a marker for the page of result before the current page, if available (nullif not).
A UDF’s implementation must account for these additional arguments and
call the appropriate form of the FQL Paginate function to return
a database page to the GraphQL API.
Schema import
When a schema is imported that
involves the @resolver directive, the import logic checks for an
existing UDF having the specified name. If no UDF exists with the
specified name, a "template" UDF is created for you. The template UDF
aborts the query with an error, since the actual logic to handle the
field has not been implemented.
Once a template UDF has been created, you can update the UDF with the actual functionality. For example:
Update(
Function("my_function"),
{
"body": Query(
Lambda(["param1", "param2"],
// your logic here
)
}
)If you attempt to run CreateFunction using the template UDF’s
name, you receive an error because the UDF already exists.
Examples
A UDF that returns a scalar type:
The following is a UDF, using Fauna Shell syntax, that returns a scalar type:
CreateFunction({
name: "say_hello",
body: Query(Lambda(["name"],
Concat(["Hello ", Var("name")])
))
})A GraphQL schema that uses the UDF:
type Query {
sayHello(name: String!): String! @resolver(name: "say_hello")
}
With these in place, when you run the following query:
{
sayHello(name: "Jane")
}
The result should be:
{
"data": {
"sayHello": "Hello Jane"
}
}
A UDF that returns an embedded object
The following is a UDF, in Fauna Shell syntax, that returns an embedded object:
CreateFunction({
name: "sample_obj",
body: Query(Lambda([], {
time: Time("now"),
sample: true
}))
})A GraphQL schema that uses the UDF:
type SampleObj @embedded {
time: Time!
sample: Boolean!
}
type Query {
sampleObj: SampleObj! @resolver(name: "sample_obj")
}
With these in place, when you run the following query:
{
sampleObj {
time
sample
}
}
The result should be:
{
"data": {
"sampleObj": {
"time": "2019-06-14T17:42:54.001987Z",
"sample": true
}
}
}
A UDF that returns a database page
The following is a UDF (along with a Collection and Index), in Fauna Shell syntax, that handles paginated results, so that repeated querying can retrieve all of the paginated results.
CreateCollection({
name: "users"
})CreateIndex({
name: "vip_users",
source: Collection("users"),
terms: [{ field: [ "data", "vip" ] }]
})CreateFunction({
name: "vip_users",
body: Query(Lambda(["size", "after", "before"],
Let(
{
match: Match(Index("vip_users"), true),
page: If(
Equals(Var("before"), null),
If(
Equals(Var("after"), null),
Paginate(Var("match"), { size: Var("size") }),
Paginate(Var("match"), { size: Var("size"), after: Var("after") })
),
Paginate(Var("match"), { size: Var("size"), before: Var("before") }),
)
},
Map(Var("page"), Lambda("ref", Get(Var("ref"))))
)
))
})The function accepts the following parameters:
-
size: The maximum number of items to include in a page of results. -
after: a marker representing the next page of results. If there are no more results,afterisnull. If there are more results following the current page, theafterpoints to the first item in the following page. -
before: a marker representing the previous page of results. If there are no previous results,beforeisnull. If there are previous results before the current page, thebeforepoints tosizeitems before the current page, or the first available item if the current page does not start on a multiple ofsize.
The function’s complexity comes from handling the null cases for
after and before. In each case, a the result of a Paginate
call is assigned to the page variable, which is used in the Map
function call (at the end) to fetch the details for the page of results.
A GraphQL schema that uses the UDF:
type User @collection(name: "users") {
username: String!
vip: Boolean!
}
type Query {
vips: [User!] @resolver(name: "vip_users", paginated: true)
}
With these in place, when you run the following query:
{
vips(
_size: 2
_cursor: "2DOB2DRyMjM1MTcwOTY5MDA0NTQwNDIzgWV1c2Vyc4FnY2xhc3Nlc4CAgIA="
) {
data {
username
}
after
before
}
}
Notice that our query includes _size and _cursor as arguments to the
resolver, and after and before in the results. The _cursor
parameter is a value acquired after performing the initial query (not
shown here), which provided the value in the after or before fields
in the results. A cursor includes both the position and direction, which
the GraphQL API uses to populate the after and before parameters
passed to the underlying FQL function.
The result should be:
{
"data": {
"vips": {
"data": [
{ "username": "Mary" },
{ "username": "Ted" }
],
"after": null,
"before": "2DKB2DRyMjM1MTcwOTY5MDA0NTQwNDIzgWV1c2Vyc4FnY2xhc3Nlc4CAgIA="
}
}
}
From the results, we can see that after is null, which means that
there are no more following pages of results, but that before is
defined. A subsequent query can pass the value of before as the
_cursor argument to receive the page of results prior to the current
results.
Is this article helpful?
Tell Fauna how the article can be improved:
Visit Fauna's forums
or email docs@fauna.com
Thank you for your feedback!