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

Cheat sheet

Call an API

The 17 operations of a table All APIs

Operation Method After …/<table> Body Id, after SCHEMA_ or GEN_
Get all GET / query params GET_ALL
Get all by stream GET /stream query params GET_ALL_STREAM
Get by id GET /get-by-id/:id[/:primaryKey] query params GET_BY_ID
Query for get data POST /query { find, select, sort, skip, limit, deep, getTotalCount } POST_QUERY
Query by stream POST /query-stream the same as query POST_QUERY_STREAM
Count POST /count { find } POST_COUNT
Distinct GET /distinct/:field[/:order] GET_DISTINCT
Distinct with query POST /distinct/:field[/:order] { find } POST_DISTINCT_QUERY
Aggregate MongoDB POST /aggregate a pipeline array POST_AGGREGATE
Save single or multiple POST /save-single-or-multiple an object or an array POST_BULK_INSERT
Master save POST /master-save objects with their related objects MASTER_SAVE
Update by id PUT /update-by-id/:id[/:primaryKey] the fields to change PUT_UPDATE_BY_ID
Update many PUT /update-many { find, updateData } UPDATE_MANY
Replace by id MongoDB PUT /replace-by-id/:id[/:primaryKey] the whole document PUT_REPLACE_BY_ID
Array operations MongoDB PUT /array-operations { find, operations } ARRAY_OPERATIONS
Remove by id DELETE /:id[/:primaryKey] DEL_DELETE_BY_ID
Remove by query POST /query/delete { find }, never empty POST_QUERY_DELETE

The path goes after /api/schema/<user-path>/<instance>/<database>/<table>, or the same under /api/gen. MongoDB has all 17. MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, TiDB and Percona have 14. The schema APIs exist once a table has a schema. :primaryKey names another column to use as the key. The ids name the APIs in groups, hooks, events and WebSocket subscriptions : SCHEMA_GET_ALL.

Request headers Request headers

Who calls, how it runs Value
x-am-authorization token of the API user (the app)
x-am-user-authorization token of a person, from your users table
x-google-authorization
x-azure-authorization
x-aws-authorization
x-custom-authorization
token of a person, from that provider
x-am-tenant-username tenant of a multi-tenant instance : acme
x-am-sandbox-timeout milliseconds the code may run, 13000 by default
x-am-run-in-sandbox 0 any sandbox, 1 the first one, n one of the first n
x-am-run-in-debugger true : the code stops at breakpoints, while debugging is on
x-am-cache-control no_action reset_cache
x-am-encrypted-payload true when the body is { dataEncFE }
The answer Value
x-am-response-case noChange camelCase capitalCase constantCase dotCase headerCase noCase paramCase pascalCase pathCase sentenceCase snakeCase
x-am-content-type-response application/json text/xml text/yaml text/plain text/html application/octet-stream
x-am-response-object-type no_action make_flat : state_id_country_name
x-am-get-encrypted-data no_encryption get_only_encryption get_data_and_encryption
x-am-internationalization a language of i18N Management : Hindi
x-am-meta true : time, groups and server in meta, in the admin panel only
x-no-compression true : never compressed (else from 51200 bytes)
accept-encoding gzip, else br, else deflate

URLs All APIs

Schema APIs /api/schema/<user-path>/<instance>/<db>/<table>
Generated APIs, no schema /api/gen/<user-path>/<instance>/<db>/<table>
Custom APIs /api/custom-api/<user-path>/<path>
System APIs, all POST /api/system-api/<user-path>/<name>
A tenant <instance>::<tenant> in the path, or the header x-am-tenant-username
WebSocket wss://<api host>/?x-am-authorization=<token>
SSR website /ssr/<user-path>/<website>, or its own domain

<user-path> is the API path of the account : admin for the first admin. <db> is the database.

Tokens Get token

POST /api/system-api/admin/token
{ "u": "default", "p": "…" }        the app
{ "name": "users_tg", "u", "p" }    a person
{ "refresh_token": "…" }            renew
→ { token, refresh_token, expires_in }
x-am-authorization the app : an API user. Always.
x-am-user-authorization a person of your users table
x-google-authorization
x-azure-authorization
x-aws-authorization
x-custom-authorization
a person signed in with Google, Azure AD, AWS Cognito or your own provider

A token lives 259200 s (3 days), unless expiresInSeconds says otherwise. Its refresh token works 900 s longer.

Who may call Secure your APIs

Access type Who may call
TOKEN_ACCESS a token whose groups allow the API. The default.
IS_PUBLIC anyone, without a token
NO_ACCESS only your own code and API Testing

Set it as apiAccessType in the settings of a table or a custom API. A call passes when a group of the API user grants the API. With a person token, a group of the person must grant it too.

System APIs All APIs

Security /token /encrypt-data /decrypt-data /hash-data /get-secret-by-name
Redis and cache /get-redis-key /set-redis-key /remove-redis-key /reset-redis-cache-db /reset-redis-cache-custom-apis /reset-redis-cache-system-apis
Databases /get-table-meta /create-indexes /drop-indexes /get-indexes /execute-plain-query /multi-tenant-instance-updated
Events /emit-event /emit-event-ws
Checks /is-valid-data-for-table /is-valid-data-for-custom-api /is-valid-connection-string
Outside /call-external-api

