User-defined functions

User-defined functions (UDF) are Fauna Query Language Lambda functions, and they can be exposed through the GraphQL API by using the @resolver directive at fields in the Query and Mutation types. This directive has no effect if placed elsewhere.

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.

The UDF result type 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 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 (null if not).

  • before: a marker for the page of result before the current page, if available (null if 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.

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, after is null. If there are more results following the current page, the after points to the first item in the following page.

  • before: a marker representing the previous page of results. If there are no previous results, before is null. If there are previous results before the current page, the before points to size items before the current page, or the first available item if the current page does not start on a multiple of size.

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!