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 calleddatabaseId
contains the id of the database the function is attached todb
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 awith db
block, automatically commiting changes.
graphql
contains a GraphQL client for the current database- Deno In Deno, a client provided by the graphql_request library is created
- Python In Python, a client provided by the python-graphql-client library is provided
method
the HTTP method the function got called withheaders
the HTTP headers the function got called withdata
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 Functions | Fair-use policy |
---|---|
RAM per Function | 256 MB |
Maximum Execution Time | 10 s |
Log Retention | Last 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.
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
Package Version numpy 1.26.4 scipy 1.12.0 sympy 1.12 pandas 2.2.1 matplotlib 3.8.3 seaborn 0.13.2 sqlalchemy 2.0.27 requests 2.31.0 requests-html 0.10.0 openpyxl 3.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.