All are POST …/system-api/<user-path>/…. From code they are g.sys.system.* and g.sys.cache.* : /token is getToken, /get-redis-key is cache.getKey. Their id is the path in capitals (ENCRYPT_DATA), but /token is GET_TOKEN and /get-secret-by-name is GET_SECRET.

Query and answer

Query params Query params

find={status:'ACTIVE'} filter in JSON5, with MongoDB operators, on every database
select=name,price · select=-password only these fields · all but these
sort=-created_at,name - : descending
skip=20&limit=10 paging : add a sort
getTotalCount=true adds totalCount, before paging
deep=[{s_key:'category'}] the related rows, in the same call
customer_id=42 · price>=100 · name=Bob,Alice any column is a filter : > >= < <= !=, lists
find={'owner.customer_id':42} find and join : a field of a related table
upsert=true update or replace by id : insert an unknown id
returnDocument=before update or replace by id : answer the row before
throwErrorIfRecordNotFound=true update, replace or remove by id : 404 instead of data: null

The query APIs take the same keys in their body.

find operators find

Compare $eq $ne $gt $gte $lt $lte $in $nin
Logic $and $or $nor $not
Element $exists · $type MongoDB
Text $like: 'Bob%' · $regex with $options · $text MongoDB
Others $mod $expr · $isNull SQL · $where $jsonSchema MongoDB
Arrays MongoDB $all $elemMatch $size
Bits $bitsAllSet $bitsAnySet $bitsAllClear $bitsAnyClear
Geo MongoDB $geoWithin $geoIntersects $near $nearSphere

SQL databases get the same filter, translated. $like : % is any text, _ one character, on every database.

deep deep

deep: [{
  s_key: 'customer_id',   // the field here
  t_col: 'customers',     // the other table
  t_key: 'id',            // default : its key
  t_db, t_instance,       // when they differ
  isMultiple: true,       // an array of rows
  find, select, sort, skip, limit,
  deep: [ … ],            // the next level
}]

With a relation in the schema, s_key alone is enough. One query per deep item, for all the rows.

What a write answers returnData

Save, master save the saved rows ; with ?returnData=false, only { savedRowsCount }
Update many { updatedRowsCount } ; with "returnData": true, also updatedRows
Remove by query { deletedRowsCount } ; with "returnData": true, also deletedRows and ids

From code, returnData is a key of the call (return_data in Python and Rust).

The answer Response format

{ "success": true, "statusCode": 200,
  "data": [], "totalCount": 5,
  "warnings": [], "logs": [], "meta": {} }

{ "success": false, "statusCode": 400,
  "errors": [ { "type": "required",
    "field": "name", "message": "…",
    "code": 400, "dataIndex": 0 } ] }
data the rows, the row, or what your code returns
logs the g.logger lines of the code which ran
meta with x-am-meta: true, admin panel only

Status codes Response format

200 done
201 created : save, master save
400 wrong request : validation, bad query, no body
401 token missing, malformed or expired
403 the token is fine, no group grants it ; or the origin is not allowed
404 no such path ; an unknown id with throwErrorIfRecordNotFound
500 an error of your code or of the database
Response header
x-am-data-source cache or api
x-am-request-id the id of the request in the logs
content-encoding gzip, br or deflate

Messages to know Error messages

AM_00001 a field is missing or of the wrong type
AM_00034 403 : no group of the API user grants the API
AM_00035 401 : x-am-authorization is not valid
AM_00092 the person token is missing
AM_00056 no schema on the table : call /api/gen
AM_00117 the row changed since you read it : version mismatch
AM_00119 record not found, with throwErrorIfRecordNotFound
AM_00032 the origin of the browser is not allowed

Every message comes in the language of x-am-internationalization.

Schema

A schema file Table schema

import {
  EType, ISchemaType, ISchemaProperty,
} from 'types';

const schema: ISchemaType = {
  name: EType.string,         // a type alone
  price: <ISchemaProperty>{
    __type: EType.number,
    validations: { required: true, min: 0 },
  },
  tags: [EType.string],       // an array
  address: { city: EType.string },
};
module.exports = { schema };

Only the schema APIs read it. Nested objects and arrays are for MongoDB : SQL tables are flat. A key the schema does not have is refused.

Types Data types

__type Holds
EType.string text ; also a JSON column
EType.number numbers
EType.boolean true, false
EType.date dates, ISO strings in JSON
EType.objectId a MongoDB ObjectId
EType.file · EType.files uploads, in the request schema of a custom API
[EType.number] a vector column
Other values In JSON
Vector [0.12, 0.5, 0.33]
Bytes { "type": "Buffer", "data": [1, 2] }
Shape GeoJSON in ; the form of the driver out
Interval { "days": 1, "hours": 2 }
PostgreSQL array [1, 2, 3]

Conversions Schema

trim trimStart trimEnd removes the spaces around
toLowerCase toUpperCase changes the case
conversionFun: (value, row) => … your function : it runs even when the field is absent
defaults: { defaultValue } a value for a missing field
defaults: { defaultFun: () => … } the same, computed ; defaultValue wins
shouldReplaceNullWithDefault · shouldReplaceEmptyStringWithDefault in defaults : null or "" get the default too
encryption: true stored encrypted (AES, keys of the secret)
hashing: true stored as an HMAC SHA-256 hash

The keys of conversions. They run before the validations, on save and on update.

