Functions
Functions are stored SurrealQL procedures — callable from queries, other functions, events, and permission expressions. The keyword is fn; parameters keep their $; the body is real SurrealQL.
The shape
namespace acme;
database shop;
/// Whether the current user may edit this post.
fn can_edit($p: record<post>) -> bool {
return $auth.id = $p.author or $auth.role = 'admin';
} {
permissions: $auth.admin = true;
};DEFINE NAMESPACE OVERWRITE acme;
USE NS acme;
DEFINE DATABASE OVERWRITE shop;
USE NS acme DB shop;
DEFINE FUNCTION OVERWRITE fn::can_edit($p: record<post>) -> bool {
RETURN $auth.id = $p.author OR $auth.role = 'admin'
}
PERMISSIONS WHERE $auth.admin = true
COMMENT 'Whether the current user may edit this post.';
Reading it: the signature fn name($param: type) -> return_type, then a brace block with the body, then — optionally — a second brace block with ordinary clauses (permissions:, GraphQL hints). It is the one statement with two blocks.
The body's last statement, without a trailing ;, is the return value:
namespace acme;
database shop;
fn tax_total($net: number, $rate: option<number>) {
let $r = $rate ?? 0.15;
let $tax = math::floor($net * $r);
$net + $tax
};
fn ping() -> string {
'pong'
};DEFINE NAMESPACE OVERWRITE acme;
USE NS acme;
DEFINE DATABASE OVERWRITE shop;
USE NS acme DB shop;
DEFINE FUNCTION OVERWRITE fn::tax_total($net: number, $rate: option<number>) {
LET $r = $rate ?? 0.15f;
LET $tax = math::floor($net * $r);
$net + $tax;
};
DEFINE FUNCTION OVERWRITE fn::ping() -> string { 'pong' };
Zero-parameter functions are legal (fn ping() { … };), and the body is re-emitted in the engine's canonical form — the compiler hands your SurrealQL to the official parser and prints its result, so a stored body and your file agree after a round-trip.
Permissions
Functions default to permissions: full — anyone who can run queries can call them. Lock them down per function:
namespace acme;
database shop;
table log {
schemafull;
field at: datetime;
};
fn purge_logs($before: datetime) {
delete log where at < $before;
} {
permissions: none;
};DEFINE NAMESPACE OVERWRITE acme;
USE NS acme;
DEFINE DATABASE OVERWRITE shop;
USE NS acme DB shop;
DEFINE TABLE OVERWRITE log TYPE NORMAL SCHEMAFULL;
DEFINE FIELD OVERWRITE `at` ON log TYPE datetime;
DEFINE FUNCTION OVERWRITE fn::purge_logs($before: datetime) { DELETE log WHERE at < $before }
PERMISSIONS NONE;
permissions: takes none, full, or an expression (which the compiler emits as PERMISSIONS WHERE …).
Optional parameters
A trailing option<T> parameter may be omitted at the call site — fn::last_opt(5) and fn::last_opt(5, 2) both work:
namespace acme;
database shop;
fn last_opt($req: number, $opt: option<number>) {
$opt ?? $req
};DEFINE NAMESPACE OVERWRITE acme;
USE NS acme;
DEFINE DATABASE OVERWRITE shop;
USE NS acme DB shop;
DEFINE FUNCTION OVERWRITE fn::last_opt($req: number, $opt: option<number>) { $opt ?? $req };
Names with segments, GraphQL hints
Names may carry :: segments — fn util::slugify(…) defines fn::util::slugify. GraphQL hints ride the second block:
namespace acme;
database shop;
fn util::slugify($s: string) -> string {
string::lowercase(string::replace($s, ' ', '-'))
} {
graphql {
alias: 'slugify';
};
};DEFINE NAMESPACE OVERWRITE acme;
USE NS acme;
DEFINE DATABASE OVERWRITE shop;
USE NS acme DB shop;
DEFINE FUNCTION OVERWRITE fn::util::slugify($s: string) -> string {
string::lowercase(string::replace($s, ' ', '-'))
}
GRAPHQL_ALIAS 'slugify';
Where next
- Access (sign-in & tokens) —
signin:andsignup:take query blocks, projected with the sameFieldsmachinery. - Events — call
fn::…from a trigger.