Skip to content
This page View Markdown Open in ChatGPT Open in Claude

Pre hooks

A pre hook is a function of yours, in any of the languages of API Maker, which runs before an API. It sees the request, can change it, can answer instead of the API, and can refuse it. Attach it to a whole instance, a database, a table, one API, a custom API or a system API, and change it any time without a restart.

Hooks around an API : pre hooks from the instance down to the API, the API, then post hooks from the API up to the instance request Caller with tokens pre hooks · top to bottom Instance every table of it Database every table of it Table every API of it This API get all only… The API database · cache post hooks · bottom to top This API g.res.output Table Database Instance Reply to the caller a pre hook which returns a value, or throws, answers here : nothing after it runs Also hooked Custom APIs System APIs A hook with groups runs only for API users of those groups Cached answers run no hook Calls from your own code (g.sys.db…) skip the hooks unless you pass skipHookRunning: false.

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 groupNames runs 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

import * as T from 'types';
import * as db from 'db-interfaces';

async function main(g: T.IAMGlobal) {
    if (!g.req.isApiRequestFromUser) return; // calls made by your own code : leave them alone

    // g.req.query, g.req.body, g.req.params, g.req.headers : the request, changeable
    // g.req.auth.authAMUser, g.req.auth.authAMDB… : who calls
    // g.res.shared : values for the API and the post hooks of the same request
}
module.exports = main;
from am import G


async def main(g: G):
    if not g.req.is_api_request_from_user:
        return  # calls made by your own code : leave them alone

    # g.req.query, g.req.body, g.req.params, g.req.headers : the request, changeable
    # g.req.auth.authAMUser, g.req.auth.authAMDB… : who calls
    # g.res.shared : values for the API and the post hooks of the same request
<?php
function main(AmG $g)
{
    if (!$g->req->isApiRequestFromUser) {
        return null; // calls made by your own code : leave them alone
    }

    // $g->req->query, $g->req->body, $g->req->params, $g->req->headers : the request, changeable
    // $g->req->auth->authAMUser, $g->req->auth->authAMDB… : who calls
    // $g->res->shared : values for the API and the post hooks of the same request
    return null;
}
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        if (!g.Req.IsApiRequestFromUser) return null; // calls made by your own code : leave them alone

        // g.Req.Query, g.Req.Body, g.Req.Params, g.Req.Headers : the request, changeable
        // g.Req.Auth["authAMUser"], g.Req.Auth["authAMDB"]… : who calls
        // g.Res.Shared : values for the API and the post hooks of the same request
        return null;
    }
}
package code

import "apimaker/am"

func Main(g *am.G) (any, error) {
    if !g.Req.IsApiRequestFromUser() {
        return nil, nil // calls made by your own code : leave them alone
    }

    // g.Req.Query(), g.Req.Body(), g.Req.Params(), g.Req.Headers() : the request, changeable
    // g.Req.Auth().Map("authAMUser"), g.Req.Auth().Map("authAMDB")… : who calls
    // g.Res.Shared() : values for the API and the post hooks of the same request
    return nil, nil
}
use am::{Errors, G, Value};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    if !g.req.is_api_request_from_user() {
        return Ok(Value::Null); // calls made by your own code : leave them alone
    }

    // g.req.query_mut(), g.req.body_mut(), g.req.params_mut(), g.req.headers_mut() : the request, changeable
    // g.req.auth()["authAMUser"], g.req.auth()["authAMDB"]… : who calls
    // g.res.shared_mut() : values for the API and the post hooks of the same request
    Ok(Value::Null)
}
import am.G;

class Api {
    static Object main(G g) {
        if (!g.req.isApiRequestFromUser()) return null; // calls made by your own code : leave them alone

        // g.req.getQuery(), g.req.getBody(), g.req.getParams(), g.req.getHeaders() : the request, changeable
        // g.req.getAuth().getObject("authAMUser"), g.req.getAuth().getObject("authAMDB")… : who calls
        // g.res.getShared() : values for the API and the post hooks of the same request
        return null;
    }
}
import am.G