Validations Schema

validations Error type
required: true required
min · max min · max
minLength · maxLength minLength · maxLength
email: true emailNotValid
enum: [ … ] enumValidation
unique: true unique
validatorFun: (value, row) => true invalidValue

A refused save answers 400 with every problem at once : { type, field, message, code, dataIndex }.

Keys and generated values Schema

isPrimaryKey: true the id of get, update and remove by id
isAutoIncrementByDB: true the database counts (SQL identity)
isAutoIncrementByAM: { start, step } API Maker counts, on every database ; true is 1, 1
isAutoGenerateByAM: { valueGeneratorType } ObjectID GUID_UUID ULID ShortUUID
isConcurrencyControlField: true the version field : a stale update gets 400

Relations Schema

// orderItems.order_id → orders._id
order_id: <ISchemaProperty>{
  __type: EType.objectId,
  database: 'shop', collection: 'orders',
  column: '_id',
},
// orders.items : its rows of orderItems
items: <ISchemaProperty>{
  __type: EType.objectId,
  isVirtualField: true,
  database: 'shop',
  collection: 'orderItems',
  s_columnVirtualLinker: '_id',
  t_columnVirtualLinker: 'order_id',
},

Add instance for another instance ; table works as collection. Relations feed deep, master save and the ER diagram.

Your code

A custom API Custom APIs

// Basic info : TypeScript in every language
let customApi: T.ICustomApiSettingsTypes = {
  name: 'Hello World',
  path: '/hello-world',
  requestMethod: T.ERequestMethod.GET,
  apiAccessType:
    T.EAPIAccessType.TOKEN_ACCESS,
  errorList: [],
};
module.exports = customApi;

It answers at /api/custom-api/<user-path>/hello-world, with the exact path and method. What main returns is data.

main in every language Languages

TypeScript async function main(g: T.IAMGlobal) then module.exports = main;
Python async def main(g: G):
PHP function main(AmG $g)
C# public static async Task<object?> Main(G g) in file static class Api
Go func Main(g *am.G) (any, error) in package code
Rust pub fn main(g: &mut G) -> Result<Value, Errors>
Java static Object main(G g) in class Api
Kotlin fun main(g: G): Any?

Hooks, listeners, schedulers and initializers are the same function.

Refuse a call Custom APIs

TypeScript throw [{ code: 400, message }]
Python raise Errors(message, code=400)
PHP throw new AmErrors($message, 400)
C#, Java throw new AmErrors(message, 400)
Kotlin throw AmErrors(message, 400)
Go return nil, am.Fail(400, message)
Rust Err(am::fail(400, message))

The answer : success: false, the message in errors, the code as status. Any other error answers 500. To send your own data with a status, set g.res.statusCode.

Custom API settings Settings

reqBodySchema · reqQueryParametersSchema converted and checked before the code runs
enableCaching the answer is kept in Redis
resetCacheOnModificationOf 'DB:instance:database:table' · 'CA:name'
authProviders the person tokens it needs ; [] : the API user only
acceptOnlyEncryptedData plain bodies are refused
customApiTimeoutInSeconds time of the code and its hooks ; 13 s by default
runOnNativeProcess runs on the machine of the server, not in Docker
separateSandboxSettings a sandbox of its own
fileUpload enable, the fields, sizes and extensions
errorList your messages, for i18N

Files and other answers Files

Upload : multipart/form-data, field files (up to files41). The code gets g.req.body.files : originalname, mimetype, size, path. 512 MiB per form by default.

Download : return the __am__ keys.

return {
  __am__downloadFilePath: 'r.pdf',
  __am__downloadFolderFileName: 'May.pdf',
  __am__cleanupFileOrFolderPaths: 'r.pdf',
};

Several paths in __am__downloadFileOrFolderPaths : one zip. Other content : set g.res.contentType and return the text, or bytes as base64. Headers of the answer : g.res.headers.

Shared code Utility classes

Utility class code your other code imports : import * as utils from 'utils/MyClass' in TypeScript
Process initializer runs when a sandbox process starts, before any API : subscribe, warm a cache
Migration script changes a database once on each environment, after a Git pull
g.shared values of one runner, for all its requests
g.res.shared values of one request : pre hook → API → post hook

Hooks Pre hooks

Where an instance, a database, a table, one API, a custom or a system API
Order pre : instance → database → table → API. Post : the other way.
groupNames runs only for API users of these groups
Language the language of its API
From code g.sys.db calls skip the hooks ; skipHookRunning: false runs them
Cache an answer from the cache runs no hook
A pre hook Then
changes g.req.query, body, headers the API gets the change
returns a value it is the answer : the API does not run
throws the call is refused
A post hook Then
changes g.res.output the answer changes
returns a value it is the answer ; later post hooks do not run
throws an error answer ; the write stays done

g.req.isApiRequestFromUser is false for a call of your own code.

Test cases Test cases

import * as assert from 'node:assert';

module.exports = [{
  name: 'orderCount counts the orders',
  mocks: [{ method: 'db.getAll',
    when: [{ collection: 'orders' }],
    return: [{ id: 1 }, { id: 2 }] }],
  code: async (g: T.IAMGlobal) => {
    const out = await g.sys.test
      .runCustomApi({ name: 'orderCount' });
    assert.deepStrictEqual(out, { count: 2 });
  },
}];

