Pre hooks¶
A pre hook is your function that runs before an API, written in any of the languages of API Maker. It sees the request. It can change it, answer in place of the API, or refuse it. Attach it to an instance, a database, a table, one API, a custom API or a system API. Change it any time : nothing restarts.
In short
- A pre hook is your code that runs before an API.
- It can change the request (add a filter, a limit), answer instead of the API, or refuse the call.
- Attach it to an instance, a database, a table or one API : it runs for every API below.
- Its most common job : keep each person to their own rows.
- The hook changes the request before the API runs : here, at most 50 rows.
Where hooks live¶
| Level | Runs for | Where |
|---|---|---|
| Instance | every API of every table of the instance | Instances page › the instance › hooks |
| Database | every API of every table of the database | the database › hooks |
| Table (collection) | every API of the table | the table › hooks |
| API | one API of one table, SCHEMA_GET_ALL for example |
the API › hooks |
| Custom or system API | that API | its hooks tab |
| Group | the hooks above, only for API users of the groups named on the hook | groupNames on the hook |
- Pre hooks run from the widest level to the narrowest : instance, database, table, API, each level in the order of its list. Post hooks run the other way round. See Post hooks.
- In API Testing, the Pre Hooks tab of an API of a table lists every pre hook which runs before it, in that order : the hooks of its instance, its database and its table (marked Instance, Database, Table), then its own. The ones of the other levels are changed in their categories of API Testing (Instance Hooks, Database Hooks, Collection Hooks).
- In Git, every hook is a folder of its own, named by the hook.
- A hook with
groupNamesruns only when the API user of the call belongs to one of those groups. That is how one table gets a row scoping hook for the application users and none for the back office. - A hook is written in the language of its API. The code of an API and all its hooks are one language on one runtime : the request is handed to one sandbox, which runs the hooks and the API one after the other. The form of a hook shows that language with a lock. For an API of a table, the hooks of its instance, its database, its table and the API itself are one language.
The code¶
What the return value does :
| The hook | Effect |
|---|---|
| returns nothing | The next hook, then the API, run. |
| returns a value | That value is the answer of the request, in data : the remaining hooks and the API do not run. |
sets g.res.output and returns nothing |
The API runs ; its output replaces g.res.output unless a post hook changes it again. |
| throws | The request stops with the error. An exception gives 500 and its message. An error of API Maker gives its own code as the status when it is 400, 401, 403, 404 or 500, else 500 : throw [{ code: 400, message: '…' }] in TypeScript, raise Errors('…', code=400) in Python, AmErrors in PHP, C#, Java and Kotlin, am.Fail(400, '…') in Go, am::fail(400, '…') in Rust. |
- A hook in any language other than TypeScript runs in the sandbox of its language. A hook in TypeScript runs inside API Maker when it needs nothing of a sandbox, else in its sandbox of Node.js or Bun ; the answer is the same. (
runOnNativeProcessruns a hook on the machine of the server.) Every hook runs within the sandbox timeout of the call. - When the answer of an API comes from the cache, neither pre nor post hooks run.
- Calls from your own code (
g.sys.db.getAllin a custom API) skip the hooks by default. PassskipHookRunning: falseto run them.
Narrow a request to the rows of the caller¶
A person must reach only their own rows. This is the hook the APIs Security Report installs with Add a row scoping pre-hook, here on orders, whose owner is in customer_id :
- every read, update and delete gets the owner added to its filter, with
$and, so a caller can not widen it ; - every write gets the owner stamped into its rows, so a caller can not hand a row to someone else ;
- aggregate and distinct can not be scoped this way, and any API the hook does not know is refused : it fails closed.
The report writes this hook for you, for the tables which need it, and reads it again on every scan. Handle role based permissions builds the same pattern for several tables at once, with a group which reads every row.
Answer without the API¶
Validate and change¶
Utility classes in a hook¶
A hook calls your utility classes like any other code of its language : a check shared by many hooks lives in one.
import * as T from 'types';
// The person of the request has the role, or the request is refused.
export function requireRole(g: T.IAMGlobal, role: string) {
const roles: string[] = g.req.auth.authAMDB?.roles || [];
if (!roles.includes(role)) {
throw [{ code: T.EStatusCode.FORBIDDEN, message: `The role ${role} is needed.` }];
}
}
<?php
// The person of the request has the role, or the request is refused.
class Guards
{
public static function requireRole(AmG $g, string $role): void
{
$roles = $g->req->auth->authAMDB?->roles ?? [];
if (!in_array($role, (array) $roles, true)) {
throw new AmErrors("The role {$role} is needed.", 403);
}
}
}
// The person of the request has the role, or the request is refused.
public static class Guards
{
public static void RequireRole(G g, string role)
{
if (!g.Req.Auth.GetObject("authAMDB").GetStrings("roles").Contains(role))
throw new AmErrors($"The role {role} is needed.", 403);
}
}
package guards
import "apimaker/am"
// RequireRole : the person of the request has the role, or the request is refused.
func RequireRole(g *am.G, role string) error {
for _, held := range g.Req.Auth().Map("authAMDB").List("roles") {
if held == role {
return nil
}
}
return am.Fail(403, "The role "+role+" is needed.")
}
use am::{Errors, G};
/// The person of the request has the role, or the request is refused.
pub fn require_role(g: &G, role: &str) -> Result<(), Errors> {
let roles = g.req.auth()["authAMDB"]["roles"].as_array().cloned().unwrap_or_default();
if roles.iter().any(|held| held == role) {
Ok(())
} else {
Err(am::fail(403, &format!("The role {role} is needed.")))
}
}
import am.AmErrors;
import am.G;
/** The person of the request has the role, or the request is refused. */
public class Guards {
public static void requireRole(G g, String role) {
if (!g.req.getAuth().getObject("authAMDB").getStrings("roles").contains(role)) {
throw new AmErrors("The role " + role + " is needed.", 403);
}
}
}
import am.AmErrors
import am.G
/** The person of the request has the role, or the request is refused. */
object Guards {
fun requireRole(g: G, role: String) {
if (role !in g.req.auth.getObject("authAMDB").getStrings("roles")) throw AmErrors("The role $role is needed.", 403)
}
}
- Press Ctrl+Space in the editor for the list of your utility classes.
Testing a hook¶
- The API testing page runs an API with its hooks. While you edit a hook there, the Test button of the hook runs only that hook, so you see its effect alone.
- A level holds a list of hooks, each one active or not. Keep the old hook active until the new one is ready, then switch.
- The log profile records each hook run with the request ; the log explorer shows them next to the API call.
Good to know¶
- Streams : pre hooks run before the stream starts. Post hooks do not run on a stream.
- A hook which calls the same API it is attached to creates a cycle : API Maker detects it and stops the request with an error.
- Hooks are files in Git, under the folder of their instance, database, table or API, and deploy with a Git pull.