fun main(g: G): Any? {
    if (!g.req.isApiRequestFromUser) return null // calls made by your own code : leave them alone

    // g.req.query, g.req.body, g.req.params, g.req.headers : the request, changeable
    // g.req.auth.getObject("authAMUser"), g.req.auth.getObject("authAMDB")… : who calls
    // g.res.shared : values for the API and the post hooks of the same request
    return null
}

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 : the remaining hooks and the API do not run. The response is 200 with the value in data.
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 ; the errors of API Maker give their own status (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. (runOnNativeProcess runs 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.getAll in a custom API) skip the hooks by default. Pass skipHookRunning: false to 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 row gate : the same call, without and with the row scoping pre hook without the hook GET …/sales/orders Diya signed in : customer_id 2 every order 1001 · Diya 1002 · Kabir 1003 · Diya 1004 · Kabir and every other customer with the row scoping pre hook the hook adds the owner find = { $and: [   what Diya sent,   { customer_id: 2 } ] } her orders 1001 · Diya 1003 · Diya Nothing Diya sends can widen it : $and keeps her filter and adds the owner. Writes get the owner stamped ; aggregate and distinct are refused. Groups decide which APIs the app may call. Only the hook knows which rows are Diya's.
Collection level pre hook on orders
import * as T from 'types';

/**
 * Row scoping pre-hook for orders, as the API Security Report writes it.
 * Every request is limited to the rows whose "customer_id" equals the id of the person behind it
 * (g.req.auth.authAMDB.customer_id). Edit freely : the report reads the code again on every scan.
 */
const OWNER_COLUMN = 'customer_id';
const IDENTITY_FIELD = 'customer_id';

// Where each generated API keeps its filter. API Maker ANDs query.find onto the primary key for the *-by-id APIs.
const QUERY_FIND_APIS = new Set<string>([
    'SCHEMA_GET_ALL', 'SCHEMA_GET_ALL_STREAM', 'SCHEMA_GET_BY_ID', 'SCHEMA_PUT_UPDATE_BY_ID',
    'SCHEMA_PUT_REPLACE_BY_ID', 'SCHEMA_DEL_DELETE_BY_ID', 'GEN_GET_ALL', 'GEN_GET_ALL_STREAM', 'GEN_GET_BY_ID',
    'GEN_PUT_UPDATE_BY_ID', 'GEN_PUT_REPLACE_BY_ID', 'GEN_DEL_DELETE_BY_ID',
]);
const BODY_FIND_APIS = new Set<string>([
    'SCHEMA_POST_QUERY', 'SCHEMA_POST_QUERY_STREAM', 'SCHEMA_POST_QUERY_DELETE', 'SCHEMA_UPDATE_MANY',
    'SCHEMA_POST_DISTINCT_QUERY', 'SCHEMA_ARRAY_OPERATIONS', 'SCHEMA_POST_COUNT', 'GEN_POST_QUERY',
    'GEN_POST_QUERY_STREAM', 'GEN_POST_QUERY_DELETE', 'GEN_UPDATE_MANY', 'GEN_POST_DISTINCT_QUERY',
    'GEN_ARRAY_OPERATIONS', 'GEN_POST_COUNT',
]);
const STAMP_OWNER_APIS = new Set<string>([
    'SCHEMA_POST_BULK_INSERT', 'SCHEMA_MASTER_SAVE', 'GEN_POST_BULK_INSERT', 'GEN_MASTER_SAVE',
]);

async function main(g: T.IAMGlobal) {
    // Custom APIs, schedulers and events calling g.sys.db run under their own rules, only real callers are scoped.
    if (!g.req.isApiRequestFromUser) return;

    const person = g.req.auth?.authAMDB;
    const me = person ? person[IDENTITY_FIELD] : undefined;
    if (me === undefined || me === null || me === '') {
        g.res.statusCode = T.EStatusCode.UNAUTHORIZED;
        throw new Error('A person token is required for this API.');
    }

    const scope = { [OWNER_COLUMN]: me };
    // MERGE, never replace : $and keeps what the caller asked for and adds the owner on top, so a caller
    // can never widen the rows by sending its own owner filter.
    const merge = (existing: any) => (existing && typeof existing === 'object' && Object.keys(existing).length
        ? { $and: [existing, scope] }
        : { ...scope });
    const stamp = (row: any) => { if (row && typeof row === 'object') row[OWNER_COLUMN] = me; };

    const apiId = g.req.reqInfo?.apiInfo?.id;
    if (QUERY_FIND_APIS.has(apiId)) {
        g.req.query.find = merge(g.req.query.find);
        stamp(g.req.body); // update-by-id and replace-by-id : the owner of a row can not be handed over
        return;
    }
    if (BODY_FIND_APIS.has(apiId)) {
        g.req.body = g.req.body || {};
        g.req.body.find = merge(g.req.body.find);
        if (g.req.body.updateData) stamp(g.req.body.updateData); // update-many
        return;
    }
    if (STAMP_OWNER_APIS.has(apiId)) {
        const rows = Array.isArray(g.req.body) ? g.req.body : [g.req.body];
        for (const row of rows) stamp(row); // never trust the owner sent by the caller
        return;
    }

    // Fail closed : aggregate and distinct can not be scoped generically, so they are refused for row scoped callers.
    g.res.statusCode = T.EStatusCode.FORBIDDEN;
    throw new Error(`API ${apiId} is not available for row scoped users.`);
}
module.exports = main;
Collection level pre hook on orders
from am import Errors, G, Obj

# Row scoping pre-hook for orders, as the API Security Report writes it.
# Every request is limited to the rows whose "customer_id" equals the id of the person behind it
# (g.req.auth.authAMDB.customer_id). Edit freely : the report reads the code again on every scan.
OWNER_COLUMN = 'customer_id'
IDENTITY_FIELD = 'customer_id'

# Where each generated API keeps its filter. API Maker ANDs query.find onto the primary key for the *-by-id APIs.
QUERY_FIND_APIS = {
    'SCHEMA_GET_ALL', 'SCHEMA_GET_ALL_STREAM', 'SCHEMA_GET_BY_ID', 'SCHEMA_PUT_UPDATE_BY_ID',
    'SCHEMA_PUT_REPLACE_BY_ID', 'SCHEMA_DEL_DELETE_BY_ID', 'GEN_GET_ALL', 'GEN_GET_ALL_STREAM', 'GEN_GET_BY_ID',
    'GEN_PUT_UPDATE_BY_ID', 'GEN_PUT_REPLACE_BY_ID', 'GEN_DEL_DELETE_BY_ID',
}
BODY_FIND_APIS = {
    'SCHEMA_POST_QUERY', 'SCHEMA_POST_QUERY_STREAM', 'SCHEMA_POST_QUERY_DELETE', 'SCHEMA_UPDATE_MANY',
    'SCHEMA_POST_DISTINCT_QUERY', 'SCHEMA_ARRAY_OPERATIONS', 'SCHEMA_POST_COUNT', 'GEN_POST_QUERY',
    'GEN_POST_QUERY_STREAM', 'GEN_POST_QUERY_DELETE', 'GEN_UPDATE_MANY', 'GEN_POST_DISTINCT_QUERY',
    'GEN_ARRAY_OPERATIONS', 'GEN_POST_COUNT',
}
STAMP_OWNER_APIS = {
    'SCHEMA_POST_BULK_INSERT', 'SCHEMA_MASTER_SAVE', 'GEN_POST_BULK_INSERT', 'GEN_MASTER_SAVE',
}


async def main(g: G):
    # Custom APIs, schedulers and events calling g.sys.db run under their own rules, only real callers are scoped.
    if not g.req.is_api_request_from_user:
        return

    person = g.req.auth.authAMDB
    me = person.get(IDENTITY_FIELD) if isinstance(person, dict) else None
    if me is None or me == '':
        raise Errors('A person token is required for this API.', code=401)

    scope = {OWNER_COLUMN: me}
    # MERGE, never replace : $and keeps what the caller asked for and adds the owner on top, so a caller
    # can never widen the rows by sending its own owner filter.
    def merge(existing):
        return {'$and': [existing, scope]} if isinstance(existing, dict) and existing else dict(scope)

    api_id = g.req.req_info.apiInfo.id
    if api_id in QUERY_FIND_APIS:
        g.req.query['find'] = merge(g.req.query.get('find'))
        if isinstance(g.req.body, dict):
            g.req.body[OWNER_COLUMN] = me  # update-by-id and replace-by-id : the owner of a row can not be handed over
        return
    if api_id in BODY_FIND_APIS:
        if not isinstance(g.req.body, dict):
            g.req.body = Obj()
        g.req.body['find'] = merge(g.req.body.get('find'))
        if isinstance(g.req.body.get('updateData'), dict):
            g.req.body['updateData'][OWNER_COLUMN] = me  # update-many
        return
    if api_id in STAMP_OWNER_APIS:
        rows = g.req.body if isinstance(g.req.body, list) else [g.req.body]
        for row in rows:
            if isinstance(row, dict):
                row[OWNER_COLUMN] = me  # never trust the owner sent by the caller
        return

    # Fail closed : aggregate and distinct can not be scoped generically, so they are refused for row scoped callers.
    raise Errors(f'API {api_id} is not available for row scoped users.', code=403)
Collection level pre hook on orders
<?php
// Row scoping pre-hook for orders, as the API Security Report writes it.
// Every request is limited to the rows whose "customer_id" equals the id of the person behind it
// (g.req.auth.authAMDB.customer_id). Edit freely : the report reads the code again on every scan.
const OWNER_COLUMN = 'customer_id';
const IDENTITY_FIELD = 'customer_id';

// Where each generated API keeps its filter. API Maker ANDs query.find onto the primary key for the *-by-id APIs.
const QUERY_FIND_APIS = [
    'SCHEMA_GET_ALL', 'SCHEMA_GET_ALL_STREAM', 'SCHEMA_GET_BY_ID', 'SCHEMA_PUT_UPDATE_BY_ID',
    'SCHEMA_PUT_REPLACE_BY_ID', 'SCHEMA_DEL_DELETE_BY_ID', 'GEN_GET_ALL', 'GEN_GET_ALL_STREAM', 'GEN_GET_BY_ID',
    'GEN_PUT_UPDATE_BY_ID', 'GEN_PUT_REPLACE_BY_ID', 'GEN_DEL_DELETE_BY_ID',
];
const BODY_FIND_APIS = [
    'SCHEMA_POST_QUERY', 'SCHEMA_POST_QUERY_STREAM', 'SCHEMA_POST_QUERY_DELETE', 'SCHEMA_UPDATE_MANY',
    'SCHEMA_POST_DISTINCT_QUERY', 'SCHEMA_ARRAY_OPERATIONS', 'SCHEMA_POST_COUNT', 'GEN_POST_QUERY',
    'GEN_POST_QUERY_STREAM', 'GEN_POST_QUERY_DELETE', 'GEN_UPDATE_MANY', 'GEN_POST_DISTINCT_QUERY',
    'GEN_ARRAY_OPERATIONS', 'GEN_POST_COUNT',
];
const STAMP_OWNER_APIS = [
    'SCHEMA_POST_BULK_INSERT', 'SCHEMA_MASTER_SAVE', 'GEN_POST_BULK_INSERT', 'GEN_MASTER_SAVE',
];

function main(AmG $g)
{
    // Custom APIs, schedulers and events calling g.sys.db run under their own rules, only real callers are scoped.
    if (!$g->req->isApiRequestFromUser) {
        return null;
    }

    $person = $g->req->auth->authAMDB;
    $me = $person instanceof AmObject ? $person->get(IDENTITY_FIELD) : null;
    if ($me === null || $me === '') {
        throw new AmErrors('A person token is required for this API.', 401);
    }

    $scope = [OWNER_COLUMN => $me];
    // MERGE, never replace : $and keeps what the caller asked for and adds the owner on top, so a caller
    // can never widen the rows by sending its own owner filter.
    $merge = fn ($existing) => ($existing instanceof AmObject || is_array($existing)) && count($existing) > 0
        ? ['$and' => [$existing, $scope]]
        : $scope;

    $apiId = $g->req->reqInfo->apiInfo->id;
    if (in_array($apiId, QUERY_FIND_APIS, true)) {
        $g->req->query->find = $merge($g->req->query->find);
        if ($g->req->body instanceof AmObject) {
            $g->req->body[OWNER_COLUMN] = $me; // update-by-id and replace-by-id : the owner of a row can not be handed over
        }
        return null;
    }
    if (in_array($apiId, BODY_FIND_APIS, true)) {
        if (!($g->req->body instanceof AmObject)) {
            $g->req->body = new AmObject([]);
        }
        $g->req->body->find = $merge($g->req->body->find);
        if ($g->req->body->updateData instanceof AmObject) {
            $g->req->body->updateData[OWNER_COLUMN] = $me; // update-many
        }
        return null;
    }
    if (in_array($apiId, STAMP_OWNER_APIS, true)) {
        $rows = is_array($g->req->body) ? $g->req->body : [$g->req->body];
        foreach ($rows as $row) {
            if ($row instanceof AmObject) {
                $row[OWNER_COLUMN] = $me; // never trust the owner sent by the caller
            }
        }
        return null;
    }

    // Fail closed : aggregate and distinct can not be scoped generically, so they are refused for row scoped callers.
    throw new AmErrors("API $apiId is not available for row scoped users.", 403);
}
Collection level pre hook on orders
// Row scoping pre-hook for orders, as the API Security Report writes it.
// Every request is limited to the rows whose "customer_id" equals the id of the person behind it
// (g.req.auth.authAMDB.customer_id). Edit freely : the report reads the code again on every scan.
file static class Api
{
    const string OwnerColumn = "customer_id";
    const string IdentityField = "customer_id";

    // Where each generated API keeps its filter. API Maker ANDs query.find onto the primary key for the *-by-id APIs.
    static readonly HashSet<string> QueryFindApis =
    [
        "SCHEMA_GET_ALL", "SCHEMA_GET_ALL_STREAM", "SCHEMA_GET_BY_ID", "SCHEMA_PUT_UPDATE_BY_ID",
        "SCHEMA_PUT_REPLACE_BY_ID", "SCHEMA_DEL_DELETE_BY_ID", "GEN_GET_ALL", "GEN_GET_ALL_STREAM", "GEN_GET_BY_ID",
        "GEN_PUT_UPDATE_BY_ID", "GEN_PUT_REPLACE_BY_ID", "GEN_DEL_DELETE_BY_ID",
    ];
    static readonly HashSet<string> BodyFindApis =
    [
        "SCHEMA_POST_QUERY", "SCHEMA_POST_QUERY_STREAM", "SCHEMA_POST_QUERY_DELETE", "SCHEMA_UPDATE_MANY",
        "SCHEMA_POST_DISTINCT_QUERY", "SCHEMA_ARRAY_OPERATIONS", "SCHEMA_POST_COUNT", "GEN_POST_QUERY",
        "GEN_POST_QUERY_STREAM", "GEN_POST_QUERY_DELETE", "GEN_UPDATE_MANY", "GEN_POST_DISTINCT_QUERY",
        "GEN_ARRAY_OPERATIONS", "GEN_POST_COUNT",
    ];
    static readonly HashSet<string> StampOwnerApis =
    [
        "SCHEMA_POST_BULK_INSERT", "SCHEMA_MASTER_SAVE", "GEN_POST_BULK_INSERT", "GEN_MASTER_SAVE",
    ];

    public static async Task<object?> Main(G g)
    {
        // Custom APIs, schedulers and events calling g.sys.db run under their own rules, only real callers are scoped.
        if (!g.Req.IsApiRequestFromUser) return null;

        var me = g.Req.Auth.GetObject("authAMDB")[IdentityField];
        if (me is null || me.ToString() == "")
            throw new AmErrors("A person token is required for this API.", 401);

        JsonObject Scope() => new() { [OwnerColumn] = me.DeepClone() };
        // MERGE, never replace : $and keeps what the caller asked for and adds the owner on top, so a caller
        // can never widen the rows by sending its own owner filter.
        JsonNode Merge(JsonNode? existing) => existing is JsonObject { Count: > 0 }
            ? new JsonObject { ["$and"] = new JsonArray(existing.DeepClone(), Scope()) }
            : Scope();

        var apiId = g.Req.ReqInfo.GetObject("apiInfo").GetString("id") ?? "";
        if (QueryFindApis.Contains(apiId))
        {
            g.Req.Query["find"] = Merge(g.Req.Query["find"]);
            var body = g.Req["body"];
            if (body is JsonObject) body[OwnerColumn] = me.DeepClone(); // update-by-id and replace-by-id : the owner of a row can not be handed over
            return null;
        }
        if (BodyFindApis.Contains(apiId))
        {
            g.Req.Body["find"] = Merge(g.Req.Body["find"]);
            var updateData = g.Req.Body["updateData"];
            if (updateData is JsonObject) updateData[OwnerColumn] = me.DeepClone(); // update-many
            return null;
        }
        if (StampOwnerApis.Contains(apiId))
        {
            var body = g.Req["body"];
            if (body is JsonArray rows)
            {
                foreach (var row in rows)
                {
                    if (row is JsonObject) row[OwnerColumn] = me.DeepClone(); // never trust the owner sent by the caller
                }
            }
            else if (body is JsonObject) body[OwnerColumn] = me.DeepClone();
            return null;
        }

        // Fail closed : aggregate and distinct can not be scoped generically, so they are refused for row scoped callers.
        throw new AmErrors($"API {apiId} is not available for row scoped users.", 403);
    }
}
Collection level pre hook on orders
package code

import (
    "fmt"

    "apimaker/am"
)

// Row scoping pre-hook for orders, as the API Security Report writes it.
// Every request is limited to the rows whose "customer_id" equals the id of the person behind it
// (g.req.auth.authAMDB.customer_id). Edit freely : the report reads the code again on every scan.
const ownerColumn = "customer_id"
const identityField = "customer_id"

// Where each generated API keeps its filter. API Maker ANDs query.find onto the primary key for the *-by-id APIs.
var queryFindApis = map[string]bool{
    "SCHEMA_GET_ALL": true, "SCHEMA_GET_ALL_STREAM": true, "SCHEMA_GET_BY_ID": true, "SCHEMA_PUT_UPDATE_BY_ID": true,
    "SCHEMA_PUT_REPLACE_BY_ID": true, "SCHEMA_DEL_DELETE_BY_ID": true, "GEN_GET_ALL": true, "GEN_GET_ALL_STREAM": true,
    "GEN_GET_BY_ID": true, "GEN_PUT_UPDATE_BY_ID": true, "GEN_PUT_REPLACE_BY_ID": true, "GEN_DEL_DELETE_BY_ID": true,
}
var bodyFindApis = map[string]bool{
    "SCHEMA_POST_QUERY": true, "SCHEMA_POST_QUERY_STREAM": true, "SCHEMA_POST_QUERY_DELETE": true,
    "SCHEMA_UPDATE_MANY": true, "SCHEMA_POST_DISTINCT_QUERY": true, "SCHEMA_ARRAY_OPERATIONS": true,
    "SCHEMA_POST_COUNT": true, "GEN_POST_QUERY": true, "GEN_POST_QUERY_STREAM": true, "GEN_POST_QUERY_DELETE": true,
    "GEN_UPDATE_MANY": true, "GEN_POST_DISTINCT_QUERY": true, "GEN_ARRAY_OPERATIONS": true, "GEN_POST_COUNT": true,
}
var stampOwnerApis = map[string]bool{
    "SCHEMA_POST_BULK_INSERT": true, "SCHEMA_MASTER_SAVE": true, "GEN_POST_BULK_INSERT": true, "GEN_MASTER_SAVE": true,
}

func Main(g *am.G) (any, error) {
    // Custom APIs, schedulers and events calling g.sys.db run under their own rules, only real callers are scoped.
    if !g.Req.IsApiRequestFromUser() {
        return nil, nil
    }

    me := g.Req.Auth().Map("authAMDB").Get(identityField)
    if me == nil || me == "" {
        return nil, am.Fail(401, "A person token is required for this API.")
    }

    // MERGE, never replace : $and keeps what the caller asked for and adds the owner on top, so a caller
    // can never widen the rows by sending its own owner filter.
    merge := func(existing am.M) am.M {
        if len(existing) > 0 {
            return am.M{"$and": []any{existing, am.M{ownerColumn: me}}}
        }
        return am.M{ownerColumn: me}
    }

    apiID := g.Req.ReqInfo().Map("apiInfo").String("id")
    switch {
    case queryFindApis[apiID]:
        query := g.Req.Query()
        query["find"] = merge(query.Map("find"))
        if body := g.Req.Map("body"); body != nil {
            body[ownerColumn] = me // update-by-id and replace-by-id : the owner of a row can not be handed over
        }
        return nil, nil
    case bodyFindApis[apiID]:
        body := g.Req.Body()
        body["find"] = merge(body.Map("find"))
        if updateData := body.Map("updateData"); updateData != nil {
            updateData[ownerColumn] = me // update-many
        }
        return nil, nil
    case stampOwnerApis[apiID]:
        rows := g.Req.List("body")
        if rows == nil {
            rows = []any{g.Req.Get("body")}
        }
        for _, row := range rows {
            if fields := am.AsM(row); row != nil {
                fields[ownerColumn] = me // never trust the owner sent by the caller
            }
        }
        return nil, nil
    }

    // Fail closed : aggregate and distinct can not be scoped generically, so they are refused for row scoped callers.
    return nil, am.Fail(403, fmt.Sprintf("API %s is not available for row scoped users.", apiID))
}
Collection level pre hook on orders
// Row scoping pre-hook for orders, as the API Security Report writes it.
// Every request is limited to the rows whose "customer_id" equals the id of the person behind it
// (g.req.auth.authAMDB.customer_id). Edit freely : the report reads the code again on every scan.
use am::{Errors, G, Value, json};

const OWNER_COLUMN: &str = "customer_id";
const IDENTITY_FIELD: &str = "customer_id";

// Where each generated API keeps its filter. API Maker ANDs query.find onto the primary key for the *-by-id APIs.
const QUERY_FIND_APIS: [&str; 12] = [
    "SCHEMA_GET_ALL", "SCHEMA_GET_ALL_STREAM", "SCHEMA_GET_BY_ID", "SCHEMA_PUT_UPDATE_BY_ID",
    "SCHEMA_PUT_REPLACE_BY_ID", "SCHEMA_DEL_DELETE_BY_ID", "GEN_GET_ALL", "GEN_GET_ALL_STREAM", "GEN_GET_BY_ID",
    "GEN_PUT_UPDATE_BY_ID", "GEN_PUT_REPLACE_BY_ID", "GEN_DEL_DELETE_BY_ID",
];
const BODY_FIND_APIS: [&str; 14] = [
    "SCHEMA_POST_QUERY", "SCHEMA_POST_QUERY_STREAM", "SCHEMA_POST_QUERY_DELETE", "SCHEMA_UPDATE_MANY",
    "SCHEMA_POST_DISTINCT_QUERY", "SCHEMA_ARRAY_OPERATIONS", "SCHEMA_POST_COUNT", "GEN_POST_QUERY",
    "GEN_POST_QUERY_STREAM", "GEN_POST_QUERY_DELETE", "GEN_UPDATE_MANY", "GEN_POST_DISTINCT_QUERY",
    "GEN_ARRAY_OPERATIONS", "GEN_POST_COUNT",
];
const STAMP_OWNER_APIS: [&str; 4] = [
    "SCHEMA_POST_BULK_INSERT", "SCHEMA_MASTER_SAVE", "GEN_POST_BULK_INSERT", "GEN_MASTER_SAVE",
];

pub fn main(g: &mut G) -> Result<Value, Errors> {
    // Custom APIs, schedulers and events calling g.sys.db run under their own rules, only real callers are scoped.
    if !g.req.is_api_request_from_user() {
        return Ok(Value::Null);
    }

    let me = g.req.auth()["authAMDB"][IDENTITY_FIELD].clone();
    if me.is_null() || me == "" {
        return Err(am::fail(401, "A person token is required for this API."));
    }

    let scope = json!({ OWNER_COLUMN: me.clone() });
    // MERGE, never replace : $and keeps what the caller asked for and adds the owner on top, so a caller
    // can never widen the rows by sending its own owner filter.
    let merge = |existing: &Value| match existing {
        Value::Object(find) if !find.is_empty() => json!({ "$and": [existing, scope] }),
        _ => scope.clone(),
    };

    let api_id = g.req.req_info()["apiInfo"]["id"].as_str().unwrap_or_default().to_owned();
    if QUERY_FIND_APIS.contains(&api_id.as_str()) {
        let find = merge(&g.req.query()["find"]);
        g.req.query_mut()["find"] = find;
        let body = g.req.body_mut();
        if body.is_object() {
            body[OWNER_COLUMN] = me.clone(); // update-by-id and replace-by-id : the owner of a row can not be handed over
        }
        return Ok(Value::Null);
    }
    if BODY_FIND_APIS.contains(&api_id.as_str()) {
        let find = merge(&g.req.body()["find"]);
        g.req.body_mut()["find"] = find;
        if let Some(update_data) = g.req.body_mut().get_mut("updateData").filter(|update_data| update_data.is_object()) {
            update_data[OWNER_COLUMN] = me.clone(); // update-many
        }
        return Ok(Value::Null);
    }
    if STAMP_OWNER_APIS.contains(&api_id.as_str()) {
        let rows: Vec<&mut Value> = match g.req.body_mut() {
            Value::Array(rows) => rows.iter_mut().collect(),
            row => vec![row],
        };
        for row in rows {
            if row.is_object() {
                row[OWNER_COLUMN] = me.clone(); // never trust the owner sent by the caller
            }
        }
        return Ok(Value::Null);
    }

    // Fail closed : aggregate and distinct can not be scoped generically, so they are refused for row scoped callers.
    Err(am::fail(403, format!("API {api_id} is not available for row scoped users.")))
}
Collection level pre hook on orders
// Row scoping pre-hook for orders, as the API Security Report writes it.
// Every request is limited to the rows whose "customer_id" equals the id of the person behind it
// (g.req.auth.authAMDB.customer_id). Edit freely : the report reads the code again on every scan.
import am.AmErrors;
import am.G;
import am.JsonObject;
import java.util.List;
import java.util.Map;
import java.util.Set;

class Api {
    static final String OWNER_COLUMN = "customer_id";
    static final String IDENTITY_FIELD = "customer_id";

    // Where each generated API keeps its filter. API Maker ANDs query.find onto the primary key for the *-by-id APIs.
    static final Set<String> QUERY_FIND_APIS = Set.of(
        "SCHEMA_GET_ALL", "SCHEMA_GET_ALL_STREAM", "SCHEMA_GET_BY_ID", "SCHEMA_PUT_UPDATE_BY_ID",
        "SCHEMA_PUT_REPLACE_BY_ID", "SCHEMA_DEL_DELETE_BY_ID", "GEN_GET_ALL", "GEN_GET_ALL_STREAM", "GEN_GET_BY_ID",
        "GEN_PUT_UPDATE_BY_ID", "GEN_PUT_REPLACE_BY_ID", "GEN_DEL_DELETE_BY_ID");
    static final Set<String> BODY_FIND_APIS = Set.of(
        "SCHEMA_POST_QUERY", "SCHEMA_POST_QUERY_STREAM", "SCHEMA_POST_QUERY_DELETE", "SCHEMA_UPDATE_MANY",
        "SCHEMA_POST_DISTINCT_QUERY", "SCHEMA_ARRAY_OPERATIONS", "SCHEMA_POST_COUNT", "GEN_POST_QUERY",
        "GEN_POST_QUERY_STREAM", "GEN_POST_QUERY_DELETE", "GEN_UPDATE_MANY", "GEN_POST_DISTINCT_QUERY",
        "GEN_ARRAY_OPERATIONS", "GEN_POST_COUNT");
    static final Set<String> STAMP_OWNER_APIS = Set.of(
        "SCHEMA_POST_BULK_INSERT", "SCHEMA_MASTER_SAVE", "GEN_POST_BULK_INSERT", "GEN_MASTER_SAVE");

    // MERGE, never replace : $and keeps what the caller asked for and adds the owner on top, so a caller
    // can never widen the rows by sending its own owner filter.
    static Object merge(Object existing, Map<String, Object> scope) {
        return existing instanceof Map<?, ?> find && !find.isEmpty() ? JsonObject.of("$and", List.of(existing, scope)) : new JsonObject(scope);
    }

    @SuppressWarnings("unchecked")
    static Object main(G g) {
        // Custom APIs, schedulers and events calling g.sys.db run under their own rules, only real callers are scoped.
        if (!g.req.isApiRequestFromUser()) return null;

        Object me = g.req.getAuth().getObject("authAMDB").get(IDENTITY_FIELD);
        if (me == null || "".equals(me)) throw new AmErrors("A person token is required for this API.", 401);
        JsonObject scope = JsonObject.of(OWNER_COLUMN, me);

        String apiId = g.req.getReqInfo().getObject("apiInfo").getString("id", "");
        if (QUERY_FIND_APIS.contains(apiId)) {
            JsonObject query = g.req.objectAt("query");
            query.put("find", merge(query.get("find"), scope));
            Object body = g.req.get("body");
            if (body instanceof Map<?, ?>) ((Map<String, Object>) body).put(OWNER_COLUMN, me); // update-by-id and replace-by-id : the owner of a row can not be handed over
            return null;
        }
        if (BODY_FIND_APIS.contains(apiId)) {
            JsonObject body = g.req.objectAt("body");
            body.put("find", merge(body.get("find"), scope));
            Object updateData = body.get("updateData");
            if (updateData instanceof Map<?, ?>) ((Map<String, Object>) updateData).put(OWNER_COLUMN, me); // update-many
            return null;
        }
        if (STAMP_OWNER_APIS.contains(apiId)) {
            List<?> rows = g.req.get("body") instanceof List<?> list ? list : List.of(g.req.objectAt("body"));
            for (Object row : rows) {
                if (row instanceof Map<?, ?>) ((Map<String, Object>) row).put(OWNER_COLUMN, me); // never trust the owner sent by the caller
            }
            return null;
        }

        // Fail closed : aggregate and distinct can not be scoped generically, so they are refused for row scoped callers.
        throw new AmErrors("API " + apiId + " is not available for row scoped users.", 403);
    }
}
Collection level pre hook on orders
// Row scoping pre-hook for orders, as the API Security Report writes it.
// Every request is limited to the rows whose "customer_id" equals the id of the person behind it
// (g.req.auth.authAMDB.customer_id). Edit freely : the report reads the code again on every scan.
import am.AmErrors
import am.G

const val OWNER_COLUMN = "customer_id"
const val IDENTITY_FIELD = "customer_id"

// Where each generated API keeps its filter. API Maker ANDs query.find onto the primary key for the *-by-id APIs.
val QUERY_FIND_APIS = setOf(
    "SCHEMA_GET_ALL", "SCHEMA_GET_ALL_STREAM", "SCHEMA_GET_BY_ID", "SCHEMA_PUT_UPDATE_BY_ID",
    "SCHEMA_PUT_REPLACE_BY_ID", "SCHEMA_DEL_DELETE_BY_ID", "GEN_GET_ALL", "GEN_GET_ALL_STREAM", "GEN_GET_BY_ID",
    "GEN_PUT_UPDATE_BY_ID", "GEN_PUT_REPLACE_BY_ID", "GEN_DEL_DELETE_BY_ID",
)
val BODY_FIND_APIS = setOf(
    "SCHEMA_POST_QUERY", "SCHEMA_POST_QUERY_STREAM", "SCHEMA_POST_QUERY_DELETE", "SCHEMA_UPDATE_MANY",
    "SCHEMA_POST_DISTINCT_QUERY", "SCHEMA_ARRAY_OPERATIONS", "SCHEMA_POST_COUNT", "GEN_POST_QUERY",
    "GEN_POST_QUERY_STREAM", "GEN_POST_QUERY_DELETE", "GEN_UPDATE_MANY", "GEN_POST_DISTINCT_QUERY",
    "GEN_ARRAY_OPERATIONS", "GEN_POST_COUNT",
)
val STAMP_OWNER_APIS = setOf(
    "SCHEMA_POST_BULK_INSERT", "SCHEMA_MASTER_SAVE", "GEN_POST_BULK_INSERT", "GEN_MASTER_SAVE",
)

@Suppress("UNCHECKED_CAST")
fun main(g: G): Any? {
    // Custom APIs, schedulers and events calling g.sys.db run under their own rules, only real callers are scoped.
    if (!g.req.isApiRequestFromUser) return null

    val me = g.req.auth.getObject("authAMDB")[IDENTITY_FIELD]
    if (me == null || me == "") throw AmErrors("A person token is required for this API.", 401)

    val scope = mapOf(OWNER_COLUMN to me)
    // MERGE, never replace : $and keeps what the caller asked for and adds the owner on top, so a caller
    // can never widen the rows by sending its own owner filter.
    fun merge(existing: Any?): Any = if (existing is Map<*, *> && existing.isNotEmpty()) mapOf("\$and" to listOf(existing, scope)) else scope.toMutableMap()

    val apiId = g.req.reqInfo.getObject("apiInfo").getString("id", "")
    if (apiId in QUERY_FIND_APIS) {
        val query = g.req.objectAt("query")
        query["find"] = merge(query["find"])
        val body = g.req["body"]
        if (body is MutableMap<*, *>) (body as MutableMap<String, Any?>)[OWNER_COLUMN] = me // update-by-id and replace-by-id : the owner of a row can not be handed over
        return null
    }
    if (apiId in BODY_FIND_APIS) {
        val body = g.req.objectAt("body")
        body["find"] = merge(body["find"])
        val updateData = body["updateData"]
        if (updateData is MutableMap<*, *>) (updateData as MutableMap<String, Any?>)[OWNER_COLUMN] = me // update-many
        return null
    }
    if (apiId in STAMP_OWNER_APIS) {
        val rows = g.req["body"] as? List<*> ?: listOf(g.req.objectAt("body"))
        for (row in rows) {
            if (row is MutableMap<*, *>) (row as MutableMap<String, Any?>)[OWNER_COLUMN] = me // never trust the owner sent by the caller
        }
        return null
    }

    // Fail closed : aggregate and distinct can not be scoped generically, so they are refused for row scoped callers.
    throw AmErrors("API $apiId is not available for row scoped users.", 403)
}

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

A pre hook which answers from the cache
1
2
3
4
5
6
7
import * as T from 'types';

async function main(g: T.IAMGlobal) {
    const cached = await g.sys.cache.getKey('countries');
    if (cached) return JSON.parse(cached);   // the API does not run
}
module.exports = main;
A pre hook which answers from the cache
1
2
3
4
5
6
7
8
9
import json

from am import G


async def main(g: G):
    cached = await g.sys.cache.get_key('countries')
    if cached:
        return json.loads(cached)  # the API does not run
A pre hook which answers from the cache
1
2
3
4
5
6
7
8
9
<?php
function main(AmG $g)
{
    $cached = $g->sys->cache->getKey('countries');
    if ($cached) {
        return json_decode($cached); // the API does not run
    }
    return null;
}
A pre hook which answers from the cache
1
2
3
4
5
6
7
8
9
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        var cached = await g.Sys.Cache.GetKey("countries");
        if (cached is not null) return JsonNode.Parse(cached); // the API does not run
        return null;
    }
}
A pre hook which answers from the cache
package code