npm run test in the folder of the local client : exit 0 passed, 1 failed, 2 not complete. The coverage goes to coverage/lcov.info. On the page, Cmd+E or Ctrl+E in the editor runs it.

The object g

g in every language Languages

g.req.body  g.req.query  g.req.params  g.req.headers  g.req.eventData  g.req.reqInfo  g.req.isApiRequestFromUser
g.req.auth.authAMUser  .authAMDB  .authGoogle  .authAzure  .authAWS  .authCustom
g.req.scheduler.worker  .workers  .firedAt
g.res.output  g.res.statusCode  g.res.contentType  g.res.headers  g.res.errors  g.res.warnings  g.res.shared
g.sys.db.getAll({ instance, database, collection, queryParams, headers })   getAllByStream getById
    saveSingleOrMultiple masterSave arrayOperations updateById updateMany replaceById removeById query
    queryByStream removeByQuery aggregate count distinct distinctQuery
g.sys.db.gen.getAllGen(…)                       the same, schemaless, with the Gen suffix
g.sys.system.encrypt decrypt hash getToken callExternalApi executeQuery getSecret getTableMeta createIndexes
    dropIndexes getIndexes emitEvent emitEventWS isValidDataForTable isValidDataForCustomAPI
    multiTenantInstanceUpdated isValidConnectionString getLock releaseLock
g.sys.cache.getKey setKey removeKey resetCacheDB resetCacheCustomApis resetCacheSystemApis
g.logger.debug log info warn error            g.res.shared.x = …   travels with the request : pre hook → API → post hook
g.shared.x = …                                lives as long as the runner, shared by the requests it serves
await g.sys.db.count({ … }, true)             second argument true : the whole envelope, no throw
throw [{ code: 400, message: 'Bad id.' }]     refuse : 400, the message in errors
import * as utils from 'utils/MyClass'         utility classes
g.req.body  g.req.query  g.req.params  g.req.headers  g.req.event_data  g.req.req_info  g.req.is_api_request_from_user
g.req.auth.authAMUser  .authAMDB  .authGoogle  .authAzure  .authAWS  .authCustom
g.req.scheduler.worker  .workers  .firedAt
g.res.output  g.res.status_code  g.res.content_type  g.res.headers  g.res.errors  g.res.warnings  g.res.shared
await g.sys.db.get_all('instance', 'database', 'collection', find=…, limit=…, headers=…)   get_all_by_stream get_by_id
    save_single_or_multiple master_save array_operations update_by_id update_many replace_by_id remove_by_id query
    query_by_stream remove_by_query aggregate count distinct distinct_query
await g.sys.db.gen.get_all_gen(…)               the same, schemaless, with the _gen suffix
g.sys.system.encrypt decrypt hash get_token call_external_api execute_query get_secret get_table_meta create_indexes
    drop_indexes get_indexes emit_event emit_event_ws is_valid_data_for_table is_valid_data_for_custom_api
    multi_tenant_instance_updated is_valid_connection_string get_lock release_lock
g.sys.cache.get_key set_key remove_key reset_cache_db reset_cache_custom_apis reset_cache_system_apis
g.logger.debug log info warn error            g.res.shared.x = …   travels with the request : pre hook → API → post hook
g.shared['x'] = …                             lives as long as the runner, shared by the requests it serves
await g.sys.db.full.count(…)                  full : the whole envelope, no raise
raise Errors('Bad id.', code=400)             refuse : from am import Errors
from Tax import add                           utility classes
$g->req->body  $g->req->query  $g->req->params  $g->req->headers  $g->req->eventData  $g->req->reqInfo
$g->req->isApiRequestFromUser  $g->req->scheduler->worker
$g->req->auth->authAMUser  ->authAMDB  ->authGoogle  ->authAzure  ->authAWS  ->authCustom
$g->res->output  $g->res->statusCode  $g->res->contentType  $g->res->headers  $g->res->errors  $g->res->warnings
$g->res->shared
$g->sys->db->getAll('instance', 'database', 'collection', find: …, limit: …, headers: …)   getAllByStream getById
    saveSingleOrMultiple masterSave arrayOperations updateById updateMany replaceById removeById query queryByStream
    removeByQuery aggregate count distinct distinctQuery
$g->sys->db->gen->getAllGen(…)                  the same, schemaless, with the Gen suffix
$g->sys->system->encrypt decrypt hash getToken callExternalApi executeQuery getSecret getTableMeta createIndexes
    dropIndexes getIndexes emitEvent emitEventWS isValidDataForTable isValidDataForCustomAPI
    multiTenantInstanceUpdated isValidConnectionString getLock releaseLock
