Functions

Functions enable you to create custom code callable using the Airsequel API. Functions are written in TypeScript (executed with Deno) or Python (version 3.10).

Functions generally follow the same basic structure:

Deno:

export default function(context) {
  return { data: ... }
}

Python:

async def main(context):
  return { 'data': ... }

The Context Argument

The context argument contains many useful pieces of information:

  • functionId contains the id of the functions being called
  • databaseId contains the id of the database the function is attached to
  • db contains an instance of connection to the current database
    • Deno In deno, a connection created using the deno sqlite library is provided.
    • Python In python, a connection created using the built in sqlite library is provided. Moreover, the main function is run inside a with db block, automatically commiting changes.
  • graphql contains a GraphQL client for the current database
  • method the HTTP method the function got called with
  • headers the HTTP headers the function got called with
  • data the body of the HTTP request the function got called with

Calling Functions

Functions can be called by making requests to its /fns/<function-id> endpoint as shown on the function overview page.

Runtime System

Unlike other FaaS offerings, our functions are not directly handling the HTTP requests and responses. Rather, our runtime system is handling the HTTP request, and your function is called with a simple request JSON object and can then return a response JSON object. This allows you to focus on your business logic, and not worry about the details of the HTTP request or how to implement a HTTP server.

Resource Limits

Number of FunctionsFair-use policy
RAM per Function256 MB
Maximum Execution Time10 s
Log RetentionLast 500 function calls
(Maximum 100 log entries per function call)

View Function Invocations

Currently there is no GUI for viewing a log of function invocations. However, it can be accessed via our Admin JSON API (using HTTPie here):

http GET \
  https://www.airsequel.com/api/dbs/<dbId>/functions/<funcId>/invocations \
  Authorization:"Bearer <tokenId>"

Returns:

{
  "invocations": [
    {
      "createdUtc": "2023-12-19T23:01:26.039Z",
      "logs": [{ "stream": "stdout", "message": "…", "utc": "…" }]
    }
  ]
}

It only stores the last 500 invocations for each function.

A Hands-On Example

We start by creating a simple todos table (via the SQL queries tab):

CREATE TABLE todos (
  name TEXT NOT NULL PRIMARY KEY,
  completed BOOLEAN NOT NULL DEFAULT FALSE
)

Functions can than be created from the “functions” tab in the database view.

Untitled

Let’s create a simple todo-list function:

  • TypeScript

    import type { Database } from "https://deno.land/x/sqlite3@0.10.0/mod.ts"
    
    export default async function (context: {
      method: "GET" | "POST" | "PATCH" | "PUT" | "DELETE"
      databasePath: string
      data: {
        name?: string
      }
      db: Database
    }) {
      switch (context.method) {
        case "GET":
          return {
            data: context.db
              .prepare("SELECT name from todos where completed = 0")
              .all()
              .map(({ name }) => name),
          }
    
        case "POST":
          context.db.exec(
            `INSERT INTO todos(name) VALUES('${context.data.name}')`,
          )
    
          return { data: "Todo created succesfully" }
    
        case "DELETE":
          context.db.exec(`DELETE FROM todos WHERE name = '${context.data.name}'`)
    
          return { data: "Todo deleted succesfully" }
    
        case "PATCH":
          context.db.exec(
            `UPDATE todos SET completed = 1 where name = '${context.data.name}'`,
          )
    
          return { data: "Todo completed succesfully" }
      }
    }
    
  • Python

    async def main(context):
        if context.method == "GET":
            res = context.db.execute(
                "SELECT name from todos where completed = 0"
            ).fetchall()
    
            return {"data": list(map(lambda r: r[0], res))}
        elif context.method == "POST":
            context.db.execute(
                f"""INSERT INTO todos(name) VALUES('{
                    context.data["name"]
                }')"""
            )
    
            return {"data": "Todo created succesfully"}
        elif context.method == "DELETE":
            context.db.execute(
                f"""DELETE FROM todos WHERE name = '{
                    context.data["name"]
                }'"""
            )
    
            return {"data": "Todo deleted succesfully"}
        elif context.method == "PATCH":
            context.db.execute(
                f"""UPDATE todos SET completed = 1 where name = '{
                    context.data["name"]
                }'"""
            )
    
            return {"data": "Todo completed succesfully"}
    

