FSL function schema

Learn: User-defined functions (UDFs)

This page covers the FSL syntax for function schemas. For an overview, see user-defined function (UDF).

An FSL function schema defines a user-defined function (UDF). 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.

@role("server-readonly")
// Defines the `getCustomerName` UDF.
function getCustomerName(customerId: ID): String {
  // Find the customer document by ID.
  let customer = Customer.byId(customerId)

  // Return the customer's name field.
  customer!.name
}

You can create and manage schema using any of the following:

Fauna stores each function schema as an FQL document in the Function system collection.

FSL syntax

[@role("<roleName>")]
[@alias(<aliasId>]
function <functionName> (<parameter>: <parameterType>): <returnType> {
  <functionBody>
}

Name

functionName String Required

Unique name of the UDF. The name is case insensitive and can’t be a reserved word.

Properties

Parameter Type Required Description

parameter

String

true

functionBody parameter

Supports the ... syntax for variadic arguments. See Variadic arguments.

parameterType

String

true

parameter type

returnType

String

true

functionBody return type

functionBody

String

true

FQL block.

Annotations

roleName String

Associates a runtime role with the UDF. For example: @role(server).

By default, UDFs run with the privileges of the calling query’s authentication secret. If a @role annotation is provided, the UDF runs using the annotated role’s privileges, regardless of the secret used to call it.

The Role can be a user-defined role or one of the following built-in roles:

  • admin

  • server

  • server-readonly

@role annotations are typically used to give a role controlled access to sensitive data without granting broader privileges. See Runtime privileges.

Use @role carefully. Use the role with the fewest privileges needed perform the UDF’s operations.

If a UDF returns document references, the secret used to call the UDF must have read privileges for the referenced document’s collection. As a workaround, update the UDF to project the desired fields and return the data as an array or object.

aliasId String or Identifier

The optional @alias annotation defines a second identifier for the function. The aliasId can be a String, "Foo", or an identifier, Foo.

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. An example FSL role schema:

role customer {
  // Grants `call` access to the `getOrCreateCart` UDF.
  privileges getOrCreateCart {
    call
  }
}

The built-in admin and server roles have privileges to call any UDF. The built-in server-readonly role can call UDFs that only perform read operations.

See Function privileges

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
  }
}

Resolve document references returned by UDFs

If a UDF returns document references in the query results, the secret used to run the query must have read privileges for the referenced document’s collection. This requirement applies even if the UDF’s @role has read privileges for the collection.

For example, the following UDF returns a Set of documents. Each document contains a document reference:

// Runs with the built-in `server-readonly` role's privileges.
// The role has `read` privileges for the `Category` collection.
@role("server-readonly")
function getCategory(name) {
  // Returns the `category` field, which contains
  // a reference to a `Category` collection document.
  Product.byName(name) {
    name,
    category
  }
}

If you call the UDF using a secret that lacks read privileges for the referenced document’s collection, the reference is not resolved in the results:

getCategory("limes")
{
  data: [
    {
      name: "limes",
      category: Category("789") /* permission denied */
    }
  ]
}

Although the UDF’s @role has the required privileges, document references in Sets and documents are lazily loaded. The references are resolved, or materialized, only when results are returned — after the UDF runs.

To solve this issue without granting additional privileges, update the UDF to:

  • Project or map any desired fields from the referenced document.

  • Convert the results to an eager-loading type, such as an Array or Object.

Return a Set as an array

If the UDF originally returned a Set of documents, update it to return the Set as an array:

@role("server-readonly")
function getCategory(name) {
  // Project any desired fields from the referenced
  // `Category` document.
 let products = Product.byName(name) {
    name,
    category {
      id,
      ts,
      name,
      description
    }
  }

  // Convert the Set to an array.
  products.toArray()
}

When called using a secret that lacks privileges on the referenced documents' collection:

getCategory("limes")
[
  {
    name: "limes",
    category: {
      id: "789",
      ts: Time("2099-12-12T14:22:31.560Z"),
      name: "produce",
      description: "Fresh Produce"
    }
  }
]
Return a document as an object

If the UDF originally returned a single document, update it to return the document as an object instead:

@role("server-readonly")
function getCategory(name) {
  // Project any desired fields from the referenced
  // `Category` document.
 let product = Product.byName(name).first() {
    name,
    category {
      id,
      ts,
      name,
      description
    }
  }

  // Convert the Document to an object.
  Object.assign({}, product)
}

When called using a secret that lacks privileges on the referenced document’s collection:

getCategory("limes")
{
  name: "limes",
  category: {
    id: "789",
    ts: Time("2099-12-13T16:25:53Z"),
    name: "produce",
    description: "Fresh Produce"
  }
}

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

Examples

Basic example

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 using the Fauna Dashboard or the Fauna CLI's fauna schema push command.

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)

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

A UDF 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) => {
    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
}

Variadic arguments

Use the ... syntax to create a variadic UDF that accepts an indefinite number of arguments, including zero.

// The `args` argument accepts multiple Numbers.
function getLength(...args: Number): Number {
  args.length
}

When called in an FQL query:

getLength(1, 2, 3)
3

A UDF can only accept one variadic argument. It must be the last argument.

Variadic arguments are collected into an Array. You can define a type signature to limit the types of values accepted and held in the Array.

For example, the following UDF accepts a single String argument followed by a variadic argument of zero or more Numbers:

function formatCurrency(symbol: String, ...amounts: Number): String {
  symbol + amounts.reduce((prev, cur) => prev + cur).toString()
}

When called in an FQL query:

formatCurrency("$", 2, 3)
"$5"

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) {
  // Calls the `calculateOrderTotal()` UDF.
  let total = calculateOrderTotal(order)
  // Calls the `applyDiscount()` UDF.
  applyDiscount(total, discountPercent)
}

Error handling

Use reference:fql-api/globals/abort.adoc 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.")
  }
}

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
  }
}

Pass a collection as an argument

The following example passes a collection name as an argument. Use Collection() to dynamically specify collection names in a query:

// Accepts a collection name as an argument.
function getPriceLowtoHigh(collection) {
  // Uses `Collection()` to dynamically specify
  // the collection name.
  Collection(collection).sortedByPriceLowToHigh() {
    price,
    name,
    description
  }
}

The following query calls the function:

// Calls the `getPriceLowtoHigh()` UDF with
// a `Product` collection argument.
getPriceLowtoHigh("Product")
\