$g->sys->cache->getKey setKey removeKey resetCacheDB resetCacheCustomApis resetCacheSystemApis
$g->logger->debug log info warn error          $g->res->shared->x = …   travels with the request : pre hook → API → post hook
$g->shared->x = …                              lives as long as the runner, shared by the requests it serves
$g->sys->db->full->count(…)                    full : the whole envelope, no throw
throw new AmErrors('Bad id.', 400);            refuse : 400, the message in errors
Tax::add(100)                                  utility classes
g.Req.Body  g.Req.Query  g.Req.Params  g.Req.Headers  g.Req.EventData  g.Req.ReqInfo  g.Req.IsApiRequestFromUser
g.Req.Scheduler.GetInt64("worker")
g.Req.Auth["authAMUser"]  ["authAMDB"]  ["authGoogle"]  ["authAzure"]  ["authAWS"]  ["authCustom"]
g.Res.Output  g.Res.StatusCode  g.Res.ContentType  g.Res.Headers  g.Res.Errors  g.Res.Warnings  g.Res.Shared
await g.Sys.Db.GetAll(new GetAll("instance", "database", "collection") { Find = …, Limit = …, Headers = … })
    GetAllByStream GetById SaveSingleOrMultiple MasterSave ArrayOperations UpdateById UpdateMany ReplaceById
    RemoveById Query QueryByStream RemoveByQuery Aggregate Count Distinct DistinctQuery
await g.Sys.Db.Gen.GetAllGen(…)                 the same, schemaless, with the Gen suffix
g.Sys.System.Encrypt Decrypt Hash GetToken CallExternalApi ExecuteQuery GetSecret GetTableMeta CreateIndexes
    DropIndexes GetIndexes EmitEvent EmitEventWS IsValidDataForTable IsValidDataForCustomAPI
    MultiTenantInstanceUpdated IsValidConnectionString GetLock ReleaseLock
g.Sys.Cache.GetKey SetKey RemoveKey ResetCacheDB ResetCacheCustomApis ResetCacheSystemApis
g.Logger.Debug Log Info Warn Error            g.Res.Shared["x"] = …   travels with the request : pre hook → API → post hook
g.Shared["x"] = …                             lives as long as the runner, shared by the requests it serves
await g.Sys.Db.Full.Count(…)                  Full : the whole envelope, no throw
throw new AmErrors("Bad id.", 400);           refuse : 400, the message in errors
Tax.Add(100)                                  utility classes
g.Req.Body()  g.Req.Query()  g.Req.Params()  g.Req.Headers()  g.Req.EventData()  g.Req.ReqInfo()
g.Req.IsApiRequestFromUser()  g.Req.Scheduler().Int("worker")
g.Req.Auth().Map("authAMUser")  "authAMDB"  "authGoogle"  "authAzure"  "authAWS"  "authCustom"
g.Res.Output()  g.Res.SetStatusCode(201)  g.Res.SetContentType("text/html")  g.Res.Headers()  g.Res.Errors()
g.Res.Warnings()  g.Res.Shared()
g.Sys.Db.GetAll(am.GetAll{Instance: "instance", Database: "database", Collection: "collection", Find: …, Limit: …})
    GetAllByStream GetById SaveSingleOrMultiple MasterSave ArrayOperations UpdateById UpdateMany ReplaceById
    RemoveById Query QueryByStream RemoveByQuery Aggregate Count Distinct DistinctQuery
g.Sys.Db.Gen.GetAllGen(…)                       the same, schemaless, with the Gen suffix
g.Sys.System.Encrypt Decrypt Hash GetToken CallExternalApi ExecuteQuery GetSecret GetTableMeta CreateIndexes
    DropIndexes GetIndexes EmitEvent EmitEventWS IsValidDataForTable IsValidDataForCustomAPI
    MultiTenantInstanceUpdated IsValidConnectionString GetLock ReleaseLock
g.Sys.Cache.GetKey SetKey RemoveKey ResetCacheDB ResetCacheCustomApis ResetCacheSystemApis
g.Logger.Debug Log Info Warn Error            g.Res.Shared()["x"] = …   travels with the request : pre hook → API → post hook
g.Shared.Set("x", …)                          lives as long as the runner, shared by the requests it serves
g.Sys.Db.Full.Count(…)                        Full : the whole envelope ; err is the error of the call alone
return nil, am.Fail(400, "Bad id.")           refuse : 400, the message in errors
tax.Add(100)                                  utility classes : import tax "apimaker/src/utility_classes/utils---Tax"
g.req.body()  g.req.query()  g.req.params()  g.req.headers()  g.req.event_data()   (…_mut() to change them)
g.req.req_info()  g.req.is_api_request_from_user()  g.req.scheduler()["worker"]  ["workers"]  ["firedAt"]
g.req.auth()["authAMUser"]  ["authAMDB"]  ["authGoogle"]  ["authAzure"]  ["authAWS"]  ["authCustom"]
g.res.output()  g.res.set_status_code(201)  g.res.set_content_type("text/html")  g.res.headers_mut()
g.res.errors()  g.res.warnings()  g.res.shared()
g.sys.db.get_all(GetAll::new("instance", "database", "collection").find(…).limit(…))?   get_all_by_stream get_by_id
    save_single_or_multiple master_save array_operations update_by_id update_many replace_by_id remove_by_id query
    query_by_stream remove_by_query aggregate count distinct distinct_query
g.sys.db.get_all_gen(…)?                        the same, schemaless, with the _gen suffix
g.sys.system.encrypt decrypt hash get_token call_external_api execute_query get_secret get_table_meta create_indexes
    drop_indexes get_indexes emit_event emit_event_ws is_valid_data_for_table is_valid_data_for_custom_api
    multi_tenant_instance_updated is_valid_connection_string get_lock release_lock
