Four Key Reasons to Learn Markdown
Back-End Leveling UpWriting documentation is fun—really, really fun. I know some engineers may disagree with me, but as a technical writer, creating quality documentation that will...
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.
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:
This works well, but what if you want to reuse that logic elsewhere?
GetUserByEmail()
to FQLWith 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:
while the other three repeat the value you used to create the function:
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.
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
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:
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.
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.
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 roles, Attribute-based access control (ABAC), and the fauna-shell for accessing the database as other users via the Login() function.
Writing documentation is fun—really, really fun. I know some engineers may disagree with me, but as a technical writer, creating quality documentation that will...
Humanity has come a long way in its technological journey. We have reached the cusp of an age in which the concepts we have...
Go 1.18 has finally landed, and with it comes its own flavor of generics. In a previous post, we went over the accepted proposal and dove...