Functions

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:

  1. 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.

  2. 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.

  3. Custom functions that are unrelated to any built-in action. These functions are defined using the read or write 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:

schema.keel
model MyThing {
  actions {
    read doSomething(Any) returns (Any)
  }
}

The function implementation calls the 3rd party API, pass the API token, and return its response.

functions/doSomething.ts
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.

functions/myFunction.ts
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.

functions/myRedirectFunction.ts
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

keelconfig.yaml
environment:
  default:
    - name: MY_ENV_VAR
      value: "some-value"
secrets:
	- name: MY_SECRET

We could then access these in a function like so

functions/myFunction.ts
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 typeTransactions 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);