g.sys.cache.get_key set_key remove_key reset_cache_db reset_cache_custom_apis reset_cache_system_apis
g.logger.debug log info warn error            g.res.shared_mut()["x"] = …   travels with the request : pre hook → API → post hook
g.shared.set("x", …)                          lives as long as the runner, shared by the requests it serves
g.sys.db.full.count(…)?                       full : the whole envelope, no Err for the errors of the API
return Err(am::fail(400, "Bad id."));         refuse : 400, the message in errors
use crate::Tax;  Tax::add(100.0)              utility classes
g.req.getBody()  g.req.getQuery()  g.req.getParams()  g.req.getHeaders()  g.req.getEventData()  g.req.getReqInfo()
g.req.isApiRequestFromUser()  g.req.getScheduler().getLong("worker")
g.req.getAuth().getObject("authAMUser")  "authAMDB"  "authGoogle"  "authAzure"  "authAWS"  "authCustom"
g.res.getOutput()  g.res.setStatusCode(201)  g.res.setContentType("text/html")  g.res.getHeaders()  g.res.getErrors()
g.res.getWarnings()  g.res.getShared()
g.sys.db.getAll(new GetAll("instance", "database", "collection").find(…).limit(…))   getAllByStream getById
    saveSingleOrMultiple masterSave arrayOperations updateById updateMany replaceById removeById query queryByStream
    removeByQuery aggregate count distinct distinctQuery
g.sys.db.gen.getAllGen(…)                       the same, schemaless, with the Gen suffix
g.sys.system.encrypt decrypt hash getToken callExternalApi executeQuery getSecret getTableMeta createIndexes
    dropIndexes getIndexes emitEvent emitEventWS isValidDataForTable isValidDataForCustomAPI
    multiTenantInstanceUpdated isValidConnectionString getLock releaseLock
g.sys.cache.getKey setKey removeKey resetCacheDB resetCacheCustomApis resetCacheSystemApis
g.logger.debug log info warn error            g.res.getShared().put("x", …)   travels with the request : pre hook → API → post hook
g.shared.put("x", …)                          lives as long as the runner, shared by the requests it serves
g.sys.db.full.count(…)                        full : the whole envelope, no throw
throw new AmErrors("Bad id.", 400);           refuse : 400, the message in errors
Tax.add(100)                                  utility classes
g.req.body  g.req.query  g.req.params  g.req.headers  g.req.eventData  g.req.reqInfo  g.req.isApiRequestFromUser
g.req.scheduler.getLong("worker")
g.req.auth.getObject("authAMUser")  "authAMDB"  "authGoogle"  "authAzure"  "authAWS"  "authCustom"
g.res.output  g.res.statusCode  g.res.contentType  g.res.headers  g.res.errors  g.res.warnings  g.res.shared
g.sys.db.getAll(GetAll("instance", "database", "collection").find(…).limit(…))   getAllByStream getById
    saveSingleOrMultiple masterSave arrayOperations updateById updateMany replaceById removeById query queryByStream
    removeByQuery aggregate count distinct distinctQuery
g.sys.db.gen.getAllGen(…)                       the same, schemaless, with the Gen suffix
g.sys.system.encrypt decrypt hash getToken callExternalApi executeQuery getSecret getTableMeta createIndexes
    dropIndexes getIndexes emitEvent emitEventWS isValidDataForTable isValidDataForCustomAPI
    multiTenantInstanceUpdated isValidConnectionString getLock releaseLock
g.sys.cache.getKey setKey removeKey resetCacheDB resetCacheCustomApis resetCacheSystemApis
g.logger.debug log info warn error            g.res.shared["x"] = …   travels with the request : pre hook → API → post hook
g.shared["x"] = …                             lives as long as the runner, shared by the requests it serves
g.sys.db.full.count(…)                        full : the whole envelope, no throw
throw AmErrors("Bad id.", 400)                refuse : 400, the message in errors
Tax.add(100.0)                                utility classes

g.req Global object g

body query params headers the request ; a pre hook may change it
body.files the uploaded files
auth who calls : authAMUser, authAMDB…
eventData the data of an event, in a listener
scheduler worker of workers, firedAt, in a scheduler
reqInfo url, apiCategory, reqMethod, apiInfo.id
isApiRequestFromUser false for a call of your code

g.res Global object g

statusCode 200… set it to answer another status
contentType JSON XML YAML TEXT HTML OCTET_STREAM
output the answer : a post hook reads and changes it
headers headers to add to the answer
errors · warnings the lists of the answer
shared values for the rest of this request

g.sys Code examples

db.* the schema APIs of any table
db.gen.* the same, schemaless : getAllGen…
system.* the system APIs, getLock, releaseLock
cache.* your keys in Redis, the cache resets
test.* in a test case : runCustomApi, mock, clearMocks, calls

A call takes the headers and queryParams of an HTTP call. It returns data and throws the errors. Ask for the whole answer with true last (TypeScript) or through full (the other languages).

g.logger, g.shared Global object g

g.logger.log(…) also debug info warn error : one line, in logs of the answer and in the log profiles
g.shared lives as long as the runner : a cache, never the only copy
g.res.shared lives for one request
console.log not kept on the native process : use g.logger

Events, jobs, live data

Events Events

// every listener of the event
await g.sys.system.emitEvent('order-placed',
  { order_no: 1001 });
// only these listeners
await g.sys.system.emitEvent('order-placed',
  { order_no: 1001 }, ['send-invoice']);
// in a listener
const data = g.req.eventData;