import (
    "encoding/json"

    "apimaker/am"
)

func Main(g *am.G) (any, error) {
    cached, err := g.Sys.Cache.GetKey("countries")
    if err != nil || cached == "" {
        return nil, err
    }
    var countries any
    err = json.Unmarshal([]byte(cached), &countries)
    return countries, err // the API does not run
}
A pre hook which answers from the cache
1
2
3
4
5
6
7
8
use am::{Errors, G, Value};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    match g.sys.cache.get_key("countries")? {
        Some(cached) => Ok(serde_json::from_str(&cached)?), // the API does not run
        None => Ok(Value::Null),
    }
}
A pre hook which answers from the cache
import am.G;
import am.Json;

class Api {
    static Object main(G g) {
        String cached = g.sys.cache.getKey("countries");
        if (cached != null) return Json.parse(cached); // the API does not run
        return null;
    }
}
A pre hook which answers from the cache
1
2
3
4
5
6
7
import am.G
import am.Json

fun main(g: G): Any? {
    val cached = g.sys.cache.getKey("countries")
    return cached?.let { Json.parse(it) } // the API does not run
}

Validate and change

import * as T from 'types';

async function main(g: T.IAMGlobal) {
    if (!g.req.isApiRequestFromUser) return;
    if (g.req.body?.price_cents < 0) throw [{ code: T.EStatusCode.BAD_REQUEST, message: 'A price can not be negative.' }];
    g.req.body.updated_by = g.req.auth.authAMDB?.email;
    g.req.headers['x-am-response-case'] = 'camelCase';   // headers can change too
    g.res.shared.startedAt = Date.now();                 // for the post hook of this request
}
module.exports = main;
import time

