Search

Extending FaunaDB with User-Defined Functions

Richard Flosi

5 min read

Oct 23, 2020

Extending FaunaDB with User-Defined Functions

In our Intro to FaunaDB and FQL blog post, you created a simple database with a users collection from the Cloud Dashboard using built-in FQL functions. You can also extend FQL using User-Defined Functions. This post builds on the database from the previous post and demonstrates how to create a user-defined function, how to call it, and introduces strategies for using them. Just like built-in FQL functions, functions you create are composable with other FQL functions.

Get user by email address

In the previous post, you looked-up a user by email address using your users_by_email index with the following FQL:

Get(Match(Index("users_by_email"), "rflosi@bignerdranch.com"));

Which got the response:

{
  ref: Ref(Collection("users"), "272320354017346067"),
  ts: 1595963777460000,
  data: { name: "Richard", email: "rflosi@bignerdranch.com" }
}

This response object has three fields:

  • ref field whose value is a reference to the user document,
  • ts field with an integer timestamp value, and
  • data field whose value is an object containing the user name and email address.

This works well, but what if you want to reuse that logic elsewhere?

Adding GetUserByEmail() to FQL

With FaunaDB you can define your own function for retrieving a user. Using CreateFunction(), specify a name for your new function along with the body which defines the implementation. The body uses the Query() function to delay execution along with the Lambda function to define the inputs and outputs. Optionally you can include a JSON object in the data field to document your function with additional metadata. In the following example, you are encapsulating the FQL above into a new function called GetUserByEmail which takes a single email argument and returns the same response as above.

Remember to build on your database from the previous post. Use the Cloud Dashboard to execute the following FQL:

CreateFunction({
  name: "GetUserByEmail",
  body: Query(
    Lambda(["email"], Get(Match(Index("users_by_email"), Var("email"))))
  ),
  data: { description: "Look up a user by email address." },
});

Running this CreateFunction call gives an object:

{
  ref: Ref(Ref("functions"), "GetUserByEmail"),
  ts: 1602861460300000,
  name: "GetUserByEmail",
  body: Query(
    Lambda(["email"], Get(Match(Index("users_by_email"), Var("email"))))
  ),
  data: {
    description: "Look up a user by email address."
  }
}

This object has five fields. Two fields are new with this response:

  • ref field whose value is a reference to the function,
  • ts field with an integer timestamp value,

while the other three repeat the value you used to create the function:

  • name field with the string name of the function,
  • body field whose value is the implementation of the function, and
  • data field whose value is the data object you provided.

Just like retrieving a document, you can retrieve the data returned from CreateFunction() by calling Get() on your function reference:

Get(Function("GetUserByEmail"));

This response is the same as the one above from CreateFunction(). You might use this if you wanted to programmatically read metadata from the data field to produce documentation. CreateDatabase()CreateCollection()CreateIndex() and other functions also use this optional data field to attach metadata.

Function names must be unique within the scope of the enclosing database. FaunaDB will throw an error if you try to create a function with a name that already exists. You can, however, use Replace() to update an existing function.

Calling User-Defined Functions

Your new function can now replace the FQL you originally used. To call a user-defined function, use the Call(); function:

Call(Function("GetUserByEmail"), ["rflosi@bignerdranch.com"]);

The first argument to Call() is the function reference. The second argument is the array of arguments.

As you’d expect, the result is the same as the inline FQL we started with:

{
  ref: Ref(Collection("users"), "272320354017346067"),
  ts: 1595963777460000,
  data: { name: "Richard", email: "rflosi@bignerdranch.com" }
}1

Strategies for using User-Defined Functions

Now that you know how to create custom functions, the next question is: why would you want to do so?

Often in software development, you’ll create a function to encapsulate logic that is used in multiple places in order to follow the Don’t Repeat Yourself (DRY) principle, which also applies when working with FQL. Typically you write APIs as a communication layer to a database and implement the Create, Read, Update, Delete (CRUD) pattern. With FaunaDB’s user-defined functions, you can implement CRUD within the database itself. For example, you can create the following functions to manage a user:

  • Function(“UserCreate”) creates a new user validating user input.
  • Function(“UserRead”) reads an existing user by token, id, or by email address as you did in this blog post.
  • Function(“UserUpdate”) updates an existing user.
  • Function(“UserDelete”) removes an existing user.

Implementing CRUD within the database removes that logic from the API. Each endpoint just Call()s the corresponding user-defined function. This strategy is particularly interesting because it follows the DRY principle by keeping the logic in one place, your database. Furthermore, this approach allows you to update a function in Fauna without needing to change the API code. As such, you’ll save a deploy cycle on the API, and you won’t have to worry about a client running obsolete API code since the change in Fauna will take effect immediately.

What are some ways you might reuse the Function(“GetUserByEmail”) you created in this post? In the CRUD example above you may use it as part of Function(“UserRead”) to look up the user by email address. You might even use Function(“UserRead”) in other CRUD methods like Function(“UserUpdate”) to read the current state of the user while applying a partial update like a RESTful PATCH operation. You can also reuse this function in user login and forgot password flows.

Limitations

  • User-defined function names must be unique across the database they’re defined in.
  • User-defined functions must be executed with the same number of parameters that they are defined to accept.
  • User-defined functions must complete execution within a 30-second transaction timeout before the transaction is terminated.
  • If a user-defined function exhausts available memory, the transaction is terminated.
  • User-defined functions can implement recursion, but recursion is limited to a depth of 200 calls.

Summary

In this post, you encapsulated the logic of querying a user by email address in FQL into a user-defined function (UDF). You can now Call() your function from multiple places. The Function(“GetUserByEmail”) used in this example would most likely be used in user login and forgot password flows. You could generalize this to a Function(“UserRead”) that takes a single object as an argument and uses the built-in Contains() and Select() functions to detect and destructure an email or id value, then look up a user by token, id, or email address depending on what was provided.

Next Steps

Now that you’re are able to create databases, collections, documents, and functions with the help of this blog post and the Intro to FaunaDB and FQL post, you’ll probably want to learn a bit more about FQL followed by Fauna’s User-defined rolesAttribute-based access control (ABAC), and the fauna-shell for accessing the database as other users via the Login() function.

Richard Flosi

Author Big Nerd Ranch
Richard Flosi brings 20+ years of web development experience to Big Nerd Ranch. Richard is passionate about using full-stack JavaScript serverless architecture to build scalable long-term solutions.
Speak with a Nerd

Schedule a call today! Our team of Nerds are ready to help

Let's Talk

Related Posts

We are ready to discuss your needs.

Not applicable? Click here to schedule a call.

Stay in Touch WITH Big Nerd Ranch News