Over HTTP : POST …/system-api/<user-path>/emit-event with { name, eventData, executeListeners }. Or by itself after an API : Automatically trigger on API hit. The emitter gets outputArr, the answer of each listener. A loop of events is stopped.

Schedulers Schedulers

Intervals seconds, minutes, hours, days, working days, months, a year, or cron. Each one has its time zone.
Runs on one server, which hands over when it stops ; or every server, once per server or once per runner
Timeout 10 minutes by default
g.req.scheduler worker of workers, firedAt, serverIndex of servers, runnerIndex of runners
Run now API Testing : pick the scheduler, send
Stop the Active switch, on every server at once

Cron Cron

sec  min hour day mon week
0    0   22   *   *   1-5   22:00 Mon-Fri
0    15  14   1   *   *     14:15 on the 1st
*/30 *   *    *   *   *     every 30 seconds
0    0   */6  *   *   *     every 6 hours
sec, min 0-59
hour 0-23
day day of the month, 1-31
mon 1-12 or jan-dec
week day of the week, 0-7 or sun-sat : 0 and 7 are Sunday

A field takes *, 5, 1-5, 1,15 or */10. Five fields : no seconds. Presets : @yearly @monthly @weekly @daily @hourly @minutely @secondly @weekdays @weekends.

WebSockets WebSocket events

const ws = new WebSocket('wss://host/'
  + '?x-am-authorization=' + token);
// once CONNECTED came :
ws.send(JSON.stringify({
  objType: 'REGISTER',
  onEvents: [{
    eventType: 'INSTANCES',
    apiName: 'SCHEMA_GET_ALL',
    instance: 'mongodb', database: 'shop',
    collection: 'orders',
    getEventData: true,
  }],
}));
Message
CONNECTED the first one : send REGISTER
REGISTER validOnEvents with their eventId, invalidOnEvents with why
NOTIFICATION { eventId, eventData } after each successful call
UNREGISTER send { objType: 'UNREGISTER', onEvents: [eventId] }
TOKEN_VALIDATION a token is wrong or ran out : then close 1000

Subscriptions WebSocket events

Key
eventType INSTANCES CUSTOM_APIS SYSTEM_APIS CUSTOM_WS_EVENTS
apiName an API id, a custom API, a system API id, or an event name
condition { conditionType: 'RESPONSE', criteria: { recipientId: 42 } }
select { order_no: 1 } : the fields to send
getEventData false : only the fact that it happened
Emit g.sys.system.emitEventWS(name, data), or /emit-event-ws
Close 1000 the tokens no longer let it in
Close 1001 the server stops : connect and REGISTER again
Who groups grant the events ; subscriptions end with the connection

Locks Locks

const lock = await g.sys.system.getLock(
  { name: `invoice-${id}`, ttlSeconds: 30 });
if (!lock.acquired) return { sent: false };
try {
  // the work, one run at a time
} finally {
  await g.sys.system.releaseLock(lock);
}

Options : ttlSeconds (60), waitSeconds, tries, retryDelayMs, maxHolders, throwIfNotAcquired. On every server, in every language. Code only : no HTTP API.

Cache Automatic caching

Turn on enableCaching: true in the settings of a table, a database, a custom or a system API
Kept until a write through API Maker, at most 7200 s
Reset x-am-cache-control: reset_cache · the reset system APIs · g.sys.cache.resetCacheDB
Your keys g.sys.cache.setKey getKey removeKey
See it x-am-data-source: cache · the Redis Dashboard

Each caller has its own cached answers : the tokens are part of the key.

Security

Sign-in providers Sign-in providers

Who Header In g.req.auth
The app x-am-authorization authAMUser
Your users x-am-user-authorization authAMDB
Google x-google-authorization authGoogle
Azure AD x-azure-authorization authAzure
AWS Cognito x-aws-authorization authAWS
Your own x-custom-authorization authCustom

Your code reads who calls in g.req.auth. Require a provider with authProviders: ['users_tg'] in the settings of an API, a table or a database, or in common.authProviders of the secret.

Your users table Auth of a database user

let dbTokenGenerator: T.IAuthTokenAMDB = {
  name: 'users_tg',
  instance: 'mongodb',
  database: 'shop',
  collection: 'users',    // table: in SQL
  usernameColumn: 'email',
  passwordColumn: 'password',
  groupsColumn: 'groups', // 'a,b' or '*'
  expiresInSeconds: 259200,
};
module.exports = dbTokenGenerator;

Security → Auth Providers. A person signs in with { "name": "users_tg", "u", "p" } on /token.

The secret Secrets

hashingAlgorithm nonce hashing: true, hash data
encryptionAlgorithm secret encryption: true (AES)
encryptionAlgorithmFETransfer secretFETransfer encrypt and decrypt data, encrypted payloads : the key of your apps
feTransferDataValidityInSeconds how old an encrypted payload may be
apiUserPasswords passwords of API users, by path
connectionString the strings the instance form offers
authProviders the providers every API needs
multiTenant the tenants tables

The keys of common. From code : g.sys.system.getSecret(['common.secret']). A secret never goes to Git.

Before you go live Checklist

  • Change the passwords of [email protected] and [email protected]. Turn on two-factor sign-in.
  • Replace the API user default and the group Default : it grants everything.
  • A row scoping pre hook on each table people reach.
  • Run the security report : fix every finding, aim for grade A.
  • Review the IS_PUBLIC APIs and the allowed origins.
  • HTTPS only. Databases on a private network. Backups. Your licence key.