from am import Errors, G


async def main(g: G):
    if not g.req.is_api_request_from_user:
        return
    if (g.req.body.price_cents or 0) < 0:
        raise Errors('A price can not be negative.', code=400)
    g.req.body.updated_by = (g.req.auth.authAMDB or {}).get('email')
    g.req.headers['x-am-response-case'] = 'camelCase'   # headers can change too
    g.res.shared.startedAt = int(time.time() * 1000)    # for the post hook of this request
<?php
function main(AmG $g)
{
    if (!$g->req->isApiRequestFromUser) {
        return null;
    }
    if (($g->req->body->price_cents ?? 0) < 0) {
        throw new AmErrors('A price can not be negative.', 400);
    }
    $g->req->body->updated_by = $g->req->auth->authAMDB?->email;
    $g->req->headers['x-am-response-case'] = 'camelCase';            // headers can change too
    $g->res->shared->startedAt = (int) (microtime(true) * 1000);    // for the post hook of this request
    return null;
}
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        if (!g.Req.IsApiRequestFromUser) return null;
        if (g.Req.Body.GetDouble("price_cents") < 0) throw new AmErrors("A price can not be negative.", 400);
        g.Req.Body["updated_by"] = g.Req.Auth.GetObject("authAMDB").GetString("email");
        g.Req.Headers["x-am-response-case"] = "camelCase";                                  // headers can change too
        g.Res.Shared["startedAt"] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();         // for the post hook of this request
        return null;
    }
}
package code

