User-defined functions

User-defined functions (UDFs) allow developers to combine built-in or user-defined Fauna Query Language functions into named queries that you can execute repeatedly.

By default, UDFs run with the privileges of the current query session. For example, if your client code connects to Fauna using a key with the server role, any called UDFs run with server privileges by default. When you assign a specific role to a UDF, the UDF executes with the assigned role’s privileges which can differ from the caller’s.

This section demonstrates a few of the ways that UDFs can be helpful.

First, create some documents in a collection called "inventory":

Copied!
({ name: "inventory" })
(
  [
    [1, "avocados", 100, 3.99],
    [2, "limes", 30, 0.35],
    [3, "cilantro", 100, 1.49],
  ],
  (
    ["id", "name", "quantity", "price"],
    (
      (("inventory"), ("id")),
      {
        data: {
          name: ("name"),
          quantity: ("quantity"),
          price: ("price"),
        },
      }
    )
  )
)
[
  {
    ref: ("inventory"),
    ts: 1660238557230000,
    history_days: 30,
    name: 'inventory'
  },
  {
    ref: (("inventory"), "1"),
    ts: 1660238557250000,
    data: { name: 'avocados', quantity: 100, price: 3.99 }
  },
  {
    ref: (("inventory"), "2"),
    ts: 1660238557250000,
    data: { name: 'limes', quantity: 30, price: 0.35 }
  },
  {
    ref: (("inventory"), "3"),
    ts: 1660238557250000,
    data: { name: 'cilantro', quantity: 100, price: 1.49 }
  }
]
Query metrics:
  •    bytesIn:  323

  •   bytesOut:  577

  • computeOps:    1

  •    readOps:    0

  •   writeOps:    3

  •  readBytes:   42

  • writeBytes:  672

  •  queryTime: 18ms

  •    retries:    0

Create an index to look up inventory items by name:

Copied!
({
  name: "known_names",
  source: ("inventory"),
  terms: [
    { field: ["data", "name"] },
  ],
})
{
  ref: ("known_names"),
  ts: 1660238557540000,
  active: true,
  serialized: true,
  name: 'known_names',
  source: ("inventory"),
  terms: [ { field: [ 'data', 'name' ] } ],
  partitions: 1
}
Query metrics:
  •    bytesIn:   133

  •   bytesOut:   295

  • computeOps:     1

  •    readOps:     0

  •   writeOps:     4

  •  readBytes: 1,621

  • writeBytes:   810

  •  queryTime:  14ms

  •    retries:     0

Before you add a new item to this collection, you might want to perform data validation to ensure consistent document structure. For this demonstration, assume that the name must exist within the collection, and the price must be a Double (a double-precision, floating-point number). With a UDF, you can create a function that performs the validation and creates the new document only if its fields contain valid values.

Copied!
({
  name: "create_new_inventory", (1)
  body:
    (
      ( (2)
        // the function takes three parameters
        ["name", "quantity", "price"],
        ( (3)
          {
            // define two variables
            is_name_known: ( (4)
              (((("known_names"), ("name"))), 0),
              true,
              false
            ),
            is_price_double: (("price")),
          },
          (
            (("is_name_known"), ("is_price_double")),
            ( (5)
              ("inventory"),
              {
                data: {
                  name: ("name"),
                  quantity: ("quantity"),
                  price: ("price")
                },
              }
            ),
            ( (6)
              (
                [
                  (("is_name_known"), "", "Name is unknown. "),
                  (("is_price_double"), "", "Price is not a double. "),
                ],
                ""
              )
            )
          )
        )
      )
    ),
})
{
  ref: ("create_new_inventory"),
  ts: 1660238557830000,
  name: 'create_new_inventory',
  body: ((["name", "quantity", "price"], ({"is_name_known": ((((("known_names"), ("name"))), 0), true, false), "is_price_double": (("price"))}, ((("is_name_known"), ("is_price_double")), (("inventory"), {"data": {"name": ("name"), "quantity": ("quantity"), "price": ("price")}}), (([If(("is_name_known"), "", "Name is unknown. "), (("is_price_double"), "", "Price is not a double. ")], ""))))))
}
Query metrics:
  •    bytesIn:  737

  •   bytesOut:  842

  • computeOps:    1

  •    readOps:    0

  •   writeOps:    1

  •  readBytes:   33

  • writeBytes:  849

  •  queryTime: 16ms

  •    retries:    0

