User-defined functions (UDFs)
A user-defined function (UDF) is a set of one or more FQL statements stored as a reusable resource in a Fauna database. Like a stored procedure in SQL, a UDF can accept parameters, perform operations, and return results.
You can use UDFs to encapsulate business logic, making it easier to manage complex operations within your database.
Define a UDF
You create and manage a UDF as an FSL function schema:
function getOrCreateCart(id) {
// Find the customer by ID, using the ! operator to
// assert that the customer exists.
// If the customer does not exist, fauna will throw a
// document_not_found error.
let customer = Customer.byId(id)!
if (customer!.cart == null) {
// Create a cart if the customer does not have one.
Order.create({
status: 'cart',
customer: Customer.byId(id),
createdAt: Time.now(),
payment: {}
})
} else {
// Return the cart if it already exists.
customer!.cart
}
}
You save and manage function schema in Fauna using the
Fauna Dashboard or the
Fauna CLI's
fauna schema push
command.
Reference: | FSL function schema |
---|
Function
collection
Fauna stores UDFs as documents in the Function
system collection. These
documents have the FunctionDef type and are an FQL version of the FSL
function schema. You can use
Function methods to access
and manage UDFs in FQL.
See Function FQL docs |
---|
Call a UDF
Once saved in a database, you can call the UDF in FQL queries against the database:
// Call the `getOrCreateCart()` UDF with a
// customer id of `111`.
getOrCreateCart(111)
UDFs run within the context of a single query and can be combined with other FQL expressions:
let customerId = "111"
// Call the `getOrCreateCart()` UDF.
getOrCreateCart(customerId)
// Then call the `createOrUpdateCartItem()` UDF.
createOrUpdateCartItem(customerId, "pizza", 1)
UDF features
UDFs include features that let you create complex queries and workflows.
Type checking
You can explicitly type a UDF’s arguments and return value:
// The `x` argument must be a `Number`.
// The function returns a `Number` value.
function myFunction(x: Number): Number {
x + 2
}
Multiple statements
UDFs can contain multiple statements and expressions:
function calculateOrderTotal(order) {
// Calculate the subtotal by summing up the prices of all items.
let subtotal = order.items.fold(0, (sum, orderItem) => {
let orderItem: Any = orderItem
if (orderItem.product != null) {
sum + orderItem.product.price * orderItem.quantity
} else {
sum
}
})
// Calculate the tax based on the subtotal.
let tax = subtotal * 0.1
// Return the final total including the tax.
subtotal + tax
}
Like an FQL query, a UDF returns the value of the last evaluated expression.
Composability
UDFs are composable, allowing you to combine multiple UDFs.
For example, you can define a UDF:
// Defines the `applyDiscount()` UDF.
function applyDiscount(total, discountPercent) {
total * (1 - discountPercent / 100)
}
And call the UDF in another UDF definition:
// Defines the `calculateFinalPrice()` UDF.
function calculateFinalPrice(order, discountPercent) {
let order: Any = order
// Calls the `calculateOrderTotal()` UDF.
let total = calculateOrderTotal(order)
// Calls the `applyDiscount()` UDF.
applyDiscount(total, discountPercent)
}
Error handling
Use abort()
to raise an Abort error
from a UDF:
function validateOrderStatusTransition(oldStatus, newStatus) {
if (oldStatus == "cart" && newStatus != "processing") {
// The order can only transition from cart to processing.
abort("Invalid status transition.")
} else if (oldStatus == "processing" && newStatus != "shipped") {
// The order can only transition from processing to shipped.
abort("Invalid status transition.")
} else if (oldStatus == "shipped" && newStatus != "delivered") {
// The order can only transition from shipped to delivered.
abort("Invalid status transition.")
}
}
Multi-tenancy and scope
UDFs are scoped to a single database. A child database can’t access its parent database’s UDFs.
You can copy and deploy UDFs across databases using FSL and a CI/CD pipeline. See Manage schema with a CI/CD pipeline.
Security and privileges
You can use UDFs to control how systems and end users access sensitive data.
UDF privileges
A user-defined role can grant the privilege to call a UDF:
role customer {
// Grants `call` access to the `getOrCreateCart` UDF.
privileges getOrCreateCart {
call
}
}
See Function privileges |
---|
Limits
UDFs are subject to the same global limits as FQL queries.
Runtime privileges
By default, UDFs run with the privileges of the calling query’s authentication secret.
When you define a UDF, you can include an optional @role
annotation. If
provided, the UDF runs using the role’s privileges, regardless of the secret
used to call it:
// Runs with the built-in `server` role's privileges.
@role("server")
function inventory(name) {
Product.byName(name) {
name,
description,
stock
}
}
Control access with UDFs
A common pattern is to allow access to sensitive data through a UDF. The pattern lets you control how the data is accessed without granting broader privileges.
For more control, you can customize the format of data returned by a UDF. This lets you mask, transform, or remove specific fields as needed.
Tutorial: Control access with ABAC |
---|
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!