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

Pre hooks

A pre hook is a TypeScript function of yours 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, a system API or a third party 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 Third party 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, system, third party 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.
  • 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.

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.shared : values for the post hooks of the same request
}
module.exports = main;

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 : throw new Error('…') gives 500 and the message, or throw an IResponseError[] with a code for another status.
  • Every hook runs in the sandbox (or on the native process with runOnNativeProcess), 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

The most useful pre hook : a filter added to every read and a stamp on every write, so a person only reaches their own rows.

Collection level pre hook on orders
import * as T from 'types';

async function main(g: T.IAMGlobal) {
    if (!g.req.isApiRequestFromUser) return;
    const person = g.req.auth.authAMDB;               // the person, from x-am-user-authorization
    if (!person) throw new Error('Sign in first.');
    const mine = { customer_id: person.customer_id };

    const api = g.req.reqInfo.apiInfo.id;              // SCHEMA_GET_ALL, SCHEMA_POST_QUERY, …
    if (api.endsWith('_GET_ALL') || api.endsWith('_GET_ALL_STREAM') || api.endsWith('_GET_BY_ID') || api.endsWith('_DEL_DELETE_BY_ID') || api.endsWith('_PUT_UPDATE_BY_ID')) {
        // filters travel in query.find on these APIs
        const find = g.req.query.find ? (typeof g.req.query.find === 'string' ? JSON.parse(g.req.query.find) : g.req.query.find) : {};
        g.req.query.find = { $and: [ find, mine ] };
    } else if (api.endsWith('_POST_QUERY') || api.endsWith('_POST_QUERY_STREAM') || api.endsWith('_POST_COUNT') || api.endsWith('_UPDATE_MANY') || api.endsWith('_POST_QUERY_DELETE') || api.endsWith('_POST_DISTINCT_QUERY')) {
        g.req.body.find = { $and: [ g.req.body.find || {}, mine ] };
    } else if (api.endsWith('_POST_BULK_INSERT') || api.endsWith('_MASTER_SAVE')) {
        const rows = Array.isArray(g.req.body) ? g.req.body : [ g.req.body ];
        for (const row of rows) row.customer_id = person.customer_id;   // stamp the owner
    } else if (api.endsWith('_POST_AGGREGATE') || api.endsWith('_GET_DISTINCT')) {
        throw new Error('Not allowed on this table.');
    }
}
module.exports = main;

The APIs Security Report writes this hook for you, for the tables which need it, and Handle role based permissions explains the pattern step by step.

Answer without the API

A pre hook which answers from the cache of the process
1
2
3
4
5
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;

Validate and change

1
2
3
4
5
6
7
8
async function main(g: T.IAMGlobal) {
    if (!g.req.isApiRequestFromUser) return;
    if (g.req.body?.price_cents < 0) throw new Error('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.shared.startedAt = Date.now();                     // for the post hook
}
module.exports = main;

Utility classes in a hook

1
2
3
4
5
6
7
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;

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.