Run it

Ports Install API Maker

APIs and WebSockets 38246 (48246 without Launchpad)
Admin panel, local 4626 : http://127.0.0.1:4626
MongoDB of API Maker 38248
Redis of API Maker 7479
Your databases, local MongoDB 27018 · PostgreSQL 5433 · MySQL 3307 · MariaDB 3308 · Percona 3309 · TiDB 4001 · SQL Server 14330 · Oracle 1522

Launchpad picks free ports for a new project : these are the defaults.

Defaults Configuration

Time of the code 13000 ms
Time of a scheduler 10 minutes
A token 259200 s (3 days) ; refresh 900 s more
A cached answer 7200 s, up to 1,000,000 characters
Compression answers from 51200 bytes
A body · a form with files 16 MiB · 512 MiB
Uploaded files kept 1800 s
A lock ttlSeconds 60

Settings of a server Configuration

The page Settings → Deployment Settings, root user : every server at once
.env of a server wins over the page : am__port=48246
Command line --am.port=48246 ; --envPath=<file>
Required mongo_db_connection passCommunication passJWT passDBEncryptDecrypt redisInternal.nodes
Public address BE_HOST_PORT=https://api.example.com
Log AM_LOG=debug, or info,am_sandbox=debug

Connection strings Connection strings

MongoDB mongodb://user:pass@host:27017/?authSource=admin · mongodb+srv://user:pass@cluster/
MySQL, Percona, TiDB mysql://user:pass@host:3306/db
MariaDB mariadb://user:pass@host:3306/db
PostgreSQL postgresql://user:pass@host:5432/db?sslmode=require
SQL Server Server=host,1433;Database=db;User Id=u;Password=p;TrustServerCertificate=True;
Oracle host:1521/service, with a username key and a password key

The string stays in the secret : the instance names its key. Write the password as it is.

The Git repository Git integration

AGENTS.md  package.json  tsconfig.json
src/
  custom_apis/orders/
    orders.yaml          the item
    orders.ts            its code : .py .go …
    orders.config.ts     its settings
    pre-hooks/check_stock.ts
  schedulers/  events/  schemas/
  collection_hooks/  utility_classes/
  test_cases/  admin_user/data.yaml

Commit on the Git page. Pull on another server : it also runs the new migration scripts. Secrets never go to Git.

Local client Developer tools

npm run start keeps the folder in sync, both ways
npm run test runs the test cases ; --no-sync, --push
local-client.config.js the sync token : not in Git
A CI job AM_WEB_SOCKET_URL AM_SYNC_TOKEN AM_ADMIN_USER_PATH

Up to 50 local clients on one account. Nothing is committed for you.

Debugging Debugging

  1. Menu of your user → Debug Settings → Debugging on.
  2. API Testing opens on Debug. Click left of a line, or F9 : a breakpoint.
  3. Right click the margin : a condition, a hit count, a logpoint.
  4. Another client sends x-am-run-in-debugger: true.

Every language, in the sandboxes and on the native process.

SSR websites Reference

pages/**/*.tsx a page : default, loader, action, config, head
pages/**/*.ts an endpoint : GET POST PUT PATCH DELETE ALL
_layout.tsx _middleware.ts the layout, the middleware
_404.tsx _error.tsx the pages of errors
site.config.ts defineSite({ … })

In a page, ctx has db, api, cache, auth, cookies, fetch. The module am-ssr has redirect, notFound, json, formatDate, paginate…

Admin panel

Where things are The admin panel

APIs & Logic Instance API (DB API) · Custom API · SSR Websites · IDE · System API · Events · Schedulers · WebSockets · Initializers · Utility Classes · API Testing · Test Cases · i18N Management · Generated Interfaces · Swagger Docs
Security APIs Security Report & Actions · API Group Permissions · API User Permissions · Auth Providers · Secret Management · Vulnerabilities · Encrypt Strings · Dev Account
Database ER Diagram · Index Maker · Database Migration · Auto Increments
Operations Analytics · Log Explorer · Servers · Redis Dashboard · Git Integration · Automatic Backup · Health Check AI Reporting · AI Integration · Log Profile · Native Process Logs · Sandbox Settings · Upgrade/Downgrade Engine · Licenses
Settings Web Server · Deployment Settings (root user)
Your user User Info (Git URL) · My Security · Debug Settings · Local Client Sync Token
Any page Cmd+K or Ctrl+K : its name, then Enter · ? explains the controls of a page

Keys of the IDE and the editors IDE

Mac Windows, Linux
Go to file Cmd+P Ctrl+P
Search in the files Cmd+Shift+F Ctrl+Shift+F
Explorer Cmd+Shift+E Ctrl+Shift+E
Side · panel Cmd+B · Cmd+J Ctrl+B · Ctrl+J
Back, forward Ctrl+Option+Left Ctrl+Option+Right Alt+Left Alt+Right
Mac Windows, Linux
Close the tab Option+W Alt+W
Git blame Option+A Alt+A
Save, save all Cmd+S Cmd+Option+S Ctrl+S Ctrl+Alt+S
Breakpoint F9 F9
Send, run the code Cmd+E Ctrl+E