import (
    "time"

    "apimaker/am"
)

func Main(g *am.G) (any, error) {
    if !g.Req.IsApiRequestFromUser() {
        return nil, nil
    }
    if g.Req.Body().Float("price_cents") < 0 {
        return nil, am.Fail(400, "A price can not be negative.")
    }
    g.Req.Body()["updated_by"] = g.Req.Auth().Map("authAMDB").Get("email")
    g.Req.Headers()["x-am-response-case"] = "camelCase"     // headers can change too
    g.Res.Shared()["startedAt"] = time.Now().UnixMilli() // for the post hook of this request
    return nil, nil
}
use std::time::{SystemTime, UNIX_EPOCH};

use am::{Errors, G, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    if !g.req.is_api_request_from_user() {
        return Ok(Value::Null);
    }
    if g.req.body()["price_cents"].as_f64().unwrap_or(0.0) < 0.0 {
        return Err(am::fail(400, "A price can not be negative."));
    }
    let email = g.req.auth()["authAMDB"]["email"].clone();
    g.req.body_mut()["updated_by"] = email;
    g.req.headers_mut()["x-am-response-case"] = json!("camelCase"); // headers can change too
    let now = SystemTime::now().duration_since(UNIX_EPOCH).map(|since| since.as_millis()).unwrap_or(0);
    g.res.shared_mut()["startedAt"] = json!(now as u64); // for the post hook of this request
    Ok(Value::Null)
}
import am.AmErrors;
import am.G;