1 Give the function a name: create_new_inventory.
2 Implement the function logic in a Lambda or anonymous function. This function takes three inputs: name, quantity, and price.
3 To test for the two conditions, use Let to create two variables that both return a boolean:
  • The is_name_known variable checks the known_names index for existence of the brand input.

  • The is_price_double variable checks if the price input is a Double.

4 The If function behaves like a ternary operator: "If X then Y, otherwise Z." If the number of results matching name in the known_names index is greater than 0, the function returns true. Otherwise, it returns false.
5 Notice that the If function encloses the Create function. When the checks pass, Create is called. Otherwise, Abort is called to terminate the transaction with a custom error message.
6 The error message depends on the two boolean variables, is_name_known and is_price_double.

To run the UDF, use the Call function:

Copied!
(("create_new_inventory"), ["avocados", 100, 2.89])
{
  ref: (("inventory"), "339717346418491904"),
  ts: 1660238558120000,
  data: { name: 'avocados', quantity: 100, price: 2.89 }
}
Query metrics:
  •    bytesIn:   78

  •   bytesOut:  218

  • computeOps:    1

  •    readOps:    1

  •   writeOps:    1

  •  readBytes:   67

  • writeBytes:  367

  •  queryTime: 15ms

  •    retries:    0

The example runs successfully because the string avocados is present in the known_names index, and 2.89 is a double. However, if you try to run the function with faulty inputs, you get an error:

Copied!
(("create_new_inventory"), ["bananas", 80, "1.89"])
{
  errors: [
    {
      position: [],
      code: 'call error',
      description: 'Calling the function resulted in an error.',
      cause: [
        {
          position: [
            'expr',
            'in',
            'else'
          ],
          code: 'transaction aborted',
          description: 'Name is unknown. Price is not a double. '
        }
      ]
    }
  ]
}
Query metrics:
  •    bytesIn:  78

  •   bytesOut: 237

  • computeOps:   1

  •    readOps:   0

  •   writeOps:   0

  •  readBytes:   0

  • writeBytes:   0

  •  queryTime: 5ms

  •    retries:   0

In the future, if you need to refine the create_new_item function, you can update it and continue to use the client code unmodified, provided the function accepts the same inputs. For example, if you want to allow the function to run with the server role regardless of the caller’s role, update the role field of the UDF:

Copied!
(("create_new_inventory"), { role: "server" })
{
  ref: ("create_new_inventory"),
  ts: 1660238558690000,
  name: 'create_new_inventory',
  body: ((["name", "quantity", "price"], ({"is_name_known": ((((("known_names"), ("name"))), 0), true, false), "is_price_double": (("price"))}, ((("is_name_known"), ("is_price_double")), (("inventory"), {"data": {"name": ("name"), "quantity": ("quantity"), "price": ("price")}}), (([If(("is_name_known"), "", "Name is unknown. "), (("is_price_double"), "", "Price is not a double. ")], "")))))),
  role: 'server'
}
Query metrics:
  •    bytesIn:   84

  •   bytesOut:  858

  • computeOps:    1

  •    readOps:    0

  •   writeOps:    1

  •  readBytes:  563

  • writeBytes:  593

  •  queryTime: 12ms

  •    retries:    0

Limitations

  • Fauna imposes a 30-second transaction timeout, terminating any transactions that exceed the limit. Transaction termination can happen with UDFs that take too long to execute.

  • Fauna terminates transactions when the execution of a UDF exceeds available memory.

  • Recursion is possible but is limited to a depth of 200 calls.

  • In some contexts, such as within index bindings, using "server read-only" keys, or attribute-based access control, Fauna may restrict UDFs from performing write or read operations.

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!