Functions
Functions allow you to provide custom logic to your Keel application. Your functions are deeply integrated into your app, meaning that inputs are validated, authentication and permissions are handled for you, and you have full access to your database.
We currently support TypeScript for writing functions and your function code should be located in the functions
directory of your project, with each function in its own file named the same as the function in the schema. So if you have a function called doTheThing
in your schema, the code for this function would be located at functions/doTheThing.ts
. Auth hook functions are a little different, but more on this later.
Once you have described your function in your schema (or in your keelconfig.yaml
for auth hooks), you can run keel generate
via the CLI to scaffold the code for your functions and put files in the right place to get you started. Then, you'd just fill in your application-specific logic in the generated file and you're good to go.
Keel supports three main types of functions:
-
Action hooks that are defined on existing actions like
get
,create
,update
, and so on. These functions modify the default behaviour of a built-in action by hooking in to the lifecycle of the action. For example, you might want to add constraints to a query before it is run, or perform custom permission logic on the returned rows after the data returns from the database. -
Auth hooks allow you to provide custom functionality into the authentication flow. For example, you may want to create a new
Account
model when a user signs in for the first time. -
Custom functions that are unrelated to any built-in action. These functions are defined using the
read
orwrite
action types and allow you to define custom inputs and responses. These functions are the closest to what you might think of as an API route in Next.js (opens in a new tab) as they are not tied to any built-in action. They are still deeply integrated into your app, however, and you can still use the built-in Keel types and APIs to interact with your database. You'd use this type of function to communicate with 3rd party APIs, do batch operations, or perform other custom logic.
Context
All functions receive a context object as their first argument. This object is very similar to the ctx
object that can be used in expressions in your Keel schema. It provides type-safe access to secrets and environment variables, as well as the identity of the caller.
type Secrets = {
// populated from the secrets defined in your keelconfig.yaml
};
type Environment = {
// populated from the environment variables defined in your keelconfig.yaml
};
type Context = {
secrets: Secrets;
env: Environment;
identity?: Identity;
now(): Date;
};
Using the database
The @teamkeel/sdk
package is generated based on your schema and contains type-safe APIs for interacting with your models. These APIs are all available on the exported models
object.
See Model API for full usage.
Low-level database API
If you need more complex database operations you can use the Database API to write custom queries.
Using fetch
We deploy your functions into an environment running Node.js 18.x, which means the Fetch API is available globally.
One example of using fetch
is to proxy API calls to a 3rd party service through your Keel APIs. This is often useful if the API you want to use requires an API key and you don't want to expose that to your frontend.
The following example uses the special Any
message type to allow any input and any response from the doSomething
function:
model MyThing {
actions {
read doSomething(Any) returns (Any)
}
}
The function implementation calls the 3rd party API, pass the API token, and return its response.
import { DoSomething } from "@teamkeel/sdk";
export default DoSomething(async (ctx, inputs) => {
// make an API call to 3rd party
const res = await fetch("https://some-cool-api.com", {
method: "POST",
headers: {
// use a secret to store your API token
"Api-Token": ctx.secrets.API_TOKEN,
},
body: JSON.stringify({
some: "param",
}),
});
// return the response as JSON
return res.json();
});
Using Headers
You can access request headers by using ctx.headers
, which is a read-only version of the Headers
(opens in a new tab) object, and you can set response headers by using ctx.response.headers
which is a normal Headers
object.
export default MyFunction((inputs, api, ctx) => {
// read a request header
const reqHeader = ctx.headers.get("X-My-Custom-Header");
// write a response header
ctx.response.headers.set("X-My-Other-Header", "1234");
});
HTTP Status Codes
You can set the HTTP status code of the response from your function, which is useful if you want to return a redirect response from your action.
import { MyRedirectFunction } from "@teamkeel/sdk"
export default MyRedirectFunction((inputs, api, ctx) => {
// do some stuff...
// return a redirect
ctx.response.headers.set("Location", "https://some.url.com/");
ctx.response.status = 302;
return null;
});
Setting the HTTP response status code will only have an affect on your JSON
API endpoints, so in the above example /json/api/myRedirectFunction
will
return a redirect response but using myRedirectFunction
via GraphQL or
JSON-RPC APIs will not.
Environment Variables and Secrets
Environment variables defined in your keelconfig.yaml
file will be available in your functions by using ctx.env
, which is typed according to the environment variables you've defined in your config file. No more un-typed process.env
🎉
In much the same way, any secret you define in your keelconfig.yaml
file will be available on ctx.secrets
, which is also typed. As secrets are sensitive values they are not set as environment variables and are only accessible by using ctx.secrets
.
As an example if we have the following keelconfig.yaml
file
environment:
default:
- name: MY_ENV_VAR
value: "some-value"
secrets:
- name: MY_SECRET
We could then access these in a function like so
export default MyFunction(async (ctx, inputs) => {
ctx.env.MY_ENV_VAR; // "some-value"
ctx.secrets.MY_SECRET; // will be decrypted secret value
// TypeScript will catch this with the error:
// ts(2339) Property 'FOO' does not exist on type 'Environment'.
ctx.env.FOO;
// TypeScript will catch this with the error:
// ts(2339) Property 'FOO' does not exist on type 'Secrets'.
ctx.secrets.FOO;
});
Transaction support
Mutation function hooks and the write
custom function are, by default, wrapped in transactions. This means that if an error occurs in your code the transaction will get rolled back and no changes will persist to your database.
All other function types have transactions disabled. Refer to the table below.
Function type | Transactions enabled |
---|---|
beforeQuery function hook | ❌ |
afterQuery function hook | ❌ |
beforeWrite function hook | ✅ |
afterWrite function hook | ✅ |
read custom function | ❌ |
write custom function | ✅ |
jobs | ✅ |
event subscribers | ✅ |
Enabling or disabling transactions
This is a great default but there are times where you might want to explicitly enable or disable transactions for your function. This can be achieved by setting the autoTransaction
property in your function configuration.
For example, below we demonstrate enabling transactions for a job.
MyJob.config = {
autoTransaction: true,
};
export default MyJob(async (ctx, inputs) => {
// ...
});
Simirlarly, we can disable transactions for some write hook.
const hooks: MyFunctionHooks = {
beforeWrite: async (ctx, inputs, values, record) => {
// ...
},
};
MyFunction.config = {
autoTransaction: false,
};
export default MyFunction(hooks);