class Api {
    static Object main(G g) {
        if (!g.req.isApiRequestFromUser()) return null;
        if (g.req.getBody().getDouble("price_cents") < 0) throw new AmErrors("A price can not be negative.", 400);
        g.req.getBody().put("updated_by", g.req.getAuth().getObject("authAMDB").get("email"));
        g.req.getHeaders().put("x-am-response-case", "camelCase");           // headers can change too
        g.res.getShared().put("startedAt", System.currentTimeMillis());      // for the post hook of this request
        return null;
    }
}
import am.AmErrors
import am.G

fun main(g: G): Any? {
    if (!g.req.isApiRequestFromUser) return null
    if (g.req.body.getDouble("price_cents") < 0) throw AmErrors("A price can not be negative.", 400)
    g.req.body["updated_by"] = g.req.auth.getObject("authAMDB")["email"]
    g.req.headers["x-am-response-case"] = "camelCase"            // headers can change too
    g.res.shared["startedAt"] = System.currentTimeMillis()       // for the post hook of this request
    return null
}

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.

utils/Guards : a utility class
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.` }];
    }
}
A pre hook which uses it
import * as T from 'types';
import * as Guards from 'utils/Guards';

async function main(g: T.IAMGlobal) {
    Guards.requireRole(g, 'PURCHASE_INVOICE_VIEW');
}
module.exports = main;
The utility class Guards
from am import Errors


# The person of the request has the role, or the request is refused.
def require_role(g, role):
    roles = (g.req.auth.authAMDB or {}).get('roles') or []
    if role not in roles:
        raise Errors(f'The role {role} is needed.', code=403)
A pre hook which uses it
from Guards import require_role


async def main(g):
    require_role(g, 'PURCHASE_INVOICE_VIEW')
The utility class Guards
<?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);
        }
    }
}
A pre hook which uses it
<?php
function main(AmG $g)
{
    Guards::requireRole($g, 'PURCHASE_INVOICE_VIEW');
    return null;
}
The utility class Guards
// 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);
    }
}
A pre hook which uses it
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        Guards.RequireRole(g, "PURCHASE_INVOICE_VIEW");
        return null;
    }
}
The utility class Guards, in the folder utils
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.")
}
A pre hook which uses it
package code

import (
    "apimaker/am"
    guards "apimaker/src/utility_classes/utils---Guards"
)

func Main(g *am.G) (any, error) {
    return nil, guards.RequireRole(g, "PURCHASE_INVOICE_VIEW")
}
The utility class Guards
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.")))
    }
}
A pre hook which uses it
use am::{Errors, G, Value};

use crate::Guards;

pub fn main(g: &mut G) -> Result<Value, Errors> {
    Guards::require_role(g, "PURCHASE_INVOICE_VIEW")?;
    Ok(Value::Null)
}
The utility class Guards
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);
        }
    }
}
A pre hook which uses it
import am.G;

class Api {
    static Object main(G g) {
        Guards.requireRole(g, "PURCHASE_INVOICE_VIEW");
        return null;
    }
}
The utility class Guards
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)
    }
}
A pre hook which uses it
import am.G

fun main(g: G): Any? {
    Guards.requireRole(g, "PURCHASE_INVOICE_VIEW")
    return null
}

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.
  • Every hook has versions : keep the old one active until the new one is ready.
  • 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 can not change what was streamed.
  • 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.