We can start by attempting to call our function. We will use HTTPie for all examples.

$ http GET https://www.airsequel.com/fns/01hbv54akc0zpejb7f59d63qpa
{
  "data": [],
  "extensions": { "logs": [] }
}

There’s nothing there! Let’s change that by creating a todo:

$ http POST https://www.airsequel.com/fns/01hbv54akc0zpejb7f59d63qpa name=hello
{
  "data": "Todo created succesfully",
  "extensions": { "logs": [] }
}

Running the GET method will now yield:

{
  "data": ["hello"],
  "extensions": { "logs": [] }
}

We can run the previous command with different arguments in order to create even more entries:

$ http POST http://localhost:4185/fns/01hbv54akc0zpejb7f59d63qpa name=world
$ http POST http://localhost:4185/fns/01hbv54akc0zpejb7f59d63qpa name=oops
$ http GET http://localhost:4185/fns/01hbv54akc0zpejb7f59d63qpa
{
  "data": ["hello", "world", "oops"],
  "extensions": { "logs": []}
}

Let’s delete the oops todo by using the DELETE method, and complete the hello todo using the PATCH method:

$ http DELETE http://localhost:4185/fns/01hbv54akc0zpejb7f59d63qpa name=oops
{
  "data": "Todo delete succesfully",
  "extensions": { "logs": [] }
}

$ http PATCH http://localhost:4185/fns/01hbv54akc0zpejb7f59d63qpa name=hello
{
  "data": "Todo completed succesfully",
  "extensions": { "logs": [] }
}

We can now run the GET method one last time in order to ensure everything is working as expected:

$ http GET http://localhost:4185/fns/01hbv54akc0zpejb7f59d63qpa
{
  "data": ["world"],
  "extensions": { "logs": [] }
}

Available packages

The following common packages are included in the environment:

  • Python
    PackageVersion
    numpy1.26.4
    scipy1.12.0
    sympy1.12
    pandas2.2.1
    matplotlib3.8.3
    seaborn0.13.2
    sqlalchemy2.0.27
    requests2.31.0
    requests-html0.10.0
    openpyxl3.1.2

[Unreleased] Executing GraphQL queries

We can achieve the same functionality by using the provided GraphQL API. For instance, the GET branch of our previous implementation can be rewritten as follows:

  • Typescript

    import type { GraphQLClient } from "https://deno.land/x/graphql_request/mod.ts"
    
    export default async function (
      context: {
        method: "GET" | "POST" |  "PATCH" | "PUT" | "DELETE",
        databasePath: string,
        data: {
          name?: string
        },
        graphql: GraphQLClient
      },
    ) {
      switch (context.method) {
        case "GET":
          const response = await context.graphql.request(`
            query todos {
              todos(filter: { completed: { eq: false }}) {
                name
              }
            }
          `)
    
          return {
            data: response.todos.map(t => t.name)
          }
        ...
      }
    }
    
  • Python

    async def main(context):
      if context.method == "GET":
        query = """
          query todos {
            todos(filter: { completed: { eq: false }}) {
              name
            }
          }
        """
        res = context.graphql.execute(query = query)
    
        return {"data": list(map(lambda r: r['name'], res['data']['todos']))}
      ...
    

Troubleshooting

If you get an error like this when making a network request (e.g. with fetch()):

error: Uncaught PermissionDenied:
Requires net access to "example.com", run again with the --allow-net flag
…

Please contact us at support@feram.io and request that the used URL should be allowed. We will then check if the URL is safe and allowlist it if applicable.