Surqlize can call the user-defined functions and API endpoints that you define in the database. You describe the signature in TypeScript once, TypeScript checks the arguments you pass, and Surqlize parses the value that comes back against the type you declared.
Surqlize only describes and calls these objects. It does not run DEFINE FUNCTION or DEFINE API, so define them in the database first.
User-defined functions
fn(name, params, returns) describes a function. Pass the name without the fn:: prefix, an array of parameter types, and the return type. For a function defined as fn::util::greet, pass "util::greet".
DEFINE FUNCTION fn::greet($name: string) -> string {
RETURN "Hello, " + $name;
};
DEFINE FUNCTION fn::add($a: number, $b: number) -> number {
RETURN $a + $b;
};
DEFINE FUNCTION fn::get_pi() -> number {
RETURN 3.14159;
};
DEFINE FUNCTION fn::is_adult($age: number) -> bool {
RETURN $age >= 18;
};import { fn, t } from "surqlize";
const greet = fn("greet", [t.string()], t.string());Calling a function directly
db.run() calls the function with an array of arguments and resolves to the typed result. For a function without parameters, leave the arguments out.
import { fn, orm, t, table } from "surqlize";
const user = table("user", {
name: t.string(),
email: t.string(),
age: t.number(),
});
const db = orm(surreal, user);
const message = await db.run(greet, ["world"]);
message;
//- "Hello, world"
const add = fn("add", [t.number(), t.number()], t.number());
const sum = await db.run(add, [3, 7]);
const getPi = fn("get_pi", [], t.number());
const pi = await db.run(getPi);The result is parsed against the return type. When the function returns something else, db.run() throws a TypeParseError.
A transaction has the same method, so tx.run() runs the function inside the transaction:
const greeting = await db.transaction(async (tx) => tx.run(greet, ["transactions"]));Calling a function inside a query
The value returned by fn() is also callable inside a query. Pass a field or record from the callback as the first argument, which supplies the query context, followed by the arguments. Each argument is an expression, such as a field. To pass a literal value, wrap it with db.value().
const isAdult = fn("is_adult", [t.number()], t.bool());
await db.create("user", "ada").set({
name: "Ada",
email: "ada@example.com",
age: 36,
});
const greetings = await db.select("user").return((user) => ({
greeting: greet(user, user.name),
}));
const adults = await db.select("user").where((user) => isAdult(user, user.age));API endpoints
An endpoint defined with DEFINE API can be called through the SDK. api(path, methods) describes the endpoint, with a request and response type for each HTTP method that it supports. DEFINE API needs SurrealDB 3.0 or later.
DEFINE API "/echo" FOR post THEN {
{
status: 200,
body: $request.body
};
};
DEFINE API "/status" FOR get THEN {
{
status: 200,
body: { ok: true }
};
};
DEFINE API "/version" FOR get THEN {
{
status: 200,
body: { version: "1.0.0" }
};
};import { api, t } from "surqlize";
const echo = api("/echo", {
post: {
request: t.object({ text: t.string() }),
response: t.object({ text: t.string() }),
},
});
const client = db.api(echo);
const response = await client.post("/echo", { text: "hello" });
response.status;
//- 200
response.body?.text;
//- "hello"db.api() takes one or more endpoint descriptions and returns a client with get, post, put, delete, patch, and trace methods. Each method only accepts paths that you described for that HTTP method, and a body of the declared request type. The response has a status, a headers object, and a body, each of which is optional in the type. The body is parsed against the declared response type, and a body that does not match throws a TypeParseError. TypeScript checks the request body at compile time. To also check it at runtime, see Validating arguments and request bodies.
import { api, t } from "surqlize";
const status = api("/status", { get: { response: t.object({ ok: t.bool() }) } });
const version = api("/version", { get: { response: t.object({ version: t.string() }) } });
const client = db.api(status, version);
const health = await client.get("/status");
health.body?.ok;
//- trueBoth request and response are optional. Leave out request for methods without a body, such as get. See invoking APIs in the SDK documentation for the underlying calls.
Validating arguments and request bodies
TypeScript checks arguments at compile time, but data from a form or JSON.parse is only checked when you check it. On a validated ORM, Surqlize checks the data before it is sent:
db.run()checks the arguments against the function's parameter types, including the number of arguments. A parameter declared witht.option()may be left out.API calls check the request body against the
requesttype you described for that method. Methods without arequesttype are not checked.
Every failing argument or field is reported in one ValidationError, and nothing is sent to the database.
import { fn, orm, t } from "surqlize";
const add = fn("add", [t.number(), t.number()], t.number());
const db = orm(surreal).validated();
await db.run(add, ["3", 7] as never);
//- ValidationError: Invalid arguments for fn::add: 1 validation error
//- - [0]: expected number, received "3"Validation is off by default and applies to tx.run() in a transaction as well. It does not apply to a function called inside a query, because those arguments are expressions that the database evaluates.
Using a type with the SDK
Every t.* type implements Standard Schema, so the same definition can validate in the surrealdb SDK's .args(), .returns(), .request() and .response() methods, once you are on an SDK release that includes them. See validating calls in the SDK documentation.
const sum = await surreal.run("fn::add", [1, 2